mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 20:38:17 +03:00
Database Update
This commit is contained in:
@@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
js
|
||||
scripts
|
||||
js
|
||||
Generated
+16
-2
@@ -9,6 +9,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.1",
|
||||
"commander": "^7.2.0",
|
||||
"connect-redis": "^5.1.0",
|
||||
"csv-parse": "^4.15.3",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
@@ -1438,6 +1439,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -3951,7 +3960,7 @@
|
||||
},
|
||||
"node_modules/kamaitachi-common": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "git+ssh://git@github.com/zkldi/Kamaitachi-common.git#c2147174d7be0e4cf9f36494021118d9518be4f7",
|
||||
"resolved": "git+ssh://git@github.com/zkldi/Kamaitachi-common.git#51aeec20dfe8ffa5867cd6ef8b8d18e529e500f4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"monk": "^7.3.3"
|
||||
@@ -9966,6 +9975,11 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
|
||||
},
|
||||
"commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -12013,7 +12027,7 @@
|
||||
}
|
||||
},
|
||||
"kamaitachi-common": {
|
||||
"version": "git+ssh://git@github.com/zkldi/Kamaitachi-common.git#c2147174d7be0e4cf9f36494021118d9518be4f7",
|
||||
"version": "git+ssh://git@github.com/zkldi/Kamaitachi-common.git#51aeec20dfe8ffa5867cd6ef8b8d18e529e500f4",
|
||||
"from": "kamaitachi-common@git+https://github.com/zkldi/Kamaitachi-common.git",
|
||||
"requires": {
|
||||
"monk": "^7.3.3"
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.1",
|
||||
"commander": "^7.2.0",
|
||||
"connect-redis": "^5.1.0",
|
||||
"csv-parse": "^4.15.3",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AddNewUserAPIKey,
|
||||
CreateAPIKey,
|
||||
CreateBetaKey,
|
||||
AddNewBetakey,
|
||||
ReinstateBetakey,
|
||||
CreateInviteCode,
|
||||
AddNewInvite,
|
||||
ReinstateInvite,
|
||||
} from "./auth-core";
|
||||
import t from "tap";
|
||||
import db, { CloseConnection } from "../db";
|
||||
import { BetaKeyDocument, PrivateUserDocument } from "kamaitachi-common";
|
||||
import { PrivateUserDocument } from "kamaitachi-common";
|
||||
import prAssert from "../test-utils/prassert";
|
||||
import Prudence from "prudence";
|
||||
import ResetDBState from "../test-utils/reset-db-state";
|
||||
@@ -45,7 +45,7 @@ t.test("#AddNewUserAPIKey", (t) => {
|
||||
"Data should match a public API key object"
|
||||
);
|
||||
|
||||
let inDatabase = await db.get("public-api-keys").findOne({
|
||||
let inDatabase = await db["public-api-keys"].findOne({
|
||||
_id: data._id,
|
||||
});
|
||||
|
||||
@@ -57,28 +57,28 @@ t.test("#AddNewUserAPIKey", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ReinstateBetaKey", (t) => {
|
||||
t.test("#ReinstateInvite", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should change the 'consumed' property of a betakey to true.", async (t) => {
|
||||
t.test("Should change the 'consumed' property of an invite to true.", async (t) => {
|
||||
// mock insert
|
||||
let betakey = await db.get<BetaKeyDocument>("betakeys").insert({
|
||||
betakey: "foobar",
|
||||
let inviteDoc = await db.invites.insert({
|
||||
code: "foobar",
|
||||
consumed: true,
|
||||
createdBy: 1,
|
||||
createdOn: 1,
|
||||
});
|
||||
|
||||
let response = await ReinstateBetakey(betakey);
|
||||
let response = await ReinstateInvite(inviteDoc);
|
||||
|
||||
// @ts-expect-error Monks' types are WRONG. this is nModified, not modifiedCount
|
||||
t.is(response.nModified, 1, "Should modify one document");
|
||||
|
||||
let betakey2 = await db.get<BetaKeyDocument>("betakeys").findOne({
|
||||
betakey: betakey.betakey, // lol
|
||||
let invite2 = await db.invites.findOne({
|
||||
code: inviteDoc.code, // lol
|
||||
});
|
||||
|
||||
t.is(betakey2!.consumed, false, "Should no longer be consumed");
|
||||
t.is(invite2!.consumed, false, "Should no longer be consumed");
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -86,34 +86,30 @@ t.test("#ReinstateBetaKey", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CreateBetaKey", (t) => {
|
||||
t.match(CreateBetaKey(), /^[0-9a-f]{40}$/, "Betakey should be a 40 character hex string.");
|
||||
t.test("#CreateInviteCode", (t) => {
|
||||
t.match(CreateInviteCode(), /^[0-9a-f]{40}$/, "Invite should be a 40 character hex string.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#AddNewBetakey", (t) => {
|
||||
t.test("#AddNewInvite", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should create a new beta key for a given user", async (t) => {
|
||||
let userDoc = await db.get("users").findOne({ id: 1 });
|
||||
let userDoc = await db.users.findOne({ id: 1 });
|
||||
|
||||
let result = await AddNewBetakey(userDoc);
|
||||
let result = await AddNewInvite(userDoc!);
|
||||
|
||||
t.is(result.createdBy, userDoc.id, "Betakey should be created by the requesting user.");
|
||||
t.is(result.consumed, false, "Betakey should not be consumed.");
|
||||
t.is(result.createdBy, userDoc!.id, "Invite should be created by the requesting user.");
|
||||
t.is(result.consumed, false, "Invite should not be consumed.");
|
||||
|
||||
// was created +/- 6 seconds from now. This is perhaps too lenient, but we're only really testing its just around now ish.
|
||||
t.assert(
|
||||
Math.abs(result.createdOn - Date.now()) <= 6000,
|
||||
"Betakey was created roughly now."
|
||||
"Invite was created roughly now."
|
||||
);
|
||||
|
||||
t.match(
|
||||
result.betakey,
|
||||
/^[0-9a-f]{40}$/,
|
||||
"Betakey should have contents of a 40 character hex string."
|
||||
);
|
||||
t.match(result.code, /^[0-9a-f]{40}$/, "Invite code should be a 40 character hex string.");
|
||||
});
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
BetaKeyDocument,
|
||||
InviteCodeDocument,
|
||||
PrivateUserDocument,
|
||||
PublicAPIKeyDocument,
|
||||
PublicUserDocument,
|
||||
@@ -28,7 +28,7 @@ export function CreateAPIKey(): string {
|
||||
* you know, not do that.
|
||||
* @returns A string
|
||||
*/
|
||||
export function CreateBetaKey(): string {
|
||||
export function CreateInviteCode(): string {
|
||||
return crypto.randomBytes(20).toString("hex");
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ export function AddNewUserAPIKey(
|
||||
},
|
||||
};
|
||||
|
||||
return db.get<PublicAPIKeyDocument>("public-api-keys").insert(publicApiKeyDoc);
|
||||
return db["public-api-keys"].insert(publicApiKeyDoc);
|
||||
}
|
||||
|
||||
export function ReinstateBetakey(bkDoc: BetaKeyDocument) {
|
||||
logger.info(`Reinstated beta key ${bkDoc.betakey}`);
|
||||
return db.get<BetaKeyDocument>("betakeys").update(
|
||||
export function ReinstateInvite(inviteDoc: InviteCodeDocument) {
|
||||
logger.info(`Reinstated Invite ${inviteDoc.code}`);
|
||||
return db.invites.update(
|
||||
{
|
||||
_id: bkDoc._id,
|
||||
_id: inviteDoc._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
@@ -73,29 +73,29 @@ export function ReinstateBetakey(bkDoc: BetaKeyDocument) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function AddNewBetakey(user: PublicUserDocument) {
|
||||
export async function AddNewInvite(user: PublicUserDocument) {
|
||||
// beta keys are shorter strings, and it's possible for them to be collided
|
||||
let betakey = CreateBetaKey();
|
||||
let code = CreateInviteCode();
|
||||
|
||||
let result = await db.get<BetaKeyDocument>("betakeys").insert({
|
||||
betakey,
|
||||
let result = await db.invites.insert({
|
||||
code,
|
||||
consumed: false,
|
||||
createdBy: user.id,
|
||||
createdOn: Date.now(),
|
||||
});
|
||||
|
||||
logger.info(`User ${FormatUserDoc(user)} created a beta key.`);
|
||||
logger.info(`User ${FormatUserDoc(user)} created an invite.`);
|
||||
|
||||
if (!result) {
|
||||
logger.error(
|
||||
`Fatal error in creating ${FormatUserDoc(
|
||||
user
|
||||
)}'s beta key. Database refused key ${betakey}.`
|
||||
)}'s invite code. Database refused key ${code}.`
|
||||
);
|
||||
throw new Error(
|
||||
`Fatal error in creating ${FormatUserDoc(
|
||||
user
|
||||
)}'s beta key. Database refused key ${betakey}.`
|
||||
)}'s invite code. Database refused key ${code}.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,5 +137,5 @@ export async function AddNewUser(
|
||||
},
|
||||
};
|
||||
|
||||
return db.get<PrivateUserDocument>("users").insert(userDoc);
|
||||
return db.users.insert(userDoc);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GetNextCounterValue } from "./db-core";
|
||||
import t from "tap";
|
||||
import db, { CloseConnection } from "../db";
|
||||
import { CounterDocument } from "kamaitachi-common";
|
||||
import ResetDBState from "../test-utils/reset-db-state";
|
||||
|
||||
t.test("#GetNextCounterValue", (t) => {
|
||||
@@ -13,7 +12,7 @@ t.test("#GetNextCounterValue", (t) => {
|
||||
// database starts with this at one
|
||||
t.is(response, 2, "Counter should return the current number stored");
|
||||
|
||||
let dbData = await db.get<CounterDocument>("counters").findOne({
|
||||
let dbData = await db.counters.findOne({
|
||||
counterName: "real-counter",
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const logger = createLogCtx("db-core.ts");
|
||||
import { integer } from "../types";
|
||||
|
||||
export async function GetNextCounterValue(counterName: string): Promise<integer> {
|
||||
const sequenceDoc = await db.get("counters").findOneAndUpdate(
|
||||
const sequenceDoc = await db.counters.findOneAndUpdate(
|
||||
{
|
||||
counterName,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ const PRIVATE_USER_RETURNS = {
|
||||
* @returns PublicUserDocument
|
||||
*/
|
||||
export function GetUserCaseInsensitive(username: string) {
|
||||
return db.get<PrivateUserDocument>("users").findOne(
|
||||
return db.users.findOne(
|
||||
{
|
||||
usernameLowercase: username.toLowerCase(),
|
||||
},
|
||||
@@ -31,7 +31,7 @@ export function GetUserCaseInsensitive(username: string) {
|
||||
* @returns PrivateUserDocument
|
||||
*/
|
||||
export function PRIVATEINFO_GetUserCaseInsensitive(username: string) {
|
||||
return db.get<PrivateUserDocument>("users").findOne({
|
||||
return db.users.findOne({
|
||||
usernameLowercase: username.toLowerCase(),
|
||||
});
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function ResolveUser(usernameOrID: string) {
|
||||
if (usernameOrID.match(/^[0-9]$/)) {
|
||||
let intID = parseInt(usernameOrID, 10);
|
||||
|
||||
return db.get<PrivateUserDocument>("users").findOne(
|
||||
return db.users.findOne(
|
||||
{
|
||||
id: intID,
|
||||
},
|
||||
@@ -64,7 +64,7 @@ export function ResolveUser(usernameOrID: string) {
|
||||
);
|
||||
}
|
||||
|
||||
return db.get<PrivateUserDocument>("users").findOne(
|
||||
return db.users.findOne(
|
||||
{
|
||||
usernameLowercase: usernameOrID,
|
||||
},
|
||||
|
||||
+69
-6
@@ -1,4 +1,25 @@
|
||||
import monk from "monk";
|
||||
import {
|
||||
ChartDocument,
|
||||
config,
|
||||
CounterDocument,
|
||||
FolderDocument,
|
||||
Game,
|
||||
GoalDocument,
|
||||
IIDXBPIData,
|
||||
ImportDocument,
|
||||
InviteCodeDocument,
|
||||
MilestoneDocument,
|
||||
NotificationDocument,
|
||||
PrivateUserDocument,
|
||||
PublicAPIKeyDocument,
|
||||
ScoreDocument,
|
||||
SessionDocument,
|
||||
SongDocument,
|
||||
UserGoalDocument,
|
||||
UserMilestoneDocument,
|
||||
ValidDatabases,
|
||||
} from "kamaitachi-common";
|
||||
import monk, { ICollection, IMonkManager } from "monk";
|
||||
import createLogCtx from "./logger";
|
||||
|
||||
const logger = createLogCtx("db.ts");
|
||||
@@ -12,10 +33,10 @@ if (process.env.NODE_ENV !== "test") {
|
||||
dbtime = process.hrtime();
|
||||
}
|
||||
|
||||
let db = monk(url);
|
||||
export let monkDB = monk(url);
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
db.then(() => {
|
||||
monkDB.then(() => {
|
||||
let time = process.hrtime(dbtime);
|
||||
let elapsed = time[0] + time[1] / 1000000;
|
||||
logger.info(`Database connection successful: took ${elapsed}ms`);
|
||||
@@ -23,12 +44,54 @@ if (process.env.NODE_ENV !== "test") {
|
||||
}
|
||||
|
||||
export async function ReOpenConnection() {
|
||||
db = monk(url);
|
||||
await db.then();
|
||||
monkDB = monk(url);
|
||||
await monkDB.then();
|
||||
}
|
||||
|
||||
export async function CloseConnection() {
|
||||
await db.close();
|
||||
await monkDB.close();
|
||||
}
|
||||
|
||||
const StaticCollections = {
|
||||
scores: monkDB.get<ScoreDocument>("scores"),
|
||||
folders: monkDB.get<FolderDocument>("folders"),
|
||||
goals: monkDB.get<GoalDocument>("goals"),
|
||||
"user-goals": monkDB.get<UserGoalDocument>("user-goals"),
|
||||
milestones: monkDB.get<MilestoneDocument>("milestones"),
|
||||
"user-milestones": monkDB.get<UserMilestoneDocument>("user-milestones"),
|
||||
users: monkDB.get<PrivateUserDocument>("users"),
|
||||
imports: monkDB.get<ImportDocument>("imports"),
|
||||
notifications: monkDB.get<NotificationDocument>("notifications"),
|
||||
sessions: monkDB.get<SessionDocument>("sessions"),
|
||||
"iidx-bpi-data": monkDB.get<IIDXBPIData>("iidx-bpi-data"),
|
||||
"public-api-keys": monkDB.get<PublicAPIKeyDocument>("public-api-keys"),
|
||||
invites: monkDB.get<InviteCodeDocument>("invites"),
|
||||
counters: monkDB.get<CounterDocument>("counters"),
|
||||
};
|
||||
|
||||
type GameCollections = Record<Game, ICollection>;
|
||||
|
||||
interface GCPartial {
|
||||
songs: Partial<GameCollections>;
|
||||
charts: Partial<GameCollections>;
|
||||
}
|
||||
|
||||
interface GameCollectionFull {
|
||||
songs: GameCollections;
|
||||
charts: GameCollections;
|
||||
}
|
||||
|
||||
let GameCollectionPartial: GCPartial = {
|
||||
songs: {},
|
||||
charts: {},
|
||||
};
|
||||
|
||||
for (const game of config.supportedGames) {
|
||||
GameCollectionPartial.songs[game] = monkDB.get<SongDocument>(`songs-${game}`);
|
||||
GameCollectionPartial.charts[game] = monkDB.get<ChartDocument>(`charts-${game}`);
|
||||
}
|
||||
|
||||
// a typescript-friendly interface for the database.
|
||||
const db = Object.assign(StaticCollections, GameCollectionPartial as GameCollectionFull);
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
AddNewUserAPIKey,
|
||||
CreateAPIKey,
|
||||
PasswordCompare,
|
||||
ReinstateBetakey,
|
||||
ReinstateInvite,
|
||||
ValidatePassword,
|
||||
} from "../../core/auth-core";
|
||||
import { ValidateCaptcha } from "../../core/captcha-core";
|
||||
@@ -98,7 +98,7 @@ router.post(
|
||||
|
||||
// username and password match up, we're good to check onwards
|
||||
|
||||
let apiKeyDoc = await db.get<PublicAPIKeyDocument>("public-api-keys").findOne({
|
||||
let apiKeyDoc = await db["public-api-keys"].findOne({
|
||||
assignedTo: requestedUser.id,
|
||||
"permissions.selfkey": true,
|
||||
});
|
||||
@@ -159,14 +159,14 @@ router.post(
|
||||
username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/),
|
||||
password: ValidatePassword,
|
||||
email: Prudence.regex(LAZY_EMAIL_REGEX),
|
||||
betakey: "string",
|
||||
inviteCode: "string",
|
||||
captcha: "string",
|
||||
},
|
||||
{
|
||||
username:
|
||||
"Usernames must be between 3 and 20 characters long, and can only contain alphanumeric characters!",
|
||||
email: "Invalid email.",
|
||||
betakey: "Invalid beta key.",
|
||||
inviteCode: "Invalid invite code.",
|
||||
captcha: "Please fill out the captcha.",
|
||||
}
|
||||
),
|
||||
@@ -200,9 +200,9 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
let bkeyDoc = await db.get<BetaKeyDocument>("betakeys").findOneAndUpdate(
|
||||
let inviteCodeDoc = await db.invites.findOneAndUpdate(
|
||||
{
|
||||
betakey: req.body.betakey,
|
||||
code: req.body.inviteCode,
|
||||
consumed: false,
|
||||
},
|
||||
{
|
||||
@@ -212,15 +212,15 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
if (!bkeyDoc) {
|
||||
logger.info(`Invalid betakey given: ${req.body.betakey}.`);
|
||||
if (!inviteCodeDoc) {
|
||||
logger.info(`Invalid invite code given: ${req.body.inviteCode}.`);
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: `This beta key is not valid.`,
|
||||
description: `This invite code is not valid.`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Consumed betakey ${bkeyDoc.betakey}.`);
|
||||
logger.info(`Consumed invite ${inviteCodeDoc.code}.`);
|
||||
|
||||
// if we get to this point, We're good to create the user.
|
||||
|
||||
@@ -231,7 +231,7 @@ router.post(
|
||||
`Bailed on user creation ${req.body.username} with betakey ${req.body.betakey}.`
|
||||
);
|
||||
|
||||
await ReinstateBetakey(bkeyDoc);
|
||||
await ReinstateInvite(inviteCodeDoc);
|
||||
|
||||
throw new Error("FATAL in /register - User was created, but refused?");
|
||||
}
|
||||
@@ -243,7 +243,7 @@ router.post(
|
||||
`Bailed on user creation ${req.body.username} with betakey ${req.body.betakey}`
|
||||
);
|
||||
|
||||
await ReinstateBetakey(bkeyDoc);
|
||||
await ReinstateInvite(inviteCodeDoc);
|
||||
throw new Error("FATAL in /register - apikey was created, but refused?");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
// Chart "Playtype+Difficulty" lookup. This is a common way of resolving Kamaitachi charts.
|
||||
|
||||
import {
|
||||
Difficulties,
|
||||
Game,
|
||||
integer,
|
||||
Playtypes,
|
||||
IDStrings,
|
||||
IDString,
|
||||
ChartDocument,
|
||||
} from "kamaitachi-common";
|
||||
import { Difficulties, Game, integer, Playtypes, IDStrings, IDString } from "kamaitachi-common";
|
||||
import db from "../../db";
|
||||
|
||||
export function FindChartWithPTDF<
|
||||
@@ -17,7 +9,7 @@ export function FindChartWithPTDF<
|
||||
// @ts-expect-error See kamaitachi-common.
|
||||
I extends IDStrings = IDString<G, P>
|
||||
>(game: G, songID: integer, playtype: P, difficulty: Difficulties[I]) {
|
||||
return db.get<ChartDocument>(`charts-${game}`).findOne({
|
||||
return db.charts[game].findOne({
|
||||
songID: songID,
|
||||
playtype: playtype,
|
||||
difficulty: difficulty,
|
||||
|
||||
@@ -11,7 +11,7 @@ import db from "../../db";
|
||||
* @returns SongDocument
|
||||
*/
|
||||
export function FindSongOnTitleVersion(game: Game, title: string, version: string | number) {
|
||||
return db.get<SongDocument>(`songs-${game}`).findOne({
|
||||
return db.songs[game].findOne({
|
||||
"data.version": version,
|
||||
$or: [
|
||||
{
|
||||
@@ -33,7 +33,7 @@ export function FindSongOnTitleVersion(game: Game, title: string, version: strin
|
||||
* @returns SongDocument
|
||||
*/
|
||||
export function FindSongOnTitle(game: Game, title: string) {
|
||||
return db.get<SongDocument>(`songs-${game}`).findOne({
|
||||
return db.songs[game].findOne({
|
||||
$or: [
|
||||
{
|
||||
title: title,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
ChartDocument,
|
||||
Game,
|
||||
Playtypes,
|
||||
ScoreDocument,
|
||||
SongDocument,
|
||||
TierlistDataDocument,
|
||||
} from "kamaitachi-common";
|
||||
import { DryScore, integer } from "../../../../types";
|
||||
import { CreateGameSpecific } from "./game-specific";
|
||||
import { CalculateCHUNITHMRating, CalculateGITADORARating } from "./game-specific-stats";
|
||||
|
||||
export async function CreateCalculatedData(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument,
|
||||
song: SongDocument
|
||||
): Promise<ScoreDocument["calculatedData"]> {
|
||||
const game = dryScore.game;
|
||||
const playtype = chart.playtype;
|
||||
|
||||
const [rating, gameSpecific] = await Promise.all([
|
||||
CalculateRating(dryScore, game, playtype, chart),
|
||||
CreateGameSpecific(game, playtype, chart, dryScore),
|
||||
]);
|
||||
|
||||
return {
|
||||
rating,
|
||||
lampRating: 0,
|
||||
ranking: null,
|
||||
outOf: null,
|
||||
gameSpecific,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the default rating function for a game with
|
||||
* something else.
|
||||
*/
|
||||
const OVERRIDE_RATING_FUNCTIONS = {
|
||||
gitadora: {
|
||||
Gita: CalculateGITADORARating,
|
||||
Dora: CalculateGITADORARating,
|
||||
},
|
||||
chunithm: {
|
||||
Single: CalculateCHUNITHMRating,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the rating for a score. Listens to the override functions declared above.
|
||||
*/
|
||||
async function CalculateRating<G extends Game>(
|
||||
dryScore: DryScore,
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
chart: ChartDocument
|
||||
) {
|
||||
// @todo
|
||||
let OverrideFunction: OverrideRatingFunction | undefined =
|
||||
// @ts-expect-error I'm too lazy to explain this to TS
|
||||
OVERRIDE_RATING_FUNCTIONS[game]?.[playtype];
|
||||
|
||||
if (!OverrideFunction) {
|
||||
return 0; // go to default kamaitachi rating fn ?
|
||||
}
|
||||
|
||||
return await OverrideFunction(dryScore, chart);
|
||||
}
|
||||
|
||||
async function CalculateLampRating(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument,
|
||||
tierlist: TierlistDataDocument
|
||||
) {}
|
||||
|
||||
interface OverrideRatingFunction {
|
||||
(dryScore: DryScore, chartData: ChartDocument): number | Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { ChartDocument, ESDCore, integer } from "kamaitachi-common";
|
||||
import db from "../../../../db";
|
||||
import createLogCtx from "../../../../logger";
|
||||
import { DryScore } from "../../../../types";
|
||||
const logger = createLogCtx("game-specific-stats.ts");
|
||||
|
||||
/**
|
||||
* Calculates the in-game CHUNITHM rating for a score.
|
||||
*/
|
||||
export function CalculateCHUNITHMRating(dryScore: DryScore, chartData: ChartDocument) {
|
||||
let score = dryScore.scoreData.score;
|
||||
let levelBase = chartData.levelNum * 100;
|
||||
|
||||
let val = 0;
|
||||
|
||||
if (score >= 1007500) {
|
||||
val = levelBase + 200;
|
||||
} else if (score >= 1005000) {
|
||||
val = levelBase + 150 + ((score - 1005000) * 10) / 500;
|
||||
} else if (score >= 1000000) {
|
||||
val = levelBase + 100 + ((score - 1000000) * 5) / 500;
|
||||
} else if (score >= 975000) {
|
||||
val = levelBase + ((score - 975000) * 2) / 500;
|
||||
} else if (score >= 925000) {
|
||||
val = levelBase - 300 + ((score - 925000) * 3) / 500;
|
||||
} else if (score >= 900000) {
|
||||
val = levelBase - 500 + ((score - 900000) * 4) / 500;
|
||||
} else if (score >= 800000) {
|
||||
val = (levelBase - 500) / 2 + ((score - 800000) * ((levelBase - 500) / 2)) / 100000;
|
||||
}
|
||||
|
||||
return Math.max(Math.floor(val) / 100, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the in-game GITADORA rating for a score.
|
||||
*/
|
||||
export function CalculateGITADORARating(dryScore: DryScore, chartData: ChartDocument) {
|
||||
let trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20;
|
||||
let flooredRating = Math.floor(trueRating * 100) / 100;
|
||||
return flooredRating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the PikaGreatFunction, used in BPI. I have no idea what this does.
|
||||
* @returns
|
||||
*/
|
||||
function BPIPikaGreatFn(score: integer, max: integer) {
|
||||
return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Oh boy.
|
||||
*
|
||||
* Calculates the "Beat Performance Index" of an IIDX score. This algorithm has many issues,
|
||||
* but is a direct port of Poyashi's implementation for consistencies sake.
|
||||
* https://github.com/potakusan/iidx_score_manager/blob/f21ba6b85fcc0bf8b7ca888fa2239a3951a9c9c2/src/components/bpi/index.tsx#L120
|
||||
*
|
||||
* @param kaidenEx The kaiden average EX score.
|
||||
* @param wrEx The world record's EX score.
|
||||
* @param yourEx Your EX score.
|
||||
* @param max The maximum amount of EX achievable on this chart.
|
||||
* @param powCoef What power the BPI should be raised to. This is arbitrary, and assigned on a per-song basis. Defaults to 1.175.
|
||||
* @returns A number between -15 and 100. Unless your score is better than the world record, in which case
|
||||
* returns can be above 100.
|
||||
*/
|
||||
export function CalculateBPI(
|
||||
kaidenEx: integer,
|
||||
wrEx: integer,
|
||||
yourEx: integer,
|
||||
max: integer,
|
||||
powCoef: number | null
|
||||
) {
|
||||
powCoef ??= 1.175;
|
||||
const yourPGF = BPIPikaGreatFn(yourEx, max);
|
||||
const kaidenPGF = BPIPikaGreatFn(kaidenEx, max);
|
||||
const wrPGF = BPIPikaGreatFn(wrEx, max);
|
||||
|
||||
// no idea what these var names are
|
||||
const _s_ = yourPGF / kaidenPGF,
|
||||
_z_ = wrPGF / kaidenPGF;
|
||||
|
||||
const isBetterThanKavg = yourEx >= kaidenEx;
|
||||
|
||||
// this line of code isn't mine, and that's why it's *really* bad here.
|
||||
return Math.max(
|
||||
-15,
|
||||
Math.round(
|
||||
(isBetterThanKavg ? 100 : -100) *
|
||||
Math.pow(
|
||||
(isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_),
|
||||
powCoef
|
||||
) *
|
||||
100
|
||||
) / 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the percent of Kaidens you are ahead of with this score.
|
||||
* @returns Null, if this chart has no kaidens, a percent between 0 and 100, if there are.
|
||||
*/
|
||||
export async function KaidenPercentile(scoreObj: DryScore, chartData: ChartDocument) {
|
||||
let scoreCount = await db.get("iidx-eam-scores").count({
|
||||
chartID: chartData.chartID,
|
||||
});
|
||||
|
||||
if (!scoreCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let worseScores = await db.get("iidx-eam-scores").count({
|
||||
chartID: chartData.chartID,
|
||||
score: { $lt: scoreObj.scoreData.score },
|
||||
});
|
||||
|
||||
return (100 * worseScores) / scoreCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* An experimental statistic for determining how good a score is versus the Kaiden Average
|
||||
* For those who know what ESDC is (me), this is just ESDC(kesd, your esd).
|
||||
*/
|
||||
export function CalculateKESDC(kaidenESD: number | null, yourESD: number) {
|
||||
if (!kaidenESD) {
|
||||
return null;
|
||||
}
|
||||
return ESDCore.ESDCompare(kaidenESD, yourESD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Marvelous Full Combo Points. This algorithm
|
||||
* is used in LIFE4, and described here:
|
||||
* https://life4ddr.com/requirements/#mfcpoints
|
||||
* @returns Null if this score was not eligible, a number otherwise.
|
||||
*/
|
||||
export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument) {
|
||||
if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Beginner and BASIC scores are explicitly excluded.
|
||||
if (chartData.difficulty === "BEGINNER" || chartData.difficulty === "BASIC") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (chartData.levelNum < 8) {
|
||||
return null;
|
||||
} else if (chartData.levelNum < 10) {
|
||||
return 1;
|
||||
} else if (chartData.levelNum < 12) {
|
||||
return 2;
|
||||
} else if (chartData.levelNum === 13) {
|
||||
return 4;
|
||||
} else if (chartData.levelNum === 14) {
|
||||
return 8;
|
||||
} else if (chartData.levelNum === 15) {
|
||||
return 15;
|
||||
} else if (chartData.levelNum >= 16) {
|
||||
return 25;
|
||||
}
|
||||
|
||||
// cannot be hit?
|
||||
return null;
|
||||
}
|
||||
|
||||
const VF4GradeCoefficients = {
|
||||
S: 1.0,
|
||||
"AAA+": 0.99,
|
||||
AAA: 0.98,
|
||||
"AA+": 0.97,
|
||||
AA: 0.96,
|
||||
"A+": 0.95,
|
||||
A: 0.94,
|
||||
B: 0.93,
|
||||
C: 0.92,
|
||||
D: 0.91,
|
||||
};
|
||||
|
||||
const VF5GradeCoefficients = {
|
||||
S: 1.05,
|
||||
"AAA+": 1.02,
|
||||
AAA: 1.0,
|
||||
"AA+": 0.97,
|
||||
AA: 0.94,
|
||||
"A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki.
|
||||
A: 0.88,
|
||||
B: 0.85,
|
||||
C: 0.82,
|
||||
D: 0.8,
|
||||
};
|
||||
|
||||
const VF5LampCoefficients = {
|
||||
"PERFECT ULTIMATE CHAIN": 1.1,
|
||||
"ULTIMATE CHAIN": 1.05,
|
||||
"EXCESSIVE CLEAR": 1.02,
|
||||
CLEAR: 1.0,
|
||||
FAILED: 0.5,
|
||||
};
|
||||
|
||||
export function CalculateVF4(
|
||||
dryScore: DryScore<"sdvx", "Single">,
|
||||
chartData: ChartDocument<"sdvx">
|
||||
) {
|
||||
const multiplier = 25;
|
||||
let level = chartData.levelNum;
|
||||
|
||||
let gradeCoefficient = VF4GradeCoefficients[dryScore.scoreData.grade];
|
||||
|
||||
if (!gradeCoefficient) {
|
||||
logger.warn(
|
||||
`Invalid grade of ${dryScore.scoreData.grade} passed to CalculateVF4. Returning null.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let percent = dryScore.scoreData.percent / 100;
|
||||
if (!level || !percent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.floor(multiplier * (level + 1) * percent * gradeCoefficient);
|
||||
}
|
||||
|
||||
// WARNING:
|
||||
// this formula is allegedly tentative according to bemaniwiki.
|
||||
// idk if it's right, but it must be close enough.
|
||||
export function CalculateVF5(
|
||||
dryScore: DryScore<"sdvx", "Single">,
|
||||
chartData: ChartDocument<"sdvx">
|
||||
) {
|
||||
let level = chartData.levelNum;
|
||||
|
||||
let gradeCoefficient = VF5GradeCoefficients[dryScore.scoreData.grade];
|
||||
let lampCoefficient = VF5LampCoefficients[dryScore.scoreData.lamp];
|
||||
|
||||
if (!lampCoefficient) {
|
||||
logger.warn(
|
||||
`Invalid lamp of ${dryScore.scoreData.lamp} passed to CalculateVF5. Returning null.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!gradeCoefficient) {
|
||||
logger.warn(
|
||||
`Invalid grade of ${dryScore.scoreData.grade} passed to CalculateVF5. Returning null.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let percent = dryScore.scoreData.percent / 100;
|
||||
if (!level || !percent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.floor(level * 2 * percent * gradeCoefficient * lampCoefficient) / 100;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ChartDocument, Game, IIDXBPIData, Playtypes } from "kamaitachi-common";
|
||||
import db from "../../../../db";
|
||||
import { DryScore } from "../../../../types";
|
||||
import {
|
||||
CalculateBPI,
|
||||
CalculateKESDC,
|
||||
CalculateMFCP,
|
||||
CalculateVF4,
|
||||
CalculateVF5,
|
||||
KaidenPercentile,
|
||||
} from "./game-specific-stats";
|
||||
|
||||
// Creates Game-Specific calculatedData for the provided game & playtype.
|
||||
export async function CreateGameSpecific<G extends Game>(
|
||||
game: G,
|
||||
playtype: Playtypes[G],
|
||||
chart: ChartDocument,
|
||||
dryScore: DryScore
|
||||
): Promise<Record<string, number | null>> {
|
||||
let gameSpecific: Record<string, number | null> = {};
|
||||
|
||||
if (game === "iidx") {
|
||||
let BPIData = await db["iidx-bpi-data"].findOne({
|
||||
chartID: chart.chartID,
|
||||
});
|
||||
|
||||
if (BPIData) {
|
||||
gameSpecific.BPI = CalculateBPI(
|
||||
BPIData.kavg,
|
||||
BPIData.wr,
|
||||
dryScore.scoreData.score,
|
||||
chart.notedata.notecount * 2,
|
||||
BPIData.coef
|
||||
);
|
||||
|
||||
gameSpecific.KESDC =
|
||||
dryScore.scoreData.esd === null
|
||||
? null
|
||||
: CalculateKESDC(BPIData.kesd, dryScore.scoreData.esd);
|
||||
} else {
|
||||
gameSpecific.BPI = null;
|
||||
gameSpecific.KESDC = null;
|
||||
}
|
||||
|
||||
if (playtype === "SP") {
|
||||
gameSpecific["K%"] = await KaidenPercentile(dryScore, chart);
|
||||
}
|
||||
} else if (game === "sdvx") {
|
||||
gameSpecific.VF4 = CalculateVF4(
|
||||
dryScore as DryScore<"sdvx">,
|
||||
chart as ChartDocument<"sdvx">
|
||||
);
|
||||
gameSpecific.VF5 = CalculateVF5(
|
||||
dryScore as DryScore<"sdvx">,
|
||||
chart as ChartDocument<"sdvx">
|
||||
);
|
||||
} else if (game === "ddr") {
|
||||
// either playtype
|
||||
gameSpecific.MFCP = CalculateMFCP(dryScore, chart);
|
||||
}
|
||||
|
||||
return gameSpecific;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChartDocument, config, integer, ScoreDocument, SongDocument } from "kamaitachi-common";
|
||||
import { DryScore } from "../../../types";
|
||||
import { CreateCalculatedData } from "./calculated-data/calculated-data";
|
||||
|
||||
/**
|
||||
* Takes an "intermediate" score and appends the rest of the data it needs.
|
||||
@@ -13,7 +14,7 @@ export default async function HydrateScore(
|
||||
song: SongDocument,
|
||||
scoreID: string
|
||||
): Promise<ScoreDocument> {
|
||||
const calculatedData = {} as any; // await CreateCalculatedData(); // @todo
|
||||
const calculatedData = await CreateCalculatedData(dryScore, chart, song); // @todo
|
||||
|
||||
const { scoreData: dryScoreData, ...rest } = dryScore;
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ export async function QueueScoreInsert(score: ScoreDocument) {
|
||||
export async function InsertQueue() {
|
||||
const temp = ScoreQueue.splice(0);
|
||||
if (temp.length !== 0) {
|
||||
await db.get<ScoreDocument>("scores").insert(temp);
|
||||
await db.scores.insert(temp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { integer, DryScore } from "../../../types";
|
||||
import crypto from "crypto";
|
||||
import db from "../../../db";
|
||||
import { ScoreDocument } from "kamaitachi-common";
|
||||
|
||||
function CreateScoreIDString(userID: integer, partialScore: DryScore, chartID: string) {
|
||||
const { lamp, grade } = partialScore.scoreData;
|
||||
@@ -30,7 +29,7 @@ export function CreateScoreID(userID: integer, dryScore: DryScore, chartID: stri
|
||||
}
|
||||
|
||||
export function GetWithScoreID(scoreID: string) {
|
||||
return db.get<ScoreDocument>("scores").findOne({
|
||||
return db.scores.findOne({
|
||||
scoreID,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ async function HydrateAndInsertScore(
|
||||
): Promise<ScoreDocument | null> {
|
||||
const scoreID = CreateScoreID(userID, dryScore, chart.chartID);
|
||||
|
||||
const existingScore = await db.get<ScoreDocument>("scores").findOne(
|
||||
const existingScore = await db.scores.findOne(
|
||||
{
|
||||
scoreID,
|
||||
},
|
||||
|
||||
@@ -103,7 +103,7 @@ async function EamScoreConverter(
|
||||
|
||||
// esd cannot estimate things below this level of accuracy, so only actually calculate it here
|
||||
if (percent > 0.1) {
|
||||
esd = ESDCore.ExpectedSD(config.judgementWindows.iidx[context.playtype], percent);
|
||||
esd = ESDCore.CalculateESD(config.judgementWindows.iidx[context.playtype], percent);
|
||||
}
|
||||
|
||||
// Now we need to figure out the timestamp for this score.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import fetch from "node-fetch";
|
||||
import { Command } from "commander";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { config, Difficulties, ESDCore, IIDXBPIData } from "kamaitachi-common";
|
||||
import { FindSongOnTitleVersion } from "../score-import/database-lookup/song-title";
|
||||
import { FindChartWithPTDF } from "../score-import/database-lookup/chart-ptdf";
|
||||
import db from "../db";
|
||||
import createLogCtx from "../logger";
|
||||
const program = new Command();
|
||||
|
||||
const logger = createLogCtx("update-bpi-poyashi.ts");
|
||||
|
||||
program.option("-f, --fetch", "Fetch the latest data from the poyashi repo.");
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
const dataLoc = path.join(__dirname, "../bpi-poyashi-data/output.json");
|
||||
|
||||
const difficultyResolve: Record<string, [string, string]> = {
|
||||
3: ["SP", "HYPER"],
|
||||
4: ["SP", "ANOTHER"],
|
||||
8: ["DP", "HYPER"],
|
||||
9: ["DP", "ANOTHER"],
|
||||
10: ["SP", "LEGGENDARIA"],
|
||||
11: ["DP", "LEGGENDARIA"],
|
||||
};
|
||||
|
||||
async function UpdatePoyashiData() {
|
||||
let data;
|
||||
if (options.fetch) {
|
||||
// lol!
|
||||
logger.info("Fetching data from github...");
|
||||
let rj = await fetch(
|
||||
"https://raw.githubusercontent.com/potakusan/bpim-score-repo/master/output/release.json"
|
||||
).then((r) => r.json());
|
||||
|
||||
logger.info("Fetched data.");
|
||||
fs.writeFileSync(dataLoc, JSON.stringify(rj));
|
||||
logger.info("Saved data to output.json.");
|
||||
|
||||
data = rj;
|
||||
} else {
|
||||
logger.info("Reading data from output.json...");
|
||||
data = JSON.parse(fs.readFileSync(dataLoc, "utf-8"));
|
||||
}
|
||||
|
||||
let realData: IIDXBPIData[] = [];
|
||||
for (const d of data) {
|
||||
const res = difficultyResolve[d.difficulty];
|
||||
|
||||
if (!res) {
|
||||
throw new Error(`Unknown difficulty ${d.difficulty}`);
|
||||
}
|
||||
|
||||
const [playtype, diff] = res;
|
||||
|
||||
let ktchiSong = await FindSongOnTitleVersion("iidx", d.title, 28);
|
||||
|
||||
if (!ktchiSong) {
|
||||
throw new Error(`Cannot find song ${d.title}?`);
|
||||
}
|
||||
|
||||
let ktchiChart = await FindChartWithPTDF(
|
||||
"iidx",
|
||||
ktchiSong.id,
|
||||
playtype as "SP" | "DP",
|
||||
diff as Difficulties["iidx:DP" | "iidx:SP"]
|
||||
);
|
||||
|
||||
if (!ktchiChart) {
|
||||
throw new Error(
|
||||
`Cannot find chart ${ktchiSong.title} (${ktchiSong.id}) ${playtype}, ${diff}?`
|
||||
);
|
||||
}
|
||||
|
||||
let kavg = parseInt(d.avg);
|
||||
|
||||
let kesd = ESDCore.CalculateESD(
|
||||
config.judgementWindows.iidx.SP,
|
||||
kavg / (ktchiChart.notedata.notecount * 2)
|
||||
);
|
||||
|
||||
realData.push({
|
||||
coef: d.coef === -1 ? null : d.coef,
|
||||
kavg: parseInt(d.avg),
|
||||
wr: parseInt(d.wr),
|
||||
chartID: ktchiChart.chartID,
|
||||
kesd,
|
||||
});
|
||||
}
|
||||
|
||||
await db["iidx-bpi-data"].remove({});
|
||||
await db["iidx-bpi-data"].insert(realData);
|
||||
|
||||
logger.info(`Inserted ${realData.length} documents.`);
|
||||
}
|
||||
|
||||
UpdatePoyashiData();
|
||||
@@ -10,7 +10,8 @@ const DATA_DIR = path.join(__dirname, "./mock-db");
|
||||
async function ResetState(file: string) {
|
||||
let filename = path.basename(file, ".json");
|
||||
|
||||
await db.get(filename).remove({});
|
||||
// @ts-expect-error it's right, but we know what we're doing!
|
||||
await db[filename].remove({});
|
||||
|
||||
let fileLoc = path.join(DATA_DIR, file);
|
||||
let data = JSON.parse(fs.readFileSync(fileLoc, "utf-8"));
|
||||
@@ -19,7 +20,8 @@ async function ResetState(file: string) {
|
||||
throw new Error(`Panic, ${filename} not JSONArray?`);
|
||||
}
|
||||
|
||||
await db.get(filename).insert(data);
|
||||
// @ts-expect-error see above.
|
||||
await db[filename].insert(data);
|
||||
}
|
||||
|
||||
export default async function ResetDBState() {
|
||||
|
||||
Reference in New Issue
Block a user