@pramen/server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/auth.d.ts +35 -0
  2. package/dist/auth.js +189 -0
  3. package/dist/durable-object.d.ts +48 -0
  4. package/dist/durable-object.js +282 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +20 -0
  7. package/dist/pramen.d.ts +42 -0
  8. package/dist/pramen.js +19 -0
  9. package/dist/runtime/acl.d.ts +62 -0
  10. package/dist/runtime/acl.js +289 -0
  11. package/dist/runtime/db.d.ts +139 -0
  12. package/dist/runtime/db.js +425 -0
  13. package/dist/runtime/ddl.d.ts +16 -0
  14. package/dist/runtime/ddl.js +64 -0
  15. package/dist/runtime/digest.d.ts +1 -0
  16. package/dist/runtime/digest.js +29 -0
  17. package/dist/runtime/dispatch.d.ts +12 -0
  18. package/dist/runtime/dispatch.js +37 -0
  19. package/dist/runtime/driver.d.ts +45 -0
  20. package/dist/runtime/driver.js +70 -0
  21. package/dist/runtime/errors.d.ts +34 -0
  22. package/dist/runtime/errors.js +43 -0
  23. package/dist/runtime/kv.d.ts +23 -0
  24. package/dist/runtime/kv.js +41 -0
  25. package/dist/runtime/migrate.d.ts +22 -0
  26. package/dist/runtime/migrate.js +158 -0
  27. package/dist/runtime/protocol.d.ts +40 -0
  28. package/dist/runtime/protocol.js +12 -0
  29. package/dist/runtime/read-engine.d.ts +73 -0
  30. package/dist/runtime/read-engine.js +219 -0
  31. package/dist/runtime/schema-diff.d.ts +14 -0
  32. package/dist/runtime/schema-diff.js +41 -0
  33. package/dist/runtime/storage.d.ts +74 -0
  34. package/dist/runtime/storage.js +0 -0
  35. package/dist/sdk/acl.d.ts +130 -0
  36. package/dist/sdk/acl.js +55 -0
  37. package/dist/sdk/app.d.ts +7 -0
  38. package/dist/sdk/app.js +11 -0
  39. package/dist/sdk/files.d.ts +51 -0
  40. package/dist/sdk/files.js +4 -0
  41. package/dist/sdk/handlers.d.ts +36 -0
  42. package/dist/sdk/handlers.js +11 -0
  43. package/dist/sdk/infer.d.ts +79 -0
  44. package/dist/sdk/infer.js +5 -0
  45. package/dist/sdk/schema.d.ts +112 -0
  46. package/dist/sdk/schema.js +56 -0
  47. package/dist/worker-entry.d.ts +3 -0
  48. package/dist/worker-entry.js +8 -0
  49. package/dist/worker.d.ts +41 -0
  50. package/dist/worker.js +213 -0
  51. package/package.json +43 -0
  52. package/src/auth.ts +215 -0
  53. package/src/durable-object.ts +346 -0
  54. package/src/index.ts +77 -0
  55. package/src/pramen.ts +58 -0
  56. package/src/runtime/acl.ts +362 -0
  57. package/src/runtime/db.ts +550 -0
  58. package/src/runtime/ddl.ts +67 -0
  59. package/src/runtime/digest.ts +31 -0
  60. package/src/runtime/dispatch.ts +65 -0
  61. package/src/runtime/driver.ts +95 -0
  62. package/src/runtime/errors.ts +56 -0
  63. package/src/runtime/kv.ts +47 -0
  64. package/src/runtime/migrate.ts +193 -0
  65. package/src/runtime/protocol.ts +46 -0
  66. package/src/runtime/read-engine.ts +243 -0
  67. package/src/runtime/schema-diff.ts +57 -0
  68. package/src/runtime/storage.ts +0 -0
  69. package/src/sdk/acl.ts +196 -0
  70. package/src/sdk/app.ts +25 -0
  71. package/src/sdk/files.ts +53 -0
  72. package/src/sdk/handlers.ts +65 -0
  73. package/src/sdk/infer.ts +105 -0
  74. package/src/sdk/schema.ts +122 -0
  75. package/src/worker-entry.ts +9 -0
  76. package/src/worker.ts +253 -0
@@ -0,0 +1,70 @@
1
+ // Driver + Dialect — the substrate seam. pramen's ACL, read-engine, repository, and
2
+ // migrator are written against these two interfaces, so the same data layer runs
3
+ // over any SQL backend:
4
+ //
5
+ // - Driver : how to execute SQL and run a transaction (async). One per backend
6
+ // (DO SQLite, D1, Hyperdrive→Postgres, …).
7
+ // - Dialect : how a backend spells SQL (identifier quoting, bind placeholders,
8
+ // RETURNING support, value encoding). One per SQL flavor.
9
+ //
10
+ // This makes "Worker + D1" or "Worker + Hyperdrive/Postgres" a matter of plugging
11
+ // in a Driver, rather than rewriting the engine. Live queries remain a DO-only
12
+ // capability (they need a single writer + a stateful socket host).
13
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
14
+ // Column/table names come from developer schema keys (and validated `where` keys),
15
+ // never raw user input — but we still guard the identifier shape before interpolating.
16
+ function checkIdent(name) {
17
+ if (!IDENT_RE.test(name))
18
+ throw new Error(`invalid identifier: ${name}`);
19
+ return name;
20
+ }
21
+ /** SQLite (DO SQLite and D1 both speak this). Bare identifiers, `?` placeholders,
22
+ * booleans stored as INTEGER 0/1, RETURNING supported. */
23
+ export const sqliteDialect = {
24
+ id: checkIdent,
25
+ placeholder: () => "?",
26
+ returning: true,
27
+ encode: (v) => (typeof v === "boolean" ? (v ? 1 : 0) : v),
28
+ };
29
+ /** Postgres (e.g. over Hyperdrive). Double-quoted identifiers preserve case (so
30
+ * `ownerId` doesn't fold to `ownerid`), `$n` placeholders, native booleans,
31
+ * RETURNING supported. */
32
+ export const postgresDialect = {
33
+ id: (name) => `"${checkIdent(name)}"`,
34
+ placeholder: (n) => `$${n}`,
35
+ returning: true,
36
+ encode: (v) => v, // the pg driver handles type encoding
37
+ };
38
+ /** DO SQLite — the in-process store. `SqlStorage` is synchronous; we wrap it as an
39
+ * async Driver. Transactions use the DO's atomic `transaction()`. */
40
+ export class DoSqliteDriver {
41
+ storage;
42
+ dialect = sqliteDialect;
43
+ constructor(storage) {
44
+ this.storage = storage;
45
+ }
46
+ async exec(sql, params) {
47
+ return this.storage.sql.exec(sql, ...params).toArray();
48
+ }
49
+ transaction(fn) {
50
+ return this.storage.transaction(fn);
51
+ }
52
+ }
53
+ /** D1 — SQLite over RPC. Async by nature. D1 has no interactive transactions, so
54
+ * `transaction()` runs `fn` without one (a documented limitation: mutations don't
55
+ * roll back on throw the way they do on a DO). Use a DO when you need that. */
56
+ export class D1Driver {
57
+ db;
58
+ dialect = sqliteDialect;
59
+ constructor(db) {
60
+ this.db = db;
61
+ }
62
+ async exec(sql, params) {
63
+ const stmt = params.length ? this.db.prepare(sql).bind(...params) : this.db.prepare(sql);
64
+ const { results } = await stmt.all();
65
+ return results ?? [];
66
+ }
67
+ transaction(fn) {
68
+ return fn();
69
+ }
70
+ }
@@ -0,0 +1,34 @@
1
+ export declare class PramenError extends Error {
2
+ readonly status: number;
3
+ readonly code: string;
4
+ constructor(message: string, status: number, code: string);
5
+ }
6
+ export declare class BadRequest extends PramenError {
7
+ constructor(message: string);
8
+ }
9
+ /** 401 — the caller is unauthenticated (no/invalid identity). */
10
+ export declare class Unauthorized extends PramenError {
11
+ constructor(message?: string);
12
+ }
13
+ /** 403 — authenticated but not permitted. For handler-level checks; the Db
14
+ * chokepoint raises AclDenied for row/field ACL. */
15
+ export declare class Forbidden extends PramenError {
16
+ constructor(message?: string);
17
+ }
18
+ export interface ErrorBody {
19
+ ok: false;
20
+ error: string;
21
+ code: string;
22
+ }
23
+ declare function classify(err: unknown): {
24
+ status: number;
25
+ body: ErrorBody;
26
+ };
27
+ export declare const toResponse: typeof classify;
28
+ export declare function toWsError(id: string, err: unknown): {
29
+ type: "error";
30
+ id: string;
31
+ error: string;
32
+ code: string;
33
+ };
34
+ export {};
@@ -0,0 +1,43 @@
1
+ // Error model. Anything that is the *client's* fault carries a status + code and
2
+ // a message safe to return. Everything else is logged server-side and surfaced as
3
+ // a generic 500 — internal messages / stack traces never reach the client.
4
+ export class PramenError extends Error {
5
+ status;
6
+ code;
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "PramenError";
12
+ }
13
+ }
14
+ export class BadRequest extends PramenError {
15
+ constructor(message) {
16
+ super(message, 400, "bad_request");
17
+ }
18
+ }
19
+ /** 401 — the caller is unauthenticated (no/invalid identity). */
20
+ export class Unauthorized extends PramenError {
21
+ constructor(message = "authentication required") {
22
+ super(message, 401, "unauthorized");
23
+ }
24
+ }
25
+ /** 403 — authenticated but not permitted. For handler-level checks; the Db
26
+ * chokepoint raises AclDenied for row/field ACL. */
27
+ export class Forbidden extends PramenError {
28
+ constructor(message = "forbidden") {
29
+ super(message, 403, "forbidden");
30
+ }
31
+ }
32
+ function classify(err) {
33
+ if (err instanceof PramenError) {
34
+ return { status: err.status, body: { ok: false, error: err.message, code: err.code } };
35
+ }
36
+ console.error("pramen: unhandled error", err);
37
+ return { status: 500, body: { ok: false, error: "internal error", code: "internal" } };
38
+ }
39
+ export const toResponse = classify;
40
+ export function toWsError(id, err) {
41
+ const { body } = classify(err);
42
+ return { type: "error", id, error: body.error, code: body.code };
43
+ }
@@ -0,0 +1,23 @@
1
+ export declare class Kv {
2
+ private readonly ns;
3
+ private readonly prefix;
4
+ constructor(ns: KVNamespace, prefix?: string);
5
+ private full;
6
+ get(key: string): Promise<string | null>;
7
+ get(key: string, type: "json"): Promise<unknown>;
8
+ put(key: string, value: string, opts?: {
9
+ expirationTtl?: number;
10
+ expiration?: number;
11
+ }): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ /** List keys under an (app-relative) prefix; returned names have the internal
14
+ * prefix stripped. cursor is null when the listing is complete. */
15
+ list(opts?: {
16
+ prefix?: string;
17
+ limit?: number;
18
+ cursor?: string;
19
+ }): Promise<{
20
+ keys: string[];
21
+ cursor: string | null;
22
+ }>;
23
+ }
@@ -0,0 +1,41 @@
1
+ // Kv — a thin, prefixed wrapper over the project's Workers KV namespace, handed
2
+ // to handlers as ctx.kv.
3
+ //
4
+ // Two levels of namespacing keep things isolated:
5
+ // - Across projects: each project declares its own KV namespace in oblaka.ts
6
+ // (named per project), so projects in one account never share a namespace.
7
+ // - Within the namespace: keys are prefixed (`app:` for handler data) so they
8
+ // never collide with pramen-internal keys (the tenant registry uses `tenant:`).
9
+ //
10
+ // ctx.kv is GLOBAL across all tenants of the project — use it for config, feature
11
+ // flags, and caches, NOT per-tenant data (that's ctx.db). KV is eventually
12
+ // consistent and is NOT part of a mutation's transaction.
13
+ export class Kv {
14
+ ns;
15
+ prefix;
16
+ constructor(ns, prefix = "app:") {
17
+ this.ns = ns;
18
+ this.prefix = prefix;
19
+ }
20
+ full(key) {
21
+ return this.prefix + key;
22
+ }
23
+ get(key, type) {
24
+ return type === "json" ? this.ns.get(this.full(key), "json") : this.ns.get(this.full(key), "text");
25
+ }
26
+ async put(key, value, opts) {
27
+ await this.ns.put(this.full(key), value, opts);
28
+ }
29
+ async delete(key) {
30
+ await this.ns.delete(this.full(key));
31
+ }
32
+ /** List keys under an (app-relative) prefix; returned names have the internal
33
+ * prefix stripped. cursor is null when the listing is complete. */
34
+ async list(opts) {
35
+ const res = await this.ns.list({ prefix: this.full(opts?.prefix ?? ""), limit: opts?.limit, cursor: opts?.cursor });
36
+ return {
37
+ keys: res.keys.map((k) => k.name.slice(this.prefix.length)),
38
+ cursor: res.list_complete ? null : (res.cursor ?? null),
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ import type { Driver } from "./driver";
2
+ import type { SchemaDef } from "../sdk/schema";
3
+ export interface MigrationReport {
4
+ changed: boolean;
5
+ created: string[];
6
+ added: string[];
7
+ /** Tables rebuilt to apply a drop / rename / type change. */
8
+ rebuilt: string[];
9
+ /** Tables dropped because the schema no longer declares them. */
10
+ droppedTables: string[];
11
+ /** Destructive ops detected but NOT applied because destructive migrations are
12
+ * disabled (the default). Re-deploy with allowDestructive to apply them. */
13
+ skipped: string[];
14
+ }
15
+ export interface MigrateOptions {
16
+ /** Apply destructive changes (drop/rebuild/type-change/table-drop). Off by default
17
+ * — data-loss is gated behind an explicit opt-in (env `PRAMEN_ALLOW_DESTRUCTIVE`).
18
+ * Additive changes (create table, add column, add index) always apply. */
19
+ allowDestructive?: boolean;
20
+ }
21
+ export declare function schemaHash(schema: SchemaDef): string;
22
+ export declare function migrate(driver: Driver, schema: SchemaDef, opts?: MigrateOptions): Promise<MigrationReport>;
@@ -0,0 +1,158 @@
1
+ // Schema migration — applied on boot, wrapped in a transaction by the caller.
2
+ // Runs over a Driver, so it works on any SQLite-flavored substrate (DO SQLite and
3
+ // D1). The introspection (`PRAGMA table_info`, `sqlite_master`) and table-rebuild
4
+ // are SQLite-specific; a Postgres/MySQL migrator would be a separate adapter.
5
+ // Reconciles the live store with the declared schema in two passes:
6
+ // 1. additive (no data loss): missing table -> CREATE TABLE; missing column ->
7
+ // ALTER TABLE ADD COLUMN (nullable).
8
+ // 2. destructive: a live column the schema no longer declares is DROPPED, a type
9
+ // change is applied, and a `renamedFrom` column is renamed — all via the
10
+ // standard SQLite table-rebuild (create new, copy, drop old, rename). This is
11
+ // auto-applied: a bad deploy CAN lose data, by design (WIP, no backward-compat).
12
+ //
13
+ // A schema hash in the internal `_pramen_meta` table lets an unchanged schema skip
14
+ // introspection entirely on warm boots. The live table (PRAGMA) is the ground
15
+ // truth diffed against the schema — no stored shape needed.
16
+ //
17
+ // ADD COLUMN is always nullable (SQLite can't add NOT NULL to a populated table).
18
+ // A rename can't be inferred from a diff (a removed + added column is ambiguous),
19
+ // so it must be declared with `renamedFrom`; otherwise it is applied as drop+add.
20
+ import { addColumnSql, createTableSql, indexStatements, sqlType } from "./ddl";
21
+ import { digest } from "./digest";
22
+ /** Internal bookkeeping tables the migrator must never touch — pramen's own, SQLite's,
23
+ * and the substrate's (D1 keeps `_cf_*` / `d1_*` tables in sqlite_master and forbids
24
+ * dropping them). Matched case-insensitively. */
25
+ function isInternalTable(name) {
26
+ const n = name.toLowerCase();
27
+ return (n.startsWith("_pramen") ||
28
+ n.startsWith("__pramen") ||
29
+ n.startsWith("sqlite_") ||
30
+ n.startsWith("_cf_") ||
31
+ n.startsWith("d1_"));
32
+ }
33
+ function ident(name) {
34
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
35
+ throw new Error(`invalid identifier: ${name}`);
36
+ return name;
37
+ }
38
+ export function schemaHash(schema) {
39
+ const canon = {};
40
+ for (const [table, def] of Object.entries(schema))
41
+ canon[table] = def.fields;
42
+ return digest(canon);
43
+ }
44
+ /** Live columns of a table -> their declared SQL type (uppercased). Empty if the
45
+ * table doesn't exist. */
46
+ async function tableColumns(driver, table) {
47
+ const rows = (await driver.exec(`PRAGMA table_info(${ident(table)})`, []));
48
+ return new Map(rows.map((r) => [r.name, (r.type || "").toUpperCase()]));
49
+ }
50
+ async function readMeta(driver, key) {
51
+ const rows = (await driver.exec(`SELECT value FROM _pramen_meta WHERE key = ?`, [key]));
52
+ return rows[0]?.value;
53
+ }
54
+ async function writeMeta(driver, key, value) {
55
+ await driver.exec(`INSERT OR REPLACE INTO _pramen_meta (key, value) VALUES (?, ?)`, [key, value]);
56
+ }
57
+ /** Rebuild a table to exactly the declared schema: create a temp table, copy each
58
+ * desired column from its source (renamed or same-named live column, CAST on a
59
+ * type change; brand-new columns left NULL), drop the old table, rename the temp. */
60
+ async function rebuildTable(driver, table, def, live) {
61
+ const tmp = `__pramen_rebuild_${table}`;
62
+ await driver.exec(`DROP TABLE IF EXISTS ${ident(tmp)}`, []);
63
+ await driver.exec(createTableSql(tmp, def), []);
64
+ const destCols = [];
65
+ const srcExprs = [];
66
+ for (const [name, field] of Object.entries(def.fields)) {
67
+ const f = field;
68
+ const src = f.renamedFrom && live.has(f.renamedFrom) ? f.renamedFrom : live.has(name) ? name : undefined;
69
+ if (!src)
70
+ continue; // brand-new column with no source -> leave NULL
71
+ const target = sqlType(f);
72
+ destCols.push(ident(name));
73
+ srcExprs.push(live.get(src) === target ? ident(src) : `CAST(${ident(src)} AS ${target})`);
74
+ }
75
+ if (destCols.length > 0) {
76
+ await driver.exec(`INSERT INTO ${ident(tmp)} (${destCols.join(", ")}) SELECT ${srcExprs.join(", ")} FROM ${ident(table)}`, []);
77
+ }
78
+ await driver.exec(`DROP TABLE ${ident(table)}`, []);
79
+ await driver.exec(`ALTER TABLE ${ident(tmp)} RENAME TO ${ident(table)}`, []);
80
+ }
81
+ export async function migrate(driver, schema, opts = {}) {
82
+ await driver.exec(`CREATE TABLE IF NOT EXISTS _pramen_meta (key TEXT PRIMARY KEY, value TEXT)`, []);
83
+ const allowDestructive = opts.allowDestructive ?? false;
84
+ const current = schemaHash(schema);
85
+ if ((await readMeta(driver, "schema_hash")) === current)
86
+ return { changed: false, created: [], added: [], rebuilt: [], droppedTables: [], skipped: [] };
87
+ const created = [];
88
+ const added = [];
89
+ const rebuilt = [];
90
+ const droppedTables = [];
91
+ const skipped = [];
92
+ for (const [table, def] of Object.entries(schema)) {
93
+ const existing = await tableColumns(driver, table);
94
+ if (existing.size === 0) {
95
+ await driver.exec(createTableSql(table, def), []);
96
+ created.push(table);
97
+ continue;
98
+ }
99
+ // Pass 1 — additive: add any column the schema declares but the table lacks.
100
+ for (const [name, field] of Object.entries(def.fields)) {
101
+ if (existing.has(name))
102
+ continue;
103
+ await driver.exec(`ALTER TABLE ${ident(table)} ADD COLUMN ${addColumnSql(name, field)}`, []);
104
+ added.push(`${table}.${name}`);
105
+ }
106
+ // Pass 2 — destructive: rebuild if any live column must be dropped, a declared
107
+ // column changed type, or a rename hint points at an existing live column.
108
+ const live = await tableColumns(driver, table); // re-read (now includes additively-added columns)
109
+ const desired = new Set(Object.keys(def.fields));
110
+ const renamedSources = new Set();
111
+ for (const f of Object.values(def.fields)) {
112
+ const from = f.renamedFrom;
113
+ if (from && live.has(from))
114
+ renamedSources.add(from);
115
+ }
116
+ const needsDrop = [...live.keys()].some((c) => !desired.has(c) && !renamedSources.has(c));
117
+ const needsTypeChange = Object.entries(def.fields).some(([n, f]) => live.has(n) && live.get(n) !== sqlType(f));
118
+ if (needsDrop || needsTypeChange || renamedSources.size > 0) {
119
+ if (allowDestructive) {
120
+ await rebuildTable(driver, table, def, live);
121
+ rebuilt.push(table);
122
+ }
123
+ else {
124
+ skipped.push(`rebuild ${table} (drop/type-change/rename)`);
125
+ }
126
+ }
127
+ }
128
+ // Ensure unique/index declarations (idempotent, via IF NOT EXISTS). Indexes can be
129
+ // added to an existing table without a rebuild; a stale index from a removed
130
+ // declaration is left in place (cleanup is future work).
131
+ for (const [table, def] of Object.entries(schema)) {
132
+ for (const stmt of indexStatements(table, def))
133
+ await driver.exec(stmt, []);
134
+ }
135
+ // Drop tables the schema no longer declares (internal bookkeeping tables skipped).
136
+ const liveTables = (await driver.exec(`SELECT name FROM sqlite_master WHERE type = 'table'`, []));
137
+ for (const { name } of liveTables) {
138
+ if (isInternalTable(name) || name in schema)
139
+ continue;
140
+ if (allowDestructive) {
141
+ await driver.exec(`DROP TABLE ${ident(name)}`, []);
142
+ droppedTables.push(name);
143
+ }
144
+ else {
145
+ skipped.push(`drop table ${name}`);
146
+ }
147
+ }
148
+ // Only record the schema as applied when fully reconciled. If destructive changes
149
+ // were skipped, leave the hash so a later deploy (with allowDestructive) retries —
150
+ // additive work is idempotent, so re-running is safe.
151
+ if (skipped.length === 0) {
152
+ await writeMeta(driver, "schema_hash", current);
153
+ }
154
+ else {
155
+ console.warn(`pramen: ${skipped.length} destructive migration(s) skipped (set PRAMEN_ALLOW_DESTRUCTIVE=true to apply): ${skipped.join("; ")}`);
156
+ }
157
+ return { changed: true, created, added, rebuilt, droppedTables, skipped };
158
+ }
@@ -0,0 +1,40 @@
1
+ export interface SubscribeMsg {
2
+ type: "subscribe";
3
+ id: string;
4
+ name: string;
5
+ input?: unknown;
6
+ }
7
+ export interface UnsubscribeMsg {
8
+ type: "unsubscribe";
9
+ id: string;
10
+ }
11
+ export interface CallMsg {
12
+ type: "call";
13
+ id: string;
14
+ name: string;
15
+ input?: unknown;
16
+ }
17
+ export type ClientMsg = SubscribeMsg | UnsubscribeMsg | CallMsg;
18
+ export type ServerMsg = {
19
+ type: "data";
20
+ id: string;
21
+ result: unknown;
22
+ } | {
23
+ type: "result";
24
+ id: string;
25
+ result: unknown;
26
+ } | {
27
+ type: "error";
28
+ id: string;
29
+ error: string;
30
+ };
31
+ /** A live subscription, persisted on the socket so it survives DO hibernation. */
32
+ export interface Subscription {
33
+ id: string;
34
+ name: string;
35
+ input: unknown;
36
+ /** Tables the query read — the coarse prefilter for which writes might matter. */
37
+ tables: string[];
38
+ /** Digest of the last result pushed — used to suppress no-op (row-level) pushes. */
39
+ digest: string;
40
+ }
@@ -0,0 +1,12 @@
1
+ // Live-query wire protocol (JSON over WebSocket).
2
+ //
3
+ // Client -> server:
4
+ // { type: "subscribe", id, name, input? } // query handler; initial data + pushes
5
+ // { type: "unsubscribe", id }
6
+ // { type: "call", id, name, input? } // one-shot any handler (query or mutation)
7
+ //
8
+ // Server -> client:
9
+ // { type: "data", id, result } // initial subscription result + every update
10
+ // { type: "result", id, result } // reply to a one-shot call
11
+ // { type: "error", id, error }
12
+ export {};
@@ -0,0 +1,73 @@
1
+ import type { Dialect } from "./driver";
2
+ export type CmpOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "LIKE";
3
+ export type SqlExpr = {
4
+ t: "true";
5
+ } | {
6
+ t: "false";
7
+ } | {
8
+ t: "cmp";
9
+ op: CmpOp;
10
+ col: string;
11
+ value: unknown;
12
+ } | {
13
+ t: "in";
14
+ col: string;
15
+ values: unknown[];
16
+ negate: boolean;
17
+ } | {
18
+ t: "null";
19
+ col: string;
20
+ negate: boolean;
21
+ } | {
22
+ t: "and";
23
+ parts: SqlExpr[];
24
+ } | {
25
+ t: "or";
26
+ parts: SqlExpr[];
27
+ };
28
+ export declare const TRUE: SqlExpr;
29
+ export declare const FALSE: SqlExpr;
30
+ export declare const cmp: (op: CmpOp, col: string, value: unknown) => SqlExpr;
31
+ export declare const isNull: (col: string, negate?: boolean) => SqlExpr;
32
+ export declare const inList: (col: string, values: unknown[], negate?: boolean) => SqlExpr;
33
+ export declare const and: (...parts: SqlExpr[]) => SqlExpr;
34
+ export declare const or: (...parts: SqlExpr[]) => SqlExpr;
35
+ /** Equality (null -> IS NULL). Used by ACL scope building and relation loads. */
36
+ export declare const eq: (col: string, value: unknown) => SqlExpr;
37
+ /** Compile a structured user predicate into a SqlExpr.
38
+ * Shapes: { col: value } (eq) | { col: { gt, lt, in, like, isNull, … } } | { AND: [...] } | { OR: [...] } */
39
+ export declare function compileWhere(input: Record<string, unknown>): SqlExpr;
40
+ export interface CompiledSql {
41
+ readonly sql: string;
42
+ readonly params: unknown[];
43
+ }
44
+ export declare function compileExpr(expr: SqlExpr, dialect: Dialect, params?: unknown[]): CompiledSql;
45
+ /** Evaluate a compiled predicate against an in-memory row. Mirrors compileExpr's
46
+ * semantics (bound-boolean coercion, NULL compares false, empty-IN, LIKE) so the
47
+ * declarative cell-ACL `when` path can decide per-row field visibility without a
48
+ * round-trip to SQLite. */
49
+ export declare function evalExpr(expr: SqlExpr, row: Record<string, unknown>): boolean;
50
+ export interface OrderBy {
51
+ column: string;
52
+ dir?: "asc" | "desc";
53
+ }
54
+ export interface QuerySpec {
55
+ readonly from: string;
56
+ readonly where?: SqlExpr;
57
+ readonly orderBy?: OrderBy[];
58
+ readonly limit?: number;
59
+ readonly offset?: number;
60
+ }
61
+ export type AggFn = "count" | "sum" | "avg" | "min" | "max";
62
+ export declare function compileCount(from: string, dialect: Dialect, where?: SqlExpr): CompiledSql;
63
+ export interface Aggregation {
64
+ fn: AggFn;
65
+ column?: string;
66
+ }
67
+ export declare function compileAggregate(spec: {
68
+ from: string;
69
+ where?: SqlExpr;
70
+ groupBy?: string[];
71
+ aggregations: Record<string, Aggregation>;
72
+ }, dialect: Dialect): CompiledSql;
73
+ export declare function compileSelect(spec: QuerySpec, dialect: Dialect): CompiledSql;