feat: rearchitecture of database schemas, migration reset, and #180

This commit is contained in:
DecDuck
2025-08-20 20:35:50 +10:00
parent 6853383e86
commit 322af0b4ca
125 changed files with 1384 additions and 1837 deletions

View File

@ -14,14 +14,26 @@ export default defineEventHandler(async (h3) => {
},
include: {
versions: {
where: {
gameVersion: {
isNot: null,
},
},
orderBy: {
versionIndex: "asc",
gameVersion: {
versionIndex: "asc",
},
},
select: {
versionIndex: true,
versionId: true,
versionName: true,
platform: true,
delta: true,
gameVersion: {
select: {
versionIndex: true,
delta: true,
},
},
},
},
tags: true,

View File

@ -5,7 +5,6 @@ import prisma from "~/server/internal/db/database";
const DeleteVersion = type({
id: "string",
versionName: "string",
}).configure(throwingArktype);
export default defineEventHandler<{ body: typeof DeleteVersion }>(
@ -17,15 +16,9 @@ export default defineEventHandler<{ body: typeof DeleteVersion }>(
const body = await readDropValidatedBody(h3, DeleteVersion);
const gameId = body.id.toString();
const version = body.versionName.toString();
await prisma.gameVersion.delete({
await prisma.version.delete({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
versionId: body.id,
},
});

View File

@ -16,32 +16,24 @@ export default defineEventHandler<{ body: typeof UpdateVersionOrder }>(
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, UpdateVersionOrder);
const gameId = body.id;
// We expect an array of the version names for this game
const versions = body.versions;
const newVersions = await prisma.$transaction(
versions.map((versionName, versionIndex) =>
await prisma.$transaction(
versions.map((versionId, versionIndex) =>
prisma.gameVersion.update({
where: {
gameId_versionName: {
gameId: gameId,
versionName: versionName,
},
versionId,
},
data: {
versionIndex: versionIndex,
},
select: {
versionIndex: true,
versionName: true,
platform: true,
delta: true,
},
select: {},
}),
),
);
return newVersions;
setResponseStatus(h3, 201);
return;
},
);

View File

@ -1,48 +1,42 @@
import { type } from "arktype";
import { Platform } from "~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
import { parsePlatform } from "~/server/internal/utils/parseplatform";
const ImportVersion = type({
export const ImportVersion = type({
id: "string",
version: "string",
name: "string?",
platform: "string",
launch: "string = ''",
launchArgs: "string = ''",
platform: type.valueOf(Platform),
setup: "string = ''",
setupArgs: "string = ''",
onlySetup: "boolean = false",
delta: "boolean = false",
umuId: "string = ''",
launches: type({
name: "string > 0",
description: "string = ''",
launchCommand: "string > 0",
launchArgs: "string = ''",
}).array(),
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const {
id,
version,
platform,
launch,
launchArgs,
setup,
setupArgs,
onlySetup,
delta,
umuId,
} = await readDropValidatedBody(h3, ImportVersion);
const body = await readDropValidatedBody(h3, ImportVersion);
const platformParsed = parsePlatform(platform);
if (!platformParsed)
throw createError({ statusCode: 400, statusMessage: "Invalid platform." });
if (delta) {
if (body.delta) {
const validOverlayVersions = await prisma.gameVersion.count({
where: { gameId: id, platform: platformParsed, delta: false },
where: {
version: { gameId: body.id, platform: body.platform },
delta: false,
},
});
if (validOverlayVersions == 0)
throw createError({
@ -52,33 +46,27 @@ export default defineEventHandler(async (h3) => {
});
}
if (onlySetup) {
if (!setup)
if (body.onlySetup) {
if (!body.setup)
throw createError({
statusCode: 400,
statusMessage: 'Setup required in "setup mode".',
});
} else {
if (!delta && !launch)
if (!body.delta && body.launches.length == 0)
throw createError({
statusCode: 400,
statusMessage: "Launch executable is required for non-update versions",
statusMessage:
"At least one launch command is required for non-delta versions",
});
}
// startup & delta require more complex checking logic
const taskId = await libraryManager.importVersion(id, version, {
platform,
onlySetup,
launch,
launchArgs,
setup,
setupArgs,
umuId,
delta,
});
const taskId = await libraryManager.importVersion(
body.id,
body.version,
body,
);
if (!taskId)
throw createError({
statusCode: 400,

View File

@ -4,14 +4,13 @@ import manifestGenerator from "~/server/internal/downloads/manifest";
export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3);
const id = query.id?.toString();
const version = query.version?.toString();
if (!id || !version)
if (!id)
throw createError({
statusCode: 400,
statusMessage: "Missing id or version in query",
statusMessage: "Missing version id in query",
});
const manifest = await manifestGenerator.generateManifest(id, version);
const manifest = await manifestGenerator.generateManifest(id);
if (!manifest)
throw createError({
statusCode: 400,

View File

@ -13,10 +13,7 @@ export default defineClientEventHandler(async (h3) => {
const gameVersion = await prisma.gameVersion.findUnique({
where: {
gameId_versionName: {
gameId: id,
versionName: version,
},
versionId: id,
},
});

View File

@ -12,26 +12,15 @@ export default defineClientEventHandler(async (h3) => {
const versions = await prisma.gameVersion.findMany({
where: {
gameId: id,
version: {
gameId: id,
},
hidden: false,
},
orderBy: {
versionIndex: "desc", // Latest one first
},
});
const mappedVersions = versions
.map((version) => {
if (!version.dropletManifest) return undefined;
const newVersion = { ...version, dropletManifest: undefined };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore idk why we delete an undefined object
delete newVersion.dropletManifest;
return {
...newVersion,
};
})
.filter((e) => e);
return mappedVersions;
return versions;
});

View File

@ -15,12 +15,13 @@ class DownloadContextManager {
}
> = new Map();
async createContext(game: string, versionName: string) {
const version = await prisma.gameVersion.findUnique({
async createContext(game: string, versionPath: string) {
const version = await prisma.version.findFirst({
where: {
gameId_versionName: {
gameId: game,
versionName,
gameId: game,
versionPath,
gameVersion: {
isNot: null,
},
},
include: {
@ -38,9 +39,9 @@ class DownloadContextManager {
this.contexts.set(contextId, {
timeout: new Date(),
manifest: JSON.parse(version.dropletManifest as string) as DropManifest,
versionName,
libraryId: version.game.libraryId!,
libraryPath: version.game.libraryPath,
versionName: versionPath,
libraryId: version.game!.libraryId!,
libraryPath: version.game!.libraryPath,
});
return contextId;

View File

@ -1,4 +1,3 @@
import type { GameVersionModel } from "~/prisma/client/models";
import prisma from "../db/database";
export type DropChunk = {
@ -14,11 +13,11 @@ export type DropManifest = {
export type DropManifestMetadata = {
manifest: DropManifest;
versionName: string;
versionId: string;
};
export type DropGeneratedManifest = DropManifest & {
[key: string]: { versionName: string };
[key: string]: { versionId: string };
};
class ManifestGenerator {
@ -31,7 +30,7 @@ class ManifestGenerator {
Object.entries(rootManifest.manifest).map(([key, value]) => {
return [
key,
Object.assign({}, value, { versionName: rootManifest.versionName }),
Object.assign({}, value, { versionId: rootManifest.versionId }),
];
}),
);
@ -44,7 +43,7 @@ class ManifestGenerator {
for (const [filename, chunk] of Object.entries(version.manifest)) {
if (manifest[filename]) continue;
manifest[filename] = Object.assign({}, chunk, {
versionName: version.versionName,
versionId: version.versionId,
});
}
}
@ -53,45 +52,50 @@ class ManifestGenerator {
}
// Local function because eventual caching
async generateManifest(gameId: string, versionName: string) {
const versions: GameVersionModel[] = [];
async generateManifest(versionId: string) {
const versions = [];
const baseVersion = await prisma.gameVersion.findUnique({
const baseVersion = await prisma.version.findUnique({
where: {
gameId_versionName: {
gameId: gameId,
versionName: versionName,
},
versionId,
},
include: {
gameVersion: true,
},
});
if (!baseVersion) return undefined;
versions.push(baseVersion);
// Collect other versions if this is a delta
if (baseVersion.delta) {
if (baseVersion.gameVersion?.delta) {
// Start at the same index minus one, and keep grabbing them
// until we run out or we hit something that isn't a delta
// eslint-disable-next-line no-constant-condition
for (let i = baseVersion.versionIndex - 1; true; i--) {
const currentVersion = await prisma.gameVersion.findFirst({
for (let i = baseVersion.gameVersion.versionIndex - 1; true; i--) {
const currentVersion = await prisma.version.findFirst({
where: {
gameId: gameId,
versionIndex: i,
gameId: baseVersion.gameId,
platform: baseVersion.platform,
gameVersion: {
versionIndex: i,
},
},
include: {
gameVersion: true,
},
});
if (!currentVersion) return undefined;
versions.push(currentVersion);
if (!currentVersion.delta) break;
if (!currentVersion.gameVersion?.delta) break;
}
}
const leastToMost = versions.reverse();
const metadata: DropManifestMetadata[] = leastToMost.map((e) => {
versions.reverse();
const metadata: DropManifestMetadata[] = versions.map((version) => {
return {
manifest: JSON.parse(
e.dropletManifest?.toString() ?? "{}",
version.dropletManifest?.toString() ?? "{}",
) as DropManifest,
versionName: e.versionName,
versionId: version.versionId,
};
});

View File

@ -14,6 +14,8 @@ import notificationSystem from "../notifications";
import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import { createHash } from "node:crypto";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
import type { GameVersionLaunchCreateManyGameVersionInputEnvelope } from "~/prisma/client/models";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@ -247,21 +249,10 @@ class LibraryManager {
async importVersion(
gameId: string,
versionName: string,
metadata: {
platform: string;
onlySetup: boolean;
setup: string;
setupArgs: string;
launch: string;
launchArgs: string;
delta: boolean;
umuId: string;
},
versionPath: string,
metadata: typeof ImportVersion.infer,
) {
const taskId = createVersionImportTaskId(gameId, versionName);
const taskId = createVersionImportTaskId(gameId, versionPath);
const platform = parsePlatform(metadata.platform);
if (!platform) return undefined;
@ -278,14 +269,14 @@ class LibraryManager {
taskHandler.create({
id: taskId,
taskGroup: "import:game",
name: `Importing version ${versionName} for ${game.mName}`,
name: `Importing version "${metadata.name}" (${versionPath}) for ${game.mName}`,
acls: ["system:import:version:read"],
async run({ progress, logger }) {
// First, create the manifest via droplet.
// This takes up 90% of our progress, so we wrap it in a *0.9
const manifest = await library.generateDropletManifest(
game.libraryPath,
versionName,
versionPath,
(err, value) => {
if (err) throw err;
progress(value * 0.9);
@ -299,34 +290,52 @@ class LibraryManager {
logger.info("Created manifest successfully!");
const currentIndex = await prisma.gameVersion.count({
where: { gameId: gameId },
where: { version: { gameId: gameId } },
});
// Then, create the database object
await prisma.gameVersion.create({
await prisma.version.create({
data: {
gameId: gameId,
versionName: versionName,
gameId,
versionPath: versionPath,
versionName: metadata.name ?? versionPath,
dropletManifest: manifest,
versionIndex: currentIndex,
delta: metadata.delta,
umuIdOverride: metadata.umuId,
platform: platform,
onlySetup: metadata.onlySetup,
setupCommand: metadata.setup,
setupArgs: metadata.setupArgs.split(" "),
launchCommand: metadata.launch,
launchArgs: metadata.launchArgs.split(" "),
gameVersion: {
create: {
versionIndex: currentIndex,
delta: metadata.delta,
umuIdOverride: metadata.umuId,
onlySetup: metadata.onlySetup,
setup: metadata.setup,
setupArgs: metadata.setupArgs,
launches: {
createMany: {
data: metadata.launches.map(
(v) =>
({
name: v.name,
description: v.description,
launchCommand: v.launchCommand,
launchArgs: v.launchArgs,
}) satisfies GameVersionLaunchCreateManyGameVersionInputEnvelope["data"],
),
},
},
},
},
},
});
logger.info("Successfully created version!");
notificationSystem.systemPush({
nonce: `version-create-${gameId}-${versionName}`,
title: `'${game.mName}' ('${versionName}') finished importing.`,
description: `Drop finished importing version ${versionName} for ${game.mName}.`,
nonce: `version-create-${gameId}-${versionPath}`,
title: `'${game.mName}' ('${versionPath}') finished importing.`,
description: `Drop finished importing version ${versionPath} for ${game.mName}.`,
actions: [`View|/admin/library/${gameId}`],
acls: ["system:import:version:read"],
});