initial commit
This commit is contained in:
11
server/package.json
Normal file
11
server/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "guildlib-server",
|
||||
"version": "2.0.0",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"start": "bun run src/index.ts"
|
||||
},
|
||||
"devDependencies": { "@types/bun": "latest" }
|
||||
}
|
||||
68
server/src/index.ts
Normal file
68
server/src/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { serve } from "bun";
|
||||
import { ShardManager } from "./shardManager";
|
||||
import { manifestHandler } from "./routes/manifest";
|
||||
import { pullHandler } from "./routes/pull";
|
||||
import { pushHandler } from "./routes/push";
|
||||
import { schemaHandler } from "./routes/schema";
|
||||
import { entriesHandler } from "./routes/entries";
|
||||
import { authMiddleware } from "./middleware/auth";
|
||||
|
||||
const PORT = Number(Bun.env.PORT ?? 3000);
|
||||
const API_KEY = Bun.env.GUILDLIB_API_KEY ?? "";
|
||||
|
||||
export const shards = new ShardManager(Bun.env.SHARD_DIR ?? "./shards");
|
||||
await shards.init();
|
||||
|
||||
console.log(`[Guildlib] :${PORT} shards: ${shards.shardNames().join(", ")} auth: ${API_KEY ? "on" : "OFF"}`);
|
||||
|
||||
serve({
|
||||
port: PORT,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname;
|
||||
const method = req.method;
|
||||
|
||||
if (method === "OPTIONS") return cors(new Response(null, { status: 204 }));
|
||||
|
||||
if (path !== "/health" && API_KEY) {
|
||||
const err = authMiddleware(req, API_KEY);
|
||||
if (err) return cors(err);
|
||||
}
|
||||
|
||||
if (path === "/health" && method === "GET")
|
||||
return cors(json({ ok: true, version: "2.0.0", shards: shards.shardNames(), ts: Date.now() }));
|
||||
|
||||
if (path.startsWith("/manifest/") && method === "GET")
|
||||
return cors(await manifestHandler(shards, path.split("/")[2]));
|
||||
|
||||
if (path === "/pull" && method === "POST") return cors(await pullHandler(shards, req));
|
||||
if (path === "/push" && method === "POST") return cors(await pushHandler(shards, req));
|
||||
|
||||
if (path === "/schema") {
|
||||
if (method === "POST") return cors(await schemaHandler.post(req));
|
||||
if (method === "GET") return cors(await schemaHandler.get());
|
||||
}
|
||||
|
||||
if (path.startsWith("/entries/"))
|
||||
return cors(await entriesHandler(shards, req, url, path, method));
|
||||
|
||||
return cors(new Response("Not found", { status: 404 }));
|
||||
},
|
||||
error(e) {
|
||||
console.error(e);
|
||||
return cors(json({ error: e.message }, 500));
|
||||
},
|
||||
});
|
||||
|
||||
export function json(data: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status, headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
export function cors(res: Response) {
|
||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
||||
res.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
res.headers.set("Access-Control-Allow-Headers", "Content-Type, X-Api-Key");
|
||||
return res;
|
||||
}
|
||||
8
server/src/middleware/auth.ts
Normal file
8
server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// middleware/auth.ts
|
||||
export function authMiddleware(req: Request, key: string): Response | null {
|
||||
if (req.headers.get("X-Api-Key") !== key)
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401, headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
return null;
|
||||
}
|
||||
97
server/src/routes/entries.ts
Normal file
97
server/src/routes/entries.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// routes/entries.ts
|
||||
import type { ShardManager } from "../shardManager";
|
||||
import { json } from "../index";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
function parseRow(r: any) {
|
||||
return {
|
||||
...r,
|
||||
platforms: JSON.parse(r.platforms || '["all"]'),
|
||||
tags: JSON.parse(r.tags || "[]"),
|
||||
data: JSON.parse(r.data_json),
|
||||
data_json: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function entriesHandler(
|
||||
shards: ShardManager, req: Request, url: URL, path: string, method: string
|
||||
) {
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const shard = parts[1];
|
||||
const entryId = parts[2];
|
||||
|
||||
if (!shard) return json({ error: "Missing shard" }, 400);
|
||||
|
||||
// ── List ────────────────────────────────────────────────────────────────
|
||||
if (!entryId && method === "GET") {
|
||||
const platforms = url.searchParams.get("platforms")?.split(",").filter(Boolean);
|
||||
const tags = url.searchParams.get("tags")?.split(",").filter(Boolean);
|
||||
try {
|
||||
const rows = shards.listEntries(shard, {
|
||||
typeName: url.searchParams.get("type") ?? undefined,
|
||||
parentId: url.searchParams.get("parent") ?? undefined,
|
||||
platforms, tags,
|
||||
limit: Number(url.searchParams.get("limit") ?? 100),
|
||||
offset: Number(url.searchParams.get("offset") ?? 0),
|
||||
});
|
||||
return json(rows.map(parseRow));
|
||||
} catch (e: any) { return json({ error: e.message }, 404); }
|
||||
}
|
||||
|
||||
// ── Create ───────────────────────────────────────────────────────────────
|
||||
if (!entryId && method === "POST") {
|
||||
try {
|
||||
const body = await req.json() as {
|
||||
typeName: string; parentId?: string;
|
||||
platforms?: string[]; tags?: string[];
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
const id = randomUUID().replace(/-/g, "");
|
||||
const row = shards.upsert(shard, {
|
||||
id,
|
||||
type_name: body.typeName,
|
||||
parent_id: body.parentId ?? null,
|
||||
platforms: body.platforms ?? ["all"],
|
||||
tags: body.tags ?? [],
|
||||
data: { ...body.data, entryId: id },
|
||||
});
|
||||
return json({ id: row.id, version: row.row_version }, 201);
|
||||
} catch (e: any) { return json({ error: e.message }, 400); }
|
||||
}
|
||||
|
||||
// ── Single ───────────────────────────────────────────────────────────────
|
||||
if (entryId && method === "GET") {
|
||||
const row = shards.getEntry(shard, entryId);
|
||||
if (!row) return json({ error: "Not found" }, 404);
|
||||
return json(parseRow(row));
|
||||
}
|
||||
|
||||
// ── Update ───────────────────────────────────────────────────────────────
|
||||
if (entryId && method === "PUT") {
|
||||
const existing = shards.getEntry(shard, entryId);
|
||||
if (!existing) return json({ error: "Not found" }, 404);
|
||||
try {
|
||||
const body = await req.json() as {
|
||||
platforms?: string[]; tags?: string[];
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
const row = shards.upsert(shard, {
|
||||
id: entryId,
|
||||
type_name: existing.type_name,
|
||||
parent_id: existing.parent_id,
|
||||
platforms: body.platforms ?? JSON.parse(existing.platforms || '["all"]'),
|
||||
tags: body.tags ?? JSON.parse(existing.tags || "[]"),
|
||||
data: { ...JSON.parse(existing.data_json), ...(body.data ?? {}) },
|
||||
});
|
||||
return json({ id: row.id, version: row.row_version });
|
||||
} catch (e: any) { return json({ error: e.message }, 400); }
|
||||
}
|
||||
|
||||
// ── Delete ───────────────────────────────────────────────────────────────
|
||||
if (entryId && method === "DELETE") {
|
||||
try { shards.softDelete(shard, entryId); return json({ ok: true }); }
|
||||
catch (e: any) { return json({ error: e.message }, 400); }
|
||||
}
|
||||
|
||||
return json({ error: "Method not allowed" }, 405);
|
||||
}
|
||||
8
server/src/routes/manifest.ts
Normal file
8
server/src/routes/manifest.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// routes/manifest.ts
|
||||
import type { ShardManager } from "../shardManager";
|
||||
import { json } from "../index";
|
||||
|
||||
export async function manifestHandler(shards: ShardManager, shardName: string) {
|
||||
try { return json({ shard: shardName, rows: shards.getManifest(shardName) }); }
|
||||
catch (e: any) { return json({ error: e.message }, 404); }
|
||||
}
|
||||
34
server/src/routes/pull.ts
Normal file
34
server/src/routes/pull.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// routes/pull.ts
|
||||
import type { ShardManager } from "../shardManager";
|
||||
import { json } from "../index";
|
||||
|
||||
export async function pullHandler(shards: ShardManager, req: Request) {
|
||||
try {
|
||||
const body = await req.json() as {
|
||||
shard: string;
|
||||
ids: string[];
|
||||
filterPlatforms?: string[] | null;
|
||||
filterTags?: string[] | null;
|
||||
};
|
||||
|
||||
const rows = shards.getRowsByIds(
|
||||
body.shard,
|
||||
body.ids,
|
||||
body.filterPlatforms,
|
||||
body.filterTags
|
||||
);
|
||||
|
||||
const out = rows.map(r => ({
|
||||
id: r.id,
|
||||
type_name: r.type_name,
|
||||
parent_id: r.parent_id,
|
||||
row_version: r.row_version,
|
||||
platforms: JSON.parse(r.platforms || '["all"]'),
|
||||
tags: JSON.parse(r.tags || "[]"),
|
||||
deleted: r.deleted,
|
||||
...JSON.parse(r.data_json),
|
||||
}));
|
||||
|
||||
return json({ shard: body.shard, rows: out });
|
||||
} catch (e: any) { return json({ error: e.message }, 400); }
|
||||
}
|
||||
28
server/src/routes/push.ts
Normal file
28
server/src/routes/push.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// routes/push.ts
|
||||
import type { ShardManager } from "../shardManager";
|
||||
import { json } from "../index";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export async function pushHandler(shards: ShardManager, req: Request) {
|
||||
try {
|
||||
const body = await req.json() as {
|
||||
shard: string;
|
||||
typeName: string;
|
||||
platforms?: string[];
|
||||
tags?: string[];
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const id = (body.data.entryId as string) || randomUUID().replace(/-/g, "");
|
||||
const row = shards.upsert(body.shard, {
|
||||
id,
|
||||
type_name: body.typeName,
|
||||
parent_id: (body.data.parentId as string) ?? null,
|
||||
platforms: body.platforms ?? ["all"],
|
||||
tags: body.tags ?? [],
|
||||
data: body.data,
|
||||
});
|
||||
|
||||
return json({ ok: true, id: row.id, version: row.row_version });
|
||||
} catch (e: any) { return json({ error: e.message }, 400); }
|
||||
}
|
||||
24
server/src/routes/schema.ts
Normal file
24
server/src/routes/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// routes/schema.ts
|
||||
import { json } from "../index";
|
||||
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
|
||||
const SCHEMA_PATH = Bun.env.SCHEMA_PATH ?? "./config/schema.json";
|
||||
|
||||
export const schemaHandler = {
|
||||
async post(req: Request) {
|
||||
try {
|
||||
const text = await req.text();
|
||||
JSON.parse(text); // validate JSON
|
||||
mkdirSync(dirname(SCHEMA_PATH), { recursive: true });
|
||||
writeFileSync(SCHEMA_PATH, text, "utf8");
|
||||
return json({ ok: true });
|
||||
} catch (e: any) { return json({ error: e.message }, 400); }
|
||||
},
|
||||
async get() {
|
||||
if (!existsSync(SCHEMA_PATH)) return json({ error: "No schema yet" }, 404);
|
||||
return new Response(readFileSync(SCHEMA_PATH, "utf8"), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
};
|
||||
256
server/src/shardManager.ts
Normal file
256
server/src/shardManager.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* ShardManager v2
|
||||
*
|
||||
* Schema (per shard .sqlite file):
|
||||
*
|
||||
* entries (
|
||||
* id TEXT PRIMARY KEY,
|
||||
* type_name TEXT NOT NULL,
|
||||
* parent_id TEXT,
|
||||
* row_version INTEGER NOT NULL DEFAULT 1,
|
||||
* checksum TEXT NOT NULL,
|
||||
* platforms TEXT NOT NULL DEFAULT '["all"]', -- JSON array of platform strings
|
||||
* tags TEXT NOT NULL DEFAULT '[]', -- JSON array of tag strings
|
||||
* data_json TEXT NOT NULL,
|
||||
* created_at INTEGER NOT NULL,
|
||||
* updated_at INTEGER NOT NULL,
|
||||
* deleted INTEGER NOT NULL DEFAULT 0
|
||||
* )
|
||||
*
|
||||
* manifest_cache (
|
||||
* id TEXT PRIMARY KEY,
|
||||
* row_version INTEGER NOT NULL,
|
||||
* checksum TEXT NOT NULL
|
||||
* )
|
||||
*/
|
||||
|
||||
import { Database } from "bun:sqlite";
|
||||
import { mkdirSync, readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export interface ManifestRow {
|
||||
id: string; version: number; checksum: string;
|
||||
}
|
||||
|
||||
export interface EntryRow {
|
||||
id: string; type_name: string; parent_id: string | null;
|
||||
row_version: number; checksum: string;
|
||||
platforms: string; // stored as JSON string, e.g. '["pc","ps5"]'
|
||||
tags: string; // stored as JSON string, e.g. '["release","base_game"]'
|
||||
data_json: string;
|
||||
created_at: number; updated_at: number; deleted: number;
|
||||
}
|
||||
|
||||
export class ShardManager {
|
||||
private dir: string;
|
||||
private dbs: Map<string, Database> = new Map();
|
||||
|
||||
constructor(dir: string) { this.dir = dir; }
|
||||
|
||||
async init() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
// Open any .sqlite files already on disk
|
||||
for (const file of readdirSync(this.dir)) {
|
||||
if (file.endsWith(".sqlite"))
|
||||
this.openShard(file.replace(".sqlite", ""));
|
||||
}
|
||||
}
|
||||
|
||||
// Opens (or creates) a shard SQLite file
|
||||
openShard(name: string) {
|
||||
if (this.dbs.has(name)) return;
|
||||
const path = join(this.dir, `${name}.sqlite`);
|
||||
const db = new Database(path, { create: true });
|
||||
db.exec("PRAGMA journal_mode=WAL;");
|
||||
db.exec("PRAGMA synchronous=NORMAL;");
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id TEXT PRIMARY KEY,
|
||||
type_name TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
row_version INTEGER NOT NULL DEFAULT 1,
|
||||
checksum TEXT NOT NULL,
|
||||
platforms TEXT NOT NULL DEFAULT '["all"]',
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
data_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
deleted INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_type ON entries(type_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_parent ON entries(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_version ON entries(row_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_deleted ON entries(deleted);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manifest_cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
row_version INTEGER NOT NULL,
|
||||
checksum TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
this.dbs.set(name, db);
|
||||
console.log(`[ShardManager] Opened: ${name}.sqlite`);
|
||||
}
|
||||
|
||||
shardNames(): string[] { return Array.from(this.dbs.keys()); }
|
||||
|
||||
private require(name: string): Database {
|
||||
if (!this.dbs.has(name)) this.openShard(name); // auto-create new shards
|
||||
return this.dbs.get(name)!;
|
||||
}
|
||||
|
||||
// ── Manifest ─────────────────────────────────────────────────────────────
|
||||
|
||||
getManifest(shardName: string): ManifestRow[] {
|
||||
return this.require(shardName)
|
||||
.query("SELECT id, row_version AS version, checksum FROM manifest_cache")
|
||||
.all() as ManifestRow[];
|
||||
}
|
||||
|
||||
// ── Pull: fetch rows by IDs with optional platform/tag filtering ──────────
|
||||
|
||||
getRowsByIds(
|
||||
shardName: string,
|
||||
ids: string[],
|
||||
filterPlatforms?: string[] | null,
|
||||
filterTags?: string[] | null
|
||||
): EntryRow[] {
|
||||
if (ids.length === 0) return [];
|
||||
const db = this.require(shardName);
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
|
||||
// Start with ID list
|
||||
let rows = db.query(
|
||||
`SELECT * FROM entries WHERE id IN (${placeholders}) AND deleted = 0`
|
||||
).all(...ids) as EntryRow[];
|
||||
|
||||
// Apply platform filter in JS (SQLite JSON functions vary by version)
|
||||
if (filterPlatforms && filterPlatforms.length > 0) {
|
||||
rows = rows.filter(r => {
|
||||
const plats: string[] = JSON.parse(r.platforms || '["all"]');
|
||||
return plats.includes("all") ||
|
||||
plats.some(p => filterPlatforms.includes(p));
|
||||
});
|
||||
}
|
||||
|
||||
// Apply tag filter
|
||||
if (filterTags && filterTags.length > 0) {
|
||||
rows = rows.filter(r => {
|
||||
const rowTags: string[] = JSON.parse(r.tags || "[]");
|
||||
return rowTags.length === 0 || // no tags = unrestricted
|
||||
rowTags.some(t => filterTags.includes(t));
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ── Upsert ────────────────────────────────────────────────────────────────
|
||||
|
||||
upsert(shardName: string, entry: {
|
||||
id: string;
|
||||
type_name: string;
|
||||
parent_id?: string | null;
|
||||
platforms?: string[];
|
||||
tags?: string[];
|
||||
data: Record<string, unknown>;
|
||||
}): EntryRow {
|
||||
const db = this.require(shardName);
|
||||
const now = Date.now();
|
||||
const dataJson = JSON.stringify(entry.data);
|
||||
const checksum = md5(dataJson);
|
||||
const platforms = JSON.stringify(entry.platforms ?? ["all"]);
|
||||
const tags = JSON.stringify(entry.tags ?? []);
|
||||
|
||||
const existing = db.query(
|
||||
"SELECT row_version FROM entries WHERE id = ?"
|
||||
).get(entry.id) as { row_version: number } | null;
|
||||
|
||||
const newVersion = (existing?.row_version ?? 0) + 1;
|
||||
|
||||
db.query(`
|
||||
INSERT INTO entries
|
||||
(id, type_name, parent_id, row_version, checksum, platforms, tags, data_json, created_at, updated_at, deleted)
|
||||
VALUES
|
||||
($id, $type_name, $parent_id, $ver, $chk, $plats, $tags, $data, $now, $now, 0)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
type_name = $type_name,
|
||||
parent_id = $parent_id,
|
||||
row_version = $ver,
|
||||
checksum = $chk,
|
||||
platforms = $plats,
|
||||
tags = $tags,
|
||||
data_json = $data,
|
||||
updated_at = $now,
|
||||
deleted = 0
|
||||
`).run({
|
||||
$id: entry.id, $type_name: entry.type_name,
|
||||
$parent_id: entry.parent_id ?? null,
|
||||
$ver: newVersion, $chk: checksum,
|
||||
$plats: platforms, $tags: tags,
|
||||
$data: dataJson, $now: now,
|
||||
});
|
||||
|
||||
db.query(`
|
||||
INSERT INTO manifest_cache (id, row_version, checksum)
|
||||
VALUES ($id, $ver, $chk)
|
||||
ON CONFLICT(id) DO UPDATE SET row_version=$ver, checksum=$chk
|
||||
`).run({ $id: entry.id, $ver: newVersion, $chk: checksum });
|
||||
|
||||
return db.query("SELECT * FROM entries WHERE id = ?").get(entry.id) as EntryRow;
|
||||
}
|
||||
|
||||
// ── CRUD ──────────────────────────────────────────────────────────────────
|
||||
|
||||
listEntries(shardName: string, opts: {
|
||||
typeName?: string; parentId?: string;
|
||||
platforms?: string[]; tags?: string[];
|
||||
limit?: number; offset?: number;
|
||||
} = {}): EntryRow[] {
|
||||
const db = this.require(shardName);
|
||||
const clauses = ["deleted = 0"];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (opts.typeName) { clauses.push("type_name = ?"); params.push(opts.typeName); }
|
||||
if (opts.parentId) { clauses.push("parent_id = ?"); params.push(opts.parentId); }
|
||||
|
||||
params.push(opts.limit ?? 100, opts.offset ?? 0);
|
||||
let rows = db.query(
|
||||
`SELECT * FROM entries WHERE ${clauses.join(" AND ")} ORDER BY updated_at DESC LIMIT ? OFFSET ?`
|
||||
).all(...params) as EntryRow[];
|
||||
|
||||
// Platform filter
|
||||
if (opts.platforms?.length) {
|
||||
rows = rows.filter(r => {
|
||||
const plats: string[] = JSON.parse(r.platforms || '["all"]');
|
||||
return plats.includes("all") || plats.some(p => opts.platforms!.includes(p));
|
||||
});
|
||||
}
|
||||
// Tag filter
|
||||
if (opts.tags?.length) {
|
||||
rows = rows.filter(r => {
|
||||
const rowTags: string[] = JSON.parse(r.tags || "[]");
|
||||
return rowTags.length === 0 || rowTags.some(t => opts.tags!.includes(t));
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
getEntry(shardName: string, id: string): EntryRow | null {
|
||||
return this.require(shardName)
|
||||
.query("SELECT * FROM entries WHERE id = ? AND deleted = 0")
|
||||
.get(id) as EntryRow | null;
|
||||
}
|
||||
|
||||
softDelete(shardName: string, id: string) {
|
||||
const db = this.require(shardName);
|
||||
const now = Date.now();
|
||||
db.query("UPDATE entries SET deleted=1, updated_at=?, row_version=row_version+1 WHERE id=?").run(now, id);
|
||||
const row = db.query("SELECT row_version, checksum FROM entries WHERE id=?").get(id) as any;
|
||||
if (row) db.query("UPDATE manifest_cache SET row_version=? WHERE id=?").run(row.row_version, id);
|
||||
}
|
||||
}
|
||||
|
||||
function md5(s: string) { return createHash("md5").update(s).digest("hex"); }
|
||||
Reference in New Issue
Block a user