From cb8adadfd884308b3f99d3d3352cbb9ae46eded5 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 22 Apr 2022 18:05:56 +0100 Subject: [PATCH 1/3] refactor: simplify StaticDatabases declaration --- server/src/external/mongo/db.ts | 38 +-------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index a206842c4..ccb60cf6f 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -195,43 +195,7 @@ const db = { migrations: monkDB.get("migrations"), }; -export type StaticDatabases = - | "sessions" - | "session-view-cache" - | "folders" - | "folder-chart-lookup" - | "scores" - | "personal-bests" - | "imports" - | "import-timings" - | "goals" - | "goal-subs" - | "milestone-subs" - | "milestones" - | "game-stats" - | "game-settings" - | "users" - | "kai-auth-tokens" - | "bms-course-lookup" - | "api-tokens" - | "import-locks" - | "tables" - | "game-stats-snapshots" - | "arc-saved-profiles" - | "user-private-information" - | "api-clients" - | "oauth2-auth-codes" - | "fer-settings" - | "orphan-chart-queue" - | "password-reset-codes" - | "user-settings" - | "counters" - | "score-blacklist" - | "verify-email-codes" - | "class-achievements" - | "recent-folder-views" - | "milestone-sets" - | "migrations"; +export type StaticDatabases = Exclude; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; From bea7e1456ef2777cea525fe9ac4c89c39453d7de Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 22 Apr 2022 18:45:51 +0100 Subject: [PATCH 2/3] refactor: move ALL_PERMISSIONS to tachi-common --- server/package.json | 2 +- server/pnpm-lock.yaml | 17 ++++++++++------- server/src/server/middleware/auth.test.ts | 5 +++-- server/src/server/middleware/auth.ts | 14 ++------------ .../src/server/router/api/v1/clients/router.ts | 10 +++++++--- .../api/v1/users/_userID/api-tokens/router.ts | 5 ++--- server/src/test-utils/fake-auth.ts | 4 ++-- 7 files changed, 27 insertions(+), 30 deletions(-) diff --git a/server/package.json b/server/package.json index ff61ea3cd..12ac1fead 100644 --- a/server/package.json +++ b/server/package.json @@ -85,7 +85,7 @@ "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", "seq-logging": "1.1.1", - "tachi-common": "0.8.1", + "tachi-common": "0.9.1", "ts-node": "10.4.0", "tsconfig-paths": "3.12.0", "typescript": "4.5.5", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 600eddaba..ff7418b77 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -65,7 +65,7 @@ specifiers: safe-json-stringify: 1.2.0 seq-logging: 1.1.1 supertest: 6.2.2 - tachi-common: 0.8.1 + tachi-common: 0.9.1 tap: 15.1.6 ts-node: 10.4.0 tsconfig-paths: 3.12.0 @@ -112,7 +112,7 @@ dependencies: rimraf: 3.0.2 safe-json-stringify: 1.2.0 seq-logging: 1.1.1 - tachi-common: 0.8.1 + tachi-common: 0.9.1 ts-node: 10.4.0_2615db9039ce432b4abf2fc39ef336ec tsconfig-paths: 3.12.0 typescript: 4.5.5 @@ -1381,7 +1381,6 @@ packages: '@types/node': 17.0.15 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 - dev: true /@types/express-session/1.17.4: resolution: {integrity: sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==} @@ -1474,11 +1473,9 @@ packages: /@types/qs/6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true /@types/range-parser/1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true /@types/rate-limit-redis/1.7.4: resolution: {integrity: sha512-TDsh9CJHHhx+sGHlqF6MI+PgEv91o/wQ67e2IR02ebYPzWQ8Rk0pcVY+WBy2pjxa+cAkPwjcOFDd21VToKaOzQ==} @@ -4725,6 +4722,10 @@ packages: resolution: {integrity: sha512-7214GHTBThBI/PGlKWXUSJ++ca0ld54VpF4k98U1EDAFXai5mnvWbb4speVOnWkilqfN25x8biWUAj4gHNu5Og==} dev: false + /prudence/0.9.8: + resolution: {integrity: sha512-my6HEhcTW3aKKjAx8j6jgUj3x5Kc71NEv3DySRXun2Zspw6Fdo5zDnbpXv/j4tyk9f9XXrpu1grWVBX4WU+1Tg==} + dev: false + /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: true @@ -5363,10 +5364,12 @@ packages: engines: {node: '>= 0.4'} dev: true - /tachi-common/0.8.1: - resolution: {integrity: sha512-YDVxlxdli9u7MI9rhN76CUkfA/VoiXJKGyhg22uUAxMfENu9nxHWxFM3Iw20Gg+gZ8lPl3Yhvxq+qi7fD0SiNA==} + /tachi-common/0.9.1: + resolution: {integrity: sha512-wctqYSb+xbbbR+TRae3go8UNOUcRcRobZafCwhussI1HngirUKylofxHCy4yEsXt+G00xMdLCFgOafn+E8pIVw==} dependencies: + '@types/express-serve-static-core': 4.17.28 '@types/mongodb': 3.6.18 + prudence: 0.9.8 dev: false /tap-mocha-reporter/5.0.1: diff --git a/server/src/server/middleware/auth.test.ts b/server/src/server/middleware/auth.test.ts index 910d93b85..b114619dd 100644 --- a/server/src/server/middleware/auth.test.ts +++ b/server/src/server/middleware/auth.test.ts @@ -1,10 +1,11 @@ import expMiddlewareMock from "express-request-mock"; import db from "external/mongo/db"; import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { ALL_PERMISSIONS } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; -import { AllPermissions, SetRequestPermissions } from "./auth"; +import { SetRequestPermissions } from "./auth"; t.test("#SetRequestPermissions", (t) => { t.beforeEach(ResetDBState); @@ -112,7 +113,7 @@ t.test("#SetRequestPermissions", (t) => { userID: 1, identifier: `Session-Key 1`, token: null, - permissions: AllPermissions, + permissions: ALL_PERMISSIONS, fromAPIClient: null, }); t.end(); diff --git a/server/src/server/middleware/auth.ts b/server/src/server/middleware/auth.ts index c4139a2f1..cbd317d92 100644 --- a/server/src/server/middleware/auth.ts +++ b/server/src/server/middleware/auth.ts @@ -3,7 +3,7 @@ import db from "external/mongo/db"; import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; -import { APIPermissions, APITokenDocument, UserAuthLevels } from "tachi-common"; +import { APIPermissions, APITokenDocument, UserAuthLevels, ALL_PERMISSIONS } from "tachi-common"; import { SplitAuthorizationHeader } from "utils/misc"; const logger = CreateLogCtx(__filename); @@ -16,16 +16,6 @@ const GuestToken: APITokenDocument = { fromAPIClient: null, }; -export const AllPermissions: Record = { - customise_profile: true, - submit_score: true, - customise_session: true, - customise_score: true, - delete_score: true, - manage_targets: true, - manage_rivals: true, -}; - export const SetRequestPermissions: RequestHandler = CreateSetRequestPermissions("description"); /** @@ -49,7 +39,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler { userID: req.session.tachi.user.id, identifier: `Session-Key ${req.session.tachi.user.id}`, token: null, - permissions: AllPermissions, + permissions: ALL_PERMISSIONS, fromAPIClient: null, }; return next(); diff --git a/server/src/server/router/api/v1/clients/router.ts b/server/src/server/router/api/v1/clients/router.ts index 1d927cf00..d759b86fc 100644 --- a/server/src/server/router/api/v1/clients/router.ts +++ b/server/src/server/router/api/v1/clients/router.ts @@ -4,9 +4,13 @@ import { SYMBOL_TachiData } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import p from "prudence"; -import { AllPermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; -import { APIPermissions, TachiAPIClientDocument, UserAuthLevels } from "tachi-common"; +import { + ALL_PERMISSIONS, + APIPermissions, + TachiAPIClientDocument, + UserAuthLevels, +} from "tachi-common"; import { DedupeArr, DeleteUndefinedProps, IsValidURL, Random20Hex } from "utils/misc"; import { optNull } from "utils/prudence"; import { FormatUserDoc } from "utils/user"; @@ -79,7 +83,7 @@ router.post( return true; }, apiKeyFilename: "?string", - permissions: [p.isIn(Object.keys(AllPermissions))], + permissions: [p.isIn(Object.keys(ALL_PERMISSIONS))], }), async (req, res) => { if (!req.session.tachi?.user) { diff --git a/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts b/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts index 98e1ce9ca..3e0138421 100644 --- a/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts +++ b/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts @@ -3,9 +3,8 @@ import db from "external/mongo/db"; import { SYMBOL_TachiData } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import p from "prudence"; -import { AllPermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; -import { APIPermissions, APITokenDocument } from "tachi-common"; +import { ALL_PERMISSIONS, APIPermissions, APITokenDocument } from "tachi-common"; import { Random20Hex } from "utils/misc"; import { FormatUserDoc } from "utils/user"; import { RequireSelfRequestFromUser } from "../middleware"; @@ -49,7 +48,7 @@ router.get("/", async (req, res) => { router.post( "/create", prValidate({ - permissions: p.optional([p.isIn(Object.keys(AllPermissions))]), + permissions: p.optional([p.isIn(Object.keys(ALL_PERMISSIONS))]), identifier: "*string", clientID: "*string", }), diff --git a/server/src/test-utils/fake-auth.ts b/server/src/test-utils/fake-auth.ts index 5720f44ba..5653ffbb8 100644 --- a/server/src/test-utils/fake-auth.ts +++ b/server/src/test-utils/fake-auth.ts @@ -2,7 +2,7 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { ClearTestingRateLimitCache } from "server/middleware/rate-limiter"; import supertest from "supertest"; -import { AllPermissions } from "../server/middleware/auth"; +import { ALL_PERMISSIONS } from "tachi-common"; import ResetDBState from "./resets"; const logger = CreateLogCtx(__filename); @@ -33,7 +33,7 @@ export function InsertFakeTokenWithAllPerms(token: string): () => any { await db["api-tokens"].insert({ userID: 1, identifier: "Mock API Token", - permissions: AllPermissions, + permissions: ALL_PERMISSIONS, token, fromAPIClient: null, }); From 80db6fb410397a3a8cd539dd320731b4fd5a5a05 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 22 Apr 2022 19:12:50 +0100 Subject: [PATCH 3/3] feat: properly define schemas from tachi-common --- server/package.json | 2 +- server/pnpm-lock.yaml | 8 +- server/src/external/mongo/schemas.ts | 698 +-------------------------- 3 files changed, 21 insertions(+), 687 deletions(-) diff --git a/server/package.json b/server/package.json index 12ac1fead..15f42a519 100644 --- a/server/package.json +++ b/server/package.json @@ -85,7 +85,7 @@ "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", "seq-logging": "1.1.1", - "tachi-common": "0.9.1", + "tachi-common": "0.9.3", "ts-node": "10.4.0", "tsconfig-paths": "3.12.0", "typescript": "4.5.5", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index ff7418b77..a1193339c 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -65,7 +65,7 @@ specifiers: safe-json-stringify: 1.2.0 seq-logging: 1.1.1 supertest: 6.2.2 - tachi-common: 0.9.1 + tachi-common: 0.9.3 tap: 15.1.6 ts-node: 10.4.0 tsconfig-paths: 3.12.0 @@ -112,7 +112,7 @@ dependencies: rimraf: 3.0.2 safe-json-stringify: 1.2.0 seq-logging: 1.1.1 - tachi-common: 0.9.1 + tachi-common: 0.9.3 ts-node: 10.4.0_2615db9039ce432b4abf2fc39ef336ec tsconfig-paths: 3.12.0 typescript: 4.5.5 @@ -5364,8 +5364,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /tachi-common/0.9.1: - resolution: {integrity: sha512-wctqYSb+xbbbR+TRae3go8UNOUcRcRobZafCwhussI1HngirUKylofxHCy4yEsXt+G00xMdLCFgOafn+E8pIVw==} + /tachi-common/0.9.3: + resolution: {integrity: sha512-7wM+WAfdt0EJw5Bahu5puL0Am/5Nmop68maEyHwHZXTZDQoOqREYxv9+5+Co/wvdcApTZ2T3ctWl1UaPBQyhyQ==} dependencies: '@types/express-serve-static-core': 4.17.28 '@types/mongodb': 3.6.18 diff --git a/server/src/external/mongo/schemas.ts b/server/src/external/mongo/schemas.ts index 4ec378d81..8ed71ed51 100644 --- a/server/src/external/mongo/schemas.ts +++ b/server/src/external/mongo/schemas.ts @@ -1,18 +1,7 @@ import { TachiConfig } from "lib/setup/config"; import p, { PrudenceSchema } from "prudence"; -import { AllPermissions } from "server/middleware/auth"; -import { - Game, - GamePTConfig, - GetGameConfig, - GetGamePTConfig, - Playtypes, - UserAuthLevels, -} from "tachi-common"; -import { allIDStrings, allImportTypes } from "tachi-common/js/config/static-config"; -import { IsValidGame, IsValidPlaytype } from "utils/misc"; -import { optNull } from "utils/prudence"; -import { StaticDatabases } from "./db"; +import { SCHEMAS, SchemaValidatorFunction } from "tachi-common/js/lib/schemas"; +import { Databases } from "./db"; // The idea of this file is to export a function for every database // that will validate that collection. @@ -32,326 +21,10 @@ function prSchemaify(schema: PrudenceSchema) { }; } -const games = TachiConfig.GAMES; -const isValidPlaytype = (self: unknown, parent: Record) => { - if (!parent.game || typeof parent.game !== "string" || !IsValidGame(parent.game)) { - throw new Error(`Invalid Schema, need game to base IsValidPlaytype off of.`); - } - - if (typeof self !== "string") { - return "Expected a string."; - } - - if (!IsValidPlaytype(parent.game, self)) { - return `Expected a valid playtype for ${parent.game}`; - } - - return true; -}; - -const extractGPT = (self: unknown) => { - if (typeof self !== "object" || !self) { - throw new Error("Expected an object."); - } - - const s = self as Record; - - if (typeof s.game !== "string") { - throw new Error(`Expected a string where self.game is. Got ${s.game}`); - } - - if (typeof s.playtype !== "string") { - throw new Error(`Expected a string where self.playtype is. Got ${s.playtype}`); - } - - if (!IsValidGame(s.game)) { - throw new Error(`Expected valid game -- got ${s.game}.`); - } - - if (!IsValidPlaytype(s.game, s.playtype)) { - throw new Error(`Expected valid playtype -- got ${s.playtype}.`); - } - - return { game: s.game, playtype: s.playtype }; -}; - -export type ValidatorFunction = (s: unknown) => true; - -const PR_GameStats = (game: Game, playtype: Playtypes[Game], gptConfig: GamePTConfig) => ({ - userID: p.isPositiveNonZeroInteger, - game: p.is(game), - playtype: p.is(playtype), - ratings: Object.fromEntries(gptConfig.scoreRatingAlgs.map((s) => [s, "*number"])), - classes: Object.fromEntries( - Object.keys(gptConfig.classHumanisedFormat).map((e) => [e, p.optional(p.isInteger)]) - ), -}); - -const PR_GoalInfo = { - progress: "?number", - progressHuman: "string", - outOf: "number", - outOfHuman: "string", - achieved: "boolean", -}; - -const GetHitMeta = (game: Game): PrudenceSchema => { - if (game === "iidx") { - return { - bp: optNull(p.isPositiveInteger), - gauge: optNull(p.isBetween(0, 100)), - gaugeHistory: optNull([p.nullable(p.isBetween(0, 100))]), - comboBreak: optNull(p.isPositiveInteger), - gsm: optNull({ - EASY: [p.nullable(p.isBetween(0, 100))], - NORMAL: [p.nullable(p.isBetween(0, 100))], - HARD: [p.nullable(p.isBetween(0, 100))], - EX_HARD: [p.nullable(p.isBetween(0, 100))], - }), - }; - } else if (game === "bms") { - return { - epg: optNull(p.isPositiveInteger), - egr: optNull(p.isPositiveInteger), - egd: optNull(p.isPositiveInteger), - epr: optNull(p.isPositiveInteger), - ebd: optNull(p.isPositiveInteger), - lpg: optNull(p.isPositiveInteger), - lgr: optNull(p.isPositiveInteger), - lgd: optNull(p.isPositiveInteger), - lpr: optNull(p.isPositiveInteger), - lbd: optNull(p.isPositiveInteger), - bp: optNull(p.isPositiveInteger), - gauge: optNull(p.isBetween(0, 100)), - }; - } else if (game === "sdvx" || game === "usc") { - return { gauge: optNull(p.isBetween(0, 100)) }; - } - - return {}; -}; - -const PR_Random = p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"); - -const GetScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema => { - if (game === "iidx" && playtype === "SP") { - return { - random: optNull(PR_Random), - assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")), - range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")), - gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")), - }; - } else if (game === "iidx" && playtype === "DP") { - return { - random: optNull({ - left: PR_Random, - right: PR_Random, - }), - assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")), - range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")), - gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")), - }; - } else if (game === "sdvx") { - return { inSkillAnalyser: "*?boolean" }; - } else if (game === "usc") { - return { - noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")), - gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")), - }; - } else if (game === "bms" && playtype === "7K") { - return { - random: optNull(PR_Random), - inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")), - client: optNull(p.isIn("LR2", "lr2oraja")), - }; - } else if (game === "bms" && playtype === "14K") { - return { - random: optNull({ - left: PR_Random, - right: PR_Random, - }), - inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")), - client: optNull(p.isIn("LR2", "lr2oraja")), - }; - } - - return {}; -}; - -export const DatabaseSchemas: Record< - Exclude, - ValidatorFunction -> = { - users: prSchemaify({ - username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - usernameLowercase: (self, parent) => self === (parent.username as string).toLowerCase(), - id: p.isPositiveInteger, - socialMedia: { - discord: "*?string", - twitter: "*?string", - github: "*?string", - steam: "*?string", - youtube: "*?string", - twitch: "*?string", - }, - joinDate: p.isPositiveInteger, - about: p.isBoundedString(0, 4000), - status: p.nullable(p.isBoundedString(3, 140)), - customPfpLocation: "?string", - customBannerLocation: "?string", - clan: p.nullable(p.isBoundedString(2, 4)), - lastSeen: p.isPositiveInteger, - badges: [p.isIn("beta", "alpha", "devTeam")], - authLevel: p.isBoundedInteger(UserAuthLevels.BANNED, UserAuthLevels.ADMIN), - }), - "api-tokens": prSchemaify({ - userID: p.nullable(p.isPositiveNonZeroInteger), - token: "?string", - identifier: "string", - permissions: (self) => { - if (typeof self !== "object" || !self) { - return `Invalid permissions - expected an object.`; - } - - // Typescript moment... - const s = self as Record; - - for (const key in AllPermissions) { - if (s[key] !== undefined && typeof s[key] !== "boolean") { - return `Invalid permission value of ${s[key]} at ${key}.`; - } - } - - return true; - }, - fromOAuth2Client: "*string", - }), - "arc-saved-profiles": prSchemaify({ - userID: p.isPositiveNonZeroInteger, - accountID: "string", - forImportType: p.isIn("api/arc-iidx", "api/arc-sdvx"), - }), - "class-achievements": prSchemaify({ - game: p.isIn(games), - playtype: isValidPlaytype, - classSet: (self, parent) => { - const gptConfig = GetGamePTConfig( - parent.game as Game, - parent.playtype as Playtypes[Game] - ); - - const keys = Object.keys(gptConfig.classHumanisedFormat); - - if (typeof self !== "string") { - return "Expected a string."; - } - - if (!keys.includes(self)) { - return `Expected any of ${keys.join(", ")}`; - } - - return true; - }, - classOldValue: p.nullable(p.isInteger), - classValue: p.isInteger, - timeAchieved: p.isPositive, - userID: p.isPositiveNonZeroInteger, - }), - "fer-settings": prSchemaify({ - userID: p.isPositiveNonZeroInteger, - cards: p.nullable( - (self) => - Array.isArray(self) && self.length <= 6 && self.every((t) => typeof t === "string") - ), - forceStaticImport: "boolean", - }), - "folder-chart-lookup": prSchemaify({ - chartID: "string", - folderID: "string", - }), - "game-settings": (s) => { - const { game, playtype } = extractGPT(s); - - const gptConfig = GetGamePTConfig(game, playtype); - - return prSchemaify({ - userID: p.isPositiveNonZeroInteger, - game: p.is(game), - playtype: p.is(playtype), - preferences: { - preferredScoreAlg: p.nullable(p.isIn(gptConfig.scoreRatingAlgs)), - preferredSessionAlg: p.nullable(p.isIn(gptConfig.sessionRatingAlgs)), - preferredProfileAlg: p.nullable(p.isIn(gptConfig.profileRatingAlgs)), - // ouch - stats: p.and( - [ - p.or( - { - mode: p.is("chart"), - chartID: "string", - property: p.isIn("score", "percent", "grade", "lamp", "playcount"), - }, - { - mode: p.is("folder"), - folderID: p.or("string", ["string"]), - property: p.isIn("score", "percent", "grade", "lamp"), - gte: p.isPositive, - } - ), - ], - (self) => Array.isArray(self) && self.length <= 6 - ), - scoreBucket: p.isIn(null, "grade", "lamp"), - gameSpecific: - game === "iidx" - ? { - display2DXTra: "boolean", - } - : {}, - }, - })(s); - }, - "game-stats": (self) => { - const { game, playtype } = extractGPT(self); - - const gptConfig = GetGamePTConfig(game, playtype); - - return prSchemaify(PR_GameStats(game, playtype, gptConfig))(self); - }, - "game-stats-snapshots": (self) => { - const { game, playtype } = extractGPT(self); - - const gptConfig = GetGamePTConfig(game, playtype); - - return prSchemaify( - Object.assign(PR_GameStats(game, playtype, gptConfig), { - rankings: Object.fromEntries( - gptConfig.profileRatingAlgs.map((e) => [ - e, - { outOf: p.isPositiveNonZeroInteger, ranking: p.isPositiveNonZeroInteger }, - ]) - ), - playcount: p.isPositiveInteger, - timestamp: p.isPositive, - }) - )(self); - }, +export const DatabaseSchemas: Record = { "import-locks": prSchemaify({ userID: p.isPositiveNonZeroInteger, }), - "import-timings": prSchemaify({ - importID: "string", - timestamp: p.isPositive, - total: p.isPositive, - rel: Object.fromEntries( - ["import", "importParse", "session", "pb"].map((k) => [k, p.isPositive]) - ), - abs: Object.fromEntries( - ["parse", "import", "importParse", "session", "pb", "ugs", "goal", "milestone"].map( - (k) => [k, p.isPositive] - ) - ), - }), "kai-auth-tokens": prSchemaify({ userID: p.isPositiveNonZeroInteger, token: "string", @@ -363,368 +36,16 @@ export const DatabaseSchemas: Record< userID: p.isPositiveNonZeroInteger, createdOn: p.isPositive, }), - "api-clients": prSchemaify({ - clientID: "string", - clientSecret: "string", - name: "string", - author: p.isPositiveNonZeroInteger, - requestedPermissions: [p.isIn(Object.keys(AllPermissions))], - redirectUri: "?string", - webhookUri: "?string", - apiKeyTemplate: p.nullable((self) => { - if (typeof self !== "string") { - return "Expected a string."; - } - - if (!self.includes("%%TACHI_KEY%%")) { - return "Template must include %%TACHI_KEY%%."; - } - - return true; - }), - apiKeyFilename: "?string", - }), - "orphan-chart-queue": prSchemaify({ - idString: p.isIn(allIDStrings), - // @todo #370 Properly prudence-def orphan-chart-queue. - chartDoc: p.any, - songDoc: p.any, - userIDs: [p.isPositiveNonZeroInteger], - }), "password-reset-codes": prSchemaify({ code: "string", userID: p.isPositiveNonZeroInteger, createdOn: p.isPositive, }), - "score-blacklist": prSchemaify({ - scoreID: "string", - userID: p.isPositiveNonZeroInteger, - score: p.any, - }), - "session-view-cache": prSchemaify({ - sessionID: "string", - ip: "string", - timestamp: p.isPositive, - }), - "user-private-information": prSchemaify({ - userID: p.isPositiveNonZeroInteger, - password: "string", - email: "string", - }), "verify-email-codes": prSchemaify({ userID: p.isPositiveNonZeroInteger, code: "string", email: "string", }), - counters: prSchemaify({ - counterName: "string", - value: p.isPositiveInteger, - }), - "user-settings": prSchemaify({ - userID: p.isPositiveNonZeroInteger, - preferences: { - invisible: "boolean", - developerMode: "boolean", - advancedMode: "boolean", - contentiousContent: "boolean", - deletableScores: "boolean", - }, - }), - sessions: (self) => { - const { game, playtype } = extractGPT(self); - - const gptConfig = GetGamePTConfig(game, playtype); - - return prSchemaify({ - userID: p.isPositiveNonZeroInteger, - sessionID: "string", - name: "string", - desc: "?string", - game: p.is(game), - playtype: isValidPlaytype, - importType: p.nullable(p.isIn(allImportTypes)), - timeInserted: p.isPositiveInteger, - timeEnded: p.isPositiveInteger, - timeStarted: p.isPositiveInteger, - highlight: "boolean", - views: p.isInteger, - calculatedData: Object.fromEntries( - gptConfig.sessionRatingAlgs.map((k) => [k, "*?number"]) - ), - scoreInfo: [ - p.or( - { - scoreID: "string", - isNewScore: p.is(true), - }, - { - scoreID: "string", - isNewScore: p.is(false), - scoreDelta: "number", - gradeDelta: p.isInteger, - lampDelta: p.isInteger, - percentDelta: "number", - } - ), - ], - })(self); - }, - imports: prSchemaify({ - userID: p.isPositiveNonZeroInteger, - timeStarted: p.isPositive, - timeFinished: p.isPositive, - idStrings: [p.isIn(allIDStrings)], - importID: "string", - scoreIDs: ["string"], - game: p.isIn(games), - // @ts-expect-error We've asserted this is definitely a game. - playtypes: [(self) => p.isIn(GetGameConfig(self.game).validPlaytypes)(self)], - errors: [ - { - type: "string", - message: "?string", - }, - ], - createdSessions: [ - { - sessionID: "string", - type: p.isIn("Created", "Appended"), - }, - ], - importType: p.nullable(p.isIn(allImportTypes)), - classDeltas: [ - { - game: p.isIn(games), - playtype: isValidPlaytype, - set: "string", // Technically false, but the validation is a pain. - old: p.nullable(p.isInteger), - new: p.isInteger, - }, - ], - goalInfo: [ - { - goalID: "string", - old: PR_GoalInfo, - new: PR_GoalInfo, - }, - ], - milestoneInfo: [ - { - milestoneID: "string", - old: { progress: p.isInteger, achieved: "boolean" }, - new: { progress: p.isInteger, achieved: "boolean" }, - }, - ], - userIntent: "boolean", - }), - scores: (self) => { - const { game, playtype } = extractGPT(self); - - const gptConfig = GetGamePTConfig(game, playtype); - - const hitMeta = Object.assign( - { - fast: optNull(p.isPositiveInteger), - slow: optNull(p.isPositiveInteger), - maxCombo: optNull(p.isPositiveInteger), - }, - GetHitMeta(game) - ); - - const scoreMeta = GetScoreMeta(game, playtype); - - return prSchemaify({ - service: "string", - game: p.is(game), - playtype: p.is(playtype), - userID: p.isPositiveNonZeroInteger, - songID: p.isPositiveInteger, - chartID: "string", - isPrimary: "boolean", - highlight: "boolean", - comment: "?string", - timeAdded: p.isInteger, - scoreID: "string", - importType: p.nullable(p.isIn(allImportTypes)), - timeAchieved: p.nullable(p.isPositive), - scoreData: { - score: "number", - lamp: p.isIn(gptConfig.lamps), - lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)), - grade: p.isIn(gptConfig.grades), - gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)), - percent: p.isBetween(0, gptConfig.percentMax), - esd: "?number", - judgements: Object.fromEntries( - gptConfig.judgements.map((j) => [j, optNull(p.isInteger)]) - ), - hitMeta, - }, - scoreMeta, - calculatedData: Object.fromEntries( - gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"]) - ), - })(self); - }, - "personal-bests": (self) => { - const { game, playtype } = extractGPT(self); - - const gptConfig = GetGamePTConfig(game, playtype); - - const hitMeta = Object.assign( - { - fast: optNull(p.isPositiveInteger), - slow: optNull(p.isPositiveInteger), - maxCombo: optNull(p.isPositiveInteger), - }, - GetHitMeta(game) - ); - - return prSchemaify({ - composedFrom: { - scorePB: "string", - lampPB: "string", - other: p.optional([{ name: "string", scoreID: "string" }]), - }, - rankingData: { - rank: p.isPositiveNonZeroInteger, - outOf: p.isPositiveNonZeroInteger, - }, - userID: p.isPositiveNonZeroInteger, - chartID: "string", - game: p.is(game), - playtype: p.is(playtype), - songID: p.isPositiveNonZeroInteger, - highlight: "boolean", - isPrimary: "boolean", - timeAchieved: p.nullable(p.isPositive), - scoreData: { - score: "number", - lamp: p.isIn(gptConfig.lamps), - lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)), - grade: p.isIn(gptConfig.grades), - gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)), - percent: p.isBetween(0, gptConfig.percentMax), - esd: "?number", - judgements: Object.fromEntries( - gptConfig.judgements.map((j) => [j, optNull(p.isInteger)]) - ), - hitMeta, - }, - calculatedData: Object.fromEntries( - gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"]) - ), - })(self); - }, - goals: prSchemaify({ - game: p.isIn(games), - playtype: isValidPlaytype, - timeAdded: p.isPositive, - title: "string", - goalID: "string", - criteria: p.or( - { - mode: p.is("single"), - key: p.isIn( - "scoreData.percent", - "scoreData.lampIndex", - "scoreData.gradeIndex", - "scoreData.score" - ), - value: "number", - }, - { - mode: p.isIn("absolute", "proportion"), - countNum: p.isPositive, - key: p.isIn( - "scoreData.percent", - "scoreData.lampIndex", - "scoreData.gradeIndex", - "scoreData.score" - ), - value: "number", - } - ), - charts: p.or( - { - type: p.is("any"), - }, - { - type: p.is("folder"), - data: "string", - }, - { - type: p.is("multi"), - data: ["string"], - }, - { - type: p.is("single"), - data: "string", - } - ), - }), - milestones: prSchemaify({ - game: p.isIn(games), - playtype: isValidPlaytype, - name: "string", - desc: "string", - milestoneID: "string", - group: "?string", - groupINdex: p.nullable(p.isInteger), - milestoneData: [ - { - title: "string", - desc: "string", - goals: [{ goalID: "string", note: "*string" }], - }, - ], - criteria: p.or( - { - type: "all", - }, - { - type: p.isIn("absolute", "proportion"), - value: p.isPositive, - } - ), - }), - "goal-subs": prSchemaify({ - goalID: "string", - userID: p.isPositiveNonZeroInteger, - game: p.isIn(games), - playtype: isValidPlaytype, - achieved: "boolean", - timeSet: p.isPositive, - timeAchieved: p.nullable(p.isPositive), - lastInteraction: p.nullable(p.isPositive), - progress: "?number", - progressHuman: "string", - outOf: "number", - outOfHuman: "string", - }), - "milestone-subs": prSchemaify({ - milestoneID: "string", - userID: p.isPositiveNonZeroInteger, - game: p.isIn(games), - playtype: isValidPlaytype, - timeSet: p.isPositive, - achieved: "boolean", - timeAchieved: p.nullable(p.isPositive), - progress: p.isInteger, - }), - "recent-folder-views": prSchemaify({ - userID: p.isPositiveNonZeroInteger, - game: p.isIn(games), - playtype: isValidPlaytype, - folderID: "string", - lastViewed: "number", - }), - "milestone-sets": prSchemaify({ - setID: "string", - game: p.isIn(games), - playtype: isValidPlaytype, - milestones: ["string"], - }), migrations: prSchemaify({ migrationID: "string", status: p.isIn("applied", "pending"), @@ -739,4 +60,17 @@ export const DatabaseSchemas: Record< return p.isPositiveInteger(self); }, }), + "orphan-scores": prSchemaify({ + importType: p.isIn(TachiConfig.IMPORT_TYPES), + game: p.isIn(TachiConfig.GAMES), + userID: p.isPositiveNonZeroInteger, + orphanID: "string", + timeInserted: p.isPositive, + errMsg: "?string", + // @todo Type these properly? This will take a lot of effort, as it will require + // formal types for all parser data:context duos. + data: p.any, + context: p.any, + }), + ...SCHEMAS, };