diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index c1e1f21bb..f52db7f6c 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -60,7 +60,7 @@ dependencies: express-async-errors: 3.1.1_express@4.17.1 express-session: 1.17.1 fast-json-stable-hash: 1.0.2 - kamaitachi-common: github.com/zkldi/Kamaitachi-common/07931179339f18fb85bf4a554b81a1fffe010c1f + kamaitachi-common: github.com/zkldi/Kamaitachi-common/3ee7ec47e9f5e76a956f09af4fe851d159e31b25 mongodb: 3.6.6 monk: 7.3.4 multer: 1.4.2 @@ -4637,8 +4637,8 @@ packages: '@types/yoga-layout': 1.9.2 dev: true - github.com/zkldi/Kamaitachi-common/07931179339f18fb85bf4a554b81a1fffe010c1f: - resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/07931179339f18fb85bf4a554b81a1fffe010c1f} + github.com/zkldi/Kamaitachi-common/3ee7ec47e9f5e76a956f09af4fe851d159e31b25: + resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/3ee7ec47e9f5e76a956f09af4fe851d159e31b25} name: kamaitachi-common version: 0.1.0 dependencies: diff --git a/server/src/api/api.ts b/server/src/api/api.ts index f1add2653..5c40944c4 100644 --- a/server/src/api/api.ts +++ b/server/src/api/api.ts @@ -1,10 +1,12 @@ import { Router } from "express"; import authRouter from "./auth/auth"; import importRouter from "./import/import"; +import irRouter from "./ir/ir"; -const router = Router({ mergeParams: true }); +const router: Router = Router({ mergeParams: true }); router.use("/auth", authRouter); router.use("/import", importRouter); +router.use("/ir", irRouter); export default router; diff --git a/server/src/api/import/import.ts b/server/src/api/import/import.ts index 035ca49ae..53db4d307 100644 --- a/server/src/api/import/import.ts +++ b/server/src/api/import/import.ts @@ -97,8 +97,8 @@ router.post( } ); -import ParseEamusementCSV from "../../score-import/import-types/file/csv_eamusement-iidx/parser"; -import ParseBatchManual from "../../score-import/import-types/file/json_batch-manual/parser"; +import ParseEamusementCSV from "../../score-import/import-types/file/csv/eamusement-iidx/parser"; +import ParseBatchManual from "../../score-import/import-types/file/json/batch-manual/parser"; /** * Resolves the data from a file upload into an iterable, diff --git a/server/src/api/ir/direct-manual/direct-manual.ts b/server/src/api/ir/direct-manual/direct-manual.ts index 4a59e6d2f..bb759a6ab 100644 --- a/server/src/api/ir/direct-manual/direct-manual.ts +++ b/server/src/api/ir/direct-manual/direct-manual.ts @@ -1,12 +1,39 @@ import { Router } from "express"; +import { GetUserWithID } from "../../../core/user-core"; +import CreateLogCtx from "../../../logger"; import { RequireLoggedIn } from "../../../middleware/require-logged-in"; +import { ExpressWrappedScoreImportMain } from "../../../score-import/framework/express-wrapper"; +import ParseDirectManual from "../../../score-import/import-types/ir/json/direct-manual/parser"; -const router = Router({ mergeParams: true }); +const router: Router = Router({ mergeParams: true }); + +const logger = CreateLogCtx("direct-manual.ts"); /** * Imports scores in ir/json:direct-manual form. * @name /api/ir/direct-manual/import */ -router.post("/import", RequireLoggedIn, async (req, res) => {}); +router.post("/import", RequireLoggedIn, async (req, res) => { + const userDoc = await GetUserWithID(req.session.ktchi!.userID); + + if (!userDoc) { + logger.severe( + `User ${req.session.ktchi!.userID} does not have an associated user document.` + ); + return res.status(500).json({ + success: false, + description: "An internal error has occured.", + }); + } + + let responseData = await ExpressWrappedScoreImportMain( + userDoc, + true, + "ir/json:direct-manual", + (logger) => ParseDirectManual(req.body, logger) + ); + + return res.status(responseData.statusCode).json(responseData.body); +}); export default router; diff --git a/server/src/api/ir/ir.ts b/server/src/api/ir/ir.ts index 92c73b1eb..16d7fc085 100644 --- a/server/src/api/ir/ir.ts +++ b/server/src/api/ir/ir.ts @@ -1,5 +1,8 @@ import { Router } from "express"; +import directManualIR from "./direct-manual/direct-manual"; const router: Router = Router({ mergeParams: true }); +router.use("/direct-manual", directManualIR); + export default router; diff --git a/server/src/score-import/framework/score-import-main.ts b/server/src/score-import/framework/score-import-main.ts index e170eb941..362910aca 100644 --- a/server/src/score-import/framework/score-import-main.ts +++ b/server/src/score-import/framework/score-import-main.ts @@ -59,6 +59,7 @@ export default async function ScoreImportMain( const importTimeStart = process.hrtime.bigint(); let importInfo = await ImportAllIterableData( user.id, + importType, iterable, ConverterFunction, context, diff --git a/server/src/score-import/framework/score-importing/converter-failures.ts b/server/src/score-import/framework/score-importing/converter-failures.ts index eb6161097..a4eed7b09 100644 --- a/server/src/score-import/framework/score-importing/converter-failures.ts +++ b/server/src/score-import/framework/score-importing/converter-failures.ts @@ -1,6 +1,6 @@ // Various Errors that can occur during processing. - -import { Converters } from "../../import-types/import-types"; +import { ImportTypes } from "kamaitachi-common"; +import { ImportTypeContextMap, ImportTypeDataMap } from "../../../types"; export class ConverterFailure { message: string | null; @@ -15,16 +15,16 @@ export class ConverterFailure { * belongs to. The identifier used to try and match this chart is stored here. * Alongside any other fields used in the query. */ -export class KTDataNotFoundFailure extends ConverterFailure { - data: Parameters[0]; - converterContext: Parameters[1]; +export class KTDataNotFoundFailure extends ConverterFailure { + data: ImportTypeDataMap[T]; + converterContext: ImportTypeContextMap[T]; importType: T; constructor( message: string | null, importType: T, - data: Parameters[0], - context: Parameters[1] + data: ImportTypeDataMap[T], + context: ImportTypeContextMap[T] ) { super(message); diff --git a/server/src/score-import/framework/score-importing/score-importing.ts b/server/src/score-import/framework/score-importing/score-importing.ts index ff616c105..f8845101a 100644 --- a/server/src/score-import/framework/score-importing/score-importing.ts +++ b/server/src/score-import/framework/score-importing/score-importing.ts @@ -4,6 +4,7 @@ import { integer, ScoreDocument, AnySongDocument, + ImportTypes, } from "kamaitachi-common"; import { DryScore, @@ -34,6 +35,7 @@ import { AppendLogCtx } from "../../../logger"; */ export async function ImportAllIterableData( userID: integer, + importType: ImportTypes, iterableData: Iterable | AsyncIterable, ConverterFunction: ConverterFunction, context: C, @@ -47,7 +49,9 @@ export async function ImportAllIterableData( // An example would be making an api request after exhausting // the first set of data. for await (const data of iterableData) { - promises.push(ImportIterableDatapoint(userID, data, ConverterFunction, context, logger)); + promises.push( + ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger) + ); } // Due to the fact that ProcessIterableDatapoint may return an array instead of a single result @@ -104,6 +108,7 @@ export async function ImportAllIterableData( */ export async function ImportIterableDatapoint( userID: integer, + importType: ImportTypes, data: D, ConverterFunction: ConverterFunction, context: C, @@ -112,7 +117,7 @@ export async function ImportIterableDatapoint( let converterReturns: ConverterFunctionReturns; try { - converterReturns = await ConverterFunction(data, context, logger); + converterReturns = await ConverterFunction(data, context, importType, logger); } catch (err) { converterReturns = err; } diff --git a/server/src/score-import/import-types/file/json_batch-manual/converter.test.ts b/server/src/score-import/import-types/common/batch-manual/converter.test.ts similarity index 91% rename from server/src/score-import/import-types/file/json_batch-manual/converter.test.ts rename to server/src/score-import/import-types/common/batch-manual/converter.test.ts index 1098bb45a..7138fa965 100644 --- a/server/src/score-import/import-types/file/json_batch-manual/converter.test.ts +++ b/server/src/score-import/import-types/common/batch-manual/converter.test.ts @@ -2,7 +2,7 @@ import t from "tap"; import CreateLogCtx from "../../../../logger"; import { Testing511Song, Testing511SPA } from "../../../../test-utils/test-data"; -import ConverterFn, { ResolveChartFromSong, ResolveMatchTypeToKTData } from "./converter"; +import { ResolveChartFromSong, ResolveMatchTypeToKTData, ConverterFn } from "./converter"; import deepmerge from "deepmerge"; import escapeStringRegexp from "../../../../core/escape-string-regexp"; import { CloseMongoConnection } from "../../../../db/db"; @@ -34,11 +34,13 @@ const ktdWrap = (msg: string, game: Game = "iidx", version = null): any => ({ const logger = CreateLogCtx("converter.test.ts"); +const importType = "file/json:batch-manual" as const; + t.test("#ResolveMatchTypeToKTData", (t) => { t.beforeEach(ResetDBState); t.test("Should resolve for the songID if the matchType is songID", async (t) => { - let res = await ResolveMatchTypeToKTData(baseBatchManualScore, context, logger); + let res = await ResolveMatchTypeToKTData(baseBatchManualScore, context, importType, logger); t.hasStrict( res, @@ -52,6 +54,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { // @ts-expect-error bad deepmerge(baseBatchManualScore, { identifier: "90000" }), context, + importType, logger ), ktdWrap("Cannot find song with songID 90000") @@ -64,6 +67,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { let res = await ResolveMatchTypeToKTData( deepmerge(baseBatchManualScore, { matchType: "songTitle", identifier: "5.1.1." }), context, + importType, logger ); @@ -81,6 +85,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { identifier: "INVALID_TITLE", }), context, + importType, logger ), ktdWrap("Cannot find song with title INVALID_TITLE") @@ -96,6 +101,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { let resMD5 = await ResolveMatchTypeToKTData( deepmerge(baseBatchManualScore, { matchType: "bmsChartHash", identifier: GAZER17MD5 }), context, + importType, logger ); @@ -112,6 +118,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { identifier: GAZER17SHA256, }), context, + importType, logger ); @@ -130,6 +137,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { identifier: "bad_hash", }), context, + importType, logger ), ktdWrap("Cannot find chart with hash ", "bms") @@ -149,6 +157,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { difficulty: "EXPERT", }), context, + importType, logger ); @@ -169,6 +178,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { difficulty: "EXPERT", }), context, + importType, logger ), ktdWrap("Cannot find chart with hash", "ddr") @@ -185,6 +195,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { matchType: "BAD_MATCHTYPE", }), context, + importType, logger ), new InvalidScoreFailure(`Invalid matchType BAD_MATCHTYPE`) as any @@ -203,7 +214,8 @@ t.test("#ResolveChartFromSong", (t) => { let res = await ResolveChartFromSong( Testing511Song, baseBatchManualScore, // has playtype + diff - { game: "iidx", service: "foo", version: null } + { game: "iidx", service: "foo", version: null }, + importType ); t.hasStrict(res, Testing511SPA as any); @@ -217,7 +229,8 @@ t.test("#ResolveChartFromSong", (t) => { ResolveChartFromSong( Testing511Song, deepmerge(baseBatchManualScore, { playtype: null }), - { game: "iidx", service: "foo", version: null } + { game: "iidx", service: "foo", version: null }, + importType ), new InvalidScoreFailure( `Missing 'playtype' field, but was necessary for this lookup.` @@ -233,7 +246,8 @@ t.test("#ResolveChartFromSong", (t) => { ResolveChartFromSong( Testing511Song, deepmerge(baseBatchManualScore, { difficulty: null }), - { game: "iidx", service: "foo", version: null } + { game: "iidx", service: "foo", version: null }, + importType ), new InvalidScoreFailure( `Missing 'difficulty' field, but was necessary for this lookup.` @@ -250,7 +264,8 @@ t.test("#ResolveChartFromSong", (t) => { Testing511Song, // @ts-expect-error faulty deepmerge types deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }), - { game: "iidx", service: "foo", version: null } + { game: "iidx", service: "foo", version: null }, + importType ), new InvalidScoreFailure( `Invalid Difficulty for iidx SP - Expected any of BEGINNER, NORMAL, HYPER, ANOTHER, LEGGENDARIA` @@ -267,7 +282,8 @@ t.test("#ResolveChartFromSong", (t) => { Testing511Song, // @ts-expect-error faulty deepmerge types deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol) - { game: "iidx", service: "foo", version: null } + { game: "iidx", service: "foo", version: null }, + importType ), ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)") ); @@ -276,11 +292,16 @@ t.test("#ResolveChartFromSong", (t) => { }); t.test("Should successfully lookup if version is provided.", async (t) => { - let res = await ResolveChartFromSong(Testing511Song, baseBatchManualScore, { - game: "iidx", - service: "foo", - version: "27", - }); + let res = await ResolveChartFromSong( + Testing511Song, + baseBatchManualScore, + { + game: "iidx", + service: "foo", + version: "27", + }, + importType + ); t.hasStrict(res, Testing511SPA as any); @@ -295,6 +316,7 @@ t.test("#ConverterFn", (t) => { let res = await ConverterFn( baseBatchManualScore, { game: "iidx", service: "foo", version: null }, + importType, logger ); @@ -329,6 +351,7 @@ t.test("#ConverterFn", (t) => { // @ts-expect-error broken deepmerge deepmerge(baseBatchManualScore, { score: 1000 }), { game: "iidx", service: "foo", version: null }, + importType, logger ), { message: /Invalid score of 1000, produced percent of 127/u } as any diff --git a/server/src/score-import/import-types/file/json_batch-manual/converter.ts b/server/src/score-import/import-types/common/batch-manual/converter.ts similarity index 89% rename from server/src/score-import/import-types/file/json_batch-manual/converter.ts rename to server/src/score-import/import-types/common/batch-manual/converter.ts index 721890a3c..3dc6fb3e4 100644 --- a/server/src/score-import/import-types/file/json_batch-manual/converter.ts +++ b/server/src/score-import/import-types/common/batch-manual/converter.ts @@ -1,6 +1,6 @@ import { ConverterFnReturn, ConverterFunction, DryScore, KtLogger } from "../../../../types"; -import { BatchManualContext, BatchManualScore } from "./types"; -import { AnyChartDocument, AnySongDocument } from "kamaitachi-common"; +import { BatchManualContext, BatchManualScore } from "../../common/batch-manual/types"; +import { AnyChartDocument, AnySongDocument, ImportTypes } from "kamaitachi-common"; import { InternalFailure, InvalidScoreFailure, @@ -23,14 +23,21 @@ import { } from "../../../framework/common/score-utils"; import { gamePercentMax } from "kamaitachi-common/js/config"; -const ConverterFn: ConverterFunction = async ( +/** + * Creates a ConverterFn for the BatchManualScore format. This curries + * the importType into the function, so the right failures can be + * returned. + * @returns A BatchManualScore Converter. + */ +export const ConverterFn: ConverterFunction = async ( data, context, - logger: KtLogger + importType, + logger ): Promise => { let game = context.game; - let { song, chart } = await ResolveMatchTypeToKTData(data, context, logger); + let { song, chart } = await ResolveMatchTypeToKTData(data, context, importType, logger); let percent = GenericCalculatePercent(game, data.score, chart); @@ -46,7 +53,7 @@ const ConverterFn: ConverterFunction = asy game: game, service: context.service, comment: data.comment ?? null, - importType: "file/json:batch-manual", + importType, timeAchieved: data.timeAchieved ?? null, scoreData: { lamp: data.lamp, @@ -69,6 +76,7 @@ const ConverterFn: ConverterFunction = asy export async function ResolveMatchTypeToKTData( data: BatchManualScore, context: BatchManualContext, + importType: ImportTypes, logger: KtLogger ): Promise<{ song: AnySongDocument; chart: AnyChartDocument }> { const game = context.game; @@ -83,7 +91,7 @@ export async function ResolveMatchTypeToKTData( if (!chart) { throw new KTDataNotFoundFailure( `Cannot find chart for hash ${data.identifier}.`, - "file/json:batch-manual", + importType, data, context ); @@ -121,7 +129,7 @@ export async function ResolveMatchTypeToKTData( if (!chart) { throw new KTDataNotFoundFailure( `Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`, - "file/json:batch-manual", + importType, data, context ); @@ -146,13 +154,13 @@ export async function ResolveMatchTypeToKTData( if (!song) { throw new KTDataNotFoundFailure( `Cannot find song with songID ${data.identifier}.`, - "file/json:batch-manual", + importType, data, context ); } - let chart = await ResolveChartFromSong(song, data, context); + let chart = await ResolveChartFromSong(song, data, context, importType); return { song, chart }; } else if (data.matchType === "songTitle" || data.matchType === "title") { @@ -161,13 +169,13 @@ export async function ResolveMatchTypeToKTData( if (!song) { throw new KTDataNotFoundFailure( `Cannot find song with title ${data.identifier}.`, - "file/json:batch-manual", + importType, data, context ); } - let chart = await ResolveChartFromSong(song, data, context); + let chart = await ResolveChartFromSong(song, data, context, importType); return { song, chart }; } @@ -182,7 +190,8 @@ export async function ResolveMatchTypeToKTData( export async function ResolveChartFromSong( song: AnySongDocument, data: BatchManualScore, - context: BatchManualContext + context: BatchManualContext, + importType: ImportTypes ) { let game = context.game; @@ -217,7 +226,7 @@ export async function ResolveChartFromSong( if (!chart) { throw new KTDataNotFoundFailure( `Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`, - "file/json:batch-manual", + importType, data, context ); @@ -225,5 +234,3 @@ export async function ResolveChartFromSong( return chart; } - -export default ConverterFn; diff --git a/server/src/score-import/import-types/file/json_batch-manual/parser.test.ts b/server/src/score-import/import-types/common/batch-manual/parser.test.ts similarity index 82% rename from server/src/score-import/import-types/file/json_batch-manual/parser.test.ts rename to server/src/score-import/import-types/common/batch-manual/parser.test.ts index ac1aa3c30..0a70b25cb 100644 --- a/server/src/score-import/import-types/file/json_batch-manual/parser.test.ts +++ b/server/src/score-import/import-types/common/batch-manual/parser.test.ts @@ -2,7 +2,7 @@ import t from "tap"; import { CloseMongoConnection } from "../../../../db/db"; import CreateLogCtx from "../../../../logger"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import ParserFn from "./parser"; +import { ParseBatchManualFromObject as ParserFn } from "./parser"; import { BatchManual } from "./types"; import escapeRegex from "../../../../core/escape-string-regexp"; import deepmerge from "deepmerge"; @@ -47,7 +47,7 @@ function dm(sc: any) { t.test("#ParserFn", (t) => { t.test("Non-Object", (t) => { t.throws( - () => ParserFn(fileify(false), {}, logger), + () => ParserFn(false, "file/json:batch-manual", logger), new ScoreImportFatalError( 400, "Invalid BATCH-MANUAL (Not an object, recieved boolean.)" @@ -60,7 +60,7 @@ t.test("#ParserFn", (t) => { t.test("No Header", (t) => { t.throws( - () => ParserFn(fileify({ body: [] }), {}, logger), + () => ParserFn({ body: [] }, "file/json:batch-manual", logger), new ScoreImportFatalError( 400, "Could not retrieve head.game - is this valid BATCH-MANUAL?" @@ -73,7 +73,8 @@ t.test("#ParserFn", (t) => { t.test("No Game", (t) => { t.throws( - () => ParserFn(fileify({ body: [], head: { service: "foo" } }), {}, logger), + () => + ParserFn({ body: [], head: { service: "foo" } }, "file/json:batch-manual", logger), new ScoreImportFatalError( 400, "Could not retrieve head.game - is this valid BATCH-MANUAL?" @@ -88,8 +89,8 @@ t.test("#ParserFn", (t) => { t.throws( () => ParserFn( - fileify({ body: [], head: { service: "foo", game: "invalid_game" } }), - {}, + { body: [], head: { service: "foo", game: "invalid_game" } }, + "file/json:batch-manual", logger ), new ScoreImportFatalError( @@ -100,7 +101,12 @@ t.test("#ParserFn", (t) => { ); t.throws( - () => ParserFn(fileify({ body: [], head: { service: "foo", game: 123 } }), {}, logger), + () => + ParserFn( + { body: [], head: { service: "foo", game: 123 } }, + "file/json:batch-manual", + logger + ), new ScoreImportFatalError( 400, "Invalid game 123 - expected any of iidx, museca, maimai, jubeat, popn, sdvx, ddr, bms, chunithm, gitadora, usc" @@ -113,7 +119,12 @@ t.test("#ParserFn", (t) => { t.test("Invalid Service", (t) => { t.throws( - () => ParserFn(fileify({ body: [], head: { service: "1", game: "iidx" } }), {}, logger), + () => + ParserFn( + { body: [], head: { service: "1", game: "iidx" } }, + "file/json:batch-manual", + logger + ), new ScoreImportFatalError( 400, "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [string]." @@ -122,7 +133,12 @@ t.test("#ParserFn", (t) => { ); t.throws( - () => ParserFn(fileify({ body: [], head: { service: 1, game: "iidx" } }), {}, logger), + () => + ParserFn( + { body: [], head: { service: 1, game: "iidx" } }, + "file/json:batch-manual", + logger + ), new ScoreImportFatalError( 400, "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [number]." @@ -135,8 +151,8 @@ t.test("#ParserFn", (t) => { t.test("Valid Empty BATCH-MANUAL", (t) => { let res = ParserFn( - fileify({ body: [], head: { service: "foo", game: "iidx" } }), - {}, + { body: [], head: { service: "foo", game: "iidx" } }, + "file/json:batch-manual", logger ); @@ -190,7 +206,7 @@ t.test("#ParserFn", (t) => { ], head: { service: "foo", game: "iidx" }, } as BatchManual), - {}, + "file/json:batch-manual", logger ); @@ -239,8 +255,8 @@ t.test("#ParserFn", (t) => { t.test("Valid HitMeta", (t) => { let res = ParserFn( - fileify(dm({ hitMeta: { bp: 10, gauge: 100, gaugeHistory: null, comboBreak: 7 } })), - {}, + dm({ hitMeta: { bp: 10, gauge: 100, gaugeHistory: null, comboBreak: 7 } }), + "file/json:batch-manual", logger ); @@ -275,8 +291,8 @@ t.test("#ParserFn", (t) => { t.test("Valid HitData", (t) => { let res = ParserFn( - fileify(dm({ hitData: { pgreat: 1, great: null, bad: 0 } })), - {}, + dm({ hitData: { pgreat: 1, great: null, bad: 0 } }), + "file/json:batch-manual", logger ); @@ -328,7 +344,7 @@ t.test("#ParserFn", (t) => { ], head: { service: "foo", game: "iidx" }, }), - {}, + "file/json:batch-manual", logger ); @@ -344,7 +360,7 @@ t.test("#ParserFn", (t) => { }); t.test("Non-numeric score", (t) => { - let fn = () => ParserFn(fileify(dm({ score: "123" })), {}, logger); + let fn = () => ParserFn(dm({ score: "123" }), "file/json:batch-manual", logger); t.throws( fn, @@ -358,7 +374,8 @@ t.test("#ParserFn", (t) => { }); t.test("Invalid timeAchieved", (t) => { - let fn = () => ParserFn(fileify(dm({ timeAchieved: "string" })), {}, logger); + let fn = () => + ParserFn(dm({ timeAchieved: "string" }), "file/json:batch-manual", logger); t.throws( fn, @@ -369,7 +386,11 @@ t.test("#ParserFn", (t) => { ); let fn2 = () => - ParserFn(fileify(dm({ timeAchieved: 1_620_768_609_637 / 1000 })), {}, logger); + ParserFn( + dm({ timeAchieved: 1_620_768_609_637 / 1000 }), + "file/json:batch-manual", + logger + ); t.throws( fn2, @@ -385,7 +406,7 @@ t.test("#ParserFn", (t) => { t.test("Invalid Playtype", (t) => { // this is not a valid playtype for IIDX - let fn = () => ParserFn(fileify(dm({ playtype: "Single" })), {}, logger); + let fn = () => ParserFn(dm({ playtype: "Single" }), "file/json:batch-manual", logger); t.throws( fn, @@ -400,7 +421,7 @@ t.test("#ParserFn", (t) => { t.test("Invalid Identifier", (t) => { // this is not a valid playtype for IIDX - let fn = () => ParserFn(fileify(dm({ identifier: null })), {}, logger); + let fn = () => ParserFn(dm({ identifier: null }), "file/json:batch-manual", logger); t.throws(fn, mockErr("body[0].identifier | Expected string", "Received null [null]")); @@ -408,7 +429,8 @@ t.test("#ParserFn", (t) => { }); t.test("Invalid MatchType", (t) => { - let fn = () => ParserFn(fileify(dm({ matchType: "Invalid_MatchType" })), {}, logger); + let fn = () => + ParserFn(dm({ matchType: "Invalid_MatchType" }), "file/json:batch-manual", logger); t.throws( fn, @@ -422,11 +444,13 @@ t.test("#ParserFn", (t) => { }); t.test("Invalid HitData", (t) => { - let fn = () => ParserFn(fileify(dm({ hitData: { not_key: 123 } })), {}, logger); + let fn = () => + ParserFn(dm({ hitData: { not_key: 123 } }), "file/json:batch-manual", logger); t.throws(fn, mockErr("body[0].hitData | Invalid Key not_key")); - let fn2 = () => ParserFn(fileify(dm({ hitData: { pgreat: "123" } })), {}, logger); + let fn2 = () => + ParserFn(dm({ hitData: { pgreat: "123" } }), "file/json:batch-manual", logger); t.throws( fn2, @@ -437,11 +461,12 @@ t.test("#ParserFn", (t) => { }); t.test("Invalid HitMeta", (t) => { - let fn = () => ParserFn(fileify(dm({ hitMeta: { not_key: 123 } })), {}, logger); + let fn = () => + ParserFn(dm({ hitMeta: { not_key: 123 } }), "file/json:batch-manual", logger); t.throws(fn, mockErr("body[0].hitMeta | Unexpected")); - let fn2 = () => ParserFn(fileify(dm({ hitMeta: { bp: -1 } })), {}, logger); + let fn2 = () => ParserFn(dm({ hitMeta: { bp: -1 } }), "file/json:batch-manual", logger); t.throws(fn2, mockErr("body[0].hitMeta.bp | Expected a positive integer")); diff --git a/server/src/score-import/import-types/file/json_batch-manual/parser.ts b/server/src/score-import/import-types/common/batch-manual/parser.ts similarity index 86% rename from server/src/score-import/import-types/file/json_batch-manual/parser.ts rename to server/src/score-import/import-types/common/batch-manual/parser.ts index 813fe8ab1..ea0ae7cbe 100644 --- a/server/src/score-import/import-types/file/json_batch-manual/parser.ts +++ b/server/src/score-import/import-types/common/batch-manual/parser.ts @@ -3,8 +3,8 @@ import ScoreImportFatalError from "../../../framework/score-importing/score-impo import { BatchManual, BatchManualContext, BatchManualScore } from "./types"; import p, { PrudenceSchema, ValidSchemaValue } from "prudence"; import { lamps, supportedGames, validHitData, validPlaytypes } from "kamaitachi-common/js/config"; -import { Game } from "kamaitachi-common"; -import ConverterFn from "./converter"; +import { Game, ImportTypes } from "kamaitachi-common"; +import { ConverterFn } from "./converter"; import deepmerge from "deepmerge"; const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v)); @@ -108,41 +108,30 @@ const PR_BatchManual = (game: Game): PrudenceSchema => ({ }); /** - * Parses a buffer of BATCH-MANUAL data. - * @param fileData - The buffer to parse. + * Parses an object of BATCH-MANUAL data. + * @param object - The object to parse. * @param body - The request body that made this file import request. */ -function ParseBatchManual( - fileData: Express.Multer.File, - body: Record, +export function ParseBatchManualFromObject( + object: unknown, + importType: ImportTypes, logger: KtLogger ): ParserFunctionReturnsSync { - let jsonData: unknown; - - try { - jsonData = JSON.parse(fileData.buffer.toString("utf-8")); - } catch (err) { - throw new ScoreImportFatalError( - 400, - `Invalid JSON. (${err?.message ?? "No Error Message Available."})` - ); - } - // now to perform some basic validation so we can return // the iterable - if (typeof jsonData !== "object" || jsonData === null) { + if (typeof object !== "object" || object === null) { throw new ScoreImportFatalError( 400, `Invalid BATCH-MANUAL (Not an object, recieved ${ - jsonData === null ? "null" : typeof jsonData + object === null ? "null" : typeof object }.)` ); } // attempt to retrieve game // @ts-expect-error man. - let possiblyGame = jsonData?.head?.game; + let possiblyGame = object?.head?.game; if (!possiblyGame) { throw new ScoreImportFatalError( @@ -165,7 +154,7 @@ function ParseBatchManual( // This mostly works as a sanity check, and doesn't // check things like whether a score is > 100% // or something. - let err = p(jsonData, PR_BatchManual(game)); + let err = p(object, PR_BatchManual(game)); if (err) { throw new ScoreImportFatalError( @@ -181,7 +170,7 @@ function ParseBatchManual( ); } - let batchManual = jsonData as BatchManual; + let batchManual = object as BatchManual; return { game, @@ -194,5 +183,3 @@ function ParseBatchManual( ConverterFunction: ConverterFn, }; } - -export default ParseBatchManual; diff --git a/server/src/score-import/import-types/file/json_batch-manual/types.ts b/server/src/score-import/import-types/common/batch-manual/types.ts similarity index 100% rename from server/src/score-import/import-types/file/json_batch-manual/types.ts rename to server/src/score-import/import-types/common/batch-manual/types.ts diff --git a/server/src/score-import/import-types/file/csv_eamusement-iidx/converter.test.ts b/server/src/score-import/import-types/file/csv/eamusement-iidx/converter.test.ts similarity index 97% rename from server/src/score-import/import-types/file/csv_eamusement-iidx/converter.test.ts rename to server/src/score-import/import-types/file/csv/eamusement-iidx/converter.test.ts index bd3b67dc9..0e8c49d5b 100644 --- a/server/src/score-import/import-types/file/csv_eamusement-iidx/converter.test.ts +++ b/server/src/score-import/import-types/file/csv/eamusement-iidx/converter.test.ts @@ -1,17 +1,17 @@ import t from "tap"; -import db, { CloseMongoConnection } from "../../../../db/db"; -import CreateLogCtx from "../../../../logger"; -import ResetDBState from "../../../../test-utils/reset-db-state"; +import db, { CloseMongoConnection } from "../../../../../db/db"; +import CreateLogCtx from "../../../../../logger"; +import ResetDBState from "../../../../../test-utils/reset-db-state"; import ConverterFn, { EamScoreConverter, EamScoreConverterWrapper } from "./converter"; import p from "prudence"; -import { prAssert } from "../../../../test-utils/asserts"; +import { prAssert } from "../../../../../test-utils/asserts"; import deepmerge from "deepmerge"; import { EamusementScoreData } from "./types"; import { ConverterFailure, InvalidScoreFailure, KTDataNotFoundFailure, -} from "../../../framework/score-importing/converter-failures"; +} from "../../../../framework/score-importing/converter-failures"; const logger = CreateLogCtx("converter.test.ts"); diff --git a/server/src/score-import/import-types/file/csv_eamusement-iidx/converter.ts b/server/src/score-import/import-types/file/csv/eamusement-iidx/converter.ts similarity index 90% rename from server/src/score-import/import-types/file/csv_eamusement-iidx/converter.ts rename to server/src/score-import/import-types/file/csv/eamusement-iidx/converter.ts index 76ce2766f..8fddb96f2 100644 --- a/server/src/score-import/import-types/file/csv_eamusement-iidx/converter.ts +++ b/server/src/score-import/import-types/file/csv/eamusement-iidx/converter.ts @@ -1,15 +1,15 @@ import { config, ESDCore, Lamps, AnySongDocument, ChartDocument } from "kamaitachi-common"; -import { DryScore, ConverterFunction, ConverterFnReturn, KtLogger } from "../../../../types"; -import { FindChartWithPTDFVersion } from "../../../database-lookup/chart"; -import { FindSongOnTitle } from "../../../database-lookup/song"; +import { DryScore, ConverterFunction, ConverterFnReturn, KtLogger } from "../../../../../types"; +import { FindChartWithPTDFVersion } from "../../../../database-lookup/chart"; +import { FindSongOnTitle } from "../../../../database-lookup/song"; import { KTDataNotFoundFailure, InternalFailure, InvalidScoreFailure, -} from "../../../framework/score-importing/converter-failures"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { GetGradeFromPercent } from "../../../framework/common/score-utils"; -import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; +} from "../../../../framework/score-importing/converter-failures"; +import ScoreImportFatalError from "../../../../framework/score-importing/score-import-error"; +import { GetGradeFromPercent } from "../../../../framework/common/score-utils"; +import { AssertStrAsPositiveInt } from "../../../../framework/common/string-asserts"; import { EamusementScoreData, IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; const EAMUSEMENT_LAMP_RESOLVER: Map = new Map([ @@ -217,7 +217,8 @@ export async function EamScoreConverterWrapper( const ConverterFn: ConverterFunction = async ( data, context, - logger: KtLogger + importType, + logger ): Promise => { let isLegacyLeggendaria = false; diff --git a/server/src/score-import/import-types/file/csv_eamusement-iidx/parser.test.ts b/server/src/score-import/import-types/file/csv/eamusement-iidx/parser.test.ts similarity index 97% rename from server/src/score-import/import-types/file/csv_eamusement-iidx/parser.test.ts rename to server/src/score-import/import-types/file/csv/eamusement-iidx/parser.test.ts index 72c290304..c7915c3e7 100644 --- a/server/src/score-import/import-types/file/csv_eamusement-iidx/parser.test.ts +++ b/server/src/score-import/import-types/file/csv/eamusement-iidx/parser.test.ts @@ -1,12 +1,12 @@ import t from "tap"; -import CreateLogCtx from "../../../../logger"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; +import CreateLogCtx from "../../../../../logger"; +import ScoreImportFatalError from "../../../../framework/score-importing/score-import-error"; import ParseEamusementCSV, { NaiveCSVParse, ResolveHeaders } from "./parser"; -import { CloseMongoConnection } from "../../../../db/db"; +import { CloseMongoConnection } from "../../../../../db/db"; import { TestingIIDXEamusementCSV26, TestingIIDXEamusementCSV27, -} from "../../../../test-utils/test-data"; +} from "../../../../../test-utils/test-data"; const logger = CreateLogCtx("parser.test.ts"); diff --git a/server/src/score-import/import-types/file/csv_eamusement-iidx/parser.ts b/server/src/score-import/import-types/file/csv/eamusement-iidx/parser.ts similarity index 95% rename from server/src/score-import/import-types/file/csv_eamusement-iidx/parser.ts rename to server/src/score-import/import-types/file/csv/eamusement-iidx/parser.ts index a327d79da..11569130a 100644 --- a/server/src/score-import/import-types/file/csv_eamusement-iidx/parser.ts +++ b/server/src/score-import/import-types/file/csv/eamusement-iidx/parser.ts @@ -1,6 +1,6 @@ import { Difficulties } from "kamaitachi-common"; -import { KtLogger, ParserFunctionReturnsSync } from "../../../../types"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; +import { KtLogger, ParserFunctionReturnsSync } from "../../../../../types"; +import ScoreImportFatalError from "../../../../framework/score-importing/score-import-error"; import ConverterFn from "./converter"; import { EamusementScoreData, IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; diff --git a/server/src/score-import/import-types/file/csv_eamusement-iidx/types.ts b/server/src/score-import/import-types/file/csv/eamusement-iidx/types.ts similarity index 100% rename from server/src/score-import/import-types/file/csv_eamusement-iidx/types.ts rename to server/src/score-import/import-types/file/csv/eamusement-iidx/types.ts diff --git a/server/src/score-import/import-types/file/json/batch-manual/parser.ts b/server/src/score-import/import-types/file/json/batch-manual/parser.ts new file mode 100644 index 000000000..a0480bf1f --- /dev/null +++ b/server/src/score-import/import-types/file/json/batch-manual/parser.ts @@ -0,0 +1,30 @@ +import { KtLogger, ParserFunctionReturnsSync } from "../../../../../types"; +import ScoreImportFatalError from "../../../../framework/score-importing/score-import-error"; +import { ParseBatchManualFromObject } from "../../../common/batch-manual/parser"; +import { BatchManualContext, BatchManualScore } from "../../../common/batch-manual/types"; + +/** + * Parses a buffer of BATCH-MANUAL data. + * @param fileData - The buffer to parse. + * @param body - The request body that made this file import request. + */ +function ParseBatchManual( + fileData: Express.Multer.File, + body: Record, + logger: KtLogger +): ParserFunctionReturnsSync { + let jsonData: unknown; + + try { + jsonData = JSON.parse(fileData.buffer.toString("utf-8")); + } catch (err) { + throw new ScoreImportFatalError( + 400, + `Invalid JSON. (${err?.message ?? "No Error Message Available."})` + ); + } + + return ParseBatchManualFromObject(jsonData, "file/json:batch-manual", logger); +} + +export default ParseBatchManual; diff --git a/server/src/score-import/import-types/import-types.ts b/server/src/score-import/import-types/import-types.ts deleted file mode 100644 index 32f847ea1..000000000 --- a/server/src/score-import/import-types/import-types.ts +++ /dev/null @@ -1,30 +0,0 @@ -import iidxEamusementCsv from "./file/csv_eamusement-iidx/converter"; -import fileBatchManual from "./file/json_batch-manual/converter"; - -/** - * Converters are a set of functions designed to convert an element - * from the iterable into the Kamaitachi Score Format. - * - * Due to some oddities with certain formats, one element may contain multiple possible - * score records, so these functions MAY return an array of their traditional values. - */ -export const Converters = { - "file/csv:eamusement-iidx": iidxEamusementCsv, - "file/json:batch-manual": fileBatchManual, -}; - -/** - * Parsers are a set of functions designed to convert unparsed data - * (files, request bodies, api responses) into an iterable. - * - * These functions should also perform validation on the "data" recieved, - * such as whether they are valid JSON/csv files. - * - * However, they need not validate the actual data recieved - only that it is - * parsable and sensible, but not whether the contents are semantically correct - * (i.e. just parse the JSON into an array, and check if it can be done) - * (don't check the content!) - * - * This is so we don't end up with dual-iteration over the set of data. - */ -export const Parsers = {}; diff --git a/server/src/score-import/import-types/ir/json/direct-manual/parser.ts b/server/src/score-import/import-types/ir/json/direct-manual/parser.ts new file mode 100644 index 000000000..3a9a6a4fd --- /dev/null +++ b/server/src/score-import/import-types/ir/json/direct-manual/parser.ts @@ -0,0 +1,17 @@ +import { KtLogger, ParserFunctionReturnsSync } from "../../../../../types"; +import { ParseBatchManualFromObject } from "../../../common/batch-manual/parser"; +import { BatchManualContext, BatchManualScore } from "../../../common/batch-manual/types"; + +/** + * Parses an object of BATCH-MANUAL data. + * @param fileData - The buffer to parse. + * @param body - The request body that made this file import request. + */ +function ParseDirectManual( + body: Record, + logger: KtLogger +): ParserFunctionReturnsSync { + return ParseBatchManualFromObject(body, "ir/json:direct-manual", logger); +} + +export default ParseDirectManual; diff --git a/server/src/types.ts b/server/src/types.ts index 5b27c3c31..5894313dd 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -11,7 +11,14 @@ import { } from "kamaitachi-common"; import { Logger, LeveledLogMethod } from "winston"; import { ConverterFailure } from "./score-import/framework/score-importing/converter-failures"; -import { Converters } from "./score-import/import-types/import-types"; +import { + BatchManualContext, + BatchManualScore, +} from "./score-import/import-types/common/batch-manual/types"; +import { + IIDXEamusementCSVContext, + IIDXEamusementCSVData, +} from "./score-import/import-types/file/csv/eamusement-iidx/types"; declare module "express-session" { // Inject additional properties on express-session @@ -49,7 +56,12 @@ export type ConverterFnReturn = ConverterFailure | ConverterFnSuccessReturn | nu export type ConverterFunctionReturns = ConverterFnReturn | ConverterFnReturn[]; export interface ConverterFunction { - (data: D, processContext: C, logger: KtLogger): Promise; + ( + data: D, + processContext: C, + importType: ImportTypes, + logger: KtLogger + ): Promise; } export interface ImportInputParser { @@ -96,8 +108,8 @@ export interface ScoreConverterInformation { export interface OrphanedScore { importType: T; - data: Parameters[0]; - converterContext: Parameters[1]; + data: ImportTypeDataMap[T]; + converterContext: ImportTypeContextMap[T]; humanisedIdentifier: string; } @@ -108,3 +120,15 @@ export type RevaluedObject = { export interface TextDocument extends MongoDBDocument { text: string; } + +export interface ImportTypeDataMap { + "file/csv:eamusement-iidx": IIDXEamusementCSVData; + "file/json:batch-manual": BatchManualScore; + "ir/json:direct-manual": BatchManualScore; +} + +export interface ImportTypeContextMap { + "file/csv:eamusement-iidx": IIDXEamusementCSVContext; + "file/json:batch-manual": BatchManualContext; + "ir/json:direct-manual": BatchManualContext; +}