import { createRequire } from "node:module"; import type BetterSqlite3 from "better-sqlite3"; const require = createRequire(import.meta.url); const Database = require("better-sqlite3") as typeof BetterSqlite3; export type RecentAssetKind = "static_sticker" | "text_template"; export interface RecentAssetItem { asset_id: string; asset_kind: RecentAssetKind; resource_version: string; } export class RecentAssetService { readonly database: BetterSqlite3.Database; private readonly ownsDatabase: boolean; private readonly clock: () => number; constructor(input: { clock?: () => number; database?: BetterSqlite3.Database; databasePath?: string }) { if (!input.database && !input.databasePath) throw new Error("recent_asset_database_required"); this.database = input.database ?? new Database(input.databasePath!); this.ownsDatabase = !input.database; this.clock = input.clock ?? Date.now; this.database.exec(` CREATE TABLE IF NOT EXISTS recent_assets ( user_id TEXT NOT NULL, asset_kind TEXT NOT NULL CHECK (asset_kind IN ('text_template', 'static_sticker')), asset_id TEXT NOT NULL, resource_version TEXT NOT NULL, used_at INTEGER NOT NULL, PRIMARY KEY (user_id, asset_kind, asset_id) ); CREATE INDEX IF NOT EXISTS recent_assets_user_kind_used ON recent_assets (user_id, asset_kind, used_at DESC, asset_id ASC); `); } close() { if (this.ownsDatabase) this.database.close(); } list(userId: string, assetKind: RecentAssetKind, limit = 12): RecentAssetItem[] { if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw new Error("recent_asset_limit_invalid"); return this.database.prepare(` SELECT asset_id, asset_kind, resource_version FROM recent_assets WHERE user_id = ? AND asset_kind = ? ORDER BY used_at DESC, asset_id ASC LIMIT ? `).all(userId, assetKind, limit) as RecentAssetItem[]; } recordSuccessfulUse(input: { assetId: string; assetKind: RecentAssetKind; resourceVersion: string; userId: string }) { if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$/.test(input.assetId) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$/.test(input.resourceVersion)) { throw new Error("recent_asset_reference_invalid"); } this.database.prepare(` INSERT INTO recent_assets (user_id, asset_kind, asset_id, resource_version, used_at) VALUES (@userId, @assetKind, @assetId, @resourceVersion, @usedAt) ON CONFLICT (user_id, asset_kind, asset_id) DO UPDATE SET resource_version = excluded.resource_version, used_at = excluded.used_at `).run({ ...input, usedAt: this.clock() }); } }