success pt. 3
This commit is contained in:
11
src/lib/db/client-db/index.ts
Normal file
11
src/lib/db/client-db/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./queries";
|
||||
export * from "./seed";
|
||||
|
||||
import { drizzle } from "drizzle-orm/sqlite-proxy";
|
||||
import { SQLocalDrizzle } from "sqlocal/drizzle";
|
||||
|
||||
const { driver, batchDriver, overwriteDatabaseFile } = new SQLocalDrizzle(
|
||||
"database.sqlite3",
|
||||
);
|
||||
export const db = drizzle(driver, batchDriver);
|
||||
export { overwriteDatabaseFile };
|
||||
98
src/lib/db/client-db/queries.ts
Normal file
98
src/lib/db/client-db/queries.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { desc, like } from "drizzle-orm";
|
||||
import { animeTable } from "$lib/db/schema";
|
||||
|
||||
/**
|
||||
* Client-side DB query helpers (read-only).
|
||||
*
|
||||
* These functions assume the SQLocal-backed Drizzle `db` is already wired up.
|
||||
* Seeding/overwriting the database file should be handled separately (see `seed.ts`).
|
||||
*
|
||||
* Keep these as pure query operations so pages/components can:
|
||||
* 1) ensure seeded
|
||||
* 2) query
|
||||
*/
|
||||
|
||||
export const DEFAULT_LIST_LIMIT = 20;
|
||||
export const MAX_LIST_LIMIT = 200;
|
||||
|
||||
export type AnimeListItem = {
|
||||
annId: number;
|
||||
mainName: string;
|
||||
year: number;
|
||||
seasonId: number;
|
||||
malId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a short list of anime entries for browsing.
|
||||
*
|
||||
* Sorted by (year desc, seasonId desc, annId desc).
|
||||
*/
|
||||
export async function getAnimeList(
|
||||
db: { select: (...args: any[]) => any },
|
||||
limit = DEFAULT_LIST_LIMIT,
|
||||
): Promise<AnimeListItem[]> {
|
||||
const safeLimit = clampLimit(limit);
|
||||
|
||||
// NOTE: using explicit selection keeps payload small and stable
|
||||
const rows = await (db as any)
|
||||
.select({
|
||||
annId: animeTable.annId,
|
||||
mainName: animeTable.mainName,
|
||||
year: animeTable.year,
|
||||
seasonId: animeTable.seasonId,
|
||||
malId: animeTable.malId,
|
||||
})
|
||||
.from(animeTable)
|
||||
.orderBy(
|
||||
desc(animeTable.year),
|
||||
desc(animeTable.seasonId),
|
||||
desc(animeTable.annId),
|
||||
)
|
||||
.limit(safeLimit);
|
||||
|
||||
return rows as AnimeListItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple `LIKE` search over `anime.main_name`.
|
||||
*
|
||||
* If you want fast/quality search later, consider shipping an FTS5 virtual table
|
||||
* in the snapshot DB, and querying that instead.
|
||||
*/
|
||||
export async function searchAnimeByName(
|
||||
db: { select: (...args: any[]) => any },
|
||||
query: string,
|
||||
limit = DEFAULT_LIST_LIMIT,
|
||||
): Promise<AnimeListItem[]> {
|
||||
const q = query.trim();
|
||||
if (!q) return [];
|
||||
|
||||
const safeLimit = clampLimit(limit);
|
||||
const pattern = `%${q}%`;
|
||||
|
||||
const rows = await (db as any)
|
||||
.select({
|
||||
annId: animeTable.annId,
|
||||
mainName: animeTable.mainName,
|
||||
year: animeTable.year,
|
||||
seasonId: animeTable.seasonId,
|
||||
malId: animeTable.malId,
|
||||
})
|
||||
.from(animeTable)
|
||||
.where(like(animeTable.mainName, pattern))
|
||||
.orderBy(
|
||||
desc(animeTable.year),
|
||||
desc(animeTable.seasonId),
|
||||
desc(animeTable.annId),
|
||||
)
|
||||
.limit(safeLimit);
|
||||
|
||||
return rows as AnimeListItem[];
|
||||
}
|
||||
|
||||
function clampLimit(limit: number) {
|
||||
const n = Number(limit);
|
||||
if (!Number.isFinite(n)) return DEFAULT_LIST_LIMIT;
|
||||
return Math.max(1, Math.min(MAX_LIST_LIMIT, Math.floor(n)));
|
||||
}
|
||||
94
src/lib/db/client-db/seed.ts
Normal file
94
src/lib/db/client-db/seed.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { asset } from "$app/paths";
|
||||
import { overwriteDatabaseFile } from "$lib/db/client-db";
|
||||
|
||||
/**
|
||||
* Version-keyed seeding for the client-side SQLocal database.
|
||||
*
|
||||
* Your shipped snapshot lives at:
|
||||
* - `static/data/amq.sqlite` -> `/data/amq.sqlite`
|
||||
*
|
||||
* SQLocal stores its own managed DB file (named in `client-db.ts`), and we
|
||||
* overwrite its contents from the shipped snapshot when needed.
|
||||
*
|
||||
* This is intended for READ-ONLY browsing. Bump the version when you ship a new
|
||||
* snapshot so clients refresh.
|
||||
*/
|
||||
export const AMQ_DB_SEED_VERSION = 1;
|
||||
|
||||
const SEED_ASSET_PATH = "/data/amq.sqlite";
|
||||
const seededStorageKey = (version: number) => `amq.sqlocal.seeded.v${version}`;
|
||||
|
||||
let seedPromise: Promise<void> | null = null;
|
||||
|
||||
function ensureBrowser() {
|
||||
if (!browser) {
|
||||
throw new Error("Client DB seeding can only run in the browser.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the client DB has been seeded for the given version.
|
||||
*
|
||||
* - Uses `localStorage` to skip reseeding once done.
|
||||
* - Uses a shared promise to avoid races across multiple callers.
|
||||
* - Uses streaming (ReadableStream) when available for large DB files.
|
||||
*
|
||||
* @param opts.version Bump to force users to refresh from the shipped snapshot
|
||||
* @param opts.force If true, reseed even if already seeded for this version
|
||||
*/
|
||||
export async function ensureSeeded(
|
||||
opts: { version?: number; force?: boolean } = {},
|
||||
): Promise<void> {
|
||||
ensureBrowser();
|
||||
|
||||
const version = opts.version ?? AMQ_DB_SEED_VERSION;
|
||||
const key = seededStorageKey(version);
|
||||
|
||||
if (!opts.force && localStorage.getItem(key) === "1") return;
|
||||
|
||||
// Serialize seeding work so multiple callers don't overwrite concurrently.
|
||||
if (seedPromise) return seedPromise;
|
||||
|
||||
seedPromise = (async () => {
|
||||
// Re-check inside the serialized section in case another caller finished first.
|
||||
if (!opts.force && localStorage.getItem(key) === "1") return;
|
||||
|
||||
const url = asset(SEED_ASSET_PATH);
|
||||
|
||||
const res = await fetch(url, { cache: "no-cache" });
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch seed DB from ${url}: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Prefer streaming when possible.
|
||||
if (res.body) {
|
||||
await overwriteDatabaseFile(res.body);
|
||||
} else {
|
||||
const blob = await res.blob();
|
||||
await overwriteDatabaseFile(blob);
|
||||
}
|
||||
|
||||
localStorage.setItem(key, "1");
|
||||
})();
|
||||
|
||||
try {
|
||||
await seedPromise;
|
||||
} finally {
|
||||
// If seeding failed, allow future attempts (and let the error surface).
|
||||
if (localStorage.getItem(key) !== "1") {
|
||||
seedPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for debugging / support: clears the seeded marker for a given version.
|
||||
* This does NOT delete SQLocal's stored DB file; it only forces reseed on next call.
|
||||
*/
|
||||
export function clearSeedMarker(version = AMQ_DB_SEED_VERSION) {
|
||||
ensureBrowser();
|
||||
localStorage.removeItem(seededStorageKey(version));
|
||||
}
|
||||
Reference in New Issue
Block a user