From f6ec0065074474f8b05862845e882f9d5d1048e4 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 8 Aug 2024 15:10:26 +0200 Subject: [PATCH 01/35] impr: use tsrest for psa endpoints (@fehmer) (#5712) !nuf --- backend/__tests__/api/controllers/psa.spec.ts | 64 +++++++++ backend/__tests__/utils/misc.spec.ts | 3 + backend/package.json | 3 +- backend/redocly.yaml | 2 +- backend/scripts/openapi.ts | 5 + backend/src/api/controllers/psa.ts | 10 +- backend/src/api/routes/index.ts | 2 +- backend/src/api/routes/psas.ts | 19 +-- backend/src/dal/psa.ts | 4 +- .../src/documentation/internal-swagger.json | 18 --- backend/src/utils/misc.ts | 1 + frontend/src/ts/ape/endpoints/index.ts | 2 - frontend/src/ts/ape/endpoints/psas.ts | 13 -- frontend/src/ts/ape/index.ts | 3 +- frontend/src/ts/elements/psa.ts | 4 +- packages/contracts/src/index.ts | 2 + packages/contracts/src/psas.ts | 37 +++++ packages/contracts/src/schemas/api.ts | 5 +- packages/contracts/src/schemas/psas.ts | 11 ++ packages/shared-types/src/index.ts | 8 -- pnpm-lock.yaml | 136 +++++++++++++++++- 21 files changed, 289 insertions(+), 63 deletions(-) create mode 100644 backend/__tests__/api/controllers/psa.spec.ts delete mode 100644 frontend/src/ts/ape/endpoints/psas.ts create mode 100644 packages/contracts/src/psas.ts create mode 100644 packages/contracts/src/schemas/psas.ts diff --git a/backend/__tests__/api/controllers/psa.spec.ts b/backend/__tests__/api/controllers/psa.spec.ts new file mode 100644 index 000000000000..edc3631b0491 --- /dev/null +++ b/backend/__tests__/api/controllers/psa.spec.ts @@ -0,0 +1,64 @@ +import request from "supertest"; +import app from "../../../src/app"; +import * as PsaDal from "../../../src/dal/psa"; +import { ObjectId } from "mongodb"; +const mockApp = request(app); + +describe("Psa Controller", () => { + describe("get psa", () => { + const getPsaMock = vi.spyOn(PsaDal, "get"); + + afterEach(() => { + getPsaMock.mockReset(); + }); + + it("get psas without authorization", async () => { + //GIVEN + const psaOne: PsaDal.DBPSA = { + _id: new ObjectId(), + message: "test2", + date: 1000, + level: 1, + sticky: true, + }; + const psaTwo: PsaDal.DBPSA = { + _id: new ObjectId(), + message: "test2", + date: 2000, + level: 2, + sticky: false, + }; + getPsaMock.mockResolvedValue([psaOne, psaTwo]); + + //WHEN + const { body } = await mockApp.get("/psas").expect(200); + + //THEN + expect(body).toEqual({ + message: "PSAs retrieved", + data: [ + { + _id: psaOne._id.toHexString(), + date: 1000, + level: 1, + message: "test2", + sticky: true, + }, + { + _id: psaTwo._id.toHexString(), + date: 2000, + level: 2, + message: "test2", + sticky: false, + }, + ], + }); + }); + it("get psas with authorization", async () => { + await mockApp + .get("/psas") + .set("authorization", `Uid 123456789`) + .expect(200); + }); + }); +}); diff --git a/backend/__tests__/utils/misc.spec.ts b/backend/__tests__/utils/misc.spec.ts index 71c32cad5a5c..a379e0fb07f8 100644 --- a/backend/__tests__/utils/misc.spec.ts +++ b/backend/__tests__/utils/misc.spec.ts @@ -649,5 +649,8 @@ describe("Misc Utils", () => { }, ]); }); + it("handles undefined", () => { + expect(misc.replaceObjectIds(undefined as any)).toBeUndefined(); + }); }); }); diff --git a/backend/package.json b/backend/package.json index 7a36cc3069f1..d668b5ef5287 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,7 +16,7 @@ "knip": "knip", "docker-db-only": "docker compose -f docker/compose.db-only.yml up", "docker": "docker compose -f docker/compose.yml up", - "gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2" + "gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && openapi-recursive-tagging dist/static/api/openapi.json dist/static/api/openapi-tagged.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2" }, "engines": { "node": "20.16.0" @@ -90,6 +90,7 @@ "eslint": "8.57.0", "eslint-watch": "8.0.0", "ioredis-mock": "7.4.0", + "openapi-recursive-tagging": "0.0.6", "readline-sync": "1.4.10", "supertest": "6.2.3", "tsx": "4.16.2", diff --git a/backend/redocly.yaml b/backend/redocly.yaml index 7bacf646af47..1898e389184a 100644 --- a/backend/redocly.yaml +++ b/backend/redocly.yaml @@ -5,7 +5,7 @@ apis: internal@v2: root: dist/static/api/openapi.json public-filter: - root: dist/static/api/openapi.json + root: dist/static/api/openapi-tagged.json decorators: filter-in: property: x-public diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts index 75317f4c8ce2..4b444caa19ca 100644 --- a/backend/scripts/openapi.ts +++ b/backend/scripts/openapi.ts @@ -66,6 +66,11 @@ export function getOpenApi(): OpenAPIObject { description: "Ape keys provide access to certain API endpoints.", "x-displayName": "Ape Keys", }, + { + name: "psas", + description: "Public service announcements.", + "x-displayName": "PSAs", + }, { name: "admin", description: diff --git a/backend/src/api/controllers/psa.ts b/backend/src/api/controllers/psa.ts index 18a3e31b93d9..721527eef8ea 100644 --- a/backend/src/api/controllers/psa.ts +++ b/backend/src/api/controllers/psa.ts @@ -1,7 +1,11 @@ +import { GetPsaResponse } from "@monkeytype/contracts/psas"; import * as PsaDAL from "../../dal/psa"; -import { MonkeyResponse } from "../../utils/monkey-response"; +import { MonkeyResponse2 } from "../../utils/monkey-response"; +import { replaceObjectIds } from "../../utils/misc"; -export async function getPsas(): Promise { +export async function getPsas( + _req: MonkeyTypes.Request2 +): Promise { const data = await PsaDAL.get(); - return new MonkeyResponse("PSAs retrieved", data); + return new MonkeyResponse2("PSAs retrieved", replaceObjectIds(data)); } diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 6f3b22b6c375..c64ee8cfd41e 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -43,7 +43,6 @@ const APP_START_TIME = Date.now(); const API_ROUTE_MAP = { "/users": users, "/results": results, - "/psas": psas, "/public": publicStats, "/leaderboards": leaderboards, "/quotes": quotes, @@ -57,6 +56,7 @@ const router = s.router(contract, { apeKeys, configs, presets, + psas, }); export function addApiRoutes(app: Application): void { diff --git a/backend/src/api/routes/psas.ts b/backend/src/api/routes/psas.ts index 6fbbd67598bd..4a283009a128 100644 --- a/backend/src/api/routes/psas.ts +++ b/backend/src/api/routes/psas.ts @@ -1,10 +1,13 @@ -import { Router } from "express"; -import * as PsaController from "../controllers/psa"; +import { psasContract } from "@monkeytype/contracts/psas"; +import { initServer } from "@ts-rest/express"; import * as RateLimit from "../../middlewares/rate-limit"; -import { asyncHandler } from "../../middlewares/utility"; - -const router = Router(); - -router.get("/", RateLimit.psaGet, asyncHandler(PsaController.getPsas)); +import * as PsaController from "../controllers/psa"; +import { callController } from "../ts-rest-adapter"; -export default router; +const s = initServer(); +export default s.router(psasContract, { + get: { + middleware: [RateLimit.psaGet], + handler: async (r) => callController(PsaController.getPsas)(r), + }, +}); diff --git a/backend/src/dal/psa.ts b/backend/src/dal/psa.ts index a343c49dd59c..904c30b8a09a 100644 --- a/backend/src/dal/psa.ts +++ b/backend/src/dal/psa.ts @@ -1,7 +1,7 @@ -import { PSA } from "@monkeytype/shared-types"; +import { PSA } from "@monkeytype/contracts/schemas/psas"; import * as db from "../init/db"; -type DBPSA = MonkeyTypes.WithObjectId; +export type DBPSA = MonkeyTypes.WithObjectId; export async function get(): Promise { return await db.collection("psa").find().toArray(); diff --git a/backend/src/documentation/internal-swagger.json b/backend/src/documentation/internal-swagger.json index d19a4fc74479..a999ae0da4cd 100644 --- a/backend/src/documentation/internal-swagger.json +++ b/backend/src/documentation/internal-swagger.json @@ -23,10 +23,6 @@ "name": "users", "description": "User data and related operations" }, - { - "name": "psas", - "description": "Public service announcements" - }, { "name": "leaderboards", "description": "Leaderboard data" @@ -416,20 +412,6 @@ } } }, - "/psas": { - "get": { - "tags": ["psas"], - "summary": "Gets the latest public service announcements", - "responses": { - "default": { - "description": "", - "schema": { - "$ref": "#/definitions/Response" - } - } - } - } - }, "/leaderboards": { "get": { "tags": ["leaderboards"], diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index 5b91628d2738..26e83cf772be 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -331,5 +331,6 @@ export function replaceObjectId( export function replaceObjectIds( data: T[] ): (T & { _id: string })[] { + if (data === undefined) return data; return data.map((it) => replaceObjectId(it)); } diff --git a/frontend/src/ts/ape/endpoints/index.ts b/frontend/src/ts/ape/endpoints/index.ts index 5b7b849fef2b..2dae7057b297 100644 --- a/frontend/src/ts/ape/endpoints/index.ts +++ b/frontend/src/ts/ape/endpoints/index.ts @@ -1,5 +1,4 @@ import Leaderboards from "./leaderboards"; -import Psas from "./psas"; import Quotes from "./quotes"; import Results from "./results"; import Users from "./users"; @@ -9,7 +8,6 @@ import Dev from "./dev"; export default { Leaderboards, - Psas, Public, Quotes, Results, diff --git a/frontend/src/ts/ape/endpoints/psas.ts b/frontend/src/ts/ape/endpoints/psas.ts deleted file mode 100644 index 6477a7878f20..000000000000 --- a/frontend/src/ts/ape/endpoints/psas.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PSA } from "@monkeytype/shared-types"; - -const BASE_PATH = "/psas"; - -export default class Psas { - constructor(private httpClient: Ape.HttpClient) { - this.httpClient = httpClient; - } - - async get(): Ape.EndpointResponse { - return await this.httpClient.get(BASE_PATH); - } -} diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index 137a0887165a..6bebc1c3caba 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -5,6 +5,7 @@ import { buildClient } from "./adapters/ts-rest-adapter"; import { configsContract } from "@monkeytype/contracts/configs"; import { presetsContract } from "@monkeytype/contracts/presets"; import { apeKeysContract } from "@monkeytype/contracts/ape-keys"; +import { psasContract } from "@monkeytype/contracts/psas"; const API_PATH = ""; const BASE_URL = envConfig.backendUrl; @@ -17,7 +18,7 @@ const Ape = { users: new endpoints.Users(httpClient), configs: buildClient(configsContract, BASE_URL, 10_000), results: new endpoints.Results(httpClient), - psas: new endpoints.Psas(httpClient), + psas: buildClient(psasContract, BASE_URL, 10_000), quotes: new endpoints.Quotes(httpClient), leaderboards: new endpoints.Leaderboards(httpClient), presets: buildClient(presetsContract, BASE_URL, 10_000), diff --git a/frontend/src/ts/elements/psa.ts b/frontend/src/ts/elements/psa.ts index c6c8acae90d6..324e2a863fc9 100644 --- a/frontend/src/ts/elements/psa.ts +++ b/frontend/src/ts/elements/psa.ts @@ -4,7 +4,7 @@ import { secondsToString } from "../utils/date-and-time"; import * as Notifications from "./notifications"; import { format } from "date-fns/format"; import * as Alerts from "./alerts"; -import { PSA } from "@monkeytype/shared-types"; +import { PSA } from "@monkeytype/contracts/schemas/psas"; function clearMemory(): void { window.localStorage.setItem("confirmedPSAs", JSON.stringify([])); @@ -61,7 +61,7 @@ async function getLatest(): Promise { } else if (response.status !== 200) { return null; } - return response.data; + return response.body.data; } export async function show(): Promise { diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 533e4a3d2fe9..e7f3836394a2 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -3,6 +3,7 @@ import { adminContract } from "./admin"; import { apeKeysContract } from "./ape-keys"; import { configsContract } from "./configs"; import { presetsContract } from "./presets"; +import { psasContract } from "./psas"; const c = initContract(); @@ -11,4 +12,5 @@ export const contract = c.router({ apeKeys: apeKeysContract, configs: configsContract, presets: presetsContract, + psas: psasContract, }); diff --git a/packages/contracts/src/psas.ts b/packages/contracts/src/psas.ts new file mode 100644 index 000000000000..25e2f32265ae --- /dev/null +++ b/packages/contracts/src/psas.ts @@ -0,0 +1,37 @@ +import { initContract } from "@ts-rest/core"; +import { z } from "zod"; +import { PSASchema } from "./schemas/psas"; + +import { + CommonResponses, + EndpointMetadata, + responseWithData, +} from "./schemas/api"; +export const GetPsaResponseSchema = responseWithData(z.array(PSASchema)); +export type GetPsaResponse = z.infer; + +const c = initContract(); +export const psasContract = c.router( + { + get: { + summary: "get psas", + description: "Get list of public service announcements", + method: "GET", + path: "/", + responses: { + 200: GetPsaResponseSchema, + }, + }, + }, + { + pathPrefix: "/psas", + strictStatusCodes: true, + metadata: { + openApiTags: "psas", + authenticationOptions: { + isPublic: true, + }, + } as EndpointMetadata, + commonResponses: CommonResponses, + } +); diff --git a/packages/contracts/src/schemas/api.ts b/packages/contracts/src/schemas/api.ts index 37d3b6d2e6e0..f90e47dbd91d 100644 --- a/packages/contracts/src/schemas/api.ts +++ b/packages/contracts/src/schemas/api.ts @@ -1,6 +1,6 @@ import { z, ZodSchema } from "zod"; -export type OpenApiTag = "configs" | "presets" | "ape-keys" | "admin"; +export type OpenApiTag = "configs" | "presets" | "ape-keys" | "admin" | "psas"; export type EndpointMetadata = { /** Authentication options, by default a bearer token is required. */ @@ -74,4 +74,7 @@ export const CommonResponses = { 422: MonkeyValidationErrorSchema.describe("Request validation failed"), 429: MonkeyClientError.describe("Rate limit exceeded"), 500: MonkeyServerError.describe("Generic server error"), + 503: MonkeyServerError.describe( + "Endpoint disabled or server is under maintenance" + ), }; diff --git a/packages/contracts/src/schemas/psas.ts b/packages/contracts/src/schemas/psas.ts new file mode 100644 index 000000000000..2ec6d074ade9 --- /dev/null +++ b/packages/contracts/src/schemas/psas.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { IdSchema } from "./util"; + +export const PSASchema = z.object({ + _id: IdSchema, + message: z.string(), + date: z.number().int().min(0).optional(), + level: z.number().int().optional(), + sticky: z.boolean().optional(), +}); +export type PSA = z.infer; diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index 695c637b470e..fe8d50ea60d8 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -301,14 +301,6 @@ export type ResultFilters = { } & Record; }; -export type PSA = { - _id: string; - message: string; - sticky?: boolean; - level?: number; - date?: number; -}; - export type SpeedHistogram = { [key: string]: number; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eb2832a2abb..cea3e4b34a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,6 +264,9 @@ importers: ioredis-mock: specifier: 7.4.0 version: 7.4.0(ioredis@4.28.5) + openapi-recursive-tagging: + specifier: 0.0.6 + version: 0.0.6 readline-sync: specifier: 1.4.10 version: 1.4.10 @@ -2844,6 +2847,10 @@ packages: resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} engines: {node: '>=0.10.0'} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3449,6 +3456,9 @@ packages: cliui@3.2.0: resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==} + cliui@5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -4189,6 +4199,9 @@ packages: electron-to-chromium@1.5.5: resolution: {integrity: sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==} + emoji-regex@7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4665,6 +4678,10 @@ packages: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5504,6 +5521,10 @@ packages: resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -6028,6 +6049,10 @@ packages: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -6989,6 +7014,10 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openapi-recursive-tagging@0.0.6: + resolution: {integrity: sha512-ZtHR0jHeIMTuyeenbee7j0fK58Uf9ZMFGTXv3abQvsdjImfdEXfmmNlMNgpW27DmoomOgwwpGNGldUUQd0oJ4g==} + hasBin: true + openapi-sampler@1.5.1: resolution: {integrity: sha512-tIWIrZUKNAsbqf3bd9U1oH6JEXo8LNYuDlXw26By67EygpjT+ArFnsxxyTMjFWRfbqo5ozkvgSQDK69Gd8CddA==} @@ -7041,6 +7070,10 @@ packages: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -7764,6 +7797,9 @@ packages: require-main-filename@1.0.1: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requirejs-config-file@4.0.0: resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} engines: {node: '>=10.13.0'} @@ -8300,6 +8336,10 @@ packages: resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} engines: {node: '>=0.10.0'} + string-width@3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -8340,6 +8380,10 @@ packages: resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} engines: {node: '>=0.10.0'} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -9292,6 +9336,9 @@ packages: which-module@1.0.0: resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -9397,6 +9444,10 @@ packages: resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} engines: {node: '>=0.10.0'} + wrap-ansi@5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9438,6 +9489,9 @@ packages: y18n@3.2.2: resolution: {integrity: sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -9460,6 +9514,9 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@15.0.3: + resolution: {integrity: sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -9471,6 +9528,9 @@ packages: yargs-parser@5.0.1: resolution: {integrity: sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==} + yargs@14.2.3: + resolution: {integrity: sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} @@ -12174,6 +12234,8 @@ snapshots: ansi-regex@2.1.1: {} + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.0.1: {} @@ -12887,6 +12949,12 @@ snapshots: strip-ansi: 3.0.1 wrap-ansi: 2.1.0 + cliui@5.0.0: + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -13651,6 +13719,8 @@ snapshots: electron-to-chromium@1.5.5: {} + emoji-regex@7.0.3: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -14464,6 +14534,10 @@ snapshots: dependencies: locate-path: 2.0.0 + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -15594,6 +15668,8 @@ snapshots: dependencies: number-is-nan: 1.0.1 + is-fullwidth-code-point@2.0.0: {} + is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} @@ -16132,6 +16208,11 @@ snapshots: p-locate: 2.0.0 path-exists: 3.0.0 + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -17206,7 +17287,7 @@ snapshots: oas-kit-common: 1.0.8 reftools: 1.1.9 yaml: 1.10.2 - yargs: 17.0.1 + yargs: 17.7.2 oas-schema-walker@1.1.5: {} @@ -17348,6 +17429,12 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-recursive-tagging@0.0.6: + dependencies: + reftools: 1.1.9 + yaml: 1.10.2 + yargs: 14.2.3 + openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -17414,6 +17501,10 @@ snapshots: dependencies: p-limit: 1.3.0 + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -18203,6 +18294,8 @@ snapshots: require-main-filename@1.0.1: {} + require-main-filename@2.0.0: {} + requirejs-config-file@4.0.0: dependencies: esprima: 4.0.1 @@ -18789,6 +18882,12 @@ snapshots: is-fullwidth-code-point: 1.0.0 strip-ansi: 3.0.1 + string-width@3.1.0: + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -18855,6 +18954,10 @@ snapshots: dependencies: ansi-regex: 2.1.1 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -19037,7 +19140,7 @@ snapshots: oas-validator: 5.0.8 reftools: 1.1.9 yaml: 1.10.2 - yargs: 17.0.1 + yargs: 17.7.2 transitivePeerDependencies: - encoding @@ -19924,6 +20027,8 @@ snapshots: which-module@1.0.0: {} + which-module@2.0.1: {} + which-pm-runs@1.1.0: {} which-typed-array@1.1.15: @@ -20105,6 +20210,12 @@ snapshots: string-width: 1.0.2 strip-ansi: 3.0.1 + wrap-ansi@5.1.0: + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -20140,6 +20251,8 @@ snapshots: y18n@3.2.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -20152,6 +20265,11 @@ snapshots: yaml@2.5.0: {} + yargs-parser@15.0.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -20161,6 +20279,20 @@ snapshots: camelcase: 3.0.0 object.assign: 4.1.5 + yargs@14.2.3: + dependencies: + cliui: 5.0.0 + decamelize: 1.2.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 15.0.3 + yargs@16.2.0: dependencies: cliui: 7.0.4 From 1eebf748ba64f4cf21ec27ca9fe9ebd26ffe6e05 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 8 Aug 2024 16:16:01 +0200 Subject: [PATCH 02/35] refactor: move release code to a package (@miodec) (#5743) --- backend/__tests__/dal/admin-uids.spec.ts | 4 +- package.json | 12 +- packages/eslint-config/index.js | 8 + packages/release/.eslintrc.cjs | 5 + .../release/bin}/deployBackend.sh | 0 {bin => packages/release/bin}/purgeCfCache.sh | 0 packages/release/package.json | 23 +++ .../release/src/buildChangelog.js | 5 +- .../release/src/index.js | 57 +++++-- pnpm-lock.yaml | 158 +++++++++++++++++- 10 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 packages/release/.eslintrc.cjs rename {bin => packages/release/bin}/deployBackend.sh (100%) rename {bin => packages/release/bin}/purgeCfCache.sh (100%) create mode 100644 packages/release/package.json rename bin/buildChangelog.mjs => packages/release/src/buildChangelog.js (98%) rename bin/release.mjs => packages/release/src/index.js (80%) mode change 100644 => 100755 diff --git a/backend/__tests__/dal/admin-uids.spec.ts b/backend/__tests__/dal/admin-uids.spec.ts index aecf35355882..daadbbcab537 100644 --- a/backend/__tests__/dal/admin-uids.spec.ts +++ b/backend/__tests__/dal/admin-uids.spec.ts @@ -6,7 +6,7 @@ describe("AdminUidsDal", () => { it("should return true for existing admin user", async () => { //GIVEN const uid = new ObjectId().toHexString(); - AdminUidsDal.getCollection().insertOne({ + await AdminUidsDal.getCollection().insertOne({ _id: new ObjectId(), uid: uid, }); @@ -17,7 +17,7 @@ describe("AdminUidsDal", () => { it("should return false for non-existing admin user", async () => { //GIVEN - AdminUidsDal.getCollection().insertOne({ + await AdminUidsDal.getCollection().insertOne({ _id: new ObjectId(), uid: "admin", }); diff --git a/package.json b/package.json index 30a4b388dd7b..e247ca245fe6 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,11 @@ "start-fe": "turbo run start --filter @monkeytype/frontend", "docker": "cd backend && docker compose up", "audit-fe": "cd frontend && npm run audit", - "release": "node ./bin/release.mjs", - "release-fe": "node ./bin/release.mjs --fe", - "release-be": "node ./bin/release.mjs --be", - "release-no-deploy": "node ./bin/release.mjs --no-deploy", - "release-dry": "node ./bin/release.mjs --dry", + "release": "monkeytype-release", + "release-fe": "monkeytype-release --fe", + "release-be": "monkeytype-release --be", + "release-no-deploy": "monkeytype-release --no-deploy", + "release-dry": "monkeytype-release --dry", "hotfix": "npm run build-fe && cd frontend && npm run deploy-live && cd .. && sh ./bin/purgeCfCache.sh", "pretty-check": "prettier --check .", "pretty-check-be": "prettier --check ./backend", @@ -54,6 +54,7 @@ "node": "20.16.0" }, "devDependencies": { + "@monkeytype/release": "workspace:*", "@commitlint/cli": "17.7.1", "@commitlint/config-conventional": "17.7.0", "conventional-changelog": "4.0.0", @@ -68,7 +69,6 @@ "lint-staged": "13.2.3", "only-allow": "1.2.1", "prettier": "2.5.1", - "readline-sync": "1.4.10", "turbo": "2.0.9", "typescript": "5.5.4", "wait-for-localhost-cli": "3.2.0" diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index f757881ea464..46af1081d2da 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -31,6 +31,14 @@ module.exports = { "no-duplicate-imports": ["error"], "no-constant-condition": ["error"], "no-constant-binary-expression": "error", + "no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^(_|e|event)", + caughtErrorsIgnorePattern: "^(_|e|error)", + varsIgnorePattern: "^_", + }, + ], "import/no-duplicates": "off", "import/no-unresolved": [ "error", diff --git a/packages/release/.eslintrc.cjs b/packages/release/.eslintrc.cjs new file mode 100644 index 000000000000..922de4abe568 --- /dev/null +++ b/packages/release/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@monkeytype/eslint-config"], +}; diff --git a/bin/deployBackend.sh b/packages/release/bin/deployBackend.sh similarity index 100% rename from bin/deployBackend.sh rename to packages/release/bin/deployBackend.sh diff --git a/bin/purgeCfCache.sh b/packages/release/bin/purgeCfCache.sh similarity index 100% rename from bin/purgeCfCache.sh rename to packages/release/bin/purgeCfCache.sh diff --git a/packages/release/package.json b/packages/release/package.json new file mode 100644 index 000000000000..ef127a4f9fe3 --- /dev/null +++ b/packages/release/package.json @@ -0,0 +1,23 @@ +{ + "name": "@monkeytype/release", + "private": true, + "type": "module", + "scripts": { + "dev": "nodemon --watch src --exec 'node ./src/index.js --dry'", + "dev-changelog": "nodemon ./src/buildChangelog.js", + "lint": "eslint \"./**/*.js\"" + }, + "devDependencies": { + "@monkeytype/eslint-config": "workspace:*", + "eslint": "8.57.0", + "nodemon": "3.1.4" + }, + "bin": { + "monkeytype-release": "./src/index.js" + }, + "dependencies": { + "@octokit/rest": "20.1.1", + "dotenv": "16.4.5", + "readline-sync": "1.4.10" + } +} diff --git a/bin/buildChangelog.mjs b/packages/release/src/buildChangelog.js similarity index 98% rename from bin/buildChangelog.mjs rename to packages/release/src/buildChangelog.js index 6bc330c927f5..2f1f832f16d7 100644 --- a/bin/buildChangelog.mjs +++ b/packages/release/src/buildChangelog.js @@ -1,4 +1,3 @@ -import conventionalChangelog from "conventional-changelog"; import { exec } from "child_process"; // const stream = conventionalChangelog( @@ -41,7 +40,7 @@ async function getLog() { return new Promise((resolve, reject) => { exec( `git log --oneline $(git describe --tags --abbrev=0 @^)..@ --pretty="format:${lineDelimiter}%H${logDelimiter}%h${logDelimiter}%s${logDelimiter}%b"`, - (err, stdout, stderr) => { + (err, stdout, _stderr) => { if (err) { reject(err); } @@ -250,7 +249,7 @@ function convertStringToLog(logString) { //split message using regex based on fix(language): spelling mistakes in Nepali wordlist and quotes (sapradhan) (#4528) //scope is optional, username is optional, pr number is optional - const [__, type, scope, message, message2, message3] = title.split( + const [_, type, scope, message, message2, message3] = title.split( /^(\w+)(?:\(([^)]+)\))?:\s+(.+?)\s*(?:\(([^)]+)\))?(?:\s+\(([^)]+)\))?(?:\s+\(([^)]+)\))?$/ ); diff --git a/bin/release.mjs b/packages/release/src/index.js old mode 100644 new mode 100755 similarity index 80% rename from bin/release.mjs rename to packages/release/src/index.js index f285f97e04d9..d802a9c3914f --- a/bin/release.mjs +++ b/packages/release/src/index.js @@ -1,12 +1,10 @@ import { execSync } from "child_process"; import { Octokit } from "@octokit/rest"; import dotenv from "dotenv"; -import { readFileSync } from "fs"; +import fs, { readFileSync } from "fs"; import readlineSync from "readline-sync"; -import path from "path"; -import fs from "fs"; +import path, { dirname } from "path"; import { fileURLToPath } from "url"; -import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -18,6 +16,9 @@ const isFrontend = args.includes("--fe"); const noDeploy = args.includes("--no-deploy"); const isBackend = args.includes("--be"); const isDryRun = args.includes("--dry"); +const noSyncCheck = args.includes("--no-sync-check"); + +const PROJECT_ROOT = path.resolve(__dirname, "../../../"); const runCommand = (command, force) => { if (isDryRun && !force) { @@ -35,19 +36,41 @@ const runCommand = (command, force) => { } }; +const runProjectRootCommand = (command, force) => { + if (isDryRun && !force) { + console.log(`[Dry Run] Command: ${command}`); + return "[Dry Run] Command executed."; + } else { + try { + const output = execSync(`cd ${PROJECT_ROOT} && ${command}`, { + stdio: "pipe", + }).toString(); + return output; + } catch (error) { + console.error(`Error executing command ${command}`); + console.error(error); + process.exit(1); + } + } +}; + const checkBranchSync = () => { console.log("Checking if local master branch is in sync with origin..."); - if (isDryRun) { + if (noSyncCheck) { + console.log("Skipping sync check."); + } else if (isDryRun) { console.log("[Dry Run] Checking sync..."); } else { try { // Fetch the latest changes from the remote repository - runCommand("git fetch origin"); + runProjectRootCommand("git fetch origin"); // Get the commit hashes of the local and remote master branches - const localMaster = runCommand("git rev-parse master").trim(); - const remoteMaster = runCommand("git rev-parse origin/master").trim(); + const localMaster = runProjectRootCommand("git rev-parse master").trim(); + const remoteMaster = runProjectRootCommand( + "git rev-parse origin/master" + ).trim(); if (localMaster !== remoteMaster) { console.error( @@ -65,8 +88,12 @@ const checkBranchSync = () => { const getCurrentVersion = () => { console.log("Getting current version..."); - const packageJson = JSON.parse(readFileSync("./package.json", "utf-8")); - return packageJson.version; + + const rootPackageJson = JSON.parse( + readFileSync(`${PROJECT_ROOT}/package.json`, "utf-8") + ); + + return rootPackageJson.version; }; const incrementVersion = (currentVersion) => { @@ -134,7 +161,7 @@ const buildProject = () => { filter = "--filter @monkeytype/backend"; } - runCommand("npx turbo lint test validate-json build " + filter); + runProjectRootCommand("npx turbo lint test validate-json build " + filter); }; const deployBackend = () => { @@ -144,7 +171,9 @@ const deployBackend = () => { const deployFrontend = () => { console.log("Deploying frontend..."); - runCommand("cd frontend && npx firebase deploy -P live --only hosting"); + runProjectRootCommand( + "cd frontend && npx firebase deploy -P live --only hosting" + ); }; const purgeCache = () => { @@ -155,7 +184,9 @@ const purgeCache = () => { const generateChangelog = async () => { console.log("Generating changelog..."); - const changelog = runCommand("node bin/buildChangelog.mjs", true); + const p = path.resolve(__dirname, "./buildChangelog.js"); + + const changelog = runCommand(`node ${p}`, true); return changelog; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cea3e4b34a43..84e7440d9bf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@commitlint/config-conventional': specifier: 17.7.0 version: 17.7.0 + '@monkeytype/release': + specifier: workspace:* + version: link:packages/release conventional-changelog: specifier: 4.0.0 version: 4.0.0 @@ -50,9 +53,6 @@ importers: prettier: specifier: 2.5.1 version: 2.5.1 - readline-sync: - specifier: 1.4.10 - version: 1.4.10 turbo: specifier: 2.0.9 version: 2.0.9 @@ -528,6 +528,28 @@ importers: specifier: 9.1.0 version: 9.1.0(eslint@8.57.0) + packages/release: + dependencies: + '@octokit/rest': + specifier: 20.1.1 + version: 20.1.1 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + readline-sync: + specifier: 1.4.10 + version: 1.4.10 + devDependencies: + '@monkeytype/eslint-config': + specifier: workspace:* + version: link:../eslint-config + eslint: + specifier: 8.57.0 + version: 8.57.0 + nodemon: + specifier: 3.1.4 + version: 3.1.4 + packages/shared-types: dependencies: '@monkeytype/contracts': @@ -2196,6 +2218,58 @@ packages: resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@octokit/auth-token@4.0.0': + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + + '@octokit/core@5.2.0': + resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@9.0.5': + resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==} + engines: {node: '>= 18'} + + '@octokit/graphql@7.1.0': + resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/plugin-paginate-rest@11.3.1': + resolution: {integrity: sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + + '@octokit/plugin-request-log@4.0.1': + resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + + '@octokit/plugin-rest-endpoint-methods@13.2.2': + resolution: {integrity: sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5 + + '@octokit/request-error@5.1.0': + resolution: {integrity: sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==} + engines: {node: '>= 18'} + + '@octokit/request@8.4.0': + resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==} + engines: {node: '>= 18'} + + '@octokit/rest@20.1.1': + resolution: {integrity: sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==} + engines: {node: '>= 18'} + + '@octokit/types@13.5.0': + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -3172,6 +3246,9 @@ packages: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} + before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + bignumber.js@9.1.2: resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} @@ -4009,6 +4086,9 @@ packages: engines: {node: ^10.13 || ^12 || >=14} hasBin: true + deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -8971,6 +9051,9 @@ packages: resolution: {integrity: sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==} engines: {node: '>=12.18.2'} + universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -11525,6 +11608,69 @@ snapshots: '@npmcli/name-from-folder@2.0.0': {} + '@octokit/auth-token@4.0.0': {} + + '@octokit/core@5.2.0': + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.0 + '@octokit/request': 8.4.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + + '@octokit/endpoint@9.0.5': + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + + '@octokit/graphql@7.1.0': + dependencies: + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/plugin-paginate-rest@11.3.1(@octokit/core@5.2.0)': + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + + '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.0)': + dependencies: + '@octokit/core': 5.2.0 + + '@octokit/plugin-rest-endpoint-methods@13.2.2(@octokit/core@5.2.0)': + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + + '@octokit/request-error@5.1.0': + dependencies: + '@octokit/types': 13.5.0 + deprecation: 2.3.1 + once: 1.4.0 + + '@octokit/request@8.4.0': + dependencies: + '@octokit/endpoint': 9.0.5 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + + '@octokit/rest@20.1.1': + dependencies: + '@octokit/core': 5.2.0 + '@octokit/plugin-paginate-rest': 11.3.1(@octokit/core@5.2.0) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.0) + '@octokit/plugin-rest-endpoint-methods': 13.2.2(@octokit/core@5.2.0) + + '@octokit/types@13.5.0': + dependencies: + '@octokit/openapi-types': 22.2.0 + '@one-ini/wasm@0.1.1': {} '@opentelemetry/api@1.8.0': {} @@ -12581,6 +12727,8 @@ snapshots: - encoding - supports-color + before-after-hook@2.2.3: {} + bignumber.js@9.1.2: {} binary-extensions@1.13.1: {} @@ -13495,6 +13643,8 @@ snapshots: transitivePeerDependencies: - supports-color + deprecation@2.3.1: {} + destroy@1.2.0: {} detect-file@1.0.0: {} @@ -19600,6 +19750,8 @@ snapshots: transitivePeerDependencies: - supports-color + universal-user-agent@6.0.1: {} + universalify@2.0.1: {} unpipe@1.0.0: {} From cdfcc9e663b57c2765ee01db83c40b337371e5bf Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 16:27:47 +0200 Subject: [PATCH 03/35] fix: incorrect paths !nuf --- packages/release/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/release/src/index.js b/packages/release/src/index.js index d802a9c3914f..3d7d7a72aa71 100755 --- a/packages/release/src/index.js +++ b/packages/release/src/index.js @@ -166,7 +166,7 @@ const buildProject = () => { const deployBackend = () => { console.log("Deploying backend..."); - runCommand("sh ./bin/deployBackend.sh"); + runCommand("sh ../bin/deployBackend.sh"); }; const deployFrontend = () => { @@ -178,7 +178,7 @@ const deployFrontend = () => { const purgeCache = () => { console.log("Purging Cloudflare cache..."); - runCommand("sh ./bin/purgeCfCache.sh"); + runCommand("sh ../bin/purgeCfCache.sh"); }; const generateChangelog = async () => { From f929d65a6cb7d0f7eb6352f4c849265856818d8c Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 8 Aug 2024 17:06:50 +0200 Subject: [PATCH 04/35] chore: add more badges to readme (@fehmer) (#5744) * chore: add more badges to readme (@fehmer) * get rid of miodec --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 077d57cf161d..fa5aad3e984c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,23 @@ [![](https://github.com/monkeytypegame/monkeytype/blob/master/frontend/static/images/githubbanner2.png?raw=true)](https://monkeytype.com/)
-![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) -![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white) +![ChartJs](https://img.shields.io/badge/Chart.js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white) +![Eslint](https://img.shields.io/badge/eslint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white) +![Express](https://img.shields.io/badge/-Express-373737?style=for-the-badge&logo=Express&logoColor=white) +![Firebase](https://img.shields.io/badge/firebase-ffca28?style=for-the-badge&logo=firebase&logoColor=black) +![Fontawesome](https://img.shields.io/badge/fontawesome-538DD7?style=for-the-badge&logo=fontawesome&logoColor=white) ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) +![JQuery](https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white) +![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?style=for-the-badge&logo=mongodb&logoColor=white) +![PNPM](https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=white) +![Redis](https://img.shields.io/badge/Redis-DC382D?style=for-the-badge&logo=redis&logoColor=white) +![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white) +![TsRest](https://img.shields.io/badge/-TSREST-9333ea?style=for-the-badge&logoColor=white&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMjAuMzA2Nzc4bW0iCiAgIGhlaWdodD0iMTIuMDgzMjMzbW0iCiAgIHZpZXdCb3g9IjAgMCAyMC4zMDY3NzggMTIuMDgzMjMzIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmcxIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3MSIKICAgICBwYWdlY29sb3I9IiM1MDUwNTAiCiAgICAgYm9yZGVyY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMSIKICAgICBpbmtzY2FwZTpzaG93cGFnZXNoYWRvdz0iMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIxIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iIC8+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiIC8+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMuODE5ODA1NCwtMi4yMTQ3MTkzKSI+CiAgICA8cGF0aAogICAgICAgZD0ibSAxNS40NTgwMzUsOC45NzMzOTUzIDguNjMzMjUsMC4wNDQ4NyAwLjAwOSwtMS42NjgxOTggLTguNjMzMjIsLTAuMDQ0ODUgeiBtIDAuMDI2MywtNS4wNTYxMDggOC42MzMyNSwwLjA0NDg1IDAuMDA5LC0xLjcwMjU2OCAtOC42MzMyNSwtMC4wNDQ4NSB6IG0gLTAuMDQ0OCw4LjYzMzI0NzcgOC42MzMyMywwLjA0NDg1IC0wLjAwOSwxLjcwMjU2NyAtOC42MzMyNSwtMC4wNDQ4NSB6IgogICAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICAgIGlkPSJwYXRoMSIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSAxMS4xMTE3MjUsMTAuMjg2NjI4IGMgMS42NTEsLTAuNjE5MTI0NyAyLjU5Njg4LC0xLjk2MDU2MjcgMi41OTY4OCwtMy44MDA3Mzk3IDAsLTIuNjQ4NDc5IC0xLjkyNjE2LC00LjI0Nzg4NSAtNS4wNzMzNzk2LC00LjI0Nzg4NSBoIC00LjgxNTQyIHYgMS43MDI1OTQgaCA0Ljc0NjYzIGMgMi4wODA5Mzk2LDAgMy4xNjQ0MDk2LDAuOTI4Njg3IDMuMTY0NDA5NiwyLjU0NTI5MSAwLDEuNTk5NDA2IC0xLjA4MzQ3LDIuNTQ1MjkyIC0zLjE2NDQwOTYsMi41NDUyOTIgaCAtNC43NDY2MyB2IDUuMjQ1MzYzNyBoIDEuOTYwNTYgdiAtMy41NzcxNjYgaCAyLjg1NDg2IGMgMC4yMDYzNywwIDAuNDI5OTUsMCAwLjYxOTEyLC0wLjAxNzIgbCAyLjUyODA5OTYsMy41OTQzNjQgaCAyLjEzMjU0IHoiCiAgICAgICBmaWxsPSIjZmZmZmZmIgogICAgICAgaWQ9InBhdGgyIgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICA8L2c+Cjwvc3ZnPgo=) +![Turborepo](https://img.shields.io/badge/-Turborepo-EF4444?style=for-the-badge&logo=turborepo&logoColor=white) +![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) +![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=Vite&logoColor=white) +![Vitest](https://img.shields.io/badge/vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white) +![Zod](https://img.shields.io/badge/-Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white) # About From 316bbd33a63ad8adad126948319e8aa40c5cf3e5 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 16:58:00 +0200 Subject: [PATCH 05/35] impr: notify user that captcha api failed to respond instead of throwing a server error --- backend/src/api/controllers/user.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 11bc33b8ebe4..0bca837afe17 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -40,8 +40,18 @@ import { addImportantLog, addLog, deleteUserLogs } from "../../dal/logs"; import { sendForgotPasswordEmail as authSendForgotPasswordEmail } from "../../utils/auth"; async function verifyCaptcha(captcha: string): Promise { - if (!(await verify(captcha))) { - throw new MonkeyError(422, "Captcha check failed"); + let verified = false; + try { + verified = await verify(captcha); + } catch (e) { + //fetch to recaptcha api can sometimes fail + throw new MonkeyError( + 422, + "Request to the Captcha API failed, please try again later" + ); + } + if (!verified) { + throw new MonkeyError(422, "Captcha challenge failed"); } } From d57e318cf10652f36ca173d723269513ed8570a4 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 17:01:24 +0200 Subject: [PATCH 06/35] fix: sh scripts sometimes not finding .env file !nuf --- packages/release/bin/deployBackend.sh | 7 ++++++- packages/release/bin/purgeCfCache.sh | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/release/bin/deployBackend.sh b/packages/release/bin/deployBackend.sh index f5d89b62b5a8..13d8a8ca7d1b 100755 --- a/packages/release/bin/deployBackend.sh +++ b/packages/release/bin/deployBackend.sh @@ -1,6 +1,11 @@ #!/bin/bash -source .env +# Determine the directory of the script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the .env file from the parent directory of the script's directory +source "$SCRIPT_DIR/../.env" + echo "Deploying backend to $BE_HOST with script $BE_SCRIPT_PATH" # Connect to SSH and execute remote script diff --git a/packages/release/bin/purgeCfCache.sh b/packages/release/bin/purgeCfCache.sh index 11cfaacf23c8..eadb48c5d0ae 100755 --- a/packages/release/bin/purgeCfCache.sh +++ b/packages/release/bin/purgeCfCache.sh @@ -1,5 +1,11 @@ #!/bin/bash -source .env + +# Determine the directory of the script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the .env file from the parent directory of the script's directory +source "$SCRIPT_DIR/../.env" + echo "Purging Cloudflare cache for zone $CF_ZONE_ID" response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CF_API_KEY" \ From c2d08096ed0ee09be98cc22f8dfc69e1a2fa0bd7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 17:05:33 +0200 Subject: [PATCH 07/35] impr: export cf purge script !nuf --- package.json | 2 +- packages/release/package.json | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e247ca245fe6..39cf10ca5126 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "release-be": "monkeytype-release --be", "release-no-deploy": "monkeytype-release --no-deploy", "release-dry": "monkeytype-release --dry", - "hotfix": "npm run build-fe && cd frontend && npm run deploy-live && cd .. && sh ./bin/purgeCfCache.sh", + "hotfix": "npm run build-fe && cd frontend && npm run deploy-live && monkeytype-purge", "pretty-check": "prettier --check .", "pretty-check-be": "prettier --check ./backend", "pretty-check-fe": "prettier --check ./frontend/src", diff --git a/packages/release/package.json b/packages/release/package.json index ef127a4f9fe3..74b35d9f50de 100644 --- a/packages/release/package.json +++ b/packages/release/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "nodemon --watch src --exec 'node ./src/index.js --dry'", "dev-changelog": "nodemon ./src/buildChangelog.js", - "lint": "eslint \"./**/*.js\"" + "lint": "eslint \"./**/*.js\"", + "purge-cf-cache": "./bin/purgeCfCache.sh" }, "devDependencies": { "@monkeytype/eslint-config": "workspace:*", @@ -13,7 +14,8 @@ "nodemon": "3.1.4" }, "bin": { - "monkeytype-release": "./src/index.js" + "monkeytype-release": "./src/index.js", + "monkeytype-purge": "./bin/purgeCfCache.sh" }, "dependencies": { "@octokit/rest": "20.1.1", From e1a2020b01ea82b008963fa39f4f3ccc97cb36d6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 17:40:28 +0200 Subject: [PATCH 08/35] fix(quote): user reports --- frontend/static/quotes/code_csharp.json | 2 +- frontend/static/quotes/english.json | 18 +++--------------- frontend/static/quotes/indonesian.json | 6 ------ frontend/static/quotes/russian.json | 2 +- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/frontend/static/quotes/code_csharp.json b/frontend/static/quotes/code_csharp.json index 61f2a5a87a11..2e0f0f8a3df4 100644 --- a/frontend/static/quotes/code_csharp.json +++ b/frontend/static/quotes/code_csharp.json @@ -14,7 +14,7 @@ "length": 63 }, { - "text": "static int factorial(int n)\n{\n\tint result = 1;\n\tfor (int i = 1; i <= n; i++)\n\t{\n\t\tresult = result * i;\n\t}\n\treturn result;\n}", + "text": "static int Factorial(int n)\n{\n\tint result = 1;\n\tfor (int i = 1; i <= n; i++)\n\t{\n\t\tresult = result * i;\n\t}\n\treturn result;\n}", "id": 2, "source": "Geeks for Geeks - C# Methods", "length": 123 diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index b9c3175c6bc2..9360c4287ceb 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -8060,10 +8060,10 @@ "length": 489 }, { - "text": "Let's go in the garden. You'll see something waiting, right there where you left it, lying upside down. When you finally find it you'll see how it's faded. The underside is lighter when you turn it around.", + "text": "Let's go in the garden. You'll find something waiting, right there where you left it, lying upside down. When you finally find it you'll see how it's faded. The underside is lighter when you turn it around.", "source": "Everything Stays", "id": 1414, - "length": 205 + "length": 206 }, { "text": "When you lose control, you'll reap the harvest you have sown. And as the fear grows, the bad blood slows and turns to stone. And it's too late to lose the weight you used to need to throw around. So have a good drown, as you go down, all alone... Dragged down by the stone.", @@ -29734,12 +29734,6 @@ "length": 318, "id": 5358 }, - { - "text": "The whole world's gone mad. They claim to be my dad's best friend, and want to help me with my revenge. Bizarre, isn't it? I'm handed everything, even the names of the men who stole the people I loved...", - "source": "91 days", - "length": 203, - "id": 5359 - }, { "text": "It is nice to be important, but it's more important to be nice.", "source": "Roger Federer", @@ -31469,12 +31463,6 @@ "length": 256, "id": 5720 }, - { - "text": "When I was younger, I left a trail of broken hearts like a rockstar. I'm not proud of it.", - "source": "Modern Family", - "length": 89, - "id": 5722 - }, { "text": "Mistakes are always forgivable, if one has the courage to admit them.", "source": "Bruce Lee", @@ -31674,7 +31662,7 @@ "id": 5760 }, { - "text": "My worst breakup was with Stacy. It was a Sunday morning, we were reading the paper, and I said, \"Oh my God, I think the Eagles could clinch the NFC East!\" and she said, \"We're done\".", + "text": "My worst breakup was with Stacy. It was a Sunday morning, we were reading the paper, and I said, \"Oh my God, I think the Eagles could clinch the NFC East!\" and she said, \"We're done.\"", "source": "The Office", "length": 183, "id": 5761 diff --git a/frontend/static/quotes/indonesian.json b/frontend/static/quotes/indonesian.json index 248a0d63fef4..e3c23bcaec9b 100644 --- a/frontend/static/quotes/indonesian.json +++ b/frontend/static/quotes/indonesian.json @@ -307,12 +307,6 @@ "length": 118, "id": 53 }, - { - "text": "Jatuh hati tidak pernah bisa memilih. Tuhan memilihkan. Kita hanyalah korban. Kecewa adalah konsekuensi, bahagia adalah bonus.", - "source": "Fiersa Besari", - "length": 126, - "id": 54 - }, { "text": "Jadilah kamu manusia yang pada kelahiranmu semua orang tertawa bahagia, tetapi hanya kamu sendiri yang menangis dan pada kematianmu semua orang menangis sedih, tetapi hanya kamu sendiri yang tersenyum.", "source": "Mahatma Gandhi", diff --git a/frontend/static/quotes/russian.json b/frontend/static/quotes/russian.json index 618498d56eaf..ac9c47619abe 100644 --- a/frontend/static/quotes/russian.json +++ b/frontend/static/quotes/russian.json @@ -2308,7 +2308,7 @@ { "id": 403, "source": "Лосев Лев - Иосиф Бродский", - "text": "Создание стихотворения - всегда катартический опыт, его хочется продлить. Неопубликованные, стихи словно бы не окончены, а публикация - расставание навсегда.", + "text": "Создание стихотворения - всегда катартический опыт, его хочется продлить. Неопубликованные стихи словно бы не окончены, а публикация - расставание навсегда.", "length": 157 }, { From c33a087161203162af615fd9dba00a71da2a0b71 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 17:41:14 +0200 Subject: [PATCH 09/35] chore: quote lengths script --- frontend/static/quotes/russian.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/static/quotes/russian.json b/frontend/static/quotes/russian.json index ac9c47619abe..71da114a37b7 100644 --- a/frontend/static/quotes/russian.json +++ b/frontend/static/quotes/russian.json @@ -2309,7 +2309,7 @@ "id": 403, "source": "Лосев Лев - Иосиф Бродский", "text": "Создание стихотворения - всегда катартический опыт, его хочется продлить. Неопубликованные стихи словно бы не окончены, а публикация - расставание навсегда.", - "length": 157 + "length": 156 }, { "id": 404, From 2d24fc323e94aae83ae4897938f9d8c0f8b8542b Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 8 Aug 2024 18:12:50 +0200 Subject: [PATCH 10/35] fix: remove tailing slashes from new endpoint urls (@fehmer) (#5745) !nuf --- frontend/src/ts/ape/adapters/ts-rest-adapter.ts | 16 +++++----------- packages/contracts/src/admin.ts | 2 +- packages/contracts/src/ape-keys.ts | 4 ++-- packages/contracts/src/configs.ts | 6 +++--- packages/contracts/src/psas.ts | 2 +- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts index ded38548e18b..835f812aec8b 100644 --- a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts +++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts @@ -3,7 +3,6 @@ import { Method } from "axios"; import { getIdToken } from "firebase/auth"; import { envConfig } from "../../constants/env-config"; import { getAuthenticatedUser, isAuthenticated } from "../../firebase"; -import { EndpointMetadata } from "@monkeytype/contracts/schemas/api"; function timeoutSignal(ms: number): AbortSignal { const ctrl = new AbortController(); @@ -17,22 +16,17 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{ headers: Headers; }> { return async (request: ApiFetcherArgs) => { - const isPublicEndpoint = - (request.route.metadata as EndpointMetadata | undefined) - ?.authenticationOptions?.isPublic ?? false; - try { const headers: HeadersInit = { ...request.headers, "X-Client-Version": envConfig.clientVersion, }; - if (!isPublicEndpoint) { - const token = isAuthenticated() - ? await getIdToken(getAuthenticatedUser()) - : ""; - headers["Authorization"] = `Bearer ${token}`; - } + const token = isAuthenticated() + ? await getIdToken(getAuthenticatedUser()) + : ""; + + headers["Authorization"] = `Bearer ${token}`; const fetchOptions: RequestInit = { method: request.method as Method, diff --git a/packages/contracts/src/admin.ts b/packages/contracts/src/admin.ts index 0d68e7e41d9c..76595e930b66 100644 --- a/packages/contracts/src/admin.ts +++ b/packages/contracts/src/admin.ts @@ -58,7 +58,7 @@ export const adminContract = c.router( summary: "test permission", description: "Check for admin permission for the current user", method: "GET", - path: "/", + path: "", responses: { 200: MonkeyResponseSchema, }, diff --git a/packages/contracts/src/ape-keys.ts b/packages/contracts/src/ape-keys.ts index c5657d32cfba..4b6801646501 100644 --- a/packages/contracts/src/ape-keys.ts +++ b/packages/contracts/src/ape-keys.ts @@ -46,7 +46,7 @@ export const apeKeysContract = c.router( summary: "get ape keys", description: "Get ape keys of the current user.", method: "GET", - path: "/", + path: "", responses: { 200: GetApeKeyResponseSchema, }, @@ -55,7 +55,7 @@ export const apeKeysContract = c.router( summary: "add ape key", description: "Add an ape key for the current user.", method: "POST", - path: "/", + path: "", body: AddApeKeyRequestSchema.strict(), responses: { 200: AddApeKeyResponseSchema, diff --git a/packages/contracts/src/configs.ts b/packages/contracts/src/configs.ts index 8790de4df88e..115a27d5d457 100644 --- a/packages/contracts/src/configs.ts +++ b/packages/contracts/src/configs.ts @@ -22,14 +22,14 @@ export const configsContract = c.router( summary: "get config", description: "Get config of the current user.", method: "GET", - path: "/", + path: "", responses: { 200: GetConfigResponseSchema, }, }, save: { method: "PATCH", - path: "/", + path: "", body: PartialConfigSchema.strict(), responses: { 200: MonkeyResponseSchema, @@ -40,7 +40,7 @@ export const configsContract = c.router( }, delete: { method: "DELETE", - path: "/", + path: "", body: c.noBody(), responses: { 200: MonkeyResponseSchema, diff --git a/packages/contracts/src/psas.ts b/packages/contracts/src/psas.ts index 25e2f32265ae..a3783573210f 100644 --- a/packages/contracts/src/psas.ts +++ b/packages/contracts/src/psas.ts @@ -17,7 +17,7 @@ export const psasContract = c.router( summary: "get psas", description: "Get list of public service announcements", method: "GET", - path: "/", + path: "", responses: { 200: GetPsaResponseSchema, }, From 0a0f17529193814fcb64d55e0ad214ae35288c93 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 18:16:28 +0200 Subject: [PATCH 11/35] chore: export deploy backend script --- package.json | 3 ++- packages/release/package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 39cf10ca5126..a1fcb9a6f9c2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "release-be": "monkeytype-release --be", "release-no-deploy": "monkeytype-release --no-deploy", "release-dry": "monkeytype-release --dry", - "hotfix": "npm run build-fe && cd frontend && npm run deploy-live && monkeytype-purge", + "hotfix-fe": "npm run build-fe && cd frontend && npm run deploy-live && monkeytype-purge", + "hotfix-be": "monkeytype-deploy-be", "pretty-check": "prettier --check .", "pretty-check-be": "prettier --check ./backend", "pretty-check-fe": "prettier --check ./frontend/src", diff --git a/packages/release/package.json b/packages/release/package.json index 74b35d9f50de..04cf07237665 100644 --- a/packages/release/package.json +++ b/packages/release/package.json @@ -15,7 +15,8 @@ }, "bin": { "monkeytype-release": "./src/index.js", - "monkeytype-purge": "./bin/purgeCfCache.sh" + "monkeytype-purge": "./bin/purgeCfCache.sh", + "monkeytype-deploy-be": "./bin/deployBackend.sh" }, "dependencies": { "@octokit/rest": "20.1.1", From 45d27b5e00508d23bb702f4190a5c196b1b5829a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 8 Aug 2024 18:27:00 +0200 Subject: [PATCH 12/35] chore: add test for recordClientVersion (@fehmer) (#5746) --- backend/__tests__/api/controllers/psa.spec.ts | 16 ++++++++++++++++ packages/contracts/src/presets.ts | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/__tests__/api/controllers/psa.spec.ts b/backend/__tests__/api/controllers/psa.spec.ts index edc3631b0491..9dcff487fda4 100644 --- a/backend/__tests__/api/controllers/psa.spec.ts +++ b/backend/__tests__/api/controllers/psa.spec.ts @@ -1,15 +1,18 @@ import request from "supertest"; import app from "../../../src/app"; import * as PsaDal from "../../../src/dal/psa"; +import * as Prometheus from "../../../src/utils/prometheus"; import { ObjectId } from "mongodb"; const mockApp = request(app); describe("Psa Controller", () => { describe("get psa", () => { const getPsaMock = vi.spyOn(PsaDal, "get"); + const recordClientVersionMock = vi.spyOn(Prometheus, "recordClientVersion"); afterEach(() => { getPsaMock.mockReset(); + recordClientVersionMock.mockReset(); }); it("get psas without authorization", async () => { @@ -53,6 +56,8 @@ describe("Psa Controller", () => { }, ], }); + + expect(recordClientVersionMock).toHaveBeenCalledWith("unknown"); }); it("get psas with authorization", async () => { await mockApp @@ -60,5 +65,16 @@ describe("Psa Controller", () => { .set("authorization", `Uid 123456789`) .expect(200); }); + + it("get psas records x-client-version", async () => { + await mockApp.get("/psas").set("x-client-version", "1.0").expect(200); + + expect(recordClientVersionMock).toHaveBeenCalledWith("1.0"); + }); + it("get psas records client-version", async () => { + await mockApp.get("/psas").set("client-version", "2.0").expect(200); + + expect(recordClientVersionMock).toHaveBeenCalledWith("2.0"); + }); }); }); diff --git a/packages/contracts/src/presets.ts b/packages/contracts/src/presets.ts index c100cf310f36..9c82da01edb8 100644 --- a/packages/contracts/src/presets.ts +++ b/packages/contracts/src/presets.ts @@ -34,7 +34,7 @@ export const presetsContract = c.router( summary: "get presets", description: "Get presets of the current user.", method: "GET", - path: "/", + path: "", responses: { 200: GetPresetResponseSchema, }, @@ -43,7 +43,7 @@ export const presetsContract = c.router( summary: "add preset", description: "Add a new preset for the current user.", method: "POST", - path: "/", + path: "", body: AddPresetRequestSchema.strict(), responses: { 200: AddPresetResponseSchemna, @@ -53,7 +53,7 @@ export const presetsContract = c.router( summary: "update preset", description: "Update an existing preset for the current user.", method: "PATCH", - path: "/", + path: "", body: PresetSchema.strict(), responses: { 200: MonkeyResponseSchema, From 0d7e3cf9164ca8b0858bbf9f6257ad5c283397d1 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 8 Aug 2024 19:00:02 +0200 Subject: [PATCH 13/35] fix: don't send authentication if user is not authenticated (@fehmer (#5747) --- frontend/src/ts/ape/adapters/ts-rest-adapter.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts index 835f812aec8b..47904cf51085 100644 --- a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts +++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts @@ -22,11 +22,10 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{ "X-Client-Version": envConfig.clientVersion, }; - const token = isAuthenticated() - ? await getIdToken(getAuthenticatedUser()) - : ""; - - headers["Authorization"] = `Bearer ${token}`; + if (isAuthenticated()) { + const token = await getIdToken(getAuthenticatedUser()); + headers["Authorization"] = `Bearer ${token}`; + } const fetchOptions: RequestInit = { method: request.method as Method, From 690dee12578a49b6a263a67d06879c9c19cdb0f6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 8 Aug 2024 21:43:55 +0200 Subject: [PATCH 14/35] chore: remove word from profanities list --- backend/src/constants/profanities.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/constants/profanities.ts b/backend/src/constants/profanities.ts index e653327d1ba8..ef7330efb926 100644 --- a/backend/src/constants/profanities.ts +++ b/backend/src/constants/profanities.ts @@ -312,7 +312,6 @@ export const profanities = [ "feces", "felcher", "ficken", - "fitt", "flikker", "foreskin", "fotze", From 90e3ad4957cb183914087a033bd78d698f2b2a01 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 10:45:57 +0200 Subject: [PATCH 15/35] refactor: move psa check to after auth was initialised --- frontend/src/ts/controllers/account-controller.ts | 2 ++ frontend/src/ts/ready.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index d9a74680bb37..555b99bcd8ee 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -46,6 +46,7 @@ import * as ConnectionState from "../states/connection"; import { navigate } from "./route-controller"; import { getHtmlByUserFlags } from "./user-flag-controller"; import { FirebaseError } from "firebase/app"; +import * as PSA from "../elements/psa"; export const gmailProvider = new GoogleAuthProvider(); export const githubProvider = new GithubAuthProvider(); @@ -226,6 +227,7 @@ async function readyFunction( const hash = window.location.hash; console.debug(`account controller ready`); if (authInitialisedAndConnected) { + void PSA.show(); console.debug(`auth state changed, user ${user ? true : false}`); console.debug(user); if (user) { diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index e373d44dcd74..8af4be4e034e 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -3,7 +3,6 @@ import * as Misc from "./utils/misc"; import * as MonkeyPower from "./elements/monkey-power"; import * as Notifications from "./elements/notifications"; import * as CookiesModal from "./modals/cookies"; -import * as PSA from "./elements/psa"; import * as ConnectionState from "./states/connection"; import * as FunboxList from "./test/funbox/funbox-list"; //@ts-expect-error @@ -46,7 +45,6 @@ $((): void => { .stop(true, true) .animate({ opacity: 1 }, 250); if (ConnectionState.get()) { - void PSA.show(); void ServerConfiguration.sync().then(() => { if (!ServerConfiguration.get()?.users.signUp) { $(".signInOut").addClass("hidden"); From 6c9148624e4014f4093233df063979fb872d40ad Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 11:55:54 +0200 Subject: [PATCH 16/35] impr: use authentication state instead of the dom !nuf --- frontend/src/ts/commandline/lists/navigation.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/commandline/lists/navigation.ts b/frontend/src/ts/commandline/lists/navigation.ts index 71b2c11239fe..d9d7e1460780 100644 --- a/frontend/src/ts/commandline/lists/navigation.ts +++ b/frontend/src/ts/commandline/lists/navigation.ts @@ -1,4 +1,5 @@ import { navigate } from "../../controllers/route-controller"; +import { isAuthenticated } from "../../firebase"; import { toggleFullscreen } from "../../utils/misc"; const commands: MonkeyTypes.Command[] = [ @@ -45,10 +46,7 @@ const commands: MonkeyTypes.Command[] = [ alias: "navigate go to stats", icon: "fa-user", exec: (): void => { - //todo probably base this on some state instead of the dom - $("header nav .textButton.view-account").hasClass("hidden") - ? navigate("/login") - : navigate("/account"); + isAuthenticated() ? navigate("/account") : navigate("/login"); }, }, { From c50535cd0f2c801550ffeafca5a1de142ec341b1 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 9 Aug 2024 12:39:27 +0200 Subject: [PATCH 17/35] impr: use tsrest for public endpoints (@fehmer) (#5716) !nuf --- .../__tests__/api/controllers/public.spec.ts | 144 ++++++++++++++++++ backend/scripts/openapi.ts | 5 + backend/src/api/controllers/public.ts | 29 ++-- backend/src/api/routes/index.ts | 29 +++- backend/src/api/routes/public.ts | 54 ++----- backend/src/dal/public.ts | 9 +- backend/src/utils/pb.ts | 5 +- backend/src/utils/prometheus.ts | 2 +- frontend/src/ts/ape/endpoints/index.ts | 2 - frontend/src/ts/ape/endpoints/public.ts | 27 ---- frontend/src/ts/ape/index.ts | 3 +- frontend/src/ts/modals/pb-tables.ts | 2 +- frontend/src/ts/modals/share-test-settings.ts | 2 +- frontend/src/ts/pages/about.ts | 27 ++-- frontend/src/ts/test/pace-caret.ts | 5 +- packages/contracts/src/index.ts | 2 + packages/contracts/src/public.ts | 70 +++++++++ packages/contracts/src/schemas/api.ts | 8 +- packages/contracts/src/schemas/public.ts | 15 ++ packages/contracts/src/schemas/shared.ts | 14 +- packages/contracts/src/schemas/util.ts | 16 +- packages/shared-types/src/index.ts | 11 -- 22 files changed, 349 insertions(+), 132 deletions(-) create mode 100644 backend/__tests__/api/controllers/public.spec.ts delete mode 100644 frontend/src/ts/ape/endpoints/public.ts create mode 100644 packages/contracts/src/public.ts create mode 100644 packages/contracts/src/schemas/public.ts diff --git a/backend/__tests__/api/controllers/public.spec.ts b/backend/__tests__/api/controllers/public.spec.ts new file mode 100644 index 000000000000..accc90a9acb1 --- /dev/null +++ b/backend/__tests__/api/controllers/public.spec.ts @@ -0,0 +1,144 @@ +import request from "supertest"; +import app from "../../../src/app"; +import * as PublicDal from "../../../src/dal/public"; +const mockApp = request(app); + +describe("PublicController", () => { + describe("get speed histogram", () => { + const getSpeedHistogramMock = vi.spyOn(PublicDal, "getSpeedHistogram"); + + afterEach(() => { + getSpeedHistogramMock.mockReset(); + }); + + it("gets for english time 60", async () => { + //GIVEN + getSpeedHistogramMock.mockResolvedValue({ "0": 1, "10": 2 }); + + //WHEN + const { body } = await mockApp + .get("/public/speedHistogram") + .query({ language: "english", mode: "time", mode2: "60" }); + //.expect(200); + console.log(body); + + //THEN + expect(body).toEqual({ + message: "Public speed histogram retrieved", + data: { "0": 1, "10": 2 }, + }); + + expect(getSpeedHistogramMock).toHaveBeenCalledWith( + "english", + "time", + "60" + ); + }); + + it("gets for mode", async () => { + for (const mode of ["time", "words", "quote", "zen", "custom"]) { + const response = await mockApp + .get("/public/speedHistogram") + .query({ language: "english", mode, mode2: "custom" }); + expect(response.status, "for mode " + mode).toEqual(200); + } + }); + + it("gets for mode2", async () => { + for (const mode2 of [ + "10", + "25", + "50", + "100", + "15", + "30", + "60", + "120", + "zen", + "custom", + ]) { + const response = await mockApp + .get("/public/speedHistogram") + .query({ language: "english", mode: "words", mode2 }); + + expect(response.status, "for mode2 " + mode2).toEqual(200); + } + }); + it("fails for missing query", async () => { + const { body } = await mockApp.get("/public/speedHistogram").expect(422); + + expect(body).toEqual({ + message: "Invalid query schema", + validationErrors: [ + '"language" Required', + '"mode" Required', + '"mode2" Needs to be either a number, "zen" or "custom."', + ], + }); + }); + it("fails for invalid query", async () => { + const { body } = await mockApp + .get("/public/speedHistogram") + .query({ + language: "en?gli.sh", + mode: "unknownMode", + mode2: "unknownMode2", + }) + .expect(422); + + expect(body).toEqual({ + message: "Invalid query schema", + validationErrors: [ + '"language" Invalid', + `"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`, + '"mode2" Needs to be either a number, "zen" or "custom."', + ], + }); + }); + it("fails for unknown query", async () => { + const { body } = await mockApp + .get("/public/speedHistogram") + .query({ + language: "english", + mode: "time", + mode2: "60", + extra: "value", + }) + .expect(422); + + expect(body).toEqual({ + message: "Invalid query schema", + validationErrors: ["Unrecognized key(s) in object: 'extra'"], + }); + }); + }); + describe("get typing stats", () => { + const getTypingStatsMock = vi.spyOn(PublicDal, "getTypingStats"); + + afterEach(() => { + getTypingStatsMock.mockReset(); + }); + + it("gets without authentication", async () => { + //GIVEN + getTypingStatsMock.mockResolvedValue({ + testsCompleted: 23, + testsStarted: 42, + timeTyping: 1000, + } as any); + + //WHEN + const { body } = await mockApp.get("/public/typingStats").expect(200); + + //THEN + expect(body).toEqual({ + message: "Public typing stats retrieved", + data: { + testsCompleted: 23, + testsStarted: 42, + timeTyping: 1000, + }, + }); + }); + }); +}); diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts index 4b444caa19ca..b5140de6c76c 100644 --- a/backend/scripts/openapi.ts +++ b/backend/scripts/openapi.ts @@ -66,6 +66,11 @@ export function getOpenApi(): OpenAPIObject { description: "Ape keys provide access to certain API endpoints.", "x-displayName": "Ape Keys", }, + { + name: "public", + description: "Public endpoints such as typing stats.", + "x-displayName": "public", + }, { name: "psas", description: "Public service announcements.", diff --git a/backend/src/api/controllers/public.ts b/backend/src/api/controllers/public.ts index f49dca0310ff..505f5caa9bba 100644 --- a/backend/src/api/controllers/public.ts +++ b/backend/src/api/controllers/public.ts @@ -1,21 +1,22 @@ +import { + GetSpeedHistogramQuery, + GetSpeedHistogramResponse, + GetTypingStatsResponse, +} from "@monkeytype/contracts/public"; import * as PublicDAL from "../../dal/public"; -import { MonkeyResponse } from "../../utils/monkey-response"; +import { MonkeyResponse2 } from "../../utils/monkey-response"; -export async function getPublicSpeedHistogram( - req: MonkeyTypes.Request -): Promise { +export async function getSpeedHistogram( + req: MonkeyTypes.Request2 +): Promise { const { language, mode, mode2 } = req.query; - const data = await PublicDAL.getSpeedHistogram( - language as string, - mode as string, - mode2 as string - ); - return new MonkeyResponse("Public speed histogram retrieved", data); + const data = await PublicDAL.getSpeedHistogram(language, mode, mode2); + return new MonkeyResponse2("Public speed histogram retrieved", data); } -export async function getPublicTypingStats( - _req: MonkeyTypes.Request -): Promise { +export async function getTypingStats( + _req: MonkeyTypes.Request2 +): Promise { const data = await PublicDAL.getTypingStats(); - return new MonkeyResponse("Public typing stats retrieved", data); + return new MonkeyResponse2("Public typing stats retrieved", data); } diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index c64ee8cfd41e..453a4c12801c 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -43,7 +43,6 @@ const APP_START_TIME = Date.now(); const API_ROUTE_MAP = { "/users": users, "/results": results, - "/public": publicStats, "/leaderboards": leaderboards, "/quotes": quotes, "/webhooks": webhooks, @@ -57,6 +56,7 @@ const router = s.router(contract, { configs, presets, psas, + public: publicStats, }); export function addApiRoutes(app: Application): void { @@ -78,16 +78,29 @@ export function addApiRoutes(app: Application): void { function applyTsRestApiRoutes(app: IRouter): void { createExpressEndpoints(contract, router, app, { jsonQuery: true, - requestValidationErrorHandler(err, req, res, next) { - if (err.body?.issues === undefined) { + requestValidationErrorHandler(err, _req, res, next) { + let message: string | undefined = undefined; + let validationErrors: string[] | undefined = undefined; + + if (err.pathParams?.issues !== undefined) { + message = "Invalid path parameter schema"; + validationErrors = err.pathParams.issues.map(prettyErrorMessage); + } else if (err.query?.issues !== undefined) { + message = "Invalid query schema"; + validationErrors = err.query.issues.map(prettyErrorMessage); + } else if (err.body?.issues !== undefined) { + message = "Invalid request data schema"; + validationErrors = err.body.issues.map(prettyErrorMessage); + } + + if (message !== undefined) { + res + .status(422) + .json({ message, validationErrors } as MonkeyValidationError); + } else { next(); return; } - const issues = err.body?.issues.map(prettyErrorMessage); - res.status(422).json({ - message: "Invalid request data schema", - validationErrors: issues, - } as MonkeyValidationError); }, globalMiddleware: [authenticateTsRestRequest()], }); diff --git a/backend/src/api/routes/public.ts b/backend/src/api/routes/public.ts index 86491c6b346a..b5e310fb72e4 100644 --- a/backend/src/api/routes/public.ts +++ b/backend/src/api/routes/public.ts @@ -1,41 +1,17 @@ -import { Router } from "express"; -import * as PublicController from "../controllers/public"; +import { publicContract } from "@monkeytype/contracts/public"; +import { initServer } from "@ts-rest/express"; import * as RateLimit from "../../middlewares/rate-limit"; -import { asyncHandler } from "../../middlewares/utility"; -import joi from "joi"; -import { validateRequest } from "../../middlewares/validation"; - -const GET_MODE_STATS_VALIDATION_SCHEMA = { - language: joi - .string() - .max(50) - .pattern(/^[a-zA-Z0-9_+]+$/) - .required(), - mode: joi - .string() - .valid("time", "words", "quote", "zen", "custom") - .required(), - mode2: joi - .string() - .regex(/^(\d)+|custom|zen/) - .required(), -}; - -const router = Router(); - -router.get( - "/speedHistogram", - RateLimit.publicStatsGet, - validateRequest({ - query: GET_MODE_STATS_VALIDATION_SCHEMA, - }), - asyncHandler(PublicController.getPublicSpeedHistogram) -); - -router.get( - "/typingStats", - RateLimit.publicStatsGet, - asyncHandler(PublicController.getPublicTypingStats) -); +import * as PublicController from "../controllers/public"; +import { callController } from "../ts-rest-adapter"; -export default router; +const s = initServer(); +export default s.router(publicContract, { + getSpeedHistogram: { + middleware: [RateLimit.publicStatsGet], + handler: async (r) => callController(PublicController.getSpeedHistogram)(r), + }, + getTypingStats: { + middleware: [RateLimit.publicStatsGet], + handler: async (r) => callController(PublicController.getTypingStats)(r), + }, +}); diff --git a/backend/src/dal/public.ts b/backend/src/dal/public.ts index 08b72edd0e4a..50a230a7867c 100644 --- a/backend/src/dal/public.ts +++ b/backend/src/dal/public.ts @@ -1,10 +1,13 @@ import * as db from "../init/db"; import { roundTo2 } from "../utils/misc"; import MonkeyError from "../utils/error"; -import { PublicTypingStats, SpeedHistogram } from "@monkeytype/shared-types"; +import { + TypingStats, + SpeedHistogram, +} from "@monkeytype/contracts/schemas/public"; -type PublicTypingStatsDB = PublicTypingStats & { _id: "stats" }; -type PublicSpeedStatsDB = { +export type PublicTypingStatsDB = TypingStats & { _id: "stats" }; +export type PublicSpeedStatsDB = { _id: "speedStatsHistogram"; english_time_15: SpeedHistogram; english_time_60: SpeedHistogram; diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index 76041605d017..b00d747a5765 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -3,7 +3,6 @@ import FunboxList from "../constants/funbox-list"; import { DBResult } from "@monkeytype/shared-types"; import { Mode, - Mode2, PersonalBest, PersonalBests, } from "@monkeytype/contracts/schemas/shared"; @@ -39,7 +38,7 @@ export function checkAndUpdatePb( result: Result ): CheckAndUpdatePbResult { const mode = result.mode; - const mode2 = result.mode2 as Mode2<"time">; + const mode2 = result.mode2; const userPb = userPersonalBests ?? {}; userPb[mode] ??= {}; @@ -175,7 +174,7 @@ function updateLeaderboardPersonalBests( } const mode = result.mode; - const mode2 = result.mode2 as Mode2<"time">; + const mode2 = result.mode2; lbPersonalBests[mode] = lbPersonalBests[mode] ?? {}; const lbMode2 = lbPersonalBests[mode][mode2] as MonkeyTypes.LbPersonalBests; diff --git a/backend/src/utils/prometheus.ts b/backend/src/utils/prometheus.ts index f98fdc90c904..040e1492ed42 100644 --- a/backend/src/utils/prometheus.ts +++ b/backend/src/utils/prometheus.ts @@ -105,7 +105,7 @@ export function incrementResult(res: Result): void { punctuation, } = res; - let m2 = mode2 as string; + let m2 = mode2; if (mode === "time" && !["15", "30", "60", "120"].includes(mode2)) { m2 = "custom"; } diff --git a/frontend/src/ts/ape/endpoints/index.ts b/frontend/src/ts/ape/endpoints/index.ts index 2dae7057b297..609c791b99ad 100644 --- a/frontend/src/ts/ape/endpoints/index.ts +++ b/frontend/src/ts/ape/endpoints/index.ts @@ -2,13 +2,11 @@ import Leaderboards from "./leaderboards"; import Quotes from "./quotes"; import Results from "./results"; import Users from "./users"; -import Public from "./public"; import Configuration from "./configuration"; import Dev from "./dev"; export default { Leaderboards, - Public, Quotes, Results, Users, diff --git a/frontend/src/ts/ape/endpoints/public.ts b/frontend/src/ts/ape/endpoints/public.ts deleted file mode 100644 index 5f4b417d3630..000000000000 --- a/frontend/src/ts/ape/endpoints/public.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PublicTypingStats, SpeedHistogram } from "@monkeytype/shared-types"; - -const BASE_PATH = "/public"; - -type SpeedStatsQuery = { - language: string; - mode: string; - mode2: string; -}; - -export default class Public { - constructor(private httpClient: Ape.HttpClient) { - this.httpClient = httpClient; - } - - async getSpeedHistogram( - searchQuery: SpeedStatsQuery - ): Ape.EndpointResponse { - return await this.httpClient.get(`${BASE_PATH}/speedHistogram`, { - searchQuery, - }); - } - - async getTypingStats(): Ape.EndpointResponse { - return await this.httpClient.get(`${BASE_PATH}/typingStats`); - } -} diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index 6bebc1c3caba..1d14030a0cb7 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -6,6 +6,7 @@ import { configsContract } from "@monkeytype/contracts/configs"; import { presetsContract } from "@monkeytype/contracts/presets"; import { apeKeysContract } from "@monkeytype/contracts/ape-keys"; import { psasContract } from "@monkeytype/contracts/psas"; +import { publicContract } from "@monkeytype/contracts/public"; const API_PATH = ""; const BASE_URL = envConfig.backendUrl; @@ -22,7 +23,7 @@ const Ape = { quotes: new endpoints.Quotes(httpClient), leaderboards: new endpoints.Leaderboards(httpClient), presets: buildClient(presetsContract, BASE_URL, 10_000), - publicStats: new endpoints.Public(httpClient), + publicStats: buildClient(publicContract, BASE_URL, 10_000), apeKeys: buildClient(apeKeysContract, BASE_URL, 10_000), configuration: new endpoints.Configuration(httpClient), dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)), diff --git a/frontend/src/ts/modals/pb-tables.ts b/frontend/src/ts/modals/pb-tables.ts index 736d8766618c..a78a8c9c8573 100644 --- a/frontend/src/ts/modals/pb-tables.ts +++ b/frontend/src/ts/modals/pb-tables.ts @@ -34,7 +34,7 @@ function update(mode: Mode): void { if (allmode2 === undefined) return; const list: PBWithMode2[] = []; - (Object.keys(allmode2) as Mode2[]).forEach(function (key) { + Object.keys(allmode2).forEach(function (key) { let pbs = allmode2[key] ?? []; pbs = pbs.sort(function (a, b) { return b.wpm - a.wpm; diff --git a/frontend/src/ts/modals/share-test-settings.ts b/frontend/src/ts/modals/share-test-settings.ts index 14bed5c51725..ca6285fe4d77 100644 --- a/frontend/src/ts/modals/share-test-settings.ts +++ b/frontend/src/ts/modals/share-test-settings.ts @@ -43,7 +43,7 @@ function updateURL(): void { } if (getCheckboxValue("mode2")) { - settings[1] = getMode2(Config, currentQuote) as Mode2; + settings[1] = getMode2(Config, currentQuote); } if (getCheckboxValue("customText")) { diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index 6e93ebf05f3e..af3328ca7f7a 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -8,7 +8,10 @@ import * as ChartController from "../controllers/chart-controller"; import * as ConnectionState from "../states/connection"; import { intervalToDuration } from "date-fns/intervalToDuration"; import * as Skeleton from "../utils/skeleton"; -import { PublicTypingStats, SpeedHistogram } from "@monkeytype/shared-types"; +import { + TypingStats, + SpeedHistogram, +} from "@monkeytype/contracts/schemas/public"; function reset(): void { $(".pageAbout .contributors").empty(); @@ -19,7 +22,7 @@ function reset(): void { } let speedHistogramResponseData: SpeedHistogram | null; -let typingStatsResponseData: PublicTypingStats | null; +let typingStatsResponseData: TypingStats | null; function updateStatsAndHistogram(): void { if (speedHistogramResponseData) { @@ -98,24 +101,26 @@ async function getStatsAndHistogramData(): Promise { } const speedStats = await Ape.publicStats.getSpeedHistogram({ - language: "english", - mode: "time", - mode2: "60", + query: { + language: "english", + mode: "time", + mode2: "60", + }, }); - if (speedStats.status >= 200 && speedStats.status < 300) { - speedHistogramResponseData = speedStats.data; + if (speedStats.status === 200) { + speedHistogramResponseData = speedStats.body.data; } else { Notifications.add( - `Failed to get global speed stats for histogram: ${speedStats.message}`, + `Failed to get global speed stats for histogram: ${speedStats.body.message}`, -1 ); } const typingStats = await Ape.publicStats.getTypingStats(); - if (typingStats.status >= 200 && typingStats.status < 300) { - typingStatsResponseData = typingStats.data; + if (typingStats.status === 200) { + typingStatsResponseData = typingStats.body.data; } else { Notifications.add( - `Failed to get global typing stats: ${speedStats.message}`, + `Failed to get global typing stats: ${speedStats.body.message}`, -1 ); } diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 29a6fab9791b..76533d8dd25f 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -8,7 +8,6 @@ import * as Numbers from "../utils/numbers"; import * as JSONData from "../utils/json-data"; import * as TestState from "./test-state"; import * as ConfigEvent from "../observables/config-event"; -import { Mode2 } from "@monkeytype/contracts/schemas/shared"; type Settings = { wpm: number; @@ -67,9 +66,7 @@ async function resetCaretPosition(): Promise { export async function init(): Promise { $("#paceCaret").addClass("hidden"); - const mode2 = Misc.getMode2(Config, TestWords.currentQuote) as Mode2< - typeof Config.mode - >; + const mode2 = Misc.getMode2(Config, TestWords.currentQuote); let wpm; if (Config.paceCaret === "pb") { wpm = await DB.getLocalPB( diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index e7f3836394a2..1224a0e19041 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -4,6 +4,7 @@ import { apeKeysContract } from "./ape-keys"; import { configsContract } from "./configs"; import { presetsContract } from "./presets"; import { psasContract } from "./psas"; +import { publicContract } from "./public"; const c = initContract(); @@ -13,4 +14,5 @@ export const contract = c.router({ configs: configsContract, presets: presetsContract, psas: psasContract, + public: publicContract, }); diff --git a/packages/contracts/src/public.ts b/packages/contracts/src/public.ts new file mode 100644 index 000000000000..93b2b81f97de --- /dev/null +++ b/packages/contracts/src/public.ts @@ -0,0 +1,70 @@ +import { initContract } from "@ts-rest/core"; +import { z } from "zod"; +import { + CommonResponses, + EndpointMetadata, + responseWithData, +} from "./schemas/api"; +import { SpeedHistogramSchema, TypingStatsSchema } from "./schemas/public"; +import { Mode2Schema, ModeSchema } from "./schemas/shared"; +import { LanguageSchema } from "./schemas/util"; + +export const GetSpeedHistogramQuerySchema = z + .object({ + language: LanguageSchema, + mode: ModeSchema, + mode2: Mode2Schema, + }) + .strict(); +export type GetSpeedHistogramQuery = z.infer< + typeof GetSpeedHistogramQuerySchema +>; + +export const GetSpeedHistogramResponseSchema = + responseWithData(SpeedHistogramSchema); +export type GetSpeedHistogramResponse = z.infer< + typeof GetSpeedHistogramResponseSchema +>; + +export const GetTypingStatsResponseSchema = responseWithData(TypingStatsSchema); +export type GetTypingStatsResponse = z.infer< + typeof GetTypingStatsResponseSchema +>; + +const c = initContract(); +export const publicContract = c.router( + { + getSpeedHistogram: { + summary: "get speed histogram", + description: + "get number of users personal bests grouped by wpm level (multiples of ten)", + method: "GET", + path: "/speedHistogram", + query: GetSpeedHistogramQuerySchema, + responses: { + 200: GetSpeedHistogramResponseSchema, + }, + }, + + getTypingStats: { + summary: "get typing stats", + description: "get number of tests and time users spend typing.", + method: "GET", + path: "/typingStats", + responses: { + 200: GetTypingStatsResponseSchema, + }, + }, + }, + { + pathPrefix: "/public", + strictStatusCodes: true, + metadata: { + openApiTags: "public", + authenticationOptions: { + isPublic: true, + }, + } as EndpointMetadata, + commonResponses: CommonResponses, + } +); diff --git a/packages/contracts/src/schemas/api.ts b/packages/contracts/src/schemas/api.ts index f90e47dbd91d..305d6100aa8c 100644 --- a/packages/contracts/src/schemas/api.ts +++ b/packages/contracts/src/schemas/api.ts @@ -1,6 +1,12 @@ import { z, ZodSchema } from "zod"; -export type OpenApiTag = "configs" | "presets" | "ape-keys" | "admin" | "psas"; +export type OpenApiTag = + | "configs" + | "presets" + | "ape-keys" + | "admin" + | "psas" + | "public"; export type EndpointMetadata = { /** Authentication options, by default a bearer token is required. */ diff --git a/packages/contracts/src/schemas/public.ts b/packages/contracts/src/schemas/public.ts new file mode 100644 index 000000000000..5a89122eb1f7 --- /dev/null +++ b/packages/contracts/src/schemas/public.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { StringNumberSchema } from "./util"; + +export const SpeedHistogramSchema = z.record( + StringNumberSchema, + z.number().int() +); +export type SpeedHistogram = z.infer; + +export const TypingStatsSchema = z.object({ + timeTyping: z.number().nonnegative(), + testsCompleted: z.number().int().nonnegative(), + testsStarted: z.number().int().nonnegative(), +}); +export type TypingStats = z.infer; diff --git a/packages/contracts/src/schemas/shared.ts b/packages/contracts/src/schemas/shared.ts index 0826839a8256..5eb97b5d4fee 100644 --- a/packages/contracts/src/schemas/shared.ts +++ b/packages/contracts/src/schemas/shared.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { literal, z } from "zod"; import { StringNumberSchema } from "./util"; //used by config and shared @@ -33,9 +33,19 @@ export const PersonalBestsSchema = z.object({ }); export type PersonalBests = z.infer; -//used by user and config +//used by user, config, public export const ModeSchema = PersonalBestsSchema.keyof(); export type Mode = z.infer; + +export const Mode2Schema = z.union( + [StringNumberSchema, literal("zen"), literal("custom")], + { + errorMap: () => ({ + message: 'Needs to be either a number, "zen" or "custom."', + }), + } +); + export type Mode2 = M extends M ? keyof PersonalBests[M] : never; diff --git a/packages/contracts/src/schemas/util.ts b/packages/contracts/src/schemas/util.ts index 74e5ed963aef..25d9a4b77b2e 100644 --- a/packages/contracts/src/schemas/util.ts +++ b/packages/contracts/src/schemas/util.ts @@ -1,8 +1,12 @@ import { z, ZodString } from "zod"; -export const StringNumberSchema = z.custom<`${number}`>((val) => { - return typeof val === "string" ? /^\d+$/.test(val) : false; -}); +export const StringNumberSchema = z + + .custom<`${number}`>((val) => { + if (typeof val === "number") val = val.toString(); + return typeof val === "string" ? /^\d+$/.test(val) : false; + }, 'Needs to be a number or a number represented as a string e.g. "10".') + .transform(String); export type StringNumber = z.infer; @@ -13,3 +17,9 @@ export type Id = z.infer; export const TagSchema = token().max(50); export type Tag = z.infer; + +export const LanguageSchema = z + .string() + .max(50) + .regex(/^[a-zA-Z0-9_+]+$/); +export type Language = z.infer; diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index fe8d50ea60d8..d069ecd3ca02 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -301,17 +301,6 @@ export type ResultFilters = { } & Record; }; -export type SpeedHistogram = { - [key: string]: number; -}; - -export type PublicTypingStats = { - type: string; - timeTyping: number; - testsCompleted: number; - testsStarted: number; -}; - export type LeaderboardEntry = { _id: string; wpm: number; From f32846de32802d13e998afed038896eb2154623f Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 12:24:21 +0200 Subject: [PATCH 18/35] refactor: getLocalPb returns pb object instead of just wpm also use this function in test logic also fixes todo --- frontend/src/ts/db.ts | 29 +++++++++++++--------------- frontend/src/ts/test/pace-caret.ts | 25 +++++++++++++----------- frontend/src/ts/test/result.ts | 19 +++++++++++------- frontend/src/ts/test/test-logic.ts | 31 +++++++++++++++++++----------- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 39356c8f052f..10c8fcb5955a 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -623,29 +623,26 @@ export async function getLocalPB( difficulty: Difficulty, lazyMode: boolean, funbox: string -): Promise { +): Promise { const funboxes = (await getFunboxList()).filter((fb) => { return funbox?.split("#").includes(fb.name); }); if (!funboxes.every((f) => f.canGetPb)) { - return 0; + return undefined; } - if (dbSnapshot === null || dbSnapshot?.personalBests === null) return 0; - - const bestsByMode = dbSnapshot?.personalBests[mode][mode2] as PersonalBest[]; - - if (bestsByMode === undefined) return 0; - return ( - bestsByMode.find( - (pb) => - (pb.punctuation ?? false) === punctuation && - (pb.numbers ?? false) === numbers && - pb.difficulty === difficulty && - pb.language === language && - (pb.lazyMode ?? false) === lazyMode - )?.wpm ?? 0 + const pbs = dbSnapshot?.personalBests?.[mode]?.[mode2] as + | PersonalBest[] + | undefined; + + return pbs?.find( + (pb) => + (pb.punctuation ?? false) === punctuation && + (pb.numbers ?? false) === numbers && + pb.difficulty === difficulty && + pb.language === language && + (pb.lazyMode ?? false) === lazyMode ); } diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 76533d8dd25f..2ff540e87a6a 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -67,18 +67,21 @@ async function resetCaretPosition(): Promise { export async function init(): Promise { $("#paceCaret").addClass("hidden"); const mode2 = Misc.getMode2(Config, TestWords.currentQuote); - let wpm; + let wpm = 0; if (Config.paceCaret === "pb") { - wpm = await DB.getLocalPB( - Config.mode, - mode2, - Config.punctuation, - Config.numbers, - Config.language, - Config.difficulty, - Config.lazyMode, - Config.funbox - ); + wpm = + ( + await DB.getLocalPB( + Config.mode, + mode2, + Config.punctuation, + Config.numbers, + Config.language, + Config.difficulty, + Config.lazyMode, + Config.funbox + ) + )?.wpm ?? 0; } else if (Config.paceCaret === "tagPb") { wpm = await DB.getActiveTagsPB( Config.mode, diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 2193e6850331..c6f6ea649049 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -170,7 +170,7 @@ async function updateGraph(): Promise { export async function updateGraphPBLine(): Promise { const themecolors = await ThemeColors.getAll(); - const lpb = await DB.getLocalPB( + const localPb = await DB.getLocalPB( result.mode, result.mode2, result.punctuation ?? false, @@ -180,9 +180,12 @@ export async function updateGraphPBLine(): Promise { result.lazyMode ?? false, result.funbox ?? "none" ); - if (lpb === 0) return; + const localPbWpm = localPb?.wpm ?? 0; + if (localPbWpm === 0) return; const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); - const chartlpb = Numbers.roundTo2(typingSpeedUnit.fromWpm(lpb)).toFixed(2); + const chartlpb = Numbers.roundTo2( + typingSpeedUnit.fromWpm(localPbWpm) + ).toFixed(2); resultAnnotation.push({ display: true, type: "line", @@ -388,7 +391,7 @@ export async function updateCrown(dontSave: boolean): Promise { const canGetPb = await resultCanGetPb(); if (canGetPb.value) { - const lpb = await DB.getLocalPB( + const localPb = await DB.getLocalPB( Config.mode, result.mode2, Config.punctuation, @@ -398,7 +401,8 @@ export async function updateCrown(dontSave: boolean): Promise { Config.lazyMode, Config.funbox ); - pbDiff = result.wpm - lpb; + const localPbWpm = localPb?.wpm ?? 0; + pbDiff = result.wpm - localPbWpm; if (pbDiff <= 0) { hideCrown(); } else { @@ -409,7 +413,7 @@ export async function updateCrown(dontSave: boolean): Promise { ); } } else { - const lpb = await DB.getLocalPB( + const localPb = await DB.getLocalPB( Config.mode, result.mode2, Config.punctuation, @@ -419,7 +423,8 @@ export async function updateCrown(dontSave: boolean): Promise { Config.lazyMode, "none" ); - pbDiff = result.wpm - lpb; + const localPbWpm = localPb?.wpm ?? 0; + pbDiff = result.wpm - localPbWpm; if (pbDiff <= 0) { // hideCrown(); showCrown("warning"); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 398d02be5b9d..a53a14b335d3 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -64,6 +64,7 @@ import { } from "@monkeytype/shared-types"; import { QuoteLength } from "@monkeytype/contracts/schemas/configs"; import { Mode } from "@monkeytype/contracts/schemas/shared"; + let failReason = ""; const koInputVisual = document.getElementById("koInputVisual") as HTMLElement; @@ -1196,22 +1197,30 @@ async function saveResult( if (response?.data?.isPb !== undefined && response.data.isPb) { //new pb - if ( - //@ts-expect-error TODO fix this - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - DB.getSnapshot()?.personalBests?.[Config.mode]?.[completedEvent.mode2] - ) { + const localPb = await DB.getLocalPB( + completedEvent.mode, + completedEvent.mode2, + completedEvent.punctuation, + completedEvent.numbers, + completedEvent.language, + completedEvent.difficulty, + completedEvent.lazyMode, + completedEvent.funbox + ); + + if (localPb !== undefined) { Result.showConfetti(); } Result.showCrown("normal"); + await DB.saveLocalPB( - Config.mode, + completedEvent.mode, completedEvent.mode2, - Config.punctuation, - Config.numbers, - Config.language, - Config.difficulty, - Config.lazyMode, + completedEvent.punctuation, + completedEvent.numbers, + completedEvent.language, + completedEvent.difficulty, + completedEvent.lazyMode, completedEvent.wpm, completedEvent.acc, completedEvent.rawWpm, From 24f9a6c0799a279d3a6b1c18dc7cabd6cb20be05 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 12:42:02 +0200 Subject: [PATCH 19/35] chore: add ts-check script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a1fcb9a6f9c2..e88a55fd436c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "full-check": "turbo lint ts-check build test validate-json", "prepare": "husky install", "pre-commit": "lint-staged", + "ts-check": "turbo run ts-check", "lint": "turbo run lint", "lint-be": "turbo run lint --filter @monkeytype/backend", "lint-fe": "turbo run lint --filter @monkeytype/frontend", From d27c622943985de4975ff88bd37e4bb676537126 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 12:51:17 +0200 Subject: [PATCH 20/35] chore: move example.env to release package --- example.env => packages/release/example.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example.env => packages/release/example.env (100%) diff --git a/example.env b/packages/release/example.env similarity index 100% rename from example.env rename to packages/release/example.env From 782eea643b5596556aa6224938dc4ded3c2e9ee2 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 9 Aug 2024 13:19:53 +0200 Subject: [PATCH 21/35] chore: remove some unnecessary root dependencies, move to correct packages (@miodec) (#5751) * remove some, move some * version --- package.json | 12 +- packages/eslint-config/package.json | 6 +- pnpm-lock.yaml | 207 +++++----------------------- 3 files changed, 39 insertions(+), 186 deletions(-) diff --git a/package.json b/package.json index e88a55fd436c..9c90297e3ea1 100644 --- a/package.json +++ b/package.json @@ -56,24 +56,16 @@ "node": "20.16.0" }, "devDependencies": { - "@monkeytype/release": "workspace:*", "@commitlint/cli": "17.7.1", "@commitlint/config-conventional": "17.7.0", + "@monkeytype/release": "workspace:*", "conventional-changelog": "4.0.0", - "eslint": "8.57.0", - "eslint-config-prettier": "9.0.0", - "eslint-import-resolver-typescript": "3.6.1", - "eslint-plugin-import": "2.29.0", - "eslint-plugin-json": "2.1.2", - "eslint-plugin-require-path-exists": "1.1.9", "husky": "8.0.1", "knip": "2.19.2", "lint-staged": "13.2.3", "only-allow": "1.2.1", "prettier": "2.5.1", - "turbo": "2.0.9", - "typescript": "5.5.4", - "wait-for-localhost-cli": "3.2.0" + "turbo": "2.0.9" }, "lint-staged": { "*.{json,scss,css,html}": [ diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 52745214fb38..43411d45cf27 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -4,6 +4,10 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "8.0.1", "@typescript-eslint/parser": "8.0.1", - "eslint-config-prettier": "9.1.0" + "eslint-config-prettier": "9.1.0", + "eslint-import-resolver-typescript": "3.6.1", + "eslint-plugin-import": "2.29.0", + "eslint-plugin-json": "3.1.0", + "eslint-plugin-require-path-exists": "1.1.9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e7440d9bf2..b44f21b3d50e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,24 +20,6 @@ importers: conventional-changelog: specifier: 4.0.0 version: 4.0.0 - eslint: - specifier: 8.57.0 - version: 8.57.0 - eslint-config-prettier: - specifier: 9.0.0 - version: 9.0.0(eslint@8.57.0) - eslint-import-resolver-typescript: - specifier: 3.6.1 - version: 3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0) - eslint-plugin-import: - specifier: 2.29.0 - version: 2.29.0(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-json: - specifier: 2.1.2 - version: 2.1.2 - eslint-plugin-require-path-exists: - specifier: 1.1.9 - version: 1.1.9 husky: specifier: 8.0.1 version: 8.0.1 @@ -56,12 +38,6 @@ importers: turbo: specifier: 2.0.9 version: 2.0.9 - typescript: - specifier: 5.5.4 - version: 5.5.4 - wait-for-localhost-cli: - specifier: 3.2.0 - version: 3.2.0 backend: dependencies: @@ -527,6 +503,18 @@ importers: eslint-config-prettier: specifier: 9.1.0 version: 9.1.0(eslint@8.57.0) + eslint-import-resolver-typescript: + specifier: 3.6.1 + version: 3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0) + eslint-plugin-import: + specifier: 2.29.0 + version: 2.29.0(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-json: + specifier: 3.1.0 + version: 3.1.0 + eslint-plugin-require-path-exists: + specifier: 1.1.9 + version: 1.1.9 packages/release: dependencies: @@ -3386,10 +3374,6 @@ packages: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} - camelcase-keys@7.0.2: - resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} - engines: {node: '>=12'} - camelcase@3.0.0: resolution: {integrity: sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==} engines: {node: '>=0.10.0'} @@ -3979,10 +3963,6 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decamelize@5.0.1: - resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} - engines: {node: '>=10'} - decko@1.2.0: resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==} @@ -4421,12 +4401,6 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-config-prettier@9.0.0: - resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - eslint-config-prettier@9.1.0: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true @@ -4474,9 +4448,9 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-json@2.1.2: - resolution: {integrity: sha512-isM/fsUxS4wN1+nLsWoV5T4gLgBQnsql3nMTr8u+cEls1bL8rRQO5CP5GtxJxaOfbcKqnz401styw+H/P+e78Q==} - engines: {node: '>=8.10.0'} + eslint-plugin-json@3.1.0: + resolution: {integrity: sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g==} + engines: {node: '>=12.0'} eslint-plugin-require-path-exists@1.1.9: resolution: {integrity: sha512-moZRfrPr4GFyT/W8PHzjzC7D4Hnj7Us+GYj0fbVKQoPvP4xIF8VG702L1jzyhqE8eIYkcs8p1CoqSfjk9WkxBg==} @@ -5427,10 +5401,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - indexes-of@1.0.1: resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} @@ -6389,10 +6359,6 @@ packages: mensch@0.3.4: resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} - meow@10.1.5: - resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - meow@8.1.2: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} @@ -7630,10 +7596,6 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - quote-unquote@1.0.0: resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} @@ -7709,10 +7671,6 @@ packages: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} - read-pkg-up@8.0.0: - resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} - engines: {node: '>=12'} - read-pkg@1.1.0: resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==} engines: {node: '>=0.10.0'} @@ -7725,10 +7683,6 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} - read-pkg@6.0.0: - resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} - engines: {node: '>=12'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -7763,10 +7717,6 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - redent@4.0.0: - resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} - engines: {node: '>=12'} - redeyed@2.1.1: resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} @@ -8496,10 +8446,6 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} - strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -8787,10 +8733,6 @@ packages: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} - trim-newlines@4.1.1: - resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} - engines: {node: '>=12'} - triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -8916,10 +8858,6 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -9314,8 +9252,8 @@ packages: vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} - vscode-json-languageservice@3.11.0: - resolution: {integrity: sha512-QxI+qV97uD7HHOCjh3MrM1TfbdwmTXrMckri5Tus1/FQiG3baDZb2C9Y0y8QThs7PwHYBIQXcAc59ZveCRZKPA==} + vscode-json-languageservice@4.2.1: + resolution: {integrity: sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==} vscode-jsonrpc@6.0.0: resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} @@ -9334,9 +9272,6 @@ packages: vscode-languageserver-types@3.16.0: resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} - vscode-languageserver-types@3.16.0-next.2: - resolution: {integrity: sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==} - vscode-languageserver@7.0.0: resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} hasBin: true @@ -9344,21 +9279,9 @@ packages: vscode-nls@5.2.0: resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - vscode-uri@2.1.2: - resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} - vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - wait-for-localhost-cli@3.2.0: - resolution: {integrity: sha512-Qeb/137B3PSiQSddXMGS40i7w2G2pWJ/01bJLQ4pk+WZ7V3+i7fIbKajf6/MPcBqYPJvNdYWAuTJ53ahhqZBog==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - wait-for-localhost@4.1.0: - resolution: {integrity: sha512-i3yX7qgAnxFXWOS6Om7SNi19HRygFvCGh0iy0nOsrYOSiIlhUqoUuzLoW4jHVYS4dzNGzRU100uwTyluOMWcjw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - walkdir@0.4.1: resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} engines: {node: '>=6.0.0'} @@ -12922,13 +12845,6 @@ snapshots: map-obj: 4.3.0 quick-lru: 4.0.1 - camelcase-keys@7.0.2: - dependencies: - camelcase: 6.3.0 - map-obj: 4.3.0 - quick-lru: 5.1.1 - type-fest: 1.4.0 - camelcase@3.0.0: {} camelcase@5.3.1: {} @@ -13548,8 +13464,6 @@ snapshots: decamelize@1.2.0: {} - decamelize@5.0.1: {} - decko@1.2.0: {} decode-uri-component@0.2.2: {} @@ -14142,10 +14056,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@9.0.0(eslint@8.57.0): - dependencies: - eslint: 8.57.0 - eslint-config-prettier@9.1.0(eslint@8.57.0): dependencies: eslint: 8.57.0 @@ -14158,13 +14068,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0): dependencies: debug: 4.3.6(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.0(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -14175,17 +14085,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.0.1(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.0(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.0(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -14195,7 +14106,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.0.1(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -14205,15 +14116,17 @@ snapshots: object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.0.1(eslint@8.57.0)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-json@2.1.2: + eslint-plugin-json@3.1.0: dependencies: lodash: 4.17.21 - vscode-json-languageservice: 3.11.0 + vscode-json-languageservice: 4.2.1 eslint-plugin-require-path-exists@1.1.9: dependencies: @@ -15625,8 +15538,6 @@ snapshots: indent-string@4.0.0: {} - indent-string@5.0.0: {} - indexes-of@1.0.1: {} inflight@1.0.6: @@ -16619,21 +16530,6 @@ snapshots: mensch@0.3.4: {} - meow@10.1.5: - dependencies: - '@types/minimist': 1.2.5 - camelcase-keys: 7.0.2 - decamelize: 5.0.1 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 3.0.3 - read-pkg-up: 8.0.0 - redent: 4.0.0 - trim-newlines: 4.1.1 - type-fest: 1.4.0 - yargs-parser: 20.2.9 - meow@8.1.2: dependencies: '@types/minimist': 1.2.5 @@ -18134,8 +18030,6 @@ snapshots: quick-lru@4.0.1: {} - quick-lru@5.1.1: {} - quote-unquote@1.0.0: {} raf@3.4.1: @@ -18227,12 +18121,6 @@ snapshots: read-pkg: 5.2.0 type-fest: 0.8.1 - read-pkg-up@8.0.0: - dependencies: - find-up: 5.0.0 - read-pkg: 6.0.0 - type-fest: 1.4.0 - read-pkg@1.1.0: dependencies: load-json-file: 1.1.0 @@ -18252,13 +18140,6 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 - read-pkg@6.0.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 3.0.3 - parse-json: 5.2.0 - type-fest: 1.4.0 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -18310,11 +18191,6 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - redent@4.0.0: - dependencies: - indent-string: 5.0.0 - strip-indent: 4.0.0 - redeyed@2.1.1: dependencies: esprima: 4.0.1 @@ -19132,10 +19008,6 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-indent@4.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -19496,8 +19368,6 @@ snapshots: trim-newlines@3.0.1: {} - trim-newlines@4.1.1: {} - triple-beam@1.4.1: {} ts-api-utils@1.3.0(typescript@5.5.4): @@ -19603,8 +19473,6 @@ snapshots: type-fest@0.8.1: {} - type-fest@1.4.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -20065,13 +19933,13 @@ snapshots: vlq@0.2.3: {} - vscode-json-languageservice@3.11.0: + vscode-json-languageservice@4.2.1: dependencies: jsonc-parser: 3.3.1 vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.16.0-next.2 + vscode-languageserver-types: 3.16.0 vscode-nls: 5.2.0 - vscode-uri: 2.1.2 + vscode-uri: 3.0.8 vscode-jsonrpc@6.0.0: {} @@ -20090,25 +19958,14 @@ snapshots: vscode-languageserver-types@3.16.0: {} - vscode-languageserver-types@3.16.0-next.2: {} - vscode-languageserver@7.0.0: dependencies: vscode-languageserver-protocol: 3.16.0 vscode-nls@5.2.0: {} - vscode-uri@2.1.2: {} - vscode-uri@3.0.8: {} - wait-for-localhost-cli@3.2.0: - dependencies: - meow: 10.1.5 - wait-for-localhost: 4.1.0 - - wait-for-localhost@4.1.0: {} - walkdir@0.4.1: {} wawoff2@2.0.1: From 8e343bc39066d724d6a039e3d549a3c3f2d18627 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 13:20:59 +0200 Subject: [PATCH 22/35] chore: replace all eslint warnings with errors --- packages/eslint-config/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index 46af1081d2da..37ffb2691125 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -32,7 +32,7 @@ module.exports = { "no-constant-condition": ["error"], "no-constant-binary-expression": "error", "no-unused-vars": [ - "warn", + "error", { argsIgnorePattern: "^(_|e|event)", caughtErrorsIgnorePattern: "^(_|e|error)", @@ -93,9 +93,9 @@ module.exports = { ], "@typescript-eslint/explicit-function-return-type": ["error"], "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-empty-function": "error", "@typescript-eslint/no-unused-vars": [ - "warn", + "error", { argsIgnorePattern: "^(_|e|event)", caughtErrorsIgnorePattern: "^(_|e|error)", @@ -116,7 +116,7 @@ module.exports = { checksVoidReturn: false, }, ], - "@typescript-eslint/promise-function-async": "warn", + "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/strict-boolean-expressions": [ "error", @@ -124,7 +124,7 @@ module.exports = { ], "@typescript-eslint/non-nullable-type-assertion-style": "off", "@typescript-eslint/no-unnecessary-condition": "off", - "@typescript-eslint/consistent-type-definitions": ["warn", "type"], + "@typescript-eslint/consistent-type-definitions": ["error", "type"], "@typescript-eslint/no-invalid-void-type": "off", "import/namespace": "off", }, From 02505750ad98f3d5f3ac525458716a0c550c5993 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 9 Aug 2024 14:20:16 +0200 Subject: [PATCH 23/35] refactor: implement recordClientVersion as middleware (@fehmer) (#5750) * refactor: implement recordClientVersion as middleware (@fehmer) * move csp for docs into docs route * fix * review comments --- backend/src/api/routes/docs.ts | 11 ++++++++++- backend/src/api/routes/index.ts | 15 --------------- backend/src/api/routes/psas.ts | 3 ++- backend/src/middlewares/utility.ts | 18 +++++++++++++++++- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/backend/src/api/routes/docs.ts b/backend/src/api/routes/docs.ts index 6c02825a0cc3..5892e1eda64f 100644 --- a/backend/src/api/routes/docs.ts +++ b/backend/src/api/routes/docs.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Response, Router } from "express"; import * as swaggerUi from "swagger-ui-express"; import publicSwaggerSpec from "../../documentation/public-swagger.json"; @@ -12,6 +12,7 @@ const router = Router(); const root = __dirname + "../../../static"; router.use("/v2/internal", (req, res) => { + setCsp(res); res.sendFile("api/internal.html", { root }); }); @@ -21,6 +22,7 @@ router.use("/v2/internal.json", (req, res) => { }); router.use(["/v2/public", "/v2/"], (req, res) => { + setCsp(res); res.sendFile("api/public.html", { root }); }); @@ -38,3 +40,10 @@ router.use( ); export default router; + +function setCsp(res: Response): void { + res.setHeader( + "Content-Security-Policy", + "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' monkeytype.com cdn.redoc.ly data:;object-src 'none';script-src 'self' cdn.redoc.ly 'unsafe-inline'; worker-src blob: data;script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests" + ); +} diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 453a4c12801c..659a541804cc 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -19,7 +19,6 @@ import leaderboards from "./leaderboards"; import addSwaggerMiddlewares from "./swagger"; import { asyncHandler } from "../../middlewares/utility"; import { MonkeyResponse } from "../../utils/monkey-response"; -import { recordClientVersion } from "../../utils/prometheus"; import { Application, IRouter, @@ -152,20 +151,6 @@ function applyApiRoutes(app: Application): void { return; } - if (req.path === "/psas") { - const clientVersion = - (req.headers["x-client-version"] as string) || - req.headers["client-version"]; - recordClientVersion(clientVersion?.toString() ?? "unknown"); - } - - if (req.path.startsWith("/docs")) { - res.setHeader( - "Content-Security-Policy", - "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' monkeytype.com cdn.redoc.ly data:;object-src 'none';script-src 'self' cdn.redoc.ly 'unsafe-inline'; worker-src blob: data;script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests" - ); - } - next(); } ); diff --git a/backend/src/api/routes/psas.ts b/backend/src/api/routes/psas.ts index 4a283009a128..093b4c870e06 100644 --- a/backend/src/api/routes/psas.ts +++ b/backend/src/api/routes/psas.ts @@ -3,11 +3,12 @@ import { initServer } from "@ts-rest/express"; import * as RateLimit from "../../middlewares/rate-limit"; import * as PsaController from "../controllers/psa"; import { callController } from "../ts-rest-adapter"; +import { recordClientVersion } from "../../middlewares/utility"; const s = initServer(); export default s.router(psasContract, { get: { - middleware: [RateLimit.psaGet], + middleware: [recordClientVersion(), RateLimit.psaGet], handler: async (r) => callController(PsaController.getPsas)(r), }, }); diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index bbfa5e87ccff..ff443dc3ef66 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -1,7 +1,8 @@ import _ from "lodash"; -import type { Response, NextFunction, RequestHandler } from "express"; +import type { Request, Response, NextFunction, RequestHandler } from "express"; import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; import { isDevEnvironment } from "../utils/misc"; +import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; export const emptyMiddleware = ( _req: MonkeyTypes.Request, @@ -45,3 +46,18 @@ export function useInProduction( isDevEnvironment() ? emptyMiddleware : middleware ); } + +/** + * record the client version from the `x-client-version` or ` client-version` header to prometheus + */ +export function recordClientVersion(): RequestHandler { + return (req: Request, _res: Response, next: NextFunction) => { + const clientVersion = + (req.headers["x-client-version"] as string) || + req.headers["client-version"]; + + prometheusRecordClientVersion(clientVersion?.toString() ?? "unknown"); + + next(); + }; +} From 295e47638e48edb3fe1bf94800fcd1e5e9e8f3a8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 16:59:56 +0200 Subject: [PATCH 24/35] chore: rebuild all files instead of just what changed --- frontend/vite.config.js | 5 +++++ packages/contracts/esbuild.config.js | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c0890404911f..1360fc8f7002 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -33,6 +33,11 @@ const BASE_CONFIG = { open: process.env.SERVER_OPEN !== "false", port: 3000, host: process.env.BACKEND_URL !== undefined, + watch: { + //we rebuild the whole contracts package when a file changes + //so we only want to watch one file + ignored: [/.*\/packages\/contracts\/dist\/(?!configs).*/], + }, }, clearScreen: false, root: "src", diff --git a/packages/contracts/esbuild.config.js b/packages/contracts/esbuild.config.js index 67701bcd40fb..6da919b4a63c 100644 --- a/packages/contracts/esbuild.config.js +++ b/packages/contracts/esbuild.config.js @@ -81,9 +81,10 @@ if (isWatch) { console.log("Starting watch mode..."); chokidar.watch("./src/**/*.ts").on( "change", - (path) => { + (_path) => { console.log("File change detected..."); - build(path, false, false); + // build(path, false, false); + buildAll(false, false); }, { ignoreInitial: true, From 3eca42218212bde696c2ceb9cf2d9e1189d0595b Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 21:50:11 +0200 Subject: [PATCH 25/35] chore: add more recommended extensions --- .vscode/extensions.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index de1f33f44b47..c354a1411cd2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,9 @@ "recommendations": [ "esbenp.prettier-vscode", "vitest.explorer", - "huntertran.auto-markdown-toc" + "huntertran.auto-markdown-toc", + "ms-vscode.vscode-typescript-next", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" ] } From 4995f042ac862e2489a7d361f2cffd8e31ab017e Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 9 Aug 2024 22:03:01 +0200 Subject: [PATCH 26/35] chore: fix eslint in dev script --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index d668b5ef5287..cf0d4edea2d8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,7 +12,7 @@ "start": "node ./dist/server.js", "test": "vitest run", "test-coverage": "vitest run --coverage", - "dev": "concurrently \"tsx watch --clear-screen=false --inspect ./src/server.ts\" \"tsc --preserveWatchOutput --noEmit --watch\" \"npx eslint-watch \"./src/**/*.ts\"\"", + "dev": "concurrently -p none \"tsx watch --clear-screen=false --inspect ./src/server.ts\" \"tsc --preserveWatchOutput --noEmit --watch\" \"esw src/ -w --ext .ts --cache --color\"", "knip": "knip", "docker-db-only": "docker compose -f docker/compose.db-only.yml up", "docker": "docker compose -f docker/compose.yml up", From cfea8eef19f925a2d918e9ce3cd1eb0aadf3a955 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 9 Aug 2024 22:07:09 +0200 Subject: [PATCH 27/35] refactor: use single client for whole contract on frontend (@fehmer) (#5752) * refactor: use single client for whole contract on frontend (@fehmer) * review comments --- frontend/src/ts/ape/index.ts | 13 +++---------- frontend/src/ts/pages/about.ts | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index 1d14030a0cb7..46ddac7b3aef 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -2,29 +2,22 @@ import endpoints from "./endpoints"; import { buildHttpClient } from "./adapters/axios-adapter"; import { envConfig } from "../constants/env-config"; import { buildClient } from "./adapters/ts-rest-adapter"; -import { configsContract } from "@monkeytype/contracts/configs"; -import { presetsContract } from "@monkeytype/contracts/presets"; -import { apeKeysContract } from "@monkeytype/contracts/ape-keys"; -import { psasContract } from "@monkeytype/contracts/psas"; -import { publicContract } from "@monkeytype/contracts/public"; +import { contract } from "@monkeytype/contracts"; const API_PATH = ""; const BASE_URL = envConfig.backendUrl; const API_URL = `${BASE_URL}${API_PATH}`; const httpClient = buildHttpClient(API_URL, 10_000); +const tsRestClient = buildClient(contract, BASE_URL, 10_000); // API Endpoints const Ape = { + ...tsRestClient, users: new endpoints.Users(httpClient), - configs: buildClient(configsContract, BASE_URL, 10_000), results: new endpoints.Results(httpClient), - psas: buildClient(psasContract, BASE_URL, 10_000), quotes: new endpoints.Quotes(httpClient), leaderboards: new endpoints.Leaderboards(httpClient), - presets: buildClient(presetsContract, BASE_URL, 10_000), - publicStats: buildClient(publicContract, BASE_URL, 10_000), - apeKeys: buildClient(apeKeysContract, BASE_URL, 10_000), configuration: new endpoints.Configuration(httpClient), dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)), }; diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index af3328ca7f7a..df85802de421 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -100,7 +100,7 @@ async function getStatsAndHistogramData(): Promise { return; } - const speedStats = await Ape.publicStats.getSpeedHistogram({ + const speedStats = await Ape.public.getSpeedHistogram({ query: { language: "english", mode: "time", @@ -115,7 +115,7 @@ async function getStatsAndHistogramData(): Promise { -1 ); } - const typingStats = await Ape.publicStats.getTypingStats(); + const typingStats = await Ape.public.getTypingStats(); if (typingStats.status === 200) { typingStatsResponseData = typingStats.body.data; } else { From a9caf24427153f5679f096e40b13f2418fc1bdcb Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 9 Aug 2024 22:21:18 +0200 Subject: [PATCH 28/35] fix: documentation link in settings (@fehmer) (#5755) --- frontend/src/html/pages/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index db8520dcaa62..a4d38b440c85 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -1566,7 +1566,7 @@ ( From 002ef8f6bf4b367ed3f6c9042e523c1dbae5ca5f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 10 Aug 2024 14:42:02 +0200 Subject: [PATCH 29/35] fix(caret): not working in zen mode --- frontend/src/ts/test/caret.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index 1feb1766df12..3be4dfb4f0fe 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -63,8 +63,12 @@ function getTargetPositionLeft( const currentLetter = currentWordNodeList[inputLen] as | HTMLElement | undefined; - const lastWordLetter = currentWordNodeList[wordLen - 1] as HTMLElement; - const lastInputLetter = currentWordNodeList[inputLen - 1] as HTMLElement; + const lastWordLetter = currentWordNodeList[wordLen - 1] as + | HTMLElement + | undefined; + const lastInputLetter = currentWordNodeList[inputLen - 1] as + | HTMLElement + | undefined; if (isLanguageRightToLeft) { if (inputLen < wordLen && currentLetter) { @@ -73,11 +77,11 @@ function getTargetPositionLeft( (fullWidthCaret ? 0 : fullWidthCaretWidth); } else if (!invisibleExtraLetters) { positionOffsetToWord = - lastInputLetter.offsetLeft - + (lastInputLetter?.offsetLeft ?? 0) - (fullWidthCaret ? fullWidthCaretWidth : 0); } else { positionOffsetToWord = - lastWordLetter.offsetLeft - + (lastWordLetter?.offsetLeft ?? 0) - (fullWidthCaret ? fullWidthCaretWidth : 0); } } else { @@ -85,10 +89,12 @@ function getTargetPositionLeft( positionOffsetToWord = currentLetter?.offsetLeft; } else if (!invisibleExtraLetters) { positionOffsetToWord = - lastInputLetter.offsetLeft + lastInputLetter.offsetWidth; + (lastInputLetter?.offsetLeft ?? 0) + + (lastInputLetter?.offsetWidth ?? 0); } else { positionOffsetToWord = - lastWordLetter.offsetLeft + lastWordLetter.offsetWidth; + (lastWordLetter?.offsetLeft ?? 0) + + (lastWordLetter?.offsetWidth ?? 0); } } result = activeWordElement.offsetLeft + positionOffsetToWord; @@ -143,7 +149,9 @@ export async function updatePosition(noAnim = false): Promise { const currentLetter = currentWordNodeList[inputLen] as | HTMLElement | undefined; - const lastWordLetter = currentWordNodeList[wordLen - 1] as HTMLElement; + const lastWordLetter = currentWordNodeList[wordLen - 1] as + | HTMLElement + | undefined; const spaceWidth = getSpaceWidth(activeWordEl); @@ -156,7 +164,7 @@ export async function updatePosition(noAnim = false): Promise { lastWordLetter?.offsetHeight || Config.fontSize * Numbers.convertRemToPixels(1); - const letterPosTop = lastWordLetter.offsetTop; + const letterPosTop = lastWordLetter?.offsetTop ?? 0; const diff = letterHeight - caret.offsetHeight; let newTop = activeWordEl.offsetTop + letterPosTop + diff / 2; if (Config.caretStyle === "underline") { From 142b51cc260f1929c2f064519da405f039a273a7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 10 Aug 2024 14:45:38 +0200 Subject: [PATCH 30/35] style: sliiiightly increase the horizontal word margin --- frontend/src/styles/test.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index 3a677f3e88ae..dfdaf1149877 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -262,7 +262,7 @@ } &.tape .word { - margin: 0.25em 0.5em 0.75em 0; + margin: 0.25em 0.6em 0.75em 0; } /* a little hack for right-to-left languages */ @@ -451,7 +451,7 @@ position: relative; font-size: 1em; line-height: 1em; - margin: 0.25em; + margin: 0.25em 0.3em; font-variant: no-common-ligatures; border-bottom: 2px solid transparent; letter { From 24e94479d3cc1f95e56e7d98c29d4c28a37ed75f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 10 Aug 2024 21:45:47 +0200 Subject: [PATCH 31/35] style: only apply error underline when moving to the next word --- frontend/src/ts/test/test-ui.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 1c77cd17a544..73a03b3570eb 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -899,14 +899,6 @@ export async function updateWordElement(inputOverride?: string): Promise { ret += `` + currentWord[i] + ""; } } - - if (Config.highlightMode === "letter") { - if (input.length > currentWord.length && !Config.blindMode) { - wordAtIndex.classList.add("error"); - } else if (input.length === currentWord.length) { - wordAtIndex.classList.remove("error"); - } - } } wordAtIndex.innerHTML = ret; From 77c9cc423f288aec08d729a56c41edceef16ff7f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 10 Aug 2024 23:59:59 +0200 Subject: [PATCH 32/35] fix(server): incorrect apekeys permission check --- backend/src/api/routes/ape-keys.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/routes/ape-keys.ts b/backend/src/api/routes/ape-keys.ts index e6f85650d61b..2038c887a0a1 100644 --- a/backend/src/api/routes/ape-keys.ts +++ b/backend/src/api/routes/ape-keys.ts @@ -15,7 +15,7 @@ const commonMiddleware = [ }), checkUserPermissions({ criteria: (user) => { - return user.canManageApeKeys ?? false; + return user.canManageApeKeys ?? true; }, invalidMessage: "You have lost access to ape keys, please contact support", }), From 61c9134030d6b7ce3b1bec7a9e4da07f7cb21206 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 11 Aug 2024 00:14:39 +0200 Subject: [PATCH 33/35] chore: add vitest workspace configuration file --- package.json | 3 +- pnpm-lock.yaml | 65 +++++++++++++++++++++++++++++++++++++++++++ vitest.workspace.json | 1 + 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 vitest.workspace.json diff --git a/package.json b/package.json index 9c90297e3ea1..2eb6ee6d40fc 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "lint-staged": "13.2.3", "only-allow": "1.2.1", "prettier": "2.5.1", - "turbo": "2.0.9" + "turbo": "2.0.9", + "vitest": "1.6.0" }, "lint-staged": { "*.{json,scss,css,html}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b44f21b3d50e..c82bd171e25e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: turbo: specifier: 2.0.9 version: 2.0.9 + vitest: + specifier: 1.6.0 + version: 1.6.0(@types/node@20.5.1)(happy-dom@13.4.1)(sass@1.70.0)(terser@5.31.3) backend: dependencies: @@ -19820,6 +19823,23 @@ snapshots: - supports-color - terser + vite-node@1.6.0(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3): + dependencies: + cac: 6.7.14 + debug: 4.3.6(supports-color@5.5.0) + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite-plugin-checker@0.6.4(eslint@8.57.0)(optionator@0.9.4)(typescript@5.5.4)(vite@5.1.7(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3)): dependencies: '@babel/code-frame': 7.24.7 @@ -19885,6 +19905,17 @@ snapshots: sass: 1.70.0 terser: 5.31.3 + vite@5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3): + dependencies: + esbuild: 0.19.12 + postcss: 8.4.40 + rollup: 4.19.1 + optionalDependencies: + '@types/node': 20.5.1 + fsevents: 2.3.3 + sass: 1.70.0 + terser: 5.31.3 + vitest-mongodb@1.0.0: dependencies: debug: 4.3.6(supports-color@5.5.0) @@ -19931,6 +19962,40 @@ snapshots: - supports-color - terser + vitest@1.6.0(@types/node@20.5.1)(happy-dom@13.4.1)(sass@1.70.0)(terser@5.31.3): + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.3 + chai: 4.5.0 + debug: 4.3.6(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.11 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) + vite-node: 1.6.0(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.5.1 + happy-dom: 13.4.1 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vlq@0.2.3: {} vscode-json-languageservice@4.2.1: diff --git a/vitest.workspace.json b/vitest.workspace.json new file mode 100644 index 000000000000..61cc010455a8 --- /dev/null +++ b/vitest.workspace.json @@ -0,0 +1 @@ +["packages/*", "frontend", "backend"] From ef8dfe22726b0ce96ffcb9e21b61e880ac231278 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Sun, 11 Aug 2024 12:07:42 +0200 Subject: [PATCH 34/35] chore: add test case for apekey permission bug (@fehmer) (#5762) --- backend/__tests__/api/controllers/ape-key.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/__tests__/api/controllers/ape-key.spec.ts b/backend/__tests__/api/controllers/ape-key.spec.ts index 4855ee5f4109..5f33d32fea25 100644 --- a/backend/__tests__/api/controllers/ape-key.spec.ts +++ b/backend/__tests__/api/controllers/ape-key.spec.ts @@ -15,7 +15,7 @@ describe("ApeKeyController", () => { beforeEach(async () => { await enableApeKeysEndpoints(true); - getUserMock.mockResolvedValue(user(uid, { canManageApeKeys: true })); + getUserMock.mockResolvedValue(user(uid, {})); vi.useFakeTimers(); vi.setSystemTime(1000); }); From 092d513f01ad91cfa36eb5c20a67291bd895aebe Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Sun, 11 Aug 2024 17:50:26 +0200 Subject: [PATCH 35/35] chore: fix test coverage not working with vitest workspaces (@fehmer) (#5764) --- package.json | 1 + pnpm-lock.yaml | 22 ++++++++++++++++++++++ vitest.config.js | 11 +++++++++++ 3 files changed, 34 insertions(+) create mode 100644 vitest.config.js diff --git a/package.json b/package.json index 2eb6ee6d40fc..90caf020601d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "devDependencies": { "@commitlint/cli": "17.7.1", "@commitlint/config-conventional": "17.7.0", + "@vitest/coverage-v8": "1.6.0", "@monkeytype/release": "workspace:*", "conventional-changelog": "4.0.0", "husky": "8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c82bd171e25e..2d9ccbb0e4a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@monkeytype/release': specifier: workspace:* version: link:packages/release + '@vitest/coverage-v8': + specifier: 1.6.0 + version: 1.6.0(vitest@1.6.0(@types/node@20.5.1)(happy-dom@13.4.1)(sass@1.70.0)(terser@5.31.3)) conventional-changelog: specifier: 4.0.0 version: 4.0.0 @@ -12188,6 +12191,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.5.1)(happy-dom@13.4.1)(sass@1.70.0)(terser@5.31.3))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.6(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.11 + magicast: 0.3.4 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + vitest: 1.6.0(@types/node@20.5.1)(happy-dom@13.4.1)(sass@1.70.0)(terser@5.31.3) + transitivePeerDependencies: + - supports-color + '@vitest/expect@1.6.0': dependencies: '@vitest/spy': 1.6.0 diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000000..82eb8ea19c95 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + enabled: true, + include: ["**/*.ts"], + reporter: ["json"], + }, + }, +});