From 951a741f3eb7726bdad2766dcf537e8196a54d07 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 3 Jun 2025 17:27:35 +1000 Subject: [PATCH] feat: add ludusavi metadata import into database WARNING: includes debug route --- package.json | 3 + .../migration.sql | 19 +++ .../migration.sql | 2 + prisma/models/ludusavi.prisma | 18 +++ server/routes/ludusavi.ts | 32 +++++ server/tasks/ludusavi.ts | 133 ++++++++++++++++++ yarn.lock | 10 ++ 7 files changed, 217 insertions(+) create mode 100644 prisma/migrations/20250603065304_ludusavi_manifest/migration.sql create mode 100644 prisma/migrations/20250603070638_add_steam_id_to_ludusavi/migration.sql create mode 100644 prisma/models/ludusavi.prisma create mode 100644 server/routes/ludusavi.ts create mode 100644 server/tasks/ludusavi.ts diff --git a/package.json b/package.json index 2fb2212..e9aa7d1 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,12 @@ "fast-fuzzy": "^1.12.0", "file-type-mime": "^0.4.3", "jdenticon": "^3.3.0", + "js-yaml": "^4.1.0", "luxon": "^3.6.1", "micromark": "^4.0.1", "nuxt": "^3.17.4", "nuxt-security": "2.2.0", + "pg-tsquery": "^8.4.2", "prisma": "^6.7.0", "sanitize-filename": "^1.6.3", "semver": "^7.7.1", @@ -59,6 +61,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@types/bcryptjs": "^3.0.0", + "@types/js-yaml": "^4.0.9", "@types/luxon": "^3.6.2", "@types/node": "^22.13.16", "@types/semver": "^7.7.0", diff --git a/prisma/migrations/20250603065304_ludusavi_manifest/migration.sql b/prisma/migrations/20250603065304_ludusavi_manifest/migration.sql new file mode 100644 index 0000000..12719d5 --- /dev/null +++ b/prisma/migrations/20250603065304_ludusavi_manifest/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "LudusaviEntry" ( + "name" TEXT NOT NULL, + + CONSTRAINT "LudusaviEntry_pkey" PRIMARY KEY ("name") +); + +-- CreateTable +CREATE TABLE "LudusaviPlatformEntry" ( + "ludusaviEntryName" TEXT NOT NULL, + "platform" "Platform" NOT NULL, + "files" TEXT[], + "registry" TEXT[], + + CONSTRAINT "LudusaviPlatformEntry_pkey" PRIMARY KEY ("ludusaviEntryName","platform") +); + +-- AddForeignKey +ALTER TABLE "LudusaviPlatformEntry" ADD CONSTRAINT "LudusaviPlatformEntry_ludusaviEntryName_fkey" FOREIGN KEY ("ludusaviEntryName") REFERENCES "LudusaviEntry"("name") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250603070638_add_steam_id_to_ludusavi/migration.sql b/prisma/migrations/20250603070638_add_steam_id_to_ludusavi/migration.sql new file mode 100644 index 0000000..30a6b0c --- /dev/null +++ b/prisma/migrations/20250603070638_add_steam_id_to_ludusavi/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "LudusaviEntry" ADD COLUMN "steamId" TEXT; diff --git a/prisma/models/ludusavi.prisma b/prisma/models/ludusavi.prisma new file mode 100644 index 0000000..aea23c3 --- /dev/null +++ b/prisma/models/ludusavi.prisma @@ -0,0 +1,18 @@ +model LudusaviEntry { + name String @id + steamId String? + + entries LudusaviPlatformEntry[] +} + +model LudusaviPlatformEntry { + ludusaviEntryName String + ludusaviEntry LudusaviEntry @relation(fields: [ludusaviEntryName], references: [name]) + + platform Platform + + files String[] + registry String[] + + @@id([ludusaviEntryName, platform]) +} diff --git a/server/routes/ludusavi.ts b/server/routes/ludusavi.ts new file mode 100644 index 0000000..27eae46 --- /dev/null +++ b/server/routes/ludusavi.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable @typescript-eslint/no-extra-non-null-assertion */ + +import prisma from "../internal/db/database"; +import { parsePlatform } from "../internal/utils/parseplatform"; +import tsquery from "pg-tsquery"; + +export default defineEventHandler(async (h3) => { + const query = getQuery(h3); + const name = query.name?.toString()!!; + const platform = parsePlatform(query.platform?.toString()!!)!!; + + const parser = tsquery({}); + + return await prisma.ludusaviEntry.findMany({ + orderBy: { + _relevance: { + fields: ["name"], + search: parser(name), + sort: "desc", + }, + }, + include: { + entries: { + where: { + platform, + }, + }, + }, + take: 20, + }); +}); diff --git a/server/tasks/ludusavi.ts b/server/tasks/ludusavi.ts new file mode 100644 index 0000000..343edfa --- /dev/null +++ b/server/tasks/ludusavi.ts @@ -0,0 +1,133 @@ +import yaml from "js-yaml"; +import prisma from "../internal/db/database"; +import { Platform } from "~/prisma/client"; +import type { LudusaviPlatformEntryCreateOrConnectWithoutLudusaviEntryInput } from "~/prisma/client/models"; + +type ConnectOrCreateShorthand = + LudusaviPlatformEntryCreateOrConnectWithoutLudusaviEntryInput; + +type LudusaviModel = { + [key: string]: { + files?: { + [key: string]: { + tags?: Array; + when?: Array<{ os?: string }>; + }; + }; + registry?: { [key: string]: { tags?: Array } }; + steam?: { id: number }; + }; +}; + +export default defineTask({ + async run(_event) { + const manifest = yaml.load( + await $fetch( + "https://raw.githubusercontent.com/mtkennerly/ludusavi-manifest/refs/heads/master/data/manifest.yaml", + ), + ) as LudusaviModel; + + for (const [name, data] of Object.entries(manifest)) { + if (!data.files && !data.registry) continue; + console.log(name); + + const iterableFiles = data.files ? Object.entries(data.files) : undefined; + + function findFilesForOperatingSystem(os: string) { + return iterableFiles?.filter((e) => + e[1].when?.find((v) => v.os === os), + ); + } + + const connectOrCreate: ConnectOrCreateShorthand[] = []; + + const windowsData = { + registry: data.registry, + files: findFilesForOperatingSystem("windows"), + }; + + if (windowsData.registry || windowsData.files) { + const create: ConnectOrCreateShorthand = { + where: { + ludusaviEntryName_platform: { + ludusaviEntryName: name, + platform: Platform.Windows, + }, + }, + create: { + platform: Platform.Windows, + files: windowsData.files?.map((e) => e[0]) ?? [], + registry: Object.entries(windowsData.registry ?? {}).map( + (e) => e[0], + ), + }, + }; + + connectOrCreate.push(create); + } + + const linuxData = { + files: findFilesForOperatingSystem("linux"), + }; + + if (linuxData.files) { + const create: ConnectOrCreateShorthand = { + where: { + ludusaviEntryName_platform: { + ludusaviEntryName: name, + platform: Platform.Linux, + }, + }, + create: { + platform: Platform.Linux, + files: linuxData.files?.map((e) => e[0]) ?? [], + registry: [], + }, + }; + + connectOrCreate.push(create); + } + + const macData = { + files: findFilesForOperatingSystem("mac"), + }; + + if (macData.files) { + const create: ConnectOrCreateShorthand = { + where: { + ludusaviEntryName_platform: { + ludusaviEntryName: name, + platform: Platform.macOS, + }, + }, + create: { + platform: Platform.macOS, + files: macData.files?.map((e) => e[0]) ?? [], + registry: [], + }, + }; + + connectOrCreate.push(create); + } + + const steamId = data.steam?.id.toString() ?? null; + + await prisma.ludusaviEntry.upsert({ + where: { + name, + }, + create: { + name, + steamId, + entries: { connectOrCreate }, + }, + update: { + steamId, + entries: { connectOrCreate }, + }, + }); + } + + return { result: true }; + }, +}); diff --git a/yarn.lock b/yarn.lock index 6f4bdb9..a54a350 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2332,6 +2332,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -6982,6 +6987,11 @@ perfect-debounce@^1.0.0: resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== +pg-tsquery@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/pg-tsquery/-/pg-tsquery-8.4.2.tgz#f28e6242f15f4d8535ac08a0f9083ce04e42e1e4" + integrity sha512-waJSlBIKE+shDhuDpuQglTH6dG5zakDhnrnxu8XB8V5c7yoDSuy4pOxY6t2dyoxTjaKMcMmlByJN7n9jx9eqMA== + picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"