the asset system is reworked to support "dynamic" entries, where each entry is a separate file on disk containing the latest generation's headers+raw+gzip+zstd. when calling view.regenerate, it will look for pages that had "export const regenerate" during generation, and render those pages using the view system, but then store the results as assets instead of sending as a response. pages configured as regenerable are also bundled as views, using the non-aliasing key "page:${page.id}". this cannot alias because file paths may not contain a colon.
137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
// Guard against reloads and bundler duplication.
|
|
// @ts-ignore
|
|
const map = globalThis[Symbol.for("clover.db")] ??= new Map<
|
|
string,
|
|
WrappedDatabase
|
|
>();
|
|
|
|
export function getDb(file: string) {
|
|
let db: WrappedDatabase | null = map.get(file);
|
|
if (db) return db;
|
|
const fileWithExt = file.includes(".") ? file : file + ".sqlite";
|
|
db = new WrappedDatabase(
|
|
new DatabaseSync(
|
|
path.join(process.env.CLOVER_DB ?? ".clover", fileWithExt),
|
|
),
|
|
);
|
|
map.set(file, db);
|
|
return db;
|
|
}
|
|
|
|
export class WrappedDatabase {
|
|
node: DatabaseSync;
|
|
stmtTableMigrate: WeakRef<StatementSync> | null = null;
|
|
|
|
constructor(node: DatabaseSync) {
|
|
this.node = node;
|
|
this.node.exec(`
|
|
create table if not exists clover_migrations (
|
|
key text not null primary key,
|
|
version integer not null
|
|
);
|
|
`);
|
|
}
|
|
|
|
// TODO: add migration support
|
|
// the idea is you keep `schema` as the new schema but can add
|
|
// migrations to the mix really easily.
|
|
table(name: string, schema: string) {
|
|
let s = this.stmtTableMigrate?.deref();
|
|
s ?? (this.stmtTableMigrate = new WeakRef(
|
|
s = this.node.prepare(`
|
|
insert or ignore into clover_migrations
|
|
(key, version) values (?, ?);
|
|
`),
|
|
));
|
|
const { changes } = s.run(name, 1);
|
|
if (changes === 1) this.node.exec(schema);
|
|
}
|
|
|
|
prepare<Args extends unknown[] = [], Result = unknown>(
|
|
query: string,
|
|
): Stmt<Args, Result> {
|
|
query = query.trim();
|
|
const lines = query.split("\n");
|
|
const trim = Math.min(
|
|
...lines.map((line) =>
|
|
line.trim().length === 0 ? Infinity : line.match(/^\s*/)![0].length
|
|
),
|
|
);
|
|
query = lines.map((x) => x.slice(trim)).join("\n");
|
|
|
|
let prepared;
|
|
try {
|
|
prepared = this.node.prepare(query);
|
|
} catch (err) {
|
|
if (err) (err as { query: string }).query = query;
|
|
throw err;
|
|
}
|
|
return new Stmt(prepared);
|
|
}
|
|
}
|
|
|
|
export class Stmt<Args extends unknown[] = unknown[], Row = unknown> {
|
|
#node: StatementSync;
|
|
#class: any | null = null;
|
|
query: string;
|
|
|
|
constructor(node: StatementSync) {
|
|
this.#node = node;
|
|
this.query = node.sourceSQL;
|
|
}
|
|
|
|
/** Get one row */
|
|
get(...args: Args): Row | null {
|
|
return this.#wrap(args, () => {
|
|
const item = this.#node.get(...args as any) as Row;
|
|
if (!item) return null;
|
|
const C = this.#class;
|
|
if (C) Object.setPrototypeOf(item, C.prototype);
|
|
return item;
|
|
});
|
|
}
|
|
getNonNull(...args: Args) {
|
|
const item = this.get(...args);
|
|
if (!item) {
|
|
throw this.#wrap(args, () => new Error("Query returned no result"));
|
|
}
|
|
return item;
|
|
}
|
|
iter(...args: Args): IterableIterator<Row> {
|
|
return this.#wrap(args, () => this.array(...args)[Symbol.iterator]());
|
|
}
|
|
/** Get all rows */
|
|
array(...args: Args): Row[] {
|
|
return this.#wrap(args, () => {
|
|
const array = this.#node.all(...args as any) as Row[];
|
|
const C = this.#class;
|
|
if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype));
|
|
return array;
|
|
});
|
|
}
|
|
/** Return the number of changes / row ID */
|
|
run(...args: Args) {
|
|
return this.#wrap(args, () => this.#node.run(...args as any));
|
|
}
|
|
|
|
as<R>(Class: { new (): R }): Stmt<Args, R> {
|
|
this.#class = Class;
|
|
return this as any;
|
|
}
|
|
|
|
#wrap<T>(args: unknown[], fn: () => T) {
|
|
try {
|
|
return fn();
|
|
} catch (err: any) {
|
|
if (err && typeof err === "object") {
|
|
err.query = this.query;
|
|
args = args.flat(Infinity);
|
|
err.queryArgs = args.length === 1 ? args[0] : args;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
import { DatabaseSync, StatementSync } from "node:sqlite";
|
|
import * as path from "node:path";
|