68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
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() });
|
|
}
|
|
}
|