From 95558f10bdd15ff03589fda62b0275dc67bf6bbb Mon Sep 17 00:00:00 2001 From: tobinio Date: Sat, 01 Nov 2025 14:48:08 +0000 Subject: [PATCH] working project reverseSearch --- nuxt.config.ts | 1 + app/layouts/default.vue | 33 +++++++++++++++++++++------------ app/pages/reverseSearch.vue | 16 ++++++++++++++++ server/api/reverseSearch.ts | 12 ++++++++++++ server/plugins/db.ts | 15 +++++++++++++++ server/tasks/analyze.ts | 7 +++++++ server/utils/db/sql.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ server/utils/processing/connections/fetching.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ server/utils/processing/connections/processing.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ server/utils/processing/connections/types.ts | 4 ++++ 10 file(s) changed, 226 insertion(s)(+), 12 deletion(s)(-) diff --git a/nuxt.config.ts b/nuxt.config.ts --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -76,6 +76,7 @@ }, experimental: { tasks: true, + database: true, }, scheduledTasks: { "0 * * * *": ["analyze"], diff --git a/app/layouts/default.vue b/app/layouts/default.vue --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -18,18 +18,27 @@ - - - Charts - +
+ + + Charts + + + ModSearch + +
+ const text = ref(""); + + const result = useFetch("/api/reverseSearch", { + params: { + id: text, + }, + }); + + +> diff --git a/server/api/reverseSearch.ts b/server/api/reverseSearch.ts new file mode 100644 --- /dev/null +++ b/server/api/reverseSearch.ts @@ -0,0 +1,12 @@ +export default defineEventHandler(async (event): Promise => { + const query = getQuery(event); + + const projectIds = await DB.getForProject(query.id as string); + + if (projectIds instanceof Error) { + setResponseStatus(event, 500); + return []; + } + + return projectIds; +}); diff --git a/server/plugins/db.ts b/server/plugins/db.ts new file mode 100644 --- /dev/null +++ b/server/plugins/db.ts @@ -0,0 +1,15 @@ +import consola from "consola"; + +export default defineNitroPlugin(async () => { + const db = useDatabase(); + + //TODO: handle via migrations + await db.sql`DROP TABLE IF EXISTS connections`; + await db.sql`CREATE TABLE IF NOT EXISTS connections ( + project_id VARCHAR(8), + dependency_id VARCHAR(8), + PRIMARY KEY (project_id, dependency_id) + )`; + + consola.info("Database initialized"); +}); diff --git a/server/tasks/analyze.ts b/server/tasks/analyze.ts --- a/server/tasks/analyze.ts +++ b/server/tasks/analyze.ts @@ -2,6 +2,8 @@ import { updateGameVersions } from "~~/server/utils/processing/gameVersions/processing"; import { updateGlobalStats } from "~~/server/utils/processing/global/processing"; import { updateStatistics } from "~~/server/utils/processing/projects/processing"; +import { DB } from "../utils/db/sql"; +import { updateConnections } from "../utils/processing/connections/processing"; export const LOGGER = consola.withTag("Analyze"); @@ -20,9 +22,14 @@ const start = new Date(); LOGGER.info("starting analyze - at", start.toISOString()); try { + //TODO: do something less destructive... + await DB.clear(); + await updateConnections(); + await updateGameVersions(); await updateStatistics(); await updateGlobalStats(); + await KV.LatestDate.set(new Date()); } catch (e) { LOGGER.fail(e); diff --git a/server/utils/db/sql.ts b/server/utils/db/sql.ts new file mode 100644 --- /dev/null +++ b/server/utils/db/sql.ts @@ -0,0 +1,45 @@ +import type { Connection } from "../processing/connections/types"; + +export const DB = { + async clear(): Promise { + const db = useDatabase(); + + try { + await db.sql` + TRUNCATE TABLE connections; + `; + } catch (error) { + return new Error(`Failed to clear database: ${error}`); + } + }, + async getForProject(dependencyId: string): Promise { + const db = useDatabase(); + + const result = await db.sql` + SELECT project_id FROM connections WHERE dependency_id = ${dependencyId}; + `; + + if (!result.rows) { + return new Error( + `Failed to fetch connections for project ${dependencyId}: ${result.error}`, + ); + } + + return result.rows.map((row) => { + return row.project_id as string; + }); + }, + async addBulk(connections: Connection[]): Promise { + const db = useDatabase(); + + try { + //TODO: dont use becouse of sql injection risk + await db.sql` + INSERT INTO connections (project_id, dependency_id) + VALUES {${connections.map((connection) => `('${connection.project_id}', '${connection.dependency_id}')`).join(", ")}}; + `; + } catch (error) { + return new Error(`Failed to add connections: ${error}`); + } + }, +}; diff --git a/server/utils/processing/connections/fetching.ts b/server/utils/processing/connections/fetching.ts new file mode 100644 --- /dev/null +++ b/server/utils/processing/connections/fetching.ts @@ -0,0 +1,49 @@ +export async function getModpackIds( + offset: number, + limit: number, +): Promise { + return getProjectIds(offset, "modpack", limit); +} + +export async function getFirstVersionIds( + projectIds: string[], +): Promise { + type Project = { + versions: string[]; + }; + + const data = await $modrinthFetch("/projects", { + query: { + ids: `["${projectIds.join('","')}"]`, + }, + }); + + return data.map((value) => value.versions[0]); +} + +export async function getVersionDependencies( + versionIds: string[], +): Promise<{ project_id: string; dependencies: string[] }[]> { + type Version = { + project_id: string; + dependencies: { project_id: string }[]; + }; + + const data = await $modrinthFetch("/versions", { + query: { + ids: `["${versionIds.join('","')}"]`, + }, + }); + + //done to remove unused values from the ram + return data.map((value) => { + const dependencyIds = Array.from( + new Set(value.dependencies.map((d) => d.project_id).filter(Boolean)), + ); + + return { + project_id: value.project_id, + dependencies: dependencyIds, + }; + }); +} diff --git a/server/utils/processing/connections/processing.ts b/server/utils/processing/connections/processing.ts new file mode 100644 --- /dev/null +++ b/server/utils/processing/connections/processing.ts @@ -0,0 +1,56 @@ +import { LOGGER } from "~~/server/tasks/analyze"; +import { DB } from "../../db/sql"; +import { + getFirstVersionIds, + getModpackIds, + getVersionDependencies, +} from "./fetching"; + +export async function updateConnections() { + LOGGER.info("updating connections [starting]"); + + const BATCH_SIZE = import.meta.dev ? 1 : 10_000; + + let projectIndex = 0; + while (true) { + let done = false; + + const batchProjectIds = []; + while (batchProjectIds.length < BATCH_SIZE) { + const projectIds = await getModpackIds(projectIndex, 100); + batchProjectIds.push(...projectIds); + projectIndex += 100; + + if (projectIds.length !== 100) { + done = true; + break; + } + } + + const versionIds: string[] = []; + for (const chunk of chunkArray(batchProjectIds, 200)) { + const versions = await getFirstVersionIds(chunk); + versionIds.push(...versions); + } + + const versionDependencies = await getVersionDependencies(versionIds); + for (const pair of versionDependencies) { + const connections = pair.dependencies.map((dependency) => { + return { + project_id: pair.project_id, + dependency_id: dependency, + }; + }); + + const result = await DB.addBulk(connections); + + if (result instanceof Error) { + LOGGER.warn(result, connections); + } + } + + if (done || import.meta.dev) break; + } + + LOGGER.info("updating connections [finished]"); +} diff --git a/server/utils/processing/connections/types.ts b/server/utils/processing/connections/types.ts new file mode 100644 --- /dev/null +++ b/server/utils/processing/connections/types.ts @@ -0,0 +1,4 @@ +export type Connection = { + project_id: string; + dependency_id: string; +}; -- tangled.sh