mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 20:38:17 +03:00
Move scripts into src/
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
Hey. If by any chance I'm not available, you can (and should) run these scripts with TS-Node.
|
||||
https://github.com/TypeStrong/ts-node
|
||||
|
||||
They have only been tested for ts-node.
|
||||
|
||||
pnpm install -g ts-node
|
||||
@@ -0,0 +1,105 @@
|
||||
import { execSync } from "child_process";
|
||||
import { Command } from "commander";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import monk from "monk";
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.option("--nsFrom <Database to clone from.>")
|
||||
.option("--nsTo <Database to clone and anonymise to.>");
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
if (!options.nsTo || !options.nsFrom) {
|
||||
throw new Error(`Expected --nsFrom and --nsTo options.`);
|
||||
}
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
/**
|
||||
* Strips all name and private information from the database.
|
||||
* For use for things like future tachi-db-dumps?
|
||||
*
|
||||
* @param nsTo The database to anonymise. DO NOT PASS THE PRODUCTION DATABASE TO THIS!
|
||||
*/
|
||||
async function AnonymiseDB(nsTo: string) {
|
||||
logger.info(`Connecting to ${nsTo}.`);
|
||||
|
||||
const clonedDB = monk(`127.0.0.1/${nsTo}`);
|
||||
|
||||
logger.info(`Connected to ${nsTo}.`);
|
||||
|
||||
const r1 = await clonedDB.get("user-private-information").update(
|
||||
{},
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
// This is "password" encrypted in bcrypt12.
|
||||
password: "$2b$12$QRFCAxvFoNI2spszFPgt/e.qLy55GvYWlSHioa0AujRbFpChLwHmu",
|
||||
// emails are uniquely indexed. We need to anonymise these in
|
||||
// a way that they dont duplicate.
|
||||
email: { $concat: [{ $toString: "$userID" }, "@example.com"] },
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
multi: true,
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Stripped private info.`, { r1 });
|
||||
|
||||
const r2 = await clonedDB.get("users").update(
|
||||
{},
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
socialMedia: { $const: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
username: { $concat: ["u", { $toString: "$id" }] },
|
||||
usernameLowercase: { $concat: ["u", { $toString: "$id" }] },
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
multi: true,
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Stripped username info.`, { r2 });
|
||||
|
||||
for (const collection of [
|
||||
"api-clients",
|
||||
"oauth2-auth-codes",
|
||||
"password-reset-codes",
|
||||
"api-tokens",
|
||||
"kai-auth-tokens",
|
||||
"arc-saved-profiles",
|
||||
]) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const r3 = await clonedDB.get("collection").remove({});
|
||||
|
||||
logger.info(`Removed all ${collection} documents.`, { r3 });
|
||||
}
|
||||
|
||||
logger.info(`Done! Closing.`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function CloneDB(nsFrom: string, nsTo: string) {
|
||||
logger.info(`Cloning Database. This will take a while.`);
|
||||
execSync(`mongodump --gzip --archive=temp-dump --db=${nsFrom}`);
|
||||
execSync(`mongorestore --archive=temp-dump --gzip --nsFrom='${nsFrom}.*' --nsTo='${nsTo}.*'`);
|
||||
execSync(`rm temp-dump`);
|
||||
logger.info(`Cloned!`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
CloneDB(options.nsFrom, options.nsTo);
|
||||
AnonymiseDB(options.nsTo);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Command } from "commander";
|
||||
import { SendEmail } from "lib/email/client";
|
||||
import { MainHTMLWrapper } from "lib/email/formats";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
|
||||
const program = new Command();
|
||||
program.option("-e, --email <Email to send to>");
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
if (!options.email) {
|
||||
throw new Error(`Need an --email to send to.`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
logger.info(`Sending email to ${options.email}.`);
|
||||
await SendEmail(
|
||||
options.email,
|
||||
"Hello World",
|
||||
MainHTMLWrapper("Hello world! This is a test email for doing things."),
|
||||
"Hello world! This is a test email for doing things."
|
||||
);
|
||||
logger.info(`Done.`);
|
||||
process.exit(0);
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { InitaliseFolderChartLookup } from "../../utils/folder";
|
||||
|
||||
InitaliseFolderChartLookup().then(() => process.exit(0));
|
||||
@@ -0,0 +1,180 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-case-declarations */
|
||||
/* eslint-disable no-param-reassign */
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import fetch from "node-fetch";
|
||||
import { Difficulties, integer } from "tachi-common";
|
||||
import { FindChartWithPTDF } from "utils/queries/charts";
|
||||
import { FindSongOnTitle } from "utils/queries/songs";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
interface SP12Data {
|
||||
id: integer;
|
||||
title: string;
|
||||
n_clear: integer;
|
||||
hard: integer;
|
||||
exh: integer;
|
||||
n_clear_string: string;
|
||||
hard_string: string;
|
||||
exh_string: string;
|
||||
version: integer;
|
||||
}
|
||||
|
||||
async function FetchSP12Data() {
|
||||
const rj: { sheets: SP12Data[] } = await fetch("https://sp12.iidx.app/api/v1/sheets").then(
|
||||
(r) => r.json()
|
||||
); // will throw if somethings wrong, anyway
|
||||
|
||||
if (!rj.sheets) {
|
||||
throw new Error(`No sheets in return from sp12?`);
|
||||
}
|
||||
|
||||
for (const sh of rj.sheets) {
|
||||
let chart;
|
||||
try {
|
||||
chart = await HumanisedTitleLookup(sh.title);
|
||||
} catch (err) {
|
||||
logger.error((err as Error).message);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!chart) {
|
||||
logger.warn(`Couldn't find chart with title ${sh.title}`);
|
||||
}
|
||||
|
||||
for (const key of ["n_clear", "hard", "exh"] as const) {
|
||||
let val;
|
||||
|
||||
switch (key) {
|
||||
case "n_clear":
|
||||
const v = Math.floor(sh[key] / 2);
|
||||
|
||||
if (v === 9) {
|
||||
val = 11.8;
|
||||
} else if (v === 8) {
|
||||
val = 12.0;
|
||||
} else if (v === 7) {
|
||||
val = 12.2;
|
||||
} else if (v === 6) {
|
||||
val = 12.4;
|
||||
} else if (v === 5) {
|
||||
val = 12.6;
|
||||
} else if (v < 0) {
|
||||
continue;
|
||||
} else {
|
||||
val = 12.6 + (5 - v) * 0.1;
|
||||
}
|
||||
|
||||
break;
|
||||
case "hard":
|
||||
const v2 = Math.floor(sh[key] / 2);
|
||||
|
||||
if (v2 === 9) {
|
||||
val = 11.9;
|
||||
} else if (v2 === 8) {
|
||||
val = 12.1;
|
||||
} else if (v2 === 7) {
|
||||
val = 12.3;
|
||||
} else if (v2 === 6) {
|
||||
val = 12.5;
|
||||
} else if (v2 === 5) {
|
||||
val = 12.7;
|
||||
} else if (v2 < 0) {
|
||||
continue;
|
||||
} else {
|
||||
val = 12.7 + (5 - v2) * 0.1;
|
||||
}
|
||||
|
||||
break;
|
||||
case "exh":
|
||||
const v3 = sh[key];
|
||||
if (v3 >= 12 || v3 <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
val = 12.4 + (12 - v3) * 0.1;
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error("??");
|
||||
}
|
||||
|
||||
const stringVal = sh[`${key}_string` as const];
|
||||
|
||||
if (stringVal === "難易度未定") {
|
||||
continue;
|
||||
}
|
||||
|
||||
val = parseFloat(val.toFixed(2));
|
||||
|
||||
const ktKey = key === "exh" ? "kt-EXHC" : key === "hard" ? "kt-HC" : "kt-NC";
|
||||
|
||||
// If EXH, just save the string version as 12.xx
|
||||
// Else, use the A/S+ thing that people are used to.
|
||||
const text =
|
||||
ktKey === "kt-EXHC"
|
||||
? val.toFixed(2)
|
||||
: `12${stringVal.replace(/(個人差|地力)/u, "")}`;
|
||||
|
||||
await db.charts.iidx.update(
|
||||
{
|
||||
chartID: chart.chartID,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
[`tierlistInfo.${ktKey}`]: {
|
||||
text,
|
||||
value: val,
|
||||
individualDifference: stringVal.includes("個人差"),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Saved ${sh.title} value ${key} = ${val} (${text}).`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Done!");
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function HumanisedTitleLookup(originalTitle: string) {
|
||||
let difficulty: Difficulties["iidx:SP"] = "ANOTHER";
|
||||
|
||||
let title = originalTitle;
|
||||
|
||||
if (title.match(/(†|†LEGGENDARIA)$/u)) {
|
||||
difficulty = "LEGGENDARIA";
|
||||
title = title.split(/(†|†LEGGENDARIA)$/u)[0];
|
||||
} else if (title.match(/\[H\]$/u)) {
|
||||
difficulty = "HYPER";
|
||||
title = title.split("[")[0];
|
||||
} else if (title.match(/\[A\]$/u)) {
|
||||
difficulty = "ANOTHER"; // lmao
|
||||
title = title.split("[")[0];
|
||||
}
|
||||
|
||||
const song = await FindSongOnTitle("iidx", title);
|
||||
|
||||
if (!song) {
|
||||
throw new Error(
|
||||
`Could not resolve song ${title} (${difficulty}) (Original ${originalTitle}).`
|
||||
);
|
||||
}
|
||||
|
||||
const chart = await FindChartWithPTDF("iidx", song.id, "SP", difficulty);
|
||||
|
||||
if (!chart) {
|
||||
throw new Error(
|
||||
`Could not resolve chart ${title} ${difficulty} (Original ${originalTitle}).`
|
||||
);
|
||||
}
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
FetchSP12Data();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { InitSequenceDocs } from "external/mongo/sequence-docs";
|
||||
|
||||
if (require.main === module) {
|
||||
InitSequenceDocs().then(() => process.exit(0));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Command } from "commander";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.option("-c, --code <code>", "The code for this invite.");
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
if (!options.code) {
|
||||
options.code = Random20Hex();
|
||||
}
|
||||
|
||||
db.invites
|
||||
.insert({
|
||||
code: options.code,
|
||||
createdBy: 1,
|
||||
consumed: false,
|
||||
createdAt: Date.now(),
|
||||
consumedBy: null,
|
||||
consumedAt: null,
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`Created invite ${options.code}.`);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import { ICollection } from "monk";
|
||||
import { ChartDocument, IDStrings } from "tachi-common";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
|
||||
export async function QuickInsert<I extends IDStrings>(
|
||||
base: Omit<ChartDocument<I>, "chartID">,
|
||||
content: Partial<ChartDocument<I>>[],
|
||||
coll: ICollection
|
||||
) {
|
||||
const docs = [];
|
||||
|
||||
for (const c of content) {
|
||||
const doc = deepmerge(base, c);
|
||||
|
||||
doc.chartID = Random20Hex();
|
||||
docs.push(doc);
|
||||
}
|
||||
|
||||
await coll.insert(docs);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { Command } from "commander";
|
||||
import { SetIndexes } from "../external/mongo/indexes";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.option("-d, --db <database>", "The database to index.");
|
||||
program.option(
|
||||
"-r, --reset",
|
||||
"Whether to reset all indexes on this database before indexing or not."
|
||||
);
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
SetIndexes(options.db, options.reset).then(() => process.exit(0));
|
||||
@@ -0,0 +1,36 @@
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
async function DedupeScoreIDs() {
|
||||
const dups = await db.scores.aggregate(
|
||||
[
|
||||
{
|
||||
$group: {
|
||||
_id: "$scoreID",
|
||||
dups: { $addToSet: "$_id" },
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
count: { $gt: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
logger.info(`Found ${dups.length} dups.`);
|
||||
|
||||
for (const dup of dups) {
|
||||
dup.dups.shift();
|
||||
await db.scores.remove({ _id: { $in: dup.dups } });
|
||||
}
|
||||
|
||||
logger.info(`Done.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
DedupeScoreIDs();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { RecalcAllScores } from "utils/calculations/recalc-scores";
|
||||
|
||||
if (require.main === module) {
|
||||
RecalcAllScores();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export async function CreateGameProfiles() {
|
||||
const users = await db.users.find({});
|
||||
|
||||
for (const user of users) {
|
||||
const gpts = await db.scores.aggregate([
|
||||
{
|
||||
$match: {
|
||||
userID: user.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
game: "$game",
|
||||
playtype: "$playtype",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
logger.info(`Found ${gpts.length} GPTs for ${FormatUserDoc(user)}`);
|
||||
|
||||
for (const gpt of gpts) {
|
||||
const { game, playtype } = gpt._id;
|
||||
logger.info(`Updating ${FormatUserDoc(user)}'s ${game} ${playtype} stats.`);
|
||||
await UpdateUsersGamePlaytypeStats(game, playtype, user.id, null, logger);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Done.`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
CreateGameProfiles().then(() => process.exit(0));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { RecalcSessions } from "utils/calculations/recalc-sessions";
|
||||
|
||||
if (require.main === module) {
|
||||
RecalcSessions().then(() => process.exit(0));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { RecalcAllScores, UpdateAllPBs } from "utils/calculations/recalc-scores";
|
||||
import { RecalcSessions } from "utils/calculations/recalc-sessions";
|
||||
import { CreateGameProfiles } from "./recalc-game-profiles";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
(async () => {
|
||||
await RecalcAllScores();
|
||||
await UpdateAllPBs();
|
||||
await CreateGameProfiles();
|
||||
await RecalcSessions();
|
||||
|
||||
logger.info(`Completely done!`);
|
||||
process.exit(0);
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
|
||||
|
||||
if (require.main === module) {
|
||||
UpdateAllPBs();
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
// This script syncs this tachi instances database up with the tachi-database-seeds.
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { monkDB } from "external/mongo/db";
|
||||
import fjsh from "fast-json-stable-hash";
|
||||
import fs from "fs";
|
||||
import CreateLogCtx, { KtLogger } from "lib/logger/logger";
|
||||
import UpdateIsPrimaryStatus from "lib/score-mutation/update-isprimary";
|
||||
import { Environment, TachiConfig } from "lib/setup/config";
|
||||
import { BulkWriteOperation } from "mongodb";
|
||||
import { ICollection } from "monk";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import {
|
||||
BMSCourseDocument,
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
} from "tachi-common";
|
||||
import { RecalcAllScores } from "utils/calculations/recalc-scores";
|
||||
import { InitaliseFolderChartLookup } from "utils/folder";
|
||||
|
||||
interface SyncInstructions {
|
||||
pattern: RegExp;
|
||||
handler: (c: any[], collection: ICollection<any>, logger: KtLogger) => Promise<unknown>;
|
||||
}
|
||||
|
||||
async function RemoveNotPresent<T>(
|
||||
documents: T[],
|
||||
collection: ICollection<T>,
|
||||
field: keyof T,
|
||||
logger: KtLogger
|
||||
) {
|
||||
logger.verbose(`Removing all documents that are no longer present.`);
|
||||
// Remove anything no longer present.
|
||||
// Note that $nin is incredibly slow.
|
||||
// @ts-expect-error generic system failing
|
||||
const r = await collection.remove({
|
||||
[field]: { $nin: documents.map((e) => e[field]) },
|
||||
});
|
||||
|
||||
// @ts-expect-error These types are broken!
|
||||
if (r.deletedCount) {
|
||||
// @ts-expect-error These types are broken!
|
||||
logger.info(`Removed ${r.deletedCount} documents.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function GenericUpsert<T>(
|
||||
documents: T[],
|
||||
collection: ICollection<T>,
|
||||
field: keyof T,
|
||||
logger: KtLogger,
|
||||
remove = false
|
||||
) {
|
||||
logger.verbose(`Running bulkwrite.`);
|
||||
|
||||
const bwriteOps: BulkWriteOperation<T>[] = [];
|
||||
|
||||
const allExistingDocs = await collection.find({});
|
||||
|
||||
const map = new Map();
|
||||
for (const doc of allExistingDocs) {
|
||||
map.set(doc[field], doc);
|
||||
}
|
||||
|
||||
const changedFields = [];
|
||||
|
||||
let i = 0;
|
||||
for (const document of documents) {
|
||||
i++;
|
||||
if (i % 10_000 === 0) {
|
||||
logger.verbose(`On document ${i}/${documents.length}.`);
|
||||
}
|
||||
|
||||
const exists = map.get(document[field]);
|
||||
|
||||
if (!exists) {
|
||||
bwriteOps.push({
|
||||
// @ts-expect-error Actually, T is assignable to OptionalId<T>.
|
||||
insertOne: { document },
|
||||
});
|
||||
} else if (fjsh.hash(document, "sha256") !== fjsh.hash(exists, "sha256")) {
|
||||
bwriteOps.push({
|
||||
replaceOne: {
|
||||
// @ts-expect-error Known X->Y generic issue.
|
||||
filter: {
|
||||
[field]: document[field],
|
||||
},
|
||||
replacement: document,
|
||||
},
|
||||
});
|
||||
|
||||
changedFields.push(field);
|
||||
}
|
||||
|
||||
// free some memory.
|
||||
map.delete(document[field]);
|
||||
}
|
||||
|
||||
if (bwriteOps.length === 0) {
|
||||
logger.verbose(`No differences. Not performing any update.`);
|
||||
} else {
|
||||
const { deletedCount, insertedCount, matchedCount, upsertedCount, modifiedCount } =
|
||||
await collection.bulkWrite(bwriteOps);
|
||||
|
||||
logger.info(`Performed bulkWrite.`, {
|
||||
deletedCount,
|
||||
insertedCount,
|
||||
matchedCount,
|
||||
upsertedCount,
|
||||
modifiedCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
await RemoveNotPresent(documents, collection, field, logger);
|
||||
}
|
||||
|
||||
return {
|
||||
thingsChanged: bwriteOps.length,
|
||||
changedFields,
|
||||
};
|
||||
}
|
||||
|
||||
const syncInstructions: SyncInstructions[] = [
|
||||
{
|
||||
pattern: /^charts-(usc|bms)$/u,
|
||||
handler: async (
|
||||
charts: ChartDocument[],
|
||||
collection: ICollection<ChartDocument>,
|
||||
logger
|
||||
) => {
|
||||
// Since the USC and BMS databases are managed Bokutachi-side, we
|
||||
// shouldn't be honoring any sort of updates from tachi-database-seeds
|
||||
// aside from an initial one.
|
||||
//
|
||||
// However, in practice, these syncs are the best way to actually update
|
||||
// issues in the database.
|
||||
// We're going to disable this anyway.
|
||||
// const isInitial = (await collection.findOne()) === null;
|
||||
|
||||
const r = await GenericUpsert(charts, collection, "chartID", logger, false);
|
||||
|
||||
if (r.thingsChanged) {
|
||||
await InitaliseFolderChartLookup();
|
||||
await UpdateIsPrimaryStatus();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: /^charts-/u,
|
||||
handler: async (
|
||||
charts: ChartDocument[],
|
||||
collection: ICollection<ChartDocument>,
|
||||
logger
|
||||
) => {
|
||||
const r = await GenericUpsert(charts, collection, "chartID", logger, true);
|
||||
|
||||
if (r.thingsChanged) {
|
||||
await InitaliseFolderChartLookup();
|
||||
await UpdateIsPrimaryStatus();
|
||||
|
||||
await RecalcAllScores({
|
||||
chartID: { $in: r.changedFields },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: /^songs-/u,
|
||||
handler: async (songs: SongDocument[], collection: ICollection<SongDocument>, logger) => {
|
||||
const r = await GenericUpsert(songs, collection, "id", logger);
|
||||
|
||||
if (r.thingsChanged) {
|
||||
await RecalcAllScores({
|
||||
songID: { $in: r.changedFields },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: /^folders$/u,
|
||||
handler: async (
|
||||
folders: FolderDocument[],
|
||||
collection: ICollection<FolderDocument>,
|
||||
logger
|
||||
) => {
|
||||
const r = await GenericUpsert(folders, collection, "folderID", logger, true);
|
||||
|
||||
if (r) {
|
||||
await InitaliseFolderChartLookup();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: /^tables$/u,
|
||||
handler: (tables: TableDocument[], collection: ICollection<TableDocument>, logger) =>
|
||||
GenericUpsert(tables, collection, "tableID", logger, true),
|
||||
},
|
||||
{
|
||||
pattern: /^bms-course-lookup$/u,
|
||||
handler: (
|
||||
bmsCourseDocuments: BMSCourseDocument[],
|
||||
collection: ICollection<BMSCourseDocument>,
|
||||
logger
|
||||
) => GenericUpsert(bmsCourseDocuments, collection, "md5sums", logger),
|
||||
},
|
||||
];
|
||||
|
||||
const logger = CreateLogCtx("Database Sync");
|
||||
|
||||
async function SynchroniseDBWithSeeds() {
|
||||
const seedsDir = fs.mkdtempSync(path.join(os.tmpdir(), "tachi-database-seeds-"));
|
||||
|
||||
logger.info(`Cloning data to ${seedsDir}.`);
|
||||
|
||||
fs.rmSync(seedsDir, { recursive: true, force: true });
|
||||
|
||||
// Wait for mongo to connect first.
|
||||
await monkDB.then(() => void 0);
|
||||
|
||||
execSync(
|
||||
`git clone https://github.com/TNG-dev/tachi-database-seeds -b "${
|
||||
Environment.nodeEnv === "production" ? "master" : "develop"
|
||||
}" --depth=1 "${seedsDir}"`,
|
||||
{
|
||||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
|
||||
const collections = fs.readdirSync(path.join(seedsDir, "collections"));
|
||||
|
||||
for (const jsonName of collections) {
|
||||
const collectionName = path.parse(jsonName).name;
|
||||
const spawnLogger = CreateLogCtx(`${collectionName} Sync`);
|
||||
|
||||
if (collectionName.startsWith("songs-") || collectionName.startsWith("charts-")) {
|
||||
const game = collectionName.split("-")[1];
|
||||
|
||||
if (!TachiConfig.GAMES.includes(game as any)) {
|
||||
spawnLogger.verbose(
|
||||
`Skipping ${collectionName} (${game}) as it isn't for ${TachiConfig.NAME}.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
spawnLogger.verbose(`Getting data.`);
|
||||
|
||||
let data = JSON.parse(
|
||||
fs.readFileSync(path.join(seedsDir, "collections", jsonName), "utf-8")
|
||||
);
|
||||
|
||||
spawnLogger.verbose(`Found ${data.length} documents.`);
|
||||
|
||||
let matchedSomething = false;
|
||||
for (const syncInst of syncInstructions) {
|
||||
if (collectionName.match(syncInst.pattern)) {
|
||||
spawnLogger.verbose(`Starting handler...`);
|
||||
await syncInst.handler(data, monkDB.get(collectionName), spawnLogger);
|
||||
matchedSomething = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// free memory forcefully
|
||||
data = null;
|
||||
|
||||
if (!matchedSomething) {
|
||||
spawnLogger.warn(
|
||||
`Collection ${collectionName} didn't match any sync instructions. Skipping.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Done.`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
SynchroniseDBWithSeeds().then(() => process.exit(0));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// note: a hell of a lot of this code is copy pasted
|
||||
// this is just a script, so, feel free to refactor it.
|
||||
import { Command } from "commander";
|
||||
import { Databases, monkDB } from "external/mongo/db";
|
||||
import { DatabaseSchemas } from "external/mongo/schemas";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { PrudenceError } from "prudence";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
|
||||
const program = new Command();
|
||||
program.option("-c, --collection <collectionName>");
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export async function ValidateCollection(collectionName: Databases): Promise<void> {
|
||||
const schemaRunner = DatabaseSchemas[collectionName];
|
||||
|
||||
const documents = await monkDB.get(collectionName).count({});
|
||||
|
||||
logger.info(`Validating ${collectionName}. This is ${documents} documents.
|
||||
`);
|
||||
let success = 0;
|
||||
let fails = 0;
|
||||
|
||||
await monkDB
|
||||
.get(collectionName)
|
||||
.find({})
|
||||
// @ts-expect-error faulty monk types
|
||||
.each((c) => {
|
||||
try {
|
||||
schemaRunner(c);
|
||||
success++;
|
||||
} catch (err) {
|
||||
logger.error(`[${collectionName}]: ${FormatPrError(err as PrudenceError)}`, c);
|
||||
fails++;
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Success: ${success} (${(100 * success) / documents}%)`);
|
||||
logger.info(`Failures: ${fails} (${(100 * fails) / documents}%)`);
|
||||
}
|
||||
|
||||
export async function ValidateAllCollections() {
|
||||
for (const collectionName in DatabaseSchemas) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await ValidateCollection(collectionName as keyof typeof DatabaseSchemas);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (options.collection) {
|
||||
ValidateCollection(options.collection).then(() => process.exit(0));
|
||||
} else {
|
||||
ValidateAllCollections().then(() => process.exit(0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user