fantasy-manager/scripts/startup.ts
Lukas Schaefer 33a307d19a
Some checks failed
Build / build (pull_request) Successful in 1m2s
Cypress / cypress (pull_request) Successful in 2m31s
Build / build (push) Successful in 1m11s
Cypress / cypress (push) Has been cancelled
First commit for moving git instances
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-07-30 19:08:51 -04:00

325 lines
11 KiB
TypeScript

import { createHash } from "crypto";
import fs from "fs";
import { Plugins as PluginsType } from "#type/db"; // Renamed to avoid conflict
import { compareSemanticVersions } from "#Modules/semantic";
import store from "../types/store"; // Assuming this type is correctly defined
import dotenv from "dotenv";
import { finished } from "stream/promises";
import { Readable } from "stream";
import { ReadableStream } from "stream/web";
import { createConfig, migrateToLatest } from "#scripts/migrate";
import db from "#database"; // Kysely instance
import { Selectable } from "kysely";
// Load environment variables
if (process.env.APP_ENV !== "test") {
dotenv.config({ path: ".env.local" });
} else {
dotenv.config({ path: ".env.test.local" });
}
// Downloads and generates all the plugin code
async function compilePlugins() {
console.log("Compiling plugins");
let mainFileTextStart = `// Note that this file is autogenerated by startup.ts DO NOT EDIT\nimport dataGetter from "#type/data";\n`;
let mainFileText =
"export const plugins: { [key: string]: dataGetter } = {\n";
const request =
process.env.APP_ENV !== "test"
? await fetch(
"https://git.lschaefer.xyz/lukasdotcom/fantasy-manager/raw/branch/main/store/default_store.json",
).catch((err) => {
console.error("Could not get the default store:", err);
return "error" as const; // Ensure type is 'error' literal
})
: ("error" as const);
const defaultStore: string[] =
request instanceof Response
? await request.json().catch((err) => {
console.error("Could not parse the default store JSON:", err);
return [];
})
: // Uses a fallback store if the request fails(this is also the testing store)
[
"https://git.lschaefer.xyz/lukasdotcom/fantasy-manager/raw/branch/main/store/Bundesliga_Testing/Bundesliga_Testing.json",
];
// Installs all plugins that should be installed by default
if (defaultStore.length > 0) {
await db
.insertInto("plugins")
.onConflict((eb) => eb.doNothing())
.values(
defaultStore.map((pluginUrl) => ({
name: "",
settings: "{}",
enabled: 0,
installed: 0,
url: pluginUrl,
})),
)
.execute();
}
// Makes sure that bundesliga is enabled when testing
if (process.env.APP_ENV === "test") {
await db.updateTable("plugins").set({ enabled: 1 }).execute(); // This will enable ALL plugins
}
const currentVersion = (await import("../package.json")).default.version;
// Makes sure that the store is correct
await db
.insertInto("data")
.values({
value1: "defaultStore",
value2: JSON.stringify(defaultStore),
})
.onConflict((oc) =>
oc.column("value1").doUpdateSet({ value2: JSON.stringify(defaultStore) }),
)
.execute();
const pluginRecords: Selectable<PluginsType>[] = await db
.selectFrom("plugins")
.selectAll()
.execute();
await Promise.all(
pluginRecords.map(async (pluginEntry) => {
const dataResponse = await fetch(pluginEntry.url).catch(
() => "error" as const,
);
if (dataResponse === "error" || !(dataResponse instanceof Response)) {
console.log(`Failed to fetch plugin data from ${pluginEntry.url}`);
return;
}
const jsonStore: store | "error" = await dataResponse
.json()
.catch(() => "error" as const);
if (jsonStore === "error") {
console.log(`Failed to parse JSON for plugin at ${pluginEntry.url}`);
return;
}
// Update plugin name in DB
await db
.updateTable("plugins")
.set({ name: jsonStore.id })
.where("url", "=", pluginEntry.url)
.execute();
pluginEntry.name = jsonStore.id; // Update local copy
// Makes sure the plugin is compatible with the current version
if (
compareSemanticVersions(
jsonStore.min_version || "0.0.1",
currentVersion,
) === -1
) {
console.error(
`Plugin ${pluginEntry.name} (min_version: ${
jsonStore.min_version || "0.0.1"
}) is not compatible with the current program version ${currentVersion}. Disabling.`,
);
await db
.updateTable("plugins")
.set({ version: "", enabled: 0, installed: 0 })
.where("url", "=", pluginEntry.url)
.execute();
return;
}
const hash = createHash("sha256").update(pluginEntry.url).digest("hex");
const pluginDir = `scripts/data/${hash}`;
if (
pluginEntry.version !== jsonStore.version ||
!fs.existsSync(pluginDir) || // Check directory existence
!pluginEntry.installed
) {
if (pluginEntry.version === jsonStore.version) {
console.log(
`Updating plugin ${pluginEntry.name} (version ${jsonStore.version}) files.`,
);
} else {
console.log(
`Installing plugin ${pluginEntry.name} (version ${jsonStore.version}). Old version: ${pluginEntry.version}`,
);
}
if (!fs.existsSync("scripts/data")) {
fs.mkdirSync("scripts/data");
}
if (fs.existsSync(pluginDir)) {
fs.rmSync(pluginDir, { recursive: true, force: true });
}
fs.mkdirSync(pluginDir);
try {
await Promise.all(
jsonStore.files.map(async (fileUrl) => {
const fileName = fileUrl.split("/").pop();
if (!fileName) {
console.error(
`Could not determine filename from ${fileUrl} for plugin ${pluginEntry.name}`,
);
throw new Error(`Invalid file URL in plugin store: ${fileUrl}`);
}
// __dirname points to the directory of the current module (startup.ts)
// If scripts/data is relative to project root, adjust path.
// Assuming scripts/data is at project root:
const filePath = `scripts/data/${hash}/${fileName}`;
const stream = fs.createWriteStream(filePath);
const { body, status } = await fetch(fileUrl);
if (!body || status !== 200) {
throw new Error(
`Failed to download ${fileUrl} (status: ${status})`,
);
}
await finished(
Readable.fromWeb(body as ReadableStream<Uint8Array>).pipe(
stream,
),
);
}),
);
console.log(`Finished downloading plugin ${pluginEntry.name}`);
mainFileTextStart += `import plugin${hash} from "./data/${hash}";\n`; // Assuming default export from plugin's index
mainFileText += ` "${pluginEntry.url}":\n plugin${hash},\n`;
await db
.updateTable("plugins")
.set({
version: jsonStore.version,
installed: 1,
enabled: pluginEntry.enabled ?? 0,
}) // Preserve enabled state
.where("url", "=", pluginEntry.url)
.execute();
} catch (downloadError) {
console.error(
`Failed to download files for plugin ${pluginEntry.name}. Error: ${downloadError}. Restart server to try again.`,
);
await db
.updateTable("plugins")
.set({ version: "", enabled: 0, installed: 0 })
.where("url", "=", pluginEntry.url)
.execute();
}
} else {
// Already installed and up-to-date
mainFileTextStart += `import plugin${hash} from "./data/${hash}";\n`;
mainFileText += ` "${pluginEntry.url}":\n plugin${hash},\n`;
}
}),
);
mainFileText += "};\nexport default plugins;\n";
// Ensure scripts directory exists
if (!fs.existsSync("scripts")) {
fs.mkdirSync("scripts");
}
fs.writeFileSync("scripts/data.ts", mainFileTextStart + mainFileText);
console.log("Done compiling plugins");
}
async function startUp() {
const currentProgVersion = (await import("../package.json")).default.version;
// Ensures that invalid database migration does not happen
const dbVersionEntry = await db
.selectFrom("data")
.select("value2")
.where("value1", "=", "version")
.executeTakeFirst()
.catch(() => undefined); // Handle case where 'data' table or 'version' row might not exist yet
if (dbVersionEntry) {
const storedDbVersion = dbVersionEntry.value2;
if (compareSemanticVersions(storedDbVersion, "1.20.3") === 1) {
console.error(
`Cannot upgrade from database version ${storedDbVersion} (produced before program version 1.20.3). Please upgrade to 1.20.3 first.`,
);
process.exit(1);
}
if (compareSemanticVersions(storedDbVersion, currentProgVersion) === -1) {
console.error(
`Database version ${storedDbVersion} is newer than the current program version ${currentProgVersion}. Cannot downgrade. Please use a compatible program version or database.`,
);
process.exit(1);
}
}
await migrateToLatest(); // Assumes this handles DB schema creation/migration
// Checks if the server hash has been created and if not makes one
const serverId =
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2);
await db
.insertInto("data")
.onConflict((eb) => eb.doNothing())
.values({ value1: "serverID", value2: serverId })
.execute();
// Unlocks the database by removing 'locked' entries for plugins
const allPlugins: Selectable<PluginsType>[] = await db
.selectFrom("plugins")
.selectAll()
.execute();
for (const plugin of allPlugins) {
if (plugin.name) {
// Ensure plugin.name is not null or empty
await db
.deleteFrom("data")
.where("value1", "=", `locked${plugin.name}`)
.execute();
}
}
await createConfig(); // Assumes this sets up necessary config in 'data' table
// Makes sure that the admin user is the correct user
await db.updateTable("users").set({ admin: 0 }).execute(); // Reset all admins
if (process.env.ADMIN !== undefined) {
const adminUserId = parseInt(process.env.ADMIN);
if (!isNaN(adminUserId)) {
console.log(`Setting user ${adminUserId} as the admin user.`);
const result = await db
.updateTable("users")
.set({ admin: 1 })
.where("id", "=", adminUserId)
.executeTakeFirst(); // Kysely returns UpdateResult, check affected rows
if (
result &&
result.numUpdatedRows === BigInt(0) &&
result.numChangedRows === BigInt(0)
) {
console.warn(`Admin user ID ${adminUserId} not found. No admin set.`);
}
} else {
console.warn(
`Invalid ADMIN user ID in environment: ${process.env.ADMIN}. No admin set.`,
);
}
} else {
console.log("ADMIN environment variable not set. Admin user is disabled.");
}
// Updated version of database in table
await db
.insertInto("data")
.values({ value1: "version", value2: currentProgVersion })
.onConflict((oc) =>
oc.column("value1").doUpdateSet({ value2: currentProgVersion }),
)
.execute();
await compilePlugins(); // Compile plugins after DB setup and migration
}
startUp();