diff --git a/server/.taprc b/server/.taprc index acbfcf97b..4f615a0c3 100644 --- a/server/.taprc +++ b/server/.taprc @@ -3,7 +3,7 @@ test-env: NODE_ENV=test jobs: 1 # disable parallel to avoid db clashes timeout: 120 # might need to be increased ts: true -before: "./src/test-utils/setup.ts" +before: "./src/test-utils/hack-setup.js" check-coverage: true branches: 80 functions: 80 diff --git a/server/package.json b/server/package.json index 7c97c132a..b028fe294 100644 --- a/server/package.json +++ b/server/package.json @@ -44,7 +44,6 @@ }, "dependencies": { "bcrypt": "5.0.1", - "bee-queue": "1.4.0", "commander": "7.2.0", "connect-redis": "6.0.0", "csv-parse": "4.16.0", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 3605c8d7b..c32118f9c 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -19,7 +19,6 @@ specifiers: '@typescript-eslint/eslint-plugin': 4.28.0 '@typescript-eslint/parser': 4.28.0 bcrypt: 5.0.1 - bee-queue: 1.4.0 commander: 7.2.0 connect-redis: 6.0.0 csv-parse: 4.16.0 @@ -58,7 +57,6 @@ specifiers: dependencies: bcrypt: 5.0.1 - bee-queue: 1.4.0 commander: 7.2.0 connect-redis: 6.0.0 csv-parse: 4.16.0 @@ -80,7 +78,7 @@ dependencies: rate-limit-redis: 2.1.0 redis: 3.1.2 rimraf: 3.0.2 - tachi-common: github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63_ts-node@10.0.0+typescript@4.3.4 + tachi-common: github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -352,13 +350,6 @@ packages: '@babel/plugin-syntax-jsx': 7.14.5_@babel+core@7.14.6 '@babel/types': 7.14.5 - /@babel/runtime/7.14.6: - resolution: {integrity: sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.7 - dev: false - /@babel/template/7.14.5: resolution: {integrity: sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==} engines: {node: '>=6.9.0'} @@ -978,15 +969,6 @@ packages: - supports-color dev: false - /bee-queue/1.4.0: - resolution: {integrity: sha512-sAXVUcXe4LIKkVrMN18CrudWbq5atC9VLeBjy1BkODAupXf/MVGfGfxdiv8Uit8JIkztnLU0jQFRQtB1uKuHow==} - engines: {node: '>= 4'} - dependencies: - p-finally: 1.0.0 - promise-callbacks: 3.8.2 - redis: 2.8.0 - dev: false - /binary-extensions/2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -1076,6 +1058,7 @@ packages: dependencies: function-bind: 1.1.1 get-intrinsic: 1.1.1 + dev: true /caller-callsite/2.0.0: resolution: {integrity: sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=} @@ -1383,6 +1366,7 @@ packages: engines: {node: '>= 0.4'} dependencies: object-keys: 1.1.1 + dev: true /delayed-stream/1.0.0: resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} @@ -1454,10 +1438,6 @@ packages: engines: {node: '>=10'} dev: false - /double-ended-queue/2.1.0-0: - resolution: {integrity: sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=} - dev: false - /ecc-jsbn/0.1.2: resolution: {integrity: sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=} dependencies: @@ -1516,6 +1496,7 @@ packages: string.prototype.trimend: 1.0.4 string.prototype.trimstart: 1.0.4 unbox-primitive: 1.0.1 + dev: true /es-to-primitive/1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} @@ -1524,6 +1505,7 @@ packages: is-callable: 1.2.3 is-date-object: 1.0.4 is-symbol: 1.0.4 + dev: true /es6-error/4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -2002,6 +1984,7 @@ packages: /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true /function-loop/2.0.1: resolution: {integrity: sha512-ktIR+O6i/4h+j/ZhZJNdzeI4i9lEPeEK6UPR2EVyTVBqOwcU3Za9xYKLH64ZR9HmcROyRrOkizNyjjtWJzDDkQ==} @@ -2037,6 +2020,7 @@ packages: function-bind: 1.1.1 has: 1.0.3 has-symbols: 1.0.2 + dev: true /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} @@ -2103,6 +2087,7 @@ packages: /has-bigints/1.0.1: resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==} + dev: true /has-flag/3.0.0: resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} @@ -2115,6 +2100,7 @@ packages: /has-symbols/1.0.2: resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==} engines: {node: '>= 0.4'} + dev: true /has-unicode/2.0.1: resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=} @@ -2125,6 +2111,7 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 + dev: true /hasha/5.2.2: resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} @@ -2257,6 +2244,7 @@ packages: /is-bigint/1.0.2: resolution: {integrity: sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==} + dev: true /is-binary-path/2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -2269,10 +2257,12 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 + dev: true /is-callable/1.2.3: resolution: {integrity: sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==} engines: {node: '>= 0.4'} + dev: true /is-core-module/2.4.0: resolution: {integrity: sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==} @@ -2283,6 +2273,7 @@ packages: /is-date-object/1.0.4: resolution: {integrity: sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==} engines: {node: '>= 0.4'} + dev: true /is-extglob/2.1.1: resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} @@ -2311,10 +2302,12 @@ packages: /is-negative-zero/2.0.1: resolution: {integrity: sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==} engines: {node: '>= 0.4'} + dev: true /is-number-object/1.0.5: resolution: {integrity: sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==} engines: {node: '>= 0.4'} + dev: true /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -2326,6 +2319,7 @@ packages: dependencies: call-bind: 1.0.2 has-symbols: 1.0.2 + dev: true /is-stream/2.0.0: resolution: {integrity: sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==} @@ -2334,12 +2328,14 @@ packages: /is-string/1.0.6: resolution: {integrity: sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==} engines: {node: '>= 0.4'} + dev: true /is-symbol/1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.2 + dev: true /is-typedarray/1.0.0: resolution: {integrity: sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=} @@ -2908,10 +2904,12 @@ packages: /object-inspect/1.10.3: resolution: {integrity: sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==} + dev: true /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + dev: true /object.assign/4.1.2: resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} @@ -2921,14 +2919,7 @@ packages: define-properties: 1.1.3 has-symbols: 1.0.2 object-keys: 1.1.1 - - /object.getownpropertydescriptors/2.1.0: - resolution: {integrity: sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==} - engines: {node: '>= 0.8'} - dependencies: - define-properties: 1.1.3 - es-abstract: 1.18.3 - dev: false + dev: true /object.values/1.1.4: resolution: {integrity: sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==} @@ -2986,11 +2977,6 @@ packages: /own-or/1.0.0: resolution: {integrity: sha1-Tod/vtqaLsgAD7wLyuOWRe6L+Nw=} - /p-finally/1.0.0: - resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=} - engines: {node: '>=4'} - dev: false - /p-limit/1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -3162,14 +3148,6 @@ packages: engines: {node: '>=0.4.0'} dev: true - /promise-callbacks/3.8.2: - resolution: {integrity: sha512-g+SziwZr9eLwF+Tejuz0nirmzrYm1Ou4dExaRap1+wG/Bip1FAjMwE+oOqwv6C+CxDCQJ9l0jMSE8ui1oRC/tQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/runtime': 7.14.6 - object.getownpropertydescriptors: 2.1.0 - dev: false - /prop-types/15.7.2: resolution: {integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==} dependencies: @@ -3314,11 +3292,6 @@ packages: engines: {node: '>=4'} dev: false - /redis-parser/2.6.0: - resolution: {integrity: sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=} - engines: {node: '>=0.10.0'} - dev: false - /redis-parser/3.0.0: resolution: {integrity: sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=} engines: {node: '>=4'} @@ -3326,15 +3299,6 @@ packages: redis-errors: 1.2.0 dev: false - /redis/2.8.0: - resolution: {integrity: sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==} - engines: {node: '>=0.10.0'} - dependencies: - double-ended-queue: 2.1.0-0 - redis-commands: 1.7.0 - redis-parser: 2.6.0 - dev: false - /redis/3.1.2: resolution: {integrity: sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==} engines: {node: '>=10'} @@ -3345,10 +3309,6 @@ packages: redis-parser: 3.0.0 dev: false - /regenerator-runtime/0.13.7: - resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==} - dev: false - /regexpp/3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} @@ -3684,12 +3644,14 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.3 + dev: true /string.prototype.trimstart/1.0.4: resolution: {integrity: sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==} dependencies: call-bind: 1.0.2 define-properties: 1.1.3 + dev: true /string_decoder/0.10.31: resolution: {integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=} @@ -4045,6 +4007,7 @@ packages: has-bigints: 1.0.1 has-symbols: 1.0.2 which-boxed-primitive: 1.0.2 + dev: true /unicode-length/2.0.2: resolution: {integrity: sha512-Ph/j1VbS3/r77nhoY2WU0GWGjVYOHL3xpKp0y/Eq2e5r0mT/6b649vm7KFO6RdAdrZkYLdxphYVgvODxPB+Ebg==} @@ -4107,6 +4070,7 @@ packages: is-number-object: 1.0.5 is-string: 1.0.6 is-symbol: 1.0.4 + dev: true /which-module/2.0.0: resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=} @@ -4233,9 +4197,9 @@ packages: engines: {node: '>=6'} dev: true - github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63_ts-node@10.0.0+typescript@4.3.4: - resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/478605bdec986f10af3af32be3bf7716ecec6d63} - id: github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63 + github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d_ts-node@10.0.0+typescript@4.3.4: + resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/20ff262e31119c918684141bf82d92076425a77d} + id: github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d name: tachi-common version: 0.1.0 dependencies: diff --git a/server/scripts/package.json b/server/scripts/package.json index 38144c5b1..c62e57e5f 100644 --- a/server/scripts/package.json +++ b/server/scripts/package.json @@ -1,7 +1,7 @@ { - "name": "ktblack-server-scripts", + "name": "tachi-server-scripts", "version": "0.0.0", - "description": "Scripts and their dependencies for KTBlack. This lets us separate dependencies to avoid unecessary overhead in the CI/CD pipeline.", + "description": "Scripts and their dependencies for Tachi-Server. This lets us separate dependencies to avoid unecessary overhead in the CI/CD pipeline.", "main": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/server/scripts/save-testdata-from-ktblack.ts b/server/scripts/save-testdata-from-ktblack.ts index b6cd69e68..486f021c1 100644 --- a/server/scripts/save-testdata-from-ktblack.ts +++ b/server/scripts/save-testdata-from-ktblack.ts @@ -8,7 +8,7 @@ const program = new Command(); const logger = CreateLogCtx(__filename); -program.option("-c, --collection ", "The collection to fetch ktblack data from."); +program.option("-c, --collection ", "The collection to fetch tachi data from."); program.parse(process.argv); const options = program.opts(); @@ -18,16 +18,16 @@ logger.info(`Connecting to ${process.env.MONGO_BASE_URL}/ktblackdb...`); const ktBlackDB = monk(`${process.env.MONGO_BASE_URL}/ktblackdb`); (async () => { - logger.info(`Fetching data for ${collection}...`); - const data = await ktBlackDB.get(collection).find({}, { projection: { _id: 0 } }); + logger.info(`Fetching data for ${collection}...`); + const data = await ktBlackDB.get(collection).find({}, { projection: { _id: 0 } }); - logger.info(`Fetched ${data.length} documents, Writing...`); + logger.info(`Fetched ${data.length} documents, Writing...`); - fs.writeFileSync( - path.join(__dirname, `../src/test-utils/test-data/tachi/ktblack-${collection}.json`), - JSON.stringify(data) - ); + fs.writeFileSync( + path.join(__dirname, `../src/test-utils/test-data/tachi/ktblack-${collection}.json`), + JSON.stringify(data) + ); - logger.info("Done!"); - process.exit(0); + logger.info("Done!"); + process.exit(0); })(); diff --git a/server/scripts/set-indexes.ts b/server/scripts/set-indexes.ts index 0f1d25e6b..cf0c65395 100644 --- a/server/scripts/set-indexes.ts +++ b/server/scripts/set-indexes.ts @@ -6,8 +6,8 @@ const program = new Command(); program.option("-d, --db ", "The database to index."); program.option( - "-r, --reset", - "Whether to reset all indexes on this database before indexing or not." + "-r, --reset", + "Whether to reset all indexes on this database before indexing or not." ); program.parse(process.argv); diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index e3c90e759..fcd47fc5c 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -18,6 +18,7 @@ import { AnySongDocument, AnyChartDocument, UserGameStats, + TableDocument, UserGoalDocument, PBScoreDocument, UserMilestoneDocument, @@ -139,6 +140,7 @@ const db = { "api-tokens": monkDB.get("api-tokens"), "orphan-scores": monkDB.get("orphan-scores"), "import-locks": monkDB.get("import-locks"), + tables: monkDB.get("tables"), }; export default db; diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index 6e6b19725..b6c79eb75 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -60,6 +60,7 @@ const staticIndexes: Partial> = { index({ game: 1, playtype: 1 }), index({ game: 1, playtype: 1, table: 1 }), index({ game: 1, playtype: 1, table: 1, tableIndex: 1 }), + index({ title: "text" }), ], "kai-auth-tokens": [index({ userID: 1, service: 1 }, UNIQUE)], "charts-iidx": [ @@ -71,6 +72,7 @@ const staticIndexes: Partial> = { ], "bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)], "api-tokens": [index({ token: 1 }, UNIQUE)], + tables: [index({ tableID: 1 }, UNIQUE), index({ game: 1, playtype: 1 })], }; const indexes: Partial> = staticIndexes; diff --git a/server/src/lib/logger/logger.ts b/server/src/lib/logger/logger.ts index 2fbf9fd2b..16a2d6936 100644 --- a/server/src/lib/logger/logger.ts +++ b/server/src/lib/logger/logger.ts @@ -52,15 +52,15 @@ const formatExcessPropertiesNoStack = (meta: Record, omitKeys: return ` ${JSON.stringify(realMeta)}`; }; -const ktblackPrintf = format.printf( - ({ level, message, context = "ktblack-root", timestamp, ...meta }) => +const tachiPrintf = format.printf( + ({ level, message, context = "tachi-root", timestamp, ...meta }) => `${timestamp} [${ Array.isArray(context) ? context.join(" | ") : context }] ${level}: ${message}${formatExcessProperties(meta)}` ); -const ktblackConsolePrintf = format.printf( - ({ level, message, context = "ktblack-root", timestamp, hideFromConsole, ...meta }) => +const tachiConsolePrintf = format.printf( + ({ level, message, context = "tachi-root", timestamp, hideFromConsole, ...meta }) => `${timestamp} [${ Array.isArray(context) ? context.join(" | ") : context }] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole)}` @@ -85,13 +85,13 @@ const baseFormatRoute = format.combine( const defaultFormatRoute = format.combine( baseFormatRoute, format.errors({ stack: false }), - ktblackPrintf + tachiPrintf ); const consoleFormatRoute = format.combine( baseFormatRoute, format.errors({ stack: false }), - ktblackConsolePrintf, + tachiConsolePrintf, format.colorize({ all: true, }) @@ -109,11 +109,11 @@ if (IN_TESTING) { } else { tports = [ new transports.File({ - filename: "logs/ktblack-error.log", + filename: "logs/tachi-error.log", level: "error", format: defaultFormatRoute, }), - new transports.File({ filename: "logs/ktblack.log", format: defaultFormatRoute }), + new transports.File({ filename: "logs/tachi.log", format: defaultFormatRoute }), new transports.Console({ format: consoleFormatRoute, }), diff --git a/server/src/lib/score-import/framework/goals/goals.test.ts b/server/src/lib/score-import/framework/goals/goals.test.ts index 0a70f38d7..af2e64501 100644 --- a/server/src/lib/score-import/framework/goals/goals.test.ts +++ b/server/src/lib/score-import/framework/goals/goals.test.ts @@ -97,7 +97,7 @@ t.test("#GetRelevantGoals", (t) => { await db.songs.iidx.remove({}); await db.charts.iidx.remove({}); await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); + await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json")); const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 }); const goals: GoalDocument[] = lotsOfCharts.map((e) => ({ diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts index 79dd0ff5d..b996c068d 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts @@ -4,7 +4,7 @@ import CreateLogCtx from "../../../../logger/logger"; import ResetDBState from "../../../../../test-utils/resets"; import { GetKTDataJSON, - LoadKTBlackIIDXData, + LoadTachiIIDXData, Testing511Song, Testing511SPA, } from "../../../../../test-utils/test-data"; @@ -58,7 +58,7 @@ t.test("#ConvertFileMerIIDX", (t) => { }); t.test("Valid DP Conversion", async (t) => { - await LoadKTBlackIIDXData(); + await LoadTachiIIDXData(); const res = await merc({ play_type: "DOUBLE" }); t.hasStrict( diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts index 53f46a674..27f05fb44 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts @@ -3,7 +3,7 @@ import CreateLogCtx from "../../../../logger/logger"; import ResetDBState from "../../../../../test-utils/resets"; import { GetKTDataJSON, - LoadKTBlackIIDXData, + LoadTachiIIDXData, Testing511Song, Testing511SPA, } from "../../../../../test-utils/test-data"; @@ -87,7 +87,7 @@ t.test("#ConvertFileS3", (t) => { }); t.test("Should find song case-insensitively", async (t) => { - await LoadKTBlackIIDXData(); + await LoadTachiIIDXData(); const res = await mfile({ songname: "aBSolUte", diff: 7 }); diff --git a/server/src/lib/search/search.test.ts b/server/src/lib/search/search.test.ts index e6f37402f..a59b017ec 100644 --- a/server/src/lib/search/search.test.ts +++ b/server/src/lib/search/search.test.ts @@ -2,7 +2,7 @@ import t from "tap"; import db from "../../external/mongo/db"; import { CloseAllConnections } from "../../test-utils/close-connections"; import ResetDBState from "../../test-utils/resets"; -import { LoadKTBlackIIDXData } from "../../test-utils/test-data"; +import { LoadTachiIIDXData } from "../../test-utils/test-data"; import { SearchGameSongs, SearchCollection, @@ -13,7 +13,7 @@ import { t.test("#SearchGameSongs", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.beforeEach(async () => { await db.songs.iidx.dropIndexes(); await db.songs.iidx.createIndex( diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts index 93c67d3b3..596162bd2 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts @@ -1,14 +1,14 @@ import t from "tap"; import db from "../../../../../../../../external/mongo/db"; -import ResetDBState, { SetIndexesForDB } from "../../../../../../../../test-utils/resets"; -import { LoadKTBlackIIDXData, Testing511SPA } from "../../../../../../../../test-utils/test-data"; +import ResetDBState from "../../../../../../../../test-utils/resets"; +import { LoadTachiIIDXData, Testing511SPA } from "../../../../../../../../test-utils/test-data"; import { PBScoreDocument } from "tachi-common"; import mockApi from "../../../../../../../../test-utils/mock-api"; import { CloseAllConnections } from "../../../../../../../../test-utils/close-connections"; t.test("GET /api/v1/games/:game/:playtype/charts", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return the most popular charts if no param is set.", async (t) => { await db["personal-bests"].insert([ @@ -48,8 +48,6 @@ t.test("GET /api/v1/games/:game/:playtype/charts", (t) => { }); t.test("Should search charts if a search param is set.", async (t) => { - await SetIndexesForDB(); - await db["personal-bests"].insert([ { chartID: "fc7edc6bcfa701a261c89c999ddbba3e2195597b", //gambol hyper diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts new file mode 100644 index 000000000..ed181a033 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts @@ -0,0 +1,22 @@ +import { RequestHandler } from "express"; +import db from "../../../../../../../../external/mongo/db"; +import { SYMBOL_TachiData } from "../../../../../../../../lib/constants/tachi"; +import { AssignToReqTachiData } from "../../../../../../../../utils/req-tachi-data"; + +export const GetFolderFromParam: RequestHandler = async (req, res, next) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + + const folder = await db.folders.findOne({ folderID: req.params.folderID, game, playtype }); + + if (!folder) { + return res.status(404).json({ + success: false, + description: `This folder does not exist.`, + }); + } + + AssignToReqTachiData(req, { folderDoc: folder }); + + return next(); +}; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts new file mode 100644 index 000000000..5a84bfe20 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts @@ -0,0 +1,143 @@ +import t from "tap"; +import db from "../../../../../../../../external/mongo/db"; +import mockApi from "../../../../../../../../test-utils/mock-api"; +import ResetDBState from "../../../../../../../../test-utils/resets"; +import deepmerge from "deepmerge"; +import { FolderDocument } from "tachi-common"; +import { CloseAllConnections } from "../../../../../../../../test-utils/close-connections"; +import { Testing511SPA } from "../../../../../../../../test-utils/test-data"; +import { CreateFolderChartLookup } from "../../../../../../../../utils/folder"; + +const mockFolder: FolderDocument = { + folderID: "foo", + game: "iidx", + playtype: "SP", + title: "12", + data: { + level: "10", + }, + type: "charts", +}; + +t.test("GET /api/v1/games/:game/:playtype/folders", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should search the folders for this game.", async (t) => { + await db.folders.insert([ + deepmerge(mockFolder, {}), + deepmerge(mockFolder, { folderID: "bar", playtype: "DP" }), + deepmerge(mockFolder, { folderID: "baz", game: "bms" }), + ]); + + const res = await mockApi.get("/api/v1/games/iidx/SP/folders?search=12"); + + t.equal(res.body.body.length, 1); + t.equal(res.body.body[0].folderID, "foo"); + + t.end(); + }); + + t.test("Should return 400 if no search parameter is given.", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/folders"); + + t.equal(res.statusCode, 400); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/games/:game/:playtype/folders/:folderID", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return the folder at this ID.", async (t) => { + await db.folders.insert(deepmerge(mockFolder, {})); + await CreateFolderChartLookup(mockFolder); + + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/foo"); + + t.equal(res.body.body.folder.folderID, "foo"); + t.equal(res.body.body.songs.length, 1); + t.equal(res.body.body.charts.length, 1); + t.equal(res.body.body.songs[0].id, 1); + t.equal(res.body.body.charts[0].chartID, Testing511SPA.chartID); + + t.end(); + }); + + t.test("Should return 404 if the folder does not exist.", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/bar"); + + t.equal(res.statusCode, 404); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/games/:game/:playtype/folders/:folderID/tierlist", (t) => { + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + await db.folders.insert(deepmerge(mockFolder, {})); + await CreateFolderChartLookup(mockFolder); + }); + + t.test("Should return the songs, charts, folder and tierlist information.", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/foo/tierlist?type=lamp"); + + t.equal(res.body.body.charts.length, 1); + t.equal(res.body.body.songs.length, 1); + t.equal(res.body.body.songs[0].id, 1); + t.equal(res.body.body.charts[0].chartID, Testing511SPA.chartID); + t.equal(res.body.body.folder.folderID, "foo"); + t.equal(res.body.body.tierlistData.length, 2); + t.equal(res.body.body.tierlistData[0].chartID, Testing511SPA.chartID); + t.equal(res.body.body.tierlistData[1].chartID, Testing511SPA.chartID); + t.equal(res.body.body.tierlistData[0].type, "lamp"); + t.equal(res.body.body.tierlistData[1].type, "lamp"); + + t.end(); + }); + + t.test("Should return 404 if the specified tierlistID does not exist.", async (t) => { + const res = await mockApi.get( + "/api/v1/games/iidx/SP/folders/foo/tierlist?tierlistID=garbage&type=lamp" + ); + + t.equal(res.statusCode, 404); + + t.end(); + }); + + t.test("Should return 501 if the specified game has no default tierlist.", async (t) => { + await db.tierlists.remove({ game: "iidx", playtype: "SP" }); + + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/foo/tierlist?type=lamp"); + + t.equal(res.statusCode, 501); + + t.end(); + }); + + t.test("Should return 400 if no type is given", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/foo/tierlist"); + + t.equal(res.statusCode, 400); + + t.end(); + }); + + t.test("Should return 400 if an invalid type is given", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/folders/foo/tierlist?type=nonsense"); + + t.equal(res.statusCode, 400); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts new file mode 100644 index 000000000..7b8d8e7f4 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts @@ -0,0 +1,127 @@ +import { Router } from "express"; +import db from "../../../../../../../../external/mongo/db"; +import { SYMBOL_TachiData } from "../../../../../../../../lib/constants/tachi"; +import { SearchCollection } from "../../../../../../../../lib/search/search"; +import { GetFolderCharts } from "../../../../../../../../utils/folder"; +import { IsString } from "../../../../../../../../utils/misc"; +import { GetDefaultTierlist } from "../../../../../../../../utils/tierlist"; +import { GetFolderFromParam } from "./middleware"; + +const router: Router = Router({ mergeParams: true }); + +/** + * Search the folders for this game. + * + * @param search - The query to search for. + * + * @name GET /api/v1/games/:game/:playtype/folders + */ +router.get("/", async (req, res) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + + if (!IsString(req.query.search)) { + return res.status(400).json({ + success: false, + description: `Invalid value for search.`, + }); + } + + const folders = await SearchCollection(db.folders, req.query.search, { game, playtype }, 100); + + return res.status(200).json({ + success: true, + description: `Returned ${folders.length} folders.`, + body: folders, + }); +}); + +/** + * Get the folder at this ID, alongside its charts and songs. + * + * @name GET /api/v1/games/:game/:playtype/folders/:folderID + */ +router.get("/:folderID", GetFolderFromParam, async (req, res) => { + const folder = req[SYMBOL_TachiData]!.folderDoc!; + + const { songs, charts } = await GetFolderCharts(folder, {}, true); + + return res.status(200).json({ + success: true, + description: `Returned data for folder ${folder.title}`, + body: { + songs, + charts, + folder, + }, + }); +}); + +/** + * Return all of the tierlist information for the charts inside this folder. + * + * @param tierlistID - Optionally, specify and alternative tierlistID to use. + * If not present, uses the game's default. If the game does not have a + * default, 501 is returned. + * @param type - The type of tierlist data to return. Can be "lamp" or + * "score". + */ +router.get("/:folderID/tierlist", GetFolderFromParam, async (req, res) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + const folder = req[SYMBOL_TachiData]!.folderDoc!; + + if (req.query.type !== "lamp" && req.query.type !== "score") { + return res.status(400).json({ + success: false, + description: `Invalid value of type. Expected lamp or score.`, + }); + } + + let tierlist; + + if (IsString(req.query.tierlistID)) { + tierlist = await db.tierlists.findOne({ + tierlistID: req.query.tierlistID, + game, + playtype, + }); + + if (!tierlist) { + return res.status(404).json({ + success: false, + description: `A tierlist with this ID (for this game + playtype) does not exist.`, + }); + } + } else { + tierlist = await GetDefaultTierlist(game, playtype); + + if (!tierlist) { + return res.status(501).json({ + success: false, + description: `This game does not support tierlists, yet.`, + }); + } + } + + const { charts, songs } = await GetFolderCharts(folder, {}, true); + + const tierlistData = await db["tierlist-data"].find({ + chartID: { $in: charts.map((e) => e.chartID) }, + tierlistID: tierlist!.tierlistID, + }); + + return res.status(200).json({ + success: true, + description: `Returned ${tierlistData.length} tierlist data documents.`, + body: { + charts, + songs, + tierlistData, + tierlist, + folder, + }, + }); +}); + +export default router; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/router.ts index ff41c9d4e..193d4b26e 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/router.ts @@ -9,6 +9,8 @@ import { GetUsersWithIDs } from "../../../../../../../utils/user"; import chartsRouter from "./charts/router"; import songIDRouter from "./songs/_songID/router"; import { ValidatePlaytypeFromParam } from "./middleware"; +import foldersRouter from "./folders/router"; +import tablesRouter from "./tables/router"; const router: Router = Router({ mergeParams: true }); @@ -108,5 +110,7 @@ router.get("/leaderboard", async (req, res) => { router.use("/charts", chartsRouter); router.use("/songs/:songID", songIDRouter); +router.use("/folders", foldersRouter); +router.use("/tables", tablesRouter); export default router; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.test.ts index 40e41a5e2..d916b39f0 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.test.ts @@ -2,11 +2,11 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../../../../../test-utils/close-connections"; import mockApi from "../../../../../../../../../test-utils/mock-api"; import ResetDBState from "../../../../../../../../../test-utils/resets"; -import { LoadKTBlackIIDXData } from "../../../../../../../../../test-utils/test-data"; +import { LoadTachiIIDXData } from "../../../../../../../../../test-utils/test-data"; t.test("GET /api/v1/games/:game/:playtype/songs/:songID", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return the song at this ID and all of its charts.", async (t) => { const res = await mockApi.get("/api/v1/games/iidx/SP/songs/1"); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts new file mode 100644 index 000000000..8e17667c1 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts @@ -0,0 +1,22 @@ +import { RequestHandler } from "express"; +import db from "../../../../../../../../external/mongo/db"; +import { SYMBOL_TachiData } from "../../../../../../../../lib/constants/tachi"; +import { AssignToReqTachiData } from "../../../../../../../../utils/req-tachi-data"; + +export const GetTableFromParam: RequestHandler = async (req, res, next) => { + const game = req[SYMBOL_TachiData]!.game; + const playtype = req[SYMBOL_TachiData]!.playtype; + + const table = await db.tables.findOne({ tableID: req.params.tableID, game, playtype }); + + if (!table) { + return res.status(404).json({ + success: false, + description: `This table does not exist.`, + }); + } + + AssignToReqTachiData(req, { tableDoc: table }); + + return next(); +}; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts new file mode 100644 index 000000000..2d3b18613 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts @@ -0,0 +1,53 @@ +import t from "tap"; +import db from "../../../../../../../../external/mongo/db"; +import { CloseAllConnections } from "../../../../../../../../test-utils/close-connections"; +import mockApi from "../../../../../../../../test-utils/mock-api"; +import ResetDBState from "../../../../../../../../test-utils/resets"; +import { TestingIIDXFolderSP10 } from "../../../../../../../../test-utils/test-data"; +import deepmerge from "deepmerge"; +import { FolderDocument } from "tachi-common"; + +t.test("GET /api/v1/games/:game/:playtype/tables", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return all folders for this game.", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/tables"); + + t.equal(res.body.body.length, 1); + t.equal(res.body.body[0].tableID, "mock_table"); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/games/:game/:playtype/tables/:tableID", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return the table at this ID and its folders.", async (t) => { + await db.folders.insert( + deepmerge(TestingIIDXFolderSP10, { folderID: "testing_folder" }) as FolderDocument + ); + + const res = await mockApi.get("/api/v1/games/iidx/SP/tables/mock_table"); + + t.equal(res.body.body.folders.length, 1); + t.equal(res.body.body.folders[0].folderID, "testing_folder"); + t.equal(res.body.body.table.tableID, "mock_table"); + + t.end(); + }); + + t.test("Should return 404 if the table does not exist.", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/tables/non_existant_table"); + + t.equal(res.statusCode, 404); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts new file mode 100644 index 000000000..facc07cdb --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts @@ -0,0 +1,47 @@ +import { Router } from "express"; +import db from "../../../../../../../../external/mongo/db"; +import { SYMBOL_TachiData } from "../../../../../../../../lib/constants/tachi"; +import { GetTableFromParam } from "./middleware"; +import { GetFoldersFromTable } from "../../../../../../../../utils/folder"; + +const router: Router = Router({ mergeParams: true }); + +/** + * Return all the tables for this game. + * + * @name GET /api/v1/games/:game/:playtype/tables + */ +router.get("/", async (req, res) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + + const tables = await db.tables.find({ game, playtype }); + + return res.status(200).json({ + success: true, + description: `Returned ${tables.length} tables.`, + body: tables, + }); +}); + +/** + * Return the folder documents that make up this table. + * + * @name GET /api/v1/games/:game/:playtype/tables/:tableID + */ +router.get("/:tableID", GetTableFromParam, async (req, res) => { + const table = req[SYMBOL_TachiData]!.tableDoc!; + + const folders = await GetFoldersFromTable(table); + + return res.status(200).json({ + success: true, + description: `Returned ${folders.length} for table ${table.title}.`, + body: { + folders, + table, + }, + }); +}); + +export default router; diff --git a/server/src/server/router/api/v1/import/router.test.ts b/server/src/server/router/api/v1/import/router.test.ts index d64ace0f4..ca6db4dc0 100644 --- a/server/src/server/router/api/v1/import/router.test.ts +++ b/server/src/server/router/api/v1/import/router.test.ts @@ -2,26 +2,25 @@ import t from "tap"; import mockApi from "../../../../../test-utils/mock-api"; import { GetKTDataBuffer, - LoadKTBlackIIDXData, + LoadTachiIIDXData, TestingIIDXEamusementCSV26, TestingIIDXEamusementCSV27, } from "../../../../../test-utils/test-data"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; import { RequireAuthPerms } from "../../../../../test-utils/api-common"; import { CreateFakeAuthCookie } from "../../../../../test-utils/fake-auth"; -import ResetDBState, { SetIndexesForDB } from "../../../../../test-utils/resets"; +import ResetDBState from "../../../../../test-utils/resets"; import db from "../../../../../external/mongo/db"; t.test("POST /api/v1/import/file", async (t) => { const cookie = await CreateFakeAuthCookie(mockApi); - t.before(SetIndexesForDB); t.beforeEach(ResetDBState); RequireAuthPerms("/api/v1/import/file", "submit_score", "POST"); t.test("file/eamusement-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Mini HV import", async (t) => { const res = await mockApi @@ -109,7 +108,7 @@ t.test("POST /api/v1/import/file", async (t) => { // thats right i literally just copied it t.test("file/pli-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Mini HV import", async (t) => { const res = await mockApi @@ -260,7 +259,7 @@ t.test("POST /api/v1/import/file", async (t) => { }); t.test("file/mer-iidx", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Example Import", async (t) => { const res = await mockApi @@ -298,7 +297,7 @@ t.test("POST /api/v1/import/file", async (t) => { }); t.skip("file/solid-state-squad", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Large Import", async (t) => { const res = await mockApi diff --git a/server/src/server/router/api/v1/search/router.test.ts b/server/src/server/router/api/v1/search/router.test.ts index 95f371bb2..0135b59dc 100644 --- a/server/src/server/router/api/v1/search/router.test.ts +++ b/server/src/server/router/api/v1/search/router.test.ts @@ -1,15 +1,13 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; import mockApi from "../../../../../test-utils/mock-api"; -import ResetDBState, { SetIndexesForDB } from "../../../../../test-utils/resets"; -import { LoadKTBlackIIDXData } from "../../../../../test-utils/test-data"; +import ResetDBState from "../../../../../test-utils/resets"; +import { LoadTachiIIDXData } from "../../../../../test-utils/test-data"; import { AnySongDocument } from "tachi-common"; -t.before(SetIndexesForDB); - t.test("GET /api/v1/search", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should search users and songs.", async (t) => { const res = await mockApi.get("/api/v1/search?search=zkldi"); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts new file mode 100644 index 000000000..e7852052e --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts @@ -0,0 +1,132 @@ +import t from "tap"; +import db from "../../../../../../../../../../../external/mongo/db"; +import { CloseAllConnections } from "../../../../../../../../../../../test-utils/close-connections"; +import ResetDBState from "../../../../../../../../../../../test-utils/resets"; +import deepmerge from "deepmerge"; +import { + Testing511SPA, + TestingIIDXFolderSP10, + TestingIIDXSPScore, + TestingIIDXSPScorePB, +} from "../../../../../../../../../../../test-utils/test-data"; +import { FolderDocument } from "tachi-common"; +import mockApi from "../../../../../../../../../../../test-utils/mock-api"; +import { CreateFolderChartLookup } from "../../../../../../../../../../../utils/folder"; + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID", (t) => { + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + const folder = deepmerge(TestingIIDXFolderSP10, { + folderID: "testing_folder", + }) as FolderDocument; + await db.folders.insert(folder); + await CreateFolderChartLookup(folder); + await db["personal-bests"].insert(deepmerge(TestingIIDXSPScorePB, {})); + }); + + t.test("Should return a user's PBs on a given folder.", async (t) => { + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/folders/testing_folder"); + + t.equal(res.body.body.songs.length, 1); + t.equal(res.body.body.charts.length, 1); + t.equal(res.body.body.charts[0].chartID, Testing511SPA.chartID); + t.equal(res.body.body.pbs.length, 1); + t.equal(res.body.body.pbs[0].chartID, Testing511SPA.chartID); + t.equal(res.body.body.folder.folderID, "testing_folder"); + + t.end(); + }); + + t.test("Should return 404 if this folder does not exist.", async (t) => { + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/folders/fake_folder"); + + t.equal(res.statusCode, 404); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID/timeline", (t) => { + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + const folder = deepmerge(TestingIIDXFolderSP10, { + folderID: "testing_folder", + }) as FolderDocument; + await db.folders.insert(folder); + await CreateFolderChartLookup(folder); + await db["personal-bests"].insert(deepmerge(TestingIIDXSPScorePB, {})); + }); + + t.test("Should return the users scores on this folder in timeline order", async (t) => { + const res = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=lamp&criteriaValue=4" + ); + + t.equal(res.body.songs.length, 1); + t.equal(res.body.charts.length, 1); + t.equal(res.body.scores.length, 1); + t.equal(res.body.scores[0].scoreID, "TESTING_SCORE_ID"); + + t.end(); + }); + + t.test("Should reject invalid criteriaTypes", async (t) => { + const res = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=invalid" + ); + + t.equal(res.statusCode, 400); + + t.end(); + }); + + t.test("Should reject invalid indexes for lamps", async (t) => { + const res = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=lamp&criteriaValue=-1" + ); + + t.equal(res.statusCode, 400); + + const res2 = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=lamp&criteriaValue=0.5" + ); + + t.equal(res2.statusCode, 400); + + const res3 = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=lamp&criteriaValue=100" + ); + + t.equal(res3.statusCode, 400); + + t.end(); + }); + + t.test("Should reject invalid indexes for grades", async (t) => { + const res = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=grade&criteriaValue=-1" + ); + + t.equal(res.statusCode, 400); + + const res2 = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=grade&criteriaValue=0.5" + ); + + t.equal(res2.statusCode, 400); + + const res3 = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/folders/testing_folder/timeline?criteriaType=grade&criteriaValue=100" + ); + + t.equal(res3.statusCode, 400); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts new file mode 100644 index 000000000..c72f0a15e --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts @@ -0,0 +1,129 @@ +import { FilterQuery } from "mongodb"; +import { GetGamePTConfig, ScoreDocument } from "tachi-common"; +import { Router } from "express"; +import db from "../../../../../../../../../../../external/mongo/db"; +import { SYMBOL_TachiData } from "../../../../../../../../../../../lib/constants/tachi"; +import { GetFolderCharts, GetPBsOnFolder } from "../../../../../../../../../../../utils/folder"; +import { ParseStrPositiveInt } from "../../../../../../../../../../../utils/string-checks"; +import { GetFolderFromParam } from "../../../../../../../games/_game/_playtype/folders/middleware"; + +const router: Router = Router({ mergeParams: true }); + +router.use(GetFolderFromParam); + +/** + * Returns a users pbs on this folder. + * + * @name GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID + */ +router.get("/", async (req, res) => { + const user = req[SYMBOL_TachiData]!.requestedUser!; + const folder = req[SYMBOL_TachiData]!.folderDoc!; + + const { songs, charts, pbs } = await GetPBsOnFolder(user.id, folder); + + return res.status(200).json({ + success: true, + description: `Returned ${pbs.length} pbs.`, + body: { + songs, + charts, + pbs, + folder, + }, + }); +}); + +// note: this path is disgustingly long. :( +/** + * Returns the users scores in order of when they met this criteria. + * + * @param criteriaType - either "lamp" or "grade". + * @param crtieriaValue - An index for this lamp or grade. + * + * @name GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID/timeline + */ +router.get("/timeline", async (req, res) => { + const user = req[SYMBOL_TachiData]!.requestedUser!; + const folder = req[SYMBOL_TachiData]!.folderDoc!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + const gptConfig = GetGamePTConfig(game, playtype); + + const intIndex = ParseStrPositiveInt(req.query.criteriaValue); + + const { songs, charts } = await GetFolderCharts(folder, {}, true); + + if (intIndex === null) { + return res.status(400).json({ + success: false, + description: `Invalid value for criteriaValue.`, + }); + } + + const matchCriteria: FilterQuery = { + userID: user.id, + game, + playtype, + chartID: { $in: charts.map((e) => e.chartID) }, + }; + + if (req.query.criteriaType === "lamp") { + if (!gptConfig.lamps[intIndex]) { + return res.status(400).json({ + success: false, + description: `Invalid index for this games' lamps.`, + }); + } + + matchCriteria["scoreData.lampIndex"] = { $gte: intIndex }; + } else if (req.query.criteriaType === "grade") { + if (!gptConfig.grades[intIndex]) { + return res.status(400).json({ + success: false, + description: `Invalid index for this games' grades.`, + }); + } + + matchCriteria["scoreData.gradeIndex"] = { $gte: intIndex }; + } else { + return res.status(400).json({ + success: false, + description: `Invalid criteriaType. Expected "lamp" or "grade".`, + }); + } + + // Returns a unique score per-chart that was the first score to achieve + // this criteria on that chart. + const scores = await db.scores + .aggregate([ + { + $match: matchCriteria, + }, + { + $sort: { + timeAchieved: -1, + }, + }, + { + $group: { + _id: "$chartID", + doc: { $first: "$$ROOT" }, + }, + }, + ]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .then((r) => r.map((e: any) => e.doc)); + + return res.status(200).json({ + success: true, + description: `Returned ${scores.length} scores for ${charts.length} charts.`, + body: { + songs, + charts, + scores, + }, + }); +}); + +export default router; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts index a7d36d214..74e0e27eb 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts @@ -5,7 +5,7 @@ import mockApi from "../../../../../../../../../../test-utils/mock-api"; import ResetDBState from "../../../../../../../../../../test-utils/resets"; import { Testing511Song, - LoadKTBlackIIDXData, + LoadTachiIIDXData, GetKTDataJSON, TestingIIDXSPScorePB, Testing511SPA, @@ -58,7 +58,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => { t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return 400 if no search param is given", async (t) => { const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs"); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts index 722911d59..f170a6b5b 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts @@ -2,7 +2,7 @@ import t from "tap"; import db from "../../../../../../../../../external/mongo/db"; import { CloseAllConnections } from "../../../../../../../../../test-utils/close-connections"; import mockApi from "../../../../../../../../../test-utils/mock-api"; -import ResetDBState, { SetIndexesForDB } from "../../../../../../../../../test-utils/resets"; +import ResetDBState from "../../../../../../../../../test-utils/resets"; import { ScoreDocument, UserGoalDocument, @@ -12,7 +12,7 @@ import { } from "tachi-common"; import { GetKTDataJSON, - LoadKTBlackIIDXData, + LoadTachiIIDXData, Testing511Song, Testing511SPA, TestingIIDXSPScore, @@ -20,8 +20,6 @@ import { } from "../../../../../../../../../test-utils/test-data"; import deepmerge from "deepmerge"; -t.before(SetIndexesForDB); - t.test("GET /api/v1/users/:userID/games/:game/:playtype/", (t) => { t.beforeEach(ResetDBState); @@ -299,7 +297,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/recent", (t) => { t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return 400 if no search param is given", async (t) => { const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/scores"); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts index 11a51d9d1..e5c339cec 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts @@ -18,6 +18,8 @@ import { CheckStrProfileAlg } from "../../../../../../../../../utils/string-chec import { IsString } from "../../../../../../../../../utils/misc"; import pbsRouter from "./pbs/router"; import sessionsRouter from "./sessions/router"; +import foldersFolderIDRouter from "./folders/_folderID/router"; +import tablesRouter from "./tables/router"; const router: Router = Router({ mergeParams: true }); @@ -396,5 +398,7 @@ router.get("/leaderboard-adjacent", async (req, res) => { router.use("/pbs", pbsRouter); router.use("/sessions", sessionsRouter); +router.use("/tables", tablesRouter); +router.use("/folders/:folderID", foldersFolderIDRouter); export default router; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts index f29c37518..e2ce26d2e 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts @@ -3,12 +3,12 @@ import db from "../../../../../../../../../../external/mongo/db"; import { CloseAllConnections } from "../../../../../../../../../../test-utils/close-connections"; import mockApi from "../../../../../../../../../../test-utils/mock-api"; import ResetDBState from "../../../../../../../../../../test-utils/resets"; -import { LoadKTBlackIIDXData } from "../../../../../../../../../../test-utils/test-data"; +import { LoadTachiIIDXData } from "../../../../../../../../../../test-utils/test-data"; import { SessionDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return 400 if no search param is given", async (t) => { const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions"); @@ -70,7 +70,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => { t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(LoadTachiIIDXData); t.test("Should return a user's best 100 sessions.", async (t) => { const sessions: SessionDocument[] = []; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts new file mode 100644 index 000000000..95e31e8fb --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts @@ -0,0 +1,50 @@ +import t from "tap"; +import db from "../../../../../../../../../../external/mongo/db"; +import { CloseAllConnections } from "../../../../../../../../../../test-utils/close-connections"; +import mockApi from "../../../../../../../../../../test-utils/mock-api"; +import ResetDBState from "../../../../../../../../../../test-utils/resets"; +import { + TestingIIDXFolderSP10, + TestingIIDXSPScorePB, +} from "../../../../../../../../../../test-utils/test-data"; +import deepmerge from "deepmerge"; +import { FolderDocument } from "tachi-common"; +import { CreateFolderChartLookup } from "../../../../../../../../../../utils/folder"; + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/tables/:tableID", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return a users statistics for this table", async (t) => { + const folder = deepmerge(TestingIIDXFolderSP10, { + folderID: "testing_folder", + }) as FolderDocument; + await db.folders.insert(folder); + + await CreateFolderChartLookup(folder); + + await db["personal-bests"].insert(TestingIIDXSPScorePB); + + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/tables/mock_table"); + + t.equal(res.body.body.folders.length, 1); + t.equal(res.body.body.folders[0].folderID, "testing_folder"); + t.equal(res.body.body.table.tableID, "mock_table"); + t.equal(res.body.body.stats.length, 1); + t.equal(res.body.body.stats[0].lamps["EX HARD CLEAR"], 1); + t.equal(res.body.body.stats[0].grades.AAA, 1); + + t.end(); + }); + + t.test("Should return 404 if the table does not exist.", async (t) => { + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/tables/bad_table"); + + t.equal(res.statusCode, 404); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts new file mode 100644 index 000000000..ee66b2cae --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts @@ -0,0 +1,47 @@ +import { Router } from "express"; +import { SYMBOL_TachiData } from "../../../../../../../../../../lib/constants/tachi"; +import { + CalculateGradeDistribution, + CalculateLampDistribution, + GetFoldersFromTable, + GetPBsOnFolder, +} from "../../../../../../../../../../utils/folder"; +// @todo maybe refactor where middleware is stored to avoid paths this ugly. +import { GetTableFromParam } from "../../../../../../games/_game/_playtype/tables/middleware"; + +const router: Router = Router({ mergeParams: true }); + +/** + * Retrieves a users statistics on this table. + * + * @name GET /api/v1/users/:userID/games/:game/:playtype/tables/:tableID + */ +router.get("/:tableID", GetTableFromParam, async (req, res) => { + const table = req[SYMBOL_TachiData]!.tableDoc!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + + const folders = await GetFoldersFromTable(table); + + // @optimisable - Requests a lot of charts we don't care about + // could be cached too, i guess. + const stats = await Promise.all( + folders.map((f) => + GetPBsOnFolder(user.id, f).then((r) => ({ + grades: CalculateGradeDistribution(r.pbs), + lamps: CalculateLampDistribution(r.pbs), + })) + ) + ); + + return res.status(200).json({ + success: true, + description: `Returned stats for ${folders.length} folders.`, + body: { + folders, + stats, + table, + }, + }); +}); + +export default router; diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 8944549e5..f38397f3c 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -286,7 +286,7 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { t.test("Should accept a fervidex-static body", async (t) => { await db.songs.iidx.remove({}); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); + await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json")); await db.charts.iidx.remove({}); await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); diff --git a/server/src/test-utils/hack-setup.js b/server/src/test-utils/hack-setup.js new file mode 100644 index 000000000..8a5418ddb --- /dev/null +++ b/server/src/test-utils/hack-setup.js @@ -0,0 +1,10 @@ +// This runs as JS because of https://github.com/tapjs/node-tap/issues/761 + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { execSync } = require("child_process"); +const path = require("path"); + +const assert = require("assert"); + +assert(process.env.NODE_ENV === "test", "NODE_ENV WAS NOT TEST, BAILING"); +execSync(`ts-node ${path.join(__dirname, "./setup.ts")}`); diff --git a/server/src/test-utils/mock-db/tables.json b/server/src/test-utils/mock-db/tables.json new file mode 100644 index 000000000..9161bdca3 --- /dev/null +++ b/server/src/test-utils/mock-db/tables.json @@ -0,0 +1,8 @@ +[{ + "tableID": "mock_table", + "game": "iidx", + "playtype": "SP", + "title": "testing table", + "description": "something", + "folders": ["testing_folder"] +}] \ No newline at end of file diff --git a/server/src/test-utils/setup.ts b/server/src/test-utils/setup.ts index e7a45bd5b..f7908a28c 100644 --- a/server/src/test-utils/setup.ts +++ b/server/src/test-utils/setup.ts @@ -1,14 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const assert = require("assert"); -const path = require("path"); -const fs = require("fs"); +import { SetIndexesForDB } from "./resets"; -assert(process.env.NODE_ENV === "test", "NODE_ENV WAS NOT TEST, BAILING"); - -// empty the logs. -try { - fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests-error.log")); - fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests.log")); -} catch (err) { - // no logs to remove, who cares! -} +SetIndexesForDB().then(() => process.exit(0)); diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index cdc2b2f9c..2c56baceb 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -110,8 +110,6 @@ export const TestingIIDXFolderSP10: FolderDocument = { playtype: "SP", type: "charts", folderID: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", - table: "Levels", - tableIndex: 10, data: { level: "10", "flags¬IN BASE GAME": true, @@ -160,7 +158,7 @@ export const TestingIIDXSPMilestone: MilestoneDocument = { let KTDATA_CACHE: { songs: unknown[]; charts: unknown[] } | undefined; -export async function LoadKTBlackIIDXData() { +export async function LoadTachiIIDXData() { let songs; let charts; @@ -168,7 +166,7 @@ export async function LoadKTBlackIIDXData() { songs = KTDATA_CACHE.songs; charts = KTDATA_CACHE.charts; } else { - songs = GetKTDataJSON("./tachi/ktblack-songs-iidx.json"); + songs = GetKTDataJSON("./tachi/tachi-songs-iidx.json"); charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); KTDATA_CACHE = { songs, charts }; } diff --git a/server/src/utils/folder.ts b/server/src/utils/folder.ts index a951f06fe..a90576bc9 100644 --- a/server/src/utils/folder.ts +++ b/server/src/utils/folder.ts @@ -1,6 +1,16 @@ +import { + Grades, + Lamps, + IDStrings, + TableDocument, + AnyChartDocument, + AnySongDocument, + integer, + PBScoreDocument, + FolderDocument, +} from "tachi-common"; import db from "../external/mongo/db"; import CreateLogCtx from "../lib/logger/logger"; -import { FolderDocument, AnyChartDocument, AnySongDocument } from "tachi-common"; import { FilterQuery } from "mongodb"; import deepmerge from "deepmerge"; @@ -83,7 +93,7 @@ export async function GetFolderCharts( export async function GetFolderCharts( folder: FolderDocument, filter: FilterQuery, - getSongs?: false + getSongs: false ): Promise<{ charts: AnyChartDocument[] }>; export async function GetFolderCharts( folder: FolderDocument, @@ -148,3 +158,59 @@ export async function InitaliseFolderChartLookup() { logger.info(`Completed InitialiseFolderChartLookup.`); } + +export async function GetFoldersFromTable(table: TableDocument) { + const folders = await db.folders.find({ + folderID: { $in: table.folders }, + }); + + if (folders.length !== table.folders.length) { + // this is an error, but we can return anyway. + logger.warn( + `Table ${table.tableID} has a mismatch of real folders to stored folders. ${table.folders.length} -> ${folders.length}` + ); + } + + return folders; +} + +export async function GetPBsOnFolder(userID: integer, folder: FolderDocument) { + const { charts, songs } = await GetFolderCharts(folder, {}, true); + + const pbs = await db["personal-bests"].find({ + userID, + chartID: { $in: charts.map((e) => e.chartID) }, + }); + + return { pbs, charts, songs }; +} + +export function CalculateLampDistribution(pbs: PBScoreDocument[]) { + const lampDist: Partial> = {}; + + for (const pb of pbs) { + if (lampDist[pb.scoreData.lamp]) { + // @ts-expect-error ??? + lampDist[pb.scoreData.lamp]++; + } else { + lampDist[pb.scoreData.lamp] = 1; + } + } + + return lampDist; +} + +export function CalculateGradeDistribution(pbs: PBScoreDocument[]) { + const gradeDist: Partial> = {}; + + for (const pb of pbs) { + if (gradeDist[pb.scoreData.grade]) { + // @ts-expect-error ??? + gradeDist[pb.scoreData.grade]++; + } else { + gradeDist[pb.scoreData.grade] = 1; + } + } + + return gradeDist; +} diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index ded715ed6..d0edf3e31 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -1,4 +1,6 @@ import { + FolderDocument, + TableDocument, integer, SessionDocument, ScoreDocument, @@ -57,4 +59,6 @@ export interface TachiRequestData { songDoc?: AnySongDocument; scoreDoc?: ScoreDocument; sessionDoc?: SessionDocument; + tableDoc?: TableDocument; + folderDoc?: FolderDocument; }