@ai-matrx/data 0.26.2 → 0.28.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.
- package/CHANGELOG.md +33 -0
- package/dist/auth.cjs +113 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +66 -0
- package/dist/auth.d.ts +66 -0
- package/dist/auth.js +90 -0
- package/dist/auth.js.map +1 -0
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +4 -297
- package/dist/db.d.ts +4 -297
- package/dist/db.js.map +1 -1
- package/dist/doors-D88CwoLU.d.cts +296 -0
- package/dist/doors-D88CwoLU.d.ts +296 -0
- package/dist/script.cjs +97 -0
- package/dist/script.cjs.map +1 -0
- package/dist/script.d.cts +16 -0
- package/dist/script.d.ts +16 -0
- package/dist/script.js +76 -0
- package/dist/script.js.map +1 -0
- package/dist/short-link.cjs +136 -0
- package/dist/short-link.cjs.map +1 -0
- package/dist/short-link.d.cts +35 -0
- package/dist/short-link.d.ts +35 -0
- package/dist/short-link.js +120 -0
- package/dist/short-link.js.map +1 -0
- package/package.json +32 -2
package/dist/script.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/script.ts
|
|
21
|
+
var script_exports = {};
|
|
22
|
+
__export(script_exports, {
|
|
23
|
+
SCRIPT_KEY_VARS: () => SCRIPT_KEY_VARS,
|
|
24
|
+
SCRIPT_URL_VARS: () => SCRIPT_URL_VARS,
|
|
25
|
+
createScriptDb: () => createScriptDb
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(script_exports);
|
|
28
|
+
var import_supabase_js = require("@supabase/supabase-js");
|
|
29
|
+
|
|
30
|
+
// src/db/doors.ts
|
|
31
|
+
function credentialsOf(client) {
|
|
32
|
+
return {
|
|
33
|
+
async get() {
|
|
34
|
+
if (!client.auth) return null;
|
|
35
|
+
const { data } = await client.auth.getSession();
|
|
36
|
+
return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
|
|
37
|
+
},
|
|
38
|
+
onChange(cb) {
|
|
39
|
+
if (!client.auth) return () => {
|
|
40
|
+
};
|
|
41
|
+
const { data } = client.auth.onAuthStateChange(() => cb());
|
|
42
|
+
return () => data.subscription.unsubscribe();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createDb(options) {
|
|
47
|
+
const { client, organization } = options;
|
|
48
|
+
return {
|
|
49
|
+
client,
|
|
50
|
+
role: "user",
|
|
51
|
+
activeOrganization: () => organization?.() ?? null,
|
|
52
|
+
credentials: credentialsOf(client)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createServiceDb(options) {
|
|
56
|
+
if (typeof globalThis.window !== "undefined") {
|
|
57
|
+
throw new DoorRefusedError("createServiceDb", "the service role never runs in a browser");
|
|
58
|
+
}
|
|
59
|
+
return { client: options.client, role: "service", activeOrganization: () => null, credentials: credentialsOf(options.client) };
|
|
60
|
+
}
|
|
61
|
+
var DoorRefusedError = class extends Error {
|
|
62
|
+
constructor(door, reason) {
|
|
63
|
+
super(`${door}: ${reason}`);
|
|
64
|
+
this.door = door;
|
|
65
|
+
this.reason = reason;
|
|
66
|
+
this.name = "DoorRefusedError";
|
|
67
|
+
}
|
|
68
|
+
door;
|
|
69
|
+
reason;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/script.ts
|
|
73
|
+
var SCRIPT_URL_VARS = ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_MATRIX_URL"];
|
|
74
|
+
var SCRIPT_KEY_VARS = [
|
|
75
|
+
"SUPABASE_SECRET_KEY",
|
|
76
|
+
"SUPABASE_MATRIX_SECRET_KEY",
|
|
77
|
+
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
|
|
78
|
+
"SUPABASE_MATRIX_PUBLISHABLE_KEY"
|
|
79
|
+
];
|
|
80
|
+
function firstSet(env, names) {
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
const value = env[name];
|
|
83
|
+
if (value) return [name, value];
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function createScriptDb(options = {}) {
|
|
88
|
+
const env = options.env ?? globalThis.process?.env ?? {};
|
|
89
|
+
const url = firstSet(env, options.urlVars ?? SCRIPT_URL_VARS);
|
|
90
|
+
const key = firstSet(env, options.keyVars ?? SCRIPT_KEY_VARS);
|
|
91
|
+
if (!url || !key) return null;
|
|
92
|
+
const client = (0, import_supabase_js.createClient)(url[1], key[1], {
|
|
93
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
94
|
+
});
|
|
95
|
+
return /SECRET/i.test(key[0]) ? createServiceDb({ client }) : createDb({ client });
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=script.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/script.ts","../src/db/doors.ts"],"sourcesContent":["/**\n * @ai-matrx/data/script — a database connection for Node scripts and generators.\n *\n * A script never opens its own supabase client: it asks for one here and talks through\n * the same doors as every app. Node only (`createServiceDb` refuses a browser bundle).\n *\n * const db = createScriptDb(); // null when no URL/key is set — say what you wanted\n * if (!db) throw new Error(`set one of ${SCRIPT_URL_VARS.join(\", \")} …`);\n * const rows = await callAll(db, entityTypesList, {}, { order: [\"token\"] });\n *\n * The first set variable of each list wins. A secret key gives the service role\n * (`createServiceDb`); a publishable key gives the anonymous person (`createDb`).\n * Load your `.env` first (dotenv) — this module reads `process.env` and nothing else.\n */\nimport { createClient } from \"@supabase/supabase-js\";\n\nimport { createDb, createServiceDb, type Db } from \"./db/doors\";\n\n/** Accepted URL spellings, in order: app-side names first, repo-root names second. */\nexport const SCRIPT_URL_VARS = [\"NEXT_PUBLIC_SUPABASE_URL\", \"SUPABASE_MATRIX_URL\"] as const;\n/** Accepted key spellings, in order: secret (service role) first, publishable second. */\nexport const SCRIPT_KEY_VARS = [\n \"SUPABASE_SECRET_KEY\",\n \"SUPABASE_MATRIX_SECRET_KEY\",\n \"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY\",\n \"SUPABASE_MATRIX_PUBLISHABLE_KEY\",\n] as const;\n\nexport interface ScriptDbOptions {\n /** Where to read; default `process.env`. */\n env?: Record<string, string | undefined>;\n urlVars?: readonly string[];\n keyVars?: readonly string[];\n}\n\nfunction firstSet(env: Record<string, string | undefined>, names: readonly string[]): [string, string] | null {\n for (const name of names) {\n const value = env[name];\n if (value) return [name, value];\n }\n return null;\n}\n\n/** A script's database connection, or null when the URL or key is not set. */\nexport function createScriptDb(options: ScriptDbOptions = {}): Db | null {\n const env = options.env ?? (globalThis as { process?: { env: Record<string, string | undefined> } }).process?.env ?? {};\n const url = firstSet(env, options.urlVars ?? SCRIPT_URL_VARS);\n const key = firstSet(env, options.keyVars ?? SCRIPT_KEY_VARS);\n if (!url || !key) return null;\n const client = createClient(url[1], key[1], {\n auth: { persistSession: false, autoRefreshToken: false },\n });\n // A key read from a *SECRET* variable is the service role; anything else is the anonymous person.\n return /SECRET/i.test(key[0]) ? createServiceDb({ client }) : createDb({ client });\n}\n","/**\n * @ai-matrx/data — THE doors: one way to talk to the database.\n *\n * `createDb` owns the connection (wrapping the app's existing Supabase client\n * during migration — data never opens a second auth client or socket), the\n * session (handed to `@ai-matrx/api` as its `user` credential through\n * `db.credentials`) and the organization binding. Every operation is a\n * standalone function over a DESCRIPTOR that each app generates locally from the\n * ONE database description (`matrx-data generate`), so the rules ride the\n * descriptor's flags — read from the live table registry — never a call site's\n * memory:\n *\n * - organization (the access ladder): an entity insert takes the explicit\n * `organization_id`, else the active organization; a CHILD insert\n * (`org.rule === \"parent\"`) never takes the active organization — the\n * database's `inherit_org_from_parent` fills it from the parent, so adding to\n * a record in another organization works; reads NEVER read the active\n * organization; service doors have no binding (organization is required);\n * - complete reads: `listAll` pages past PostgREST's 1,000-row cap\n * (`readAllRows`) and throws on a short read; `page` is the explicit window;\n * - versioned tables: `update` takes `{ expectedVersion }` (guarded, conflict\n * returned) or `{ overwrite: true }` — explicit either way;\n * - lifecycle: `remove` soft-deletes where the table has `deleted_at`; an\n * archive-only table refuses `remove` (use `archive`); a hard delete exists\n * only where the table has neither;\n * - the door set by registry type (entity/detail all · ledger read/list/insert ·\n * reference/system read/list · restricted service-only · deprecated none),\n * narrowed by `client_read_only` / `client_deletes_refused` — refused BEFORE\n * the wire with a sentence that names the rule;\n * - service role: `createServiceDb` throws in a browser bundle;\n * - a table chosen at runtime: `tableByName(index, \"schema.table\")` over the\n * generated flags index — loosely typed, same rules.\n */\nimport { readAllRows } from \"./read-all-rows\";\nimport { guardedUpdate } from \"./guarded-update\";\n\n// ─── descriptors (what `matrx-data generate` emits) ─────────────────────────\n\n/** Registry type (`platform.entity_types.type`); null = untyped or no registry row. */\nexport type TableType = \"entity\" | \"detail\" | \"ledger\" | \"reference\" | \"system\" | \"restricted\" | \"deprecated\";\n\nexport type OrgRule =\n | { readonly rule: \"binding\" }\n | { readonly rule: \"parent\"; readonly parent: string; readonly via: string }\n | { readonly rule: \"column\" }\n | { readonly rule: \"none\" };\n\n/** Phantom carrier for the generated row types — never present at runtime. */\ndeclare const ROW: unique symbol;\n\nexport interface TableDescriptor<Row = Record<string, unknown>, Insert = Record<string, unknown>, Update = Partial<Insert>> {\n readonly schema: string;\n readonly name: string;\n readonly kind: \"table\" | \"view\" | \"materialized_view\";\n readonly type: TableType | null;\n readonly pk: readonly string[];\n readonly org: OrgRule;\n readonly versioned: boolean;\n readonly softDelete: boolean;\n /** The archive column, when the table archives instead of (or as well as) soft-deleting. */\n readonly archive: \"archived_at\" | \"is_archived\" | null;\n readonly readOnly: boolean;\n readonly deletesRefused: boolean;\n readonly readOnlyColumns: readonly string[];\n readonly listScope: string | null;\n readonly realtime: boolean;\n /**\n * The table has a `created_by` column. A signed-in insert that omits it is stamped with the\n * session's person (row security on tables like `workbench.notes` refuses an insert whose\n * `created_by` is not `auth.uid()`). Optional: descriptors generated before 0.25.0 lack it.\n */\n readonly createdBy?: boolean;\n readonly [ROW]?: { row: Row; insert: Insert; update: Update };\n}\n\nexport interface FunctionDescriptor<Args = Record<string, unknown>, Result = unknown> {\n readonly schema: string;\n readonly name: string;\n readonly roles: readonly (\"anon\" | \"authenticated\" | \"service_role\")[];\n readonly registered: boolean;\n readonly [ROW]?: { args: Args; result: Result };\n}\n\nexport type RowOf<T> = T extends TableDescriptor<infer R, any, any> ? R : never;\nexport type InsertOf<T> = T extends TableDescriptor<any, infer I, any> ? I : never;\nexport type UpdateOf<T> = T extends TableDescriptor<any, any, infer U> ? U : never;\nexport type ArgsOf<F> = F extends FunctionDescriptor<infer A, any> ? A : never;\nexport type ResultOf<F> = F extends FunctionDescriptor<any, infer R> ? R : never;\n\n// ─── the connection ─────────────────────────────────────────────────────────\n\ninterface PostgrestError {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\ninterface Resp<T> {\n data: T | null;\n error: PostgrestError | null;\n count?: number | null;\n}\n/** The slice of a supabase-js query builder the doors use (structural — any supabase-js 2.x client). */\nexport interface QueryLike extends PromiseLike<Resp<any>> {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n eq(column: string, value: unknown): QueryLike;\n is(column: string, value: null | boolean): QueryLike;\n order(column: string, opts?: { ascending?: boolean }): QueryLike;\n range(from: number, to: number): QueryLike;\n maybeSingle(): PromiseLike<Resp<any>>;\n single(): PromiseLike<Resp<any>>;\n}\n/** The slice of a supabase-js table builder (`client.schema(s).from(t)`) the doors use. */\nexport interface TableLike {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n insert(values: any): QueryLike;\n update(values: any): QueryLike;\n delete(): QueryLike;\n}\n/** The slice of a supabase-js client the doors use (a real `SupabaseClient` is assignable — doors.types.test.ts). */\nexport interface SupabaseLike {\n schema(name: string): {\n /**\n * A table builder (read as `TableLike` inside the doors). Typed `unknown` on purpose: matching\n * supabase-js's deep builder generics structurally costs TS2589 in every consumer.\n */\n from(table: string): unknown;\n rpc(fn: string, args?: Record<string, unknown>, opts?: { count?: \"exact\" | \"planned\" | \"estimated\" }): PromiseLike<Resp<any>>;\n };\n auth?: {\n getSession(): Promise<{ data: { session: { access_token: string; user?: { id: string } | null } | null } }>;\n onAuthStateChange(cb: (event: string, session: unknown) => void): { data: { subscription: { unsubscribe(): void } } };\n };\n}\n\n/** The credential shape `@ai-matrx/api`'s `CredentialsPort` accepts (structural; data owns the session). */\nexport interface UserCredentialsPort {\n get(): Promise<{ kind: \"user\"; accessToken: string } | null>;\n onChange?(cb: () => void): () => void;\n}\n\nexport interface Db {\n readonly client: SupabaseLike;\n readonly role: \"user\" | \"service\";\n /** The active organization — where NEW entity rows are saved. Never a read filter. */\n activeOrganization(): string | null;\n /** The session as `@ai-matrx/api`'s `user` credential (one sign-in feeds both lanes). */\n readonly credentials: UserCredentialsPort;\n}\n\nexport interface CreateDbOptions {\n /** The app's existing Supabase client (browser, RSC, route or the host's chat client). */\n client: SupabaseLike;\n /** Where new entity rows are saved. Read lazily on every insert; never consulted by a read. */\n organization?: () => string | null | undefined;\n}\n\nfunction credentialsOf(client: SupabaseLike): UserCredentialsPort {\n return {\n async get() {\n if (!client.auth) return null;\n const { data } = await client.auth.getSession();\n return data.session ? { kind: \"user\", accessToken: data.session.access_token } : null;\n },\n onChange(cb) {\n if (!client.auth) return () => {};\n const { data } = client.auth.onAuthStateChange(() => cb());\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\n/** The one database connection for a signed-in (or anonymous) person. */\nexport function createDb(options: CreateDbOptions): Db {\n const { client, organization } = options;\n return {\n client,\n role: \"user\",\n activeOrganization: () => organization?.() ?? null,\n credentials: credentialsOf(client),\n };\n}\n\n/**\n * Service-role doors — servers only. No session, no binding: every entity insert\n * names its organization. Throws in a browser bundle.\n */\nexport function createServiceDb(options: { client: SupabaseLike }): Db {\n if (typeof (globalThis as { window?: unknown }).window !== \"undefined\") {\n throw new DoorRefusedError(\"createServiceDb\", \"the service role never runs in a browser\");\n }\n return { client: options.client, role: \"service\", activeOrganization: () => null, credentials: credentialsOf(options.client) };\n}\n\n// ─── refusals ───────────────────────────────────────────────────────────────\n\nexport class DoorRefusedError extends Error {\n constructor(\n readonly door: string,\n readonly reason: string,\n ) {\n super(`${door}: ${reason}`);\n this.name = \"DoorRefusedError\";\n }\n}\n\nexport class DatabaseError extends Error {\n constructor(\n readonly door: string,\n readonly cause: PostgrestError,\n ) {\n super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : \"\"}`);\n this.name = \"DatabaseError\";\n }\n}\n\ntype Op = \"read\" | \"insert\" | \"update\" | \"remove\" | \"archive\";\nconst CLIENT_DOORS: Record<TableType, readonly Op[]> = {\n entity: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n detail: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n ledger: [\"read\", \"insert\"],\n reference: [\"read\"],\n system: [\"read\"],\n restricted: [],\n deprecated: [],\n};\n\nconst qualified = (t: { schema: string; name: string }) => `${t.schema}.${t.name}`;\n\n/** Refuse before the wire when the descriptor's rules do not open this door. */\nexport function assertDoor(db: Db, table: TableDescriptor<any, any, any>, op: Op): void {\n const door = `${op} ${qualified(table)}`;\n if (table.type === \"deprecated\") throw new DoorRefusedError(door, \"the table is deprecated — no door opens on it\");\n if (table.kind !== \"table\" && op !== \"read\") throw new DoorRefusedError(door, `a ${table.kind.replace(\"_\", \" \")} is read-only`);\n if (db.role === \"service\") return;\n if (table.type && !CLIENT_DOORS[table.type].includes(op))\n throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(\", \") || \"service\"} doors to a client`);\n if (op !== \"read\" && table.readOnly) throw new DoorRefusedError(door, \"the table is read-only to clients (client_read_only)\");\n if (op === \"remove\" && table.deletesRefused) throw new DoorRefusedError(door, \"clients may not delete from this table (client_deletes_refused)\");\n}\n\nfunction check<T>(door: string, res: Resp<T>): T {\n if (res.error) throw new DatabaseError(door, res.error);\n return res.data as T;\n}\n\nconst from = (db: Db, t: TableDescriptor<any, any, any>) => db.client.schema(t.schema).from(t.name) as TableLike;\n\nfunction byPk(q: QueryLike, t: TableDescriptor<any, any, any>, id: unknown): QueryLike {\n if (t.pk.length === 1) return q.eq(t.pk[0] as string, id);\n const key = id as Record<string, unknown>;\n return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);\n}\n\n// ─── reads (never the active organization; soft-deleted rows excluded unless asked) ─\n\ntype Col<R> = Extract<keyof R, string>;\n\n/** The typed filter a `Where` callback receives — column names and values checked against the row. */\nexport interface Filter<R> {\n eq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n neq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n like(column: Col<R>, pattern: string): Filter<R>;\n ilike(column: Col<R>, pattern: string): Filter<R>;\n in<K extends Col<R>>(column: K, values: readonly NonNullable<R[K]>[]): Filter<R>;\n is(column: Col<R>, value: null | boolean): Filter<R>;\n contains(column: Col<R>, value: unknown): Filter<R>;\n overlaps(column: Col<R>, value: unknown): Filter<R>;\n textSearch(column: Col<R>, query: string, opts?: { type?: \"plain\" | \"phrase\" | \"websearch\"; config?: string }): Filter<R>;\n /** PostgREST's raw `or` grammar (`\"pinned.eq.true,starred.eq.true\"`). */\n or(filters: string, opts?: { referencedTable?: string }): Filter<R>;\n not(column: Col<R>, operator: string, value: unknown): Filter<R>;\n}\n\n/** A filter callback — the same builder `readAllRows` callers write today, typed to the row. */\nexport type Where<R = any> = (q: Filter<R>) => Filter<R>;\n\nexport interface ReadOptions<C> {\n /** Only these columns (the result is typed to them). Default every column. */\n columns?: readonly C[];\n /** Include soft-deleted rows (default: excluded on tables with `deleted_at`). */\n includeDeleted?: boolean;\n}\n\nconst selectOf = (columns?: readonly string[]) => (columns && columns.length ? columns.join(\",\") : \"*\");\n\nfunction live(q: any, table: TableDescriptor<any, any, any>, includeDeleted?: boolean): any {\n return table.softDelete && !includeDeleted ? q.is(\"deleted_at\", null) : q;\n}\n\nfunction ordered(q: any, table: TableDescriptor<any, any, any>, orderBy?: string, ascending = true): any {\n // The FULL primary key, so paging is stable on composite keys.\n const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : [\"id\"];\n return cols.reduce((acc: any, c) => acc.order(c, { ascending }), q);\n}\n\n/** One row by primary key (a composite key takes `{ col: value }`), or null. */\nexport async function read<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: ReadOptions<C> = {},\n): Promise<Pick<R, C> | null> {\n assertDoor(db, table, \"read\");\n const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);\n return check(`read ${qualified(table)}`, await q.maybeSingle());\n}\n\n/** EVERY matching row, paged past the 1,000-row cap (ordered by the full key); throws on a short read. */\nexport async function listAll<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n where?: Where<R>,\n opts: ReadOptions<C> & { orderBy?: Col<R>; pageSize?: number; maxRows?: number } = {},\n): Promise<Pick<R, C>[]> {\n assertDoor(db, table, \"read\");\n return readAllRows<Pick<R, C>>(\n ({ from: lo, to: hi }) => {\n let q: any = live(from(db, table).select(selectOf(opts.columns), { count: \"exact\" }), table, opts.includeDeleted);\n if (where) q = where(q);\n // The database's own error (code, details) reaches the caller as a DatabaseError,\n // exactly like every other door — never a re-worded readAllRows message.\n return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);\n return res;\n });\n },\n {\n label: `listAll ${qualified(table)}`,\n ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}),\n ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}),\n },\n );\n}\n\n/** One explicit window (`from`..`to`, inclusive) plus the exact total. */\nexport async function page<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n window: ReadOptions<C> & { from: number; to: number; where?: Where<R>; orderBy?: Col<R>; ascending?: boolean },\n): Promise<{ rows: Pick<R, C>[]; total: number | null }> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(selectOf(window.columns), { count: \"exact\" }), table, window.includeDeleted);\n if (window.where) q = window.where(q);\n const res = await ordered(q, table, window.orderBy, window.ascending ?? true).range(window.from, window.to);\n return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };\n}\n\n/** The exact count of matching rows (no rows transferred). */\nexport async function count<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, where?: Where<R>, opts: { includeDeleted?: boolean } = {}): Promise<number> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(\"*\", { count: \"exact\", head: true }), table, opts.includeDeleted);\n if (where) q = where(q);\n const res = await q;\n check(`count ${qualified(table)}`, res);\n return res.count ?? 0;\n}\n\n// ─── writes ─────────────────────────────────────────────────────────────────\n\nfunction withOrganization(db: Db, table: TableDescriptor<any, any, any>, values: Record<string, unknown>): Record<string, unknown> {\n const door = `insert ${qualified(table)}`;\n const org = table.org;\n if (org.rule === \"parent\" && values[org.via] != null) {\n // A child row: its organization is its parent's. Never the active organization —\n // `inherit_org_from_parent` fills organization_id; an explicit one is checked by\n // `refuse_org_foreign_to_parent`. So adding to a record in another organization works.\n return values;\n }\n if (org.rule === \"binding\" || org.rule === \"parent\") {\n // An entity row, or a row whose optional parent is absent (a note outside any folder),\n // is saved in the named organization, else the active one.\n if (values.organization_id != null) return values;\n const active = db.activeOrganization();\n if (!active) {\n throw new DoorRefusedError(\n door,\n db.role === \"service\" ? \"a service insert names its organization_id\" : \"no organization: pass organization_id or bind createDb({ organization })\",\n );\n }\n return { ...values, organization_id: active };\n }\n return values;\n}\n\n/** Insert one row (or many) and return what the database stored. */\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: I): Promise<R>;\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: readonly I[]): Promise<R[]>;\nexport async function insert(db: Db, table: TableDescriptor<any, any, any>, values: unknown): Promise<unknown> {\n assertDoor(db, table, \"insert\");\n const many = Array.isArray(values);\n let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v as Record<string, unknown>));\n if (table.createdBy && db.role === \"user\" && rows.some((r) => r.created_by == null) && db.client.auth) {\n // Stamp the signed-in person (no session: sent as given, and row security answers).\n const { data } = await db.client.auth.getSession();\n const person = data.session?.user?.id;\n if (person) rows = rows.map((r) => (r.created_by == null ? { ...r, created_by: person } : r));\n }\n const res = await from(db, table).insert(rows).select(\"*\");\n const stored = check(`insert ${qualified(table)}`, res) as unknown[];\n return many ? stored : stored[0];\n}\n\n/** Every write on a versioned table is explicit: `{ expectedVersion }` (guarded) or `{ overwrite: true }`. */\nexport type UpdateGuard = { expectedVersion: number } | { overwrite: true };\n\nexport type WriteResult<R> =\n | { status: \"saved\"; row: R; rebasedFrom?: { expectedVersion: number; currentVersion: number } }\n | { status: \"conflict\"; currentRow: R; currentVersion: number }\n | { status: \"not_found\" };\n\n/** The one guarded write every update-shaped door goes through. */\nasync function writeRow<R>(db: Db, table: TableDescriptor<any, any, any>, id: unknown, values: Record<string, unknown>, door: string, guard?: UpdateGuard): Promise<WriteResult<R>> {\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) {\n throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned — nothing would guard it; pass no guard\");\n }\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard && guard.overwrite)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n if (expected !== undefined) {\n return (await guardedUpdate<any>({\n expectedVersion: expected,\n applyUpdate: ({ expectedVersion, nextVersion }) =>\n byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq(\"version\", expectedVersion).select(\"*\").maybeSingle(),\n fetchCurrent: () => byPk(from(db, table).select(\"*\"), table, id).maybeSingle(),\n })) as WriteResult<R>;\n }\n const row = check(door, await byPk(from(db, table).update(values), table, id).select(\"*\").maybeSingle());\n return row ? { status: \"saved\", row } : { status: \"not_found\" };\n}\n\n/**\n * Update one row by primary key. On a versioned table the guard is required:\n * `{ expectedVersion }` (a conflict comes back, never a silent overwrite) or\n * `{ overwrite: true }`; on an unversioned table `expectedVersion` is refused.\n */\nexport async function update<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, patch: U, guard?: UpdateGuard): Promise<WriteResult<R>> {\n assertDoor(db, table, \"update\");\n const door = `update ${qualified(table)}`;\n const values = patch as Record<string, unknown>;\n if (db.role === \"user\") {\n const blocked = table.readOnlyColumns.filter((c) => c in values);\n if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(\", \")}`);\n }\n return writeRow<R>(db, table, id, values, door, guard);\n}\n\n/**\n * Remove one row the way the table removes: soft delete (`deleted_at`) where it\n * has one; an archive-only table refuses (use `archive`); a hard delete only where\n * the table has neither. Guarded like `update` on a versioned table.\n */\nexport async function remove<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n guard?: UpdateGuard,\n): Promise<{ status: \"soft_deleted\" | \"deleted\" | \"not_found\" } | { status: \"conflict\"; currentVersion: number }> {\n assertDoor(db, table, \"remove\");\n const door = `remove ${qualified(table)}`;\n if (table.softDelete) {\n const res = await writeRow<R>(db, table, id, { deleted_at: new Date().toISOString() }, door, guard);\n return res.status === \"saved\" ? { status: \"soft_deleted\" } : res.status === \"conflict\" ? { status: \"conflict\", currentVersion: res.currentVersion } : res;\n }\n if (table.archive) throw new DoorRefusedError(door, \"this table archives instead of deleting — use archive()\");\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned\");\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n let q = byPk(from(db, table).delete(), table, id);\n if (expected !== undefined) q = q.eq(\"version\", expected);\n const rows = check(door, await q.select(\"*\")) as unknown[] | null;\n if (rows && rows.length) return { status: \"deleted\" };\n if (expected !== undefined) {\n const current = check(door, await byPk(from(db, table).select(\"version\"), table, id).maybeSingle()) as { version: number } | null;\n if (current) return { status: \"conflict\", currentVersion: current.version };\n }\n return { status: \"not_found\" };\n}\n\n/** Archive (or, with `restore`, bring back) one row by the table's archive column. Guarded like `update`. */\nexport async function archive<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: { restore?: boolean; guard?: UpdateGuard } = {},\n): Promise<WriteResult<R>> {\n assertDoor(db, table, \"archive\");\n const door = `${opts.restore ? \"restore\" : \"archive\"} ${qualified(table)}`;\n let values: Record<string, unknown>;\n if (table.archive === \"archived_at\") values = { archived_at: opts.restore ? null : new Date().toISOString() };\n else if (table.archive === \"is_archived\") values = { is_archived: !opts.restore };\n else if (table.softDelete && opts.restore) values = { deleted_at: null };\n else throw new DoorRefusedError(door, \"the table has no archive column\");\n return writeRow<R>(db, table, id, values, door, opts.guard);\n}\n\n/** Bring back an archived or soft-deleted row. */\nexport const restore = <R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, guard?: UpdateGuard) =>\n archive(db, table, id, guard ? { restore: true, guard } : { restore: true });\n\n// ─── functions ──────────────────────────────────────────────────────────────\n\n/** Call a database function through its generated door (PostgREST chooses an overload by argument names). */\nexport async function call<F extends FunctionDescriptor<any, any>>(db: Db, fn: F, args: ArgsOf<F>): Promise<ResultOf<F>> {\n const door = `call ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n return check(door, await db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>));\n}\n\n/** One ordering column of a set-returning function's rows (PostgREST `order`). */\nexport type CallOrder = string | { column: string; ascending?: boolean; nullsFirst?: boolean };\n\n/**\n * EVERY row a set-returning function answers, paged past the 1,000-row cap by\n * `range` over a stable `order` (name enough columns to make it total — end on a key);\n * throws a `DatabaseError` with the database's own error, or `IncompleteReadError` on a short read.\n */\nexport async function callAll<F extends FunctionDescriptor<any, any>>(\n db: Db,\n fn: F,\n args: ArgsOf<F>,\n opts: { order: readonly CallOrder[]; pageSize?: number; maxRows?: number },\n): Promise<ResultOf<F> extends readonly (infer Row)[] ? Row[] : ResultOf<F>[]> {\n const door = `callAll ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n if (!opts.order.length) throw new DoorRefusedError(door, \"a paged read needs an order (end on a key) — pages over no order can skip or repeat rows\");\n return readAllRows<any>(\n ({ from: lo, to: hi }) => {\n let q: any = db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>, { count: \"exact\" });\n for (const o of opts.order) {\n q = typeof o === \"string\" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...(o.nullsFirst !== undefined ? { nullsFirst: o.nullsFirst } : {}) });\n }\n return Promise.resolve(q.range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(door, res.error);\n return res;\n });\n },\n { label: door, ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}), ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}) },\n ) as never;\n}\n\n// ─── a table chosen at runtime ──────────────────────────────────────────────\n\n/** One schema's flags (`<schema>.flags.ts` from `matrx-data generate --flags-index`): \"schema.table\" → descriptor. */\nexport type FlagsIndex = Readonly<Record<string, TableDescriptor>>;\n/** Per-schema loaders (`flags-index.ts`): only the schema a runtime name names is loaded (pay-only). */\nexport type FlagLoaders = Readonly<Record<string, () => Promise<FlagsIndex>>>;\n\n/** The door for a table whose name arrives at runtime — loosely typed, the same rules. */\nexport function tableByName(index: FlagsIndex, name: string): TableDescriptor {\n const t = index[name];\n if (!t) throw new DoorRefusedError(`tableByName ${name}`, \"no such table in the database description (schema-qualify it: schema.table)\");\n return t;\n}\n\n/** `tableByName` over per-schema loaders: loads only the named schema's flags. */\nexport async function loadTableByName(loaders: FlagLoaders, name: string): Promise<TableDescriptor> {\n const schema = name.slice(0, name.indexOf(\".\"));\n const load = schema ? loaders[schema] : undefined;\n if (!load) throw new DoorRefusedError(`tableByName ${name}`, \"no such schema in the database description (schema-qualify it: schema.table)\");\n return tableByName(await load(), name);\n}\n\n// ─── realtime ───────────────────────────────────────────────────────────────\n\n/** The `postgres_changes` filter for a table — only published tables have one (an unpublished table delivers nothing). */\nexport function changesOf(table: TableDescriptor<any, any, any>): { schema: string; table: string } {\n if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, \"the table is not in the supabase_realtime publication\");\n return { schema: table.schema, table: table.name };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,yBAA6B;;;AC+I7B,SAAS,cAAc,QAA2C;AAChE,SAAO;AAAA,IACL,MAAM,MAAM;AACV,UAAI,CAAC,OAAO,KAAM,QAAO;AACzB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK,WAAW;AAC9C,aAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,aAAa,KAAK,QAAQ,aAAa,IAAI;AAAA,IACnF;AAAA,IACA,SAAS,IAAI;AACX,UAAI,CAAC,OAAO,KAAM,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC;AACzD,aAAO,MAAM,KAAK,aAAa,YAAY;AAAA,IAC7C;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAA8B;AACrD,QAAM,EAAE,QAAQ,aAAa,IAAI;AACjC,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,oBAAoB,MAAM,eAAe,KAAK;AAAA,IAC9C,aAAa,cAAc,MAAM;AAAA,EACnC;AACF;AAMO,SAAS,gBAAgB,SAAuC;AACrE,MAAI,OAAQ,WAAoC,WAAW,aAAa;AACtE,UAAM,IAAI,iBAAiB,mBAAmB,0CAA0C;AAAA,EAC1F;AACA,SAAO,EAAE,QAAQ,QAAQ,QAAQ,MAAM,WAAW,oBAAoB,MAAM,MAAM,aAAa,cAAc,QAAQ,MAAM,EAAE;AAC/H;AAIO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACW,MACA,QACT;AACA,UAAM,GAAG,IAAI,KAAK,MAAM,EAAE;AAHjB;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;;;ADzLO,IAAM,kBAAkB,CAAC,4BAA4B,qBAAqB;AAE1E,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASA,SAAS,SAAS,KAAyC,OAAmD;AAC5G,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,IAAI,IAAI;AACtB,QAAI,MAAO,QAAO,CAAC,MAAM,KAAK;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,eAAe,UAA2B,CAAC,GAAc;AACvE,QAAM,MAAM,QAAQ,OAAQ,WAAyE,SAAS,OAAO,CAAC;AACtH,QAAM,MAAM,SAAS,KAAK,QAAQ,WAAW,eAAe;AAC5D,QAAM,MAAM,SAAS,KAAK,QAAQ,WAAW,eAAe;AAC5D,MAAI,CAAC,OAAO,CAAC,IAAK,QAAO;AACzB,QAAM,aAAS,iCAAa,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG;AAAA,IAC1C,MAAM,EAAE,gBAAgB,OAAO,kBAAkB,MAAM;AAAA,EACzD,CAAC;AAED,SAAO,UAAU,KAAK,IAAI,CAAC,CAAC,IAAI,gBAAgB,EAAE,OAAO,CAAC,IAAI,SAAS,EAAE,OAAO,CAAC;AACnF;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { D as Db } from './doors-D88CwoLU.cjs';
|
|
2
|
+
|
|
3
|
+
/** Accepted URL spellings, in order: app-side names first, repo-root names second. */
|
|
4
|
+
declare const SCRIPT_URL_VARS: readonly ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_MATRIX_URL"];
|
|
5
|
+
/** Accepted key spellings, in order: secret (service role) first, publishable second. */
|
|
6
|
+
declare const SCRIPT_KEY_VARS: readonly ["SUPABASE_SECRET_KEY", "SUPABASE_MATRIX_SECRET_KEY", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", "SUPABASE_MATRIX_PUBLISHABLE_KEY"];
|
|
7
|
+
interface ScriptDbOptions {
|
|
8
|
+
/** Where to read; default `process.env`. */
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
urlVars?: readonly string[];
|
|
11
|
+
keyVars?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** A script's database connection, or null when the URL or key is not set. */
|
|
14
|
+
declare function createScriptDb(options?: ScriptDbOptions): Db | null;
|
|
15
|
+
|
|
16
|
+
export { SCRIPT_KEY_VARS, SCRIPT_URL_VARS, type ScriptDbOptions, createScriptDb };
|
package/dist/script.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { D as Db } from './doors-D88CwoLU.js';
|
|
2
|
+
|
|
3
|
+
/** Accepted URL spellings, in order: app-side names first, repo-root names second. */
|
|
4
|
+
declare const SCRIPT_URL_VARS: readonly ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_MATRIX_URL"];
|
|
5
|
+
/** Accepted key spellings, in order: secret (service role) first, publishable second. */
|
|
6
|
+
declare const SCRIPT_KEY_VARS: readonly ["SUPABASE_SECRET_KEY", "SUPABASE_MATRIX_SECRET_KEY", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", "SUPABASE_MATRIX_PUBLISHABLE_KEY"];
|
|
7
|
+
interface ScriptDbOptions {
|
|
8
|
+
/** Where to read; default `process.env`. */
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
urlVars?: readonly string[];
|
|
11
|
+
keyVars?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** A script's database connection, or null when the URL or key is not set. */
|
|
14
|
+
declare function createScriptDb(options?: ScriptDbOptions): Db | null;
|
|
15
|
+
|
|
16
|
+
export { SCRIPT_KEY_VARS, SCRIPT_URL_VARS, type ScriptDbOptions, createScriptDb };
|
package/dist/script.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/script.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
|
|
4
|
+
// src/db/doors.ts
|
|
5
|
+
function credentialsOf(client) {
|
|
6
|
+
return {
|
|
7
|
+
async get() {
|
|
8
|
+
if (!client.auth) return null;
|
|
9
|
+
const { data } = await client.auth.getSession();
|
|
10
|
+
return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
|
|
11
|
+
},
|
|
12
|
+
onChange(cb) {
|
|
13
|
+
if (!client.auth) return () => {
|
|
14
|
+
};
|
|
15
|
+
const { data } = client.auth.onAuthStateChange(() => cb());
|
|
16
|
+
return () => data.subscription.unsubscribe();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function createDb(options) {
|
|
21
|
+
const { client, organization } = options;
|
|
22
|
+
return {
|
|
23
|
+
client,
|
|
24
|
+
role: "user",
|
|
25
|
+
activeOrganization: () => organization?.() ?? null,
|
|
26
|
+
credentials: credentialsOf(client)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createServiceDb(options) {
|
|
30
|
+
if (typeof globalThis.window !== "undefined") {
|
|
31
|
+
throw new DoorRefusedError("createServiceDb", "the service role never runs in a browser");
|
|
32
|
+
}
|
|
33
|
+
return { client: options.client, role: "service", activeOrganization: () => null, credentials: credentialsOf(options.client) };
|
|
34
|
+
}
|
|
35
|
+
var DoorRefusedError = class extends Error {
|
|
36
|
+
constructor(door, reason) {
|
|
37
|
+
super(`${door}: ${reason}`);
|
|
38
|
+
this.door = door;
|
|
39
|
+
this.reason = reason;
|
|
40
|
+
this.name = "DoorRefusedError";
|
|
41
|
+
}
|
|
42
|
+
door;
|
|
43
|
+
reason;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/script.ts
|
|
47
|
+
var SCRIPT_URL_VARS = ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_MATRIX_URL"];
|
|
48
|
+
var SCRIPT_KEY_VARS = [
|
|
49
|
+
"SUPABASE_SECRET_KEY",
|
|
50
|
+
"SUPABASE_MATRIX_SECRET_KEY",
|
|
51
|
+
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
|
|
52
|
+
"SUPABASE_MATRIX_PUBLISHABLE_KEY"
|
|
53
|
+
];
|
|
54
|
+
function firstSet(env, names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const value = env[name];
|
|
57
|
+
if (value) return [name, value];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function createScriptDb(options = {}) {
|
|
62
|
+
const env = options.env ?? globalThis.process?.env ?? {};
|
|
63
|
+
const url = firstSet(env, options.urlVars ?? SCRIPT_URL_VARS);
|
|
64
|
+
const key = firstSet(env, options.keyVars ?? SCRIPT_KEY_VARS);
|
|
65
|
+
if (!url || !key) return null;
|
|
66
|
+
const client = createClient(url[1], key[1], {
|
|
67
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
68
|
+
});
|
|
69
|
+
return /SECRET/i.test(key[0]) ? createServiceDb({ client }) : createDb({ client });
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
SCRIPT_KEY_VARS,
|
|
73
|
+
SCRIPT_URL_VARS,
|
|
74
|
+
createScriptDb
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/script.ts","../src/db/doors.ts"],"sourcesContent":["/**\n * @ai-matrx/data/script — a database connection for Node scripts and generators.\n *\n * A script never opens its own supabase client: it asks for one here and talks through\n * the same doors as every app. Node only (`createServiceDb` refuses a browser bundle).\n *\n * const db = createScriptDb(); // null when no URL/key is set — say what you wanted\n * if (!db) throw new Error(`set one of ${SCRIPT_URL_VARS.join(\", \")} …`);\n * const rows = await callAll(db, entityTypesList, {}, { order: [\"token\"] });\n *\n * The first set variable of each list wins. A secret key gives the service role\n * (`createServiceDb`); a publishable key gives the anonymous person (`createDb`).\n * Load your `.env` first (dotenv) — this module reads `process.env` and nothing else.\n */\nimport { createClient } from \"@supabase/supabase-js\";\n\nimport { createDb, createServiceDb, type Db } from \"./db/doors\";\n\n/** Accepted URL spellings, in order: app-side names first, repo-root names second. */\nexport const SCRIPT_URL_VARS = [\"NEXT_PUBLIC_SUPABASE_URL\", \"SUPABASE_MATRIX_URL\"] as const;\n/** Accepted key spellings, in order: secret (service role) first, publishable second. */\nexport const SCRIPT_KEY_VARS = [\n \"SUPABASE_SECRET_KEY\",\n \"SUPABASE_MATRIX_SECRET_KEY\",\n \"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY\",\n \"SUPABASE_MATRIX_PUBLISHABLE_KEY\",\n] as const;\n\nexport interface ScriptDbOptions {\n /** Where to read; default `process.env`. */\n env?: Record<string, string | undefined>;\n urlVars?: readonly string[];\n keyVars?: readonly string[];\n}\n\nfunction firstSet(env: Record<string, string | undefined>, names: readonly string[]): [string, string] | null {\n for (const name of names) {\n const value = env[name];\n if (value) return [name, value];\n }\n return null;\n}\n\n/** A script's database connection, or null when the URL or key is not set. */\nexport function createScriptDb(options: ScriptDbOptions = {}): Db | null {\n const env = options.env ?? (globalThis as { process?: { env: Record<string, string | undefined> } }).process?.env ?? {};\n const url = firstSet(env, options.urlVars ?? SCRIPT_URL_VARS);\n const key = firstSet(env, options.keyVars ?? SCRIPT_KEY_VARS);\n if (!url || !key) return null;\n const client = createClient(url[1], key[1], {\n auth: { persistSession: false, autoRefreshToken: false },\n });\n // A key read from a *SECRET* variable is the service role; anything else is the anonymous person.\n return /SECRET/i.test(key[0]) ? createServiceDb({ client }) : createDb({ client });\n}\n","/**\n * @ai-matrx/data — THE doors: one way to talk to the database.\n *\n * `createDb` owns the connection (wrapping the app's existing Supabase client\n * during migration — data never opens a second auth client or socket), the\n * session (handed to `@ai-matrx/api` as its `user` credential through\n * `db.credentials`) and the organization binding. Every operation is a\n * standalone function over a DESCRIPTOR that each app generates locally from the\n * ONE database description (`matrx-data generate`), so the rules ride the\n * descriptor's flags — read from the live table registry — never a call site's\n * memory:\n *\n * - organization (the access ladder): an entity insert takes the explicit\n * `organization_id`, else the active organization; a CHILD insert\n * (`org.rule === \"parent\"`) never takes the active organization — the\n * database's `inherit_org_from_parent` fills it from the parent, so adding to\n * a record in another organization works; reads NEVER read the active\n * organization; service doors have no binding (organization is required);\n * - complete reads: `listAll` pages past PostgREST's 1,000-row cap\n * (`readAllRows`) and throws on a short read; `page` is the explicit window;\n * - versioned tables: `update` takes `{ expectedVersion }` (guarded, conflict\n * returned) or `{ overwrite: true }` — explicit either way;\n * - lifecycle: `remove` soft-deletes where the table has `deleted_at`; an\n * archive-only table refuses `remove` (use `archive`); a hard delete exists\n * only where the table has neither;\n * - the door set by registry type (entity/detail all · ledger read/list/insert ·\n * reference/system read/list · restricted service-only · deprecated none),\n * narrowed by `client_read_only` / `client_deletes_refused` — refused BEFORE\n * the wire with a sentence that names the rule;\n * - service role: `createServiceDb` throws in a browser bundle;\n * - a table chosen at runtime: `tableByName(index, \"schema.table\")` over the\n * generated flags index — loosely typed, same rules.\n */\nimport { readAllRows } from \"./read-all-rows\";\nimport { guardedUpdate } from \"./guarded-update\";\n\n// ─── descriptors (what `matrx-data generate` emits) ─────────────────────────\n\n/** Registry type (`platform.entity_types.type`); null = untyped or no registry row. */\nexport type TableType = \"entity\" | \"detail\" | \"ledger\" | \"reference\" | \"system\" | \"restricted\" | \"deprecated\";\n\nexport type OrgRule =\n | { readonly rule: \"binding\" }\n | { readonly rule: \"parent\"; readonly parent: string; readonly via: string }\n | { readonly rule: \"column\" }\n | { readonly rule: \"none\" };\n\n/** Phantom carrier for the generated row types — never present at runtime. */\ndeclare const ROW: unique symbol;\n\nexport interface TableDescriptor<Row = Record<string, unknown>, Insert = Record<string, unknown>, Update = Partial<Insert>> {\n readonly schema: string;\n readonly name: string;\n readonly kind: \"table\" | \"view\" | \"materialized_view\";\n readonly type: TableType | null;\n readonly pk: readonly string[];\n readonly org: OrgRule;\n readonly versioned: boolean;\n readonly softDelete: boolean;\n /** The archive column, when the table archives instead of (or as well as) soft-deleting. */\n readonly archive: \"archived_at\" | \"is_archived\" | null;\n readonly readOnly: boolean;\n readonly deletesRefused: boolean;\n readonly readOnlyColumns: readonly string[];\n readonly listScope: string | null;\n readonly realtime: boolean;\n /**\n * The table has a `created_by` column. A signed-in insert that omits it is stamped with the\n * session's person (row security on tables like `workbench.notes` refuses an insert whose\n * `created_by` is not `auth.uid()`). Optional: descriptors generated before 0.25.0 lack it.\n */\n readonly createdBy?: boolean;\n readonly [ROW]?: { row: Row; insert: Insert; update: Update };\n}\n\nexport interface FunctionDescriptor<Args = Record<string, unknown>, Result = unknown> {\n readonly schema: string;\n readonly name: string;\n readonly roles: readonly (\"anon\" | \"authenticated\" | \"service_role\")[];\n readonly registered: boolean;\n readonly [ROW]?: { args: Args; result: Result };\n}\n\nexport type RowOf<T> = T extends TableDescriptor<infer R, any, any> ? R : never;\nexport type InsertOf<T> = T extends TableDescriptor<any, infer I, any> ? I : never;\nexport type UpdateOf<T> = T extends TableDescriptor<any, any, infer U> ? U : never;\nexport type ArgsOf<F> = F extends FunctionDescriptor<infer A, any> ? A : never;\nexport type ResultOf<F> = F extends FunctionDescriptor<any, infer R> ? R : never;\n\n// ─── the connection ─────────────────────────────────────────────────────────\n\ninterface PostgrestError {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\ninterface Resp<T> {\n data: T | null;\n error: PostgrestError | null;\n count?: number | null;\n}\n/** The slice of a supabase-js query builder the doors use (structural — any supabase-js 2.x client). */\nexport interface QueryLike extends PromiseLike<Resp<any>> {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n eq(column: string, value: unknown): QueryLike;\n is(column: string, value: null | boolean): QueryLike;\n order(column: string, opts?: { ascending?: boolean }): QueryLike;\n range(from: number, to: number): QueryLike;\n maybeSingle(): PromiseLike<Resp<any>>;\n single(): PromiseLike<Resp<any>>;\n}\n/** The slice of a supabase-js table builder (`client.schema(s).from(t)`) the doors use. */\nexport interface TableLike {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n insert(values: any): QueryLike;\n update(values: any): QueryLike;\n delete(): QueryLike;\n}\n/** The slice of a supabase-js client the doors use (a real `SupabaseClient` is assignable — doors.types.test.ts). */\nexport interface SupabaseLike {\n schema(name: string): {\n /**\n * A table builder (read as `TableLike` inside the doors). Typed `unknown` on purpose: matching\n * supabase-js's deep builder generics structurally costs TS2589 in every consumer.\n */\n from(table: string): unknown;\n rpc(fn: string, args?: Record<string, unknown>, opts?: { count?: \"exact\" | \"planned\" | \"estimated\" }): PromiseLike<Resp<any>>;\n };\n auth?: {\n getSession(): Promise<{ data: { session: { access_token: string; user?: { id: string } | null } | null } }>;\n onAuthStateChange(cb: (event: string, session: unknown) => void): { data: { subscription: { unsubscribe(): void } } };\n };\n}\n\n/** The credential shape `@ai-matrx/api`'s `CredentialsPort` accepts (structural; data owns the session). */\nexport interface UserCredentialsPort {\n get(): Promise<{ kind: \"user\"; accessToken: string } | null>;\n onChange?(cb: () => void): () => void;\n}\n\nexport interface Db {\n readonly client: SupabaseLike;\n readonly role: \"user\" | \"service\";\n /** The active organization — where NEW entity rows are saved. Never a read filter. */\n activeOrganization(): string | null;\n /** The session as `@ai-matrx/api`'s `user` credential (one sign-in feeds both lanes). */\n readonly credentials: UserCredentialsPort;\n}\n\nexport interface CreateDbOptions {\n /** The app's existing Supabase client (browser, RSC, route or the host's chat client). */\n client: SupabaseLike;\n /** Where new entity rows are saved. Read lazily on every insert; never consulted by a read. */\n organization?: () => string | null | undefined;\n}\n\nfunction credentialsOf(client: SupabaseLike): UserCredentialsPort {\n return {\n async get() {\n if (!client.auth) return null;\n const { data } = await client.auth.getSession();\n return data.session ? { kind: \"user\", accessToken: data.session.access_token } : null;\n },\n onChange(cb) {\n if (!client.auth) return () => {};\n const { data } = client.auth.onAuthStateChange(() => cb());\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\n/** The one database connection for a signed-in (or anonymous) person. */\nexport function createDb(options: CreateDbOptions): Db {\n const { client, organization } = options;\n return {\n client,\n role: \"user\",\n activeOrganization: () => organization?.() ?? null,\n credentials: credentialsOf(client),\n };\n}\n\n/**\n * Service-role doors — servers only. No session, no binding: every entity insert\n * names its organization. Throws in a browser bundle.\n */\nexport function createServiceDb(options: { client: SupabaseLike }): Db {\n if (typeof (globalThis as { window?: unknown }).window !== \"undefined\") {\n throw new DoorRefusedError(\"createServiceDb\", \"the service role never runs in a browser\");\n }\n return { client: options.client, role: \"service\", activeOrganization: () => null, credentials: credentialsOf(options.client) };\n}\n\n// ─── refusals ───────────────────────────────────────────────────────────────\n\nexport class DoorRefusedError extends Error {\n constructor(\n readonly door: string,\n readonly reason: string,\n ) {\n super(`${door}: ${reason}`);\n this.name = \"DoorRefusedError\";\n }\n}\n\nexport class DatabaseError extends Error {\n constructor(\n readonly door: string,\n readonly cause: PostgrestError,\n ) {\n super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : \"\"}`);\n this.name = \"DatabaseError\";\n }\n}\n\ntype Op = \"read\" | \"insert\" | \"update\" | \"remove\" | \"archive\";\nconst CLIENT_DOORS: Record<TableType, readonly Op[]> = {\n entity: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n detail: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n ledger: [\"read\", \"insert\"],\n reference: [\"read\"],\n system: [\"read\"],\n restricted: [],\n deprecated: [],\n};\n\nconst qualified = (t: { schema: string; name: string }) => `${t.schema}.${t.name}`;\n\n/** Refuse before the wire when the descriptor's rules do not open this door. */\nexport function assertDoor(db: Db, table: TableDescriptor<any, any, any>, op: Op): void {\n const door = `${op} ${qualified(table)}`;\n if (table.type === \"deprecated\") throw new DoorRefusedError(door, \"the table is deprecated — no door opens on it\");\n if (table.kind !== \"table\" && op !== \"read\") throw new DoorRefusedError(door, `a ${table.kind.replace(\"_\", \" \")} is read-only`);\n if (db.role === \"service\") return;\n if (table.type && !CLIENT_DOORS[table.type].includes(op))\n throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(\", \") || \"service\"} doors to a client`);\n if (op !== \"read\" && table.readOnly) throw new DoorRefusedError(door, \"the table is read-only to clients (client_read_only)\");\n if (op === \"remove\" && table.deletesRefused) throw new DoorRefusedError(door, \"clients may not delete from this table (client_deletes_refused)\");\n}\n\nfunction check<T>(door: string, res: Resp<T>): T {\n if (res.error) throw new DatabaseError(door, res.error);\n return res.data as T;\n}\n\nconst from = (db: Db, t: TableDescriptor<any, any, any>) => db.client.schema(t.schema).from(t.name) as TableLike;\n\nfunction byPk(q: QueryLike, t: TableDescriptor<any, any, any>, id: unknown): QueryLike {\n if (t.pk.length === 1) return q.eq(t.pk[0] as string, id);\n const key = id as Record<string, unknown>;\n return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);\n}\n\n// ─── reads (never the active organization; soft-deleted rows excluded unless asked) ─\n\ntype Col<R> = Extract<keyof R, string>;\n\n/** The typed filter a `Where` callback receives — column names and values checked against the row. */\nexport interface Filter<R> {\n eq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n neq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n like(column: Col<R>, pattern: string): Filter<R>;\n ilike(column: Col<R>, pattern: string): Filter<R>;\n in<K extends Col<R>>(column: K, values: readonly NonNullable<R[K]>[]): Filter<R>;\n is(column: Col<R>, value: null | boolean): Filter<R>;\n contains(column: Col<R>, value: unknown): Filter<R>;\n overlaps(column: Col<R>, value: unknown): Filter<R>;\n textSearch(column: Col<R>, query: string, opts?: { type?: \"plain\" | \"phrase\" | \"websearch\"; config?: string }): Filter<R>;\n /** PostgREST's raw `or` grammar (`\"pinned.eq.true,starred.eq.true\"`). */\n or(filters: string, opts?: { referencedTable?: string }): Filter<R>;\n not(column: Col<R>, operator: string, value: unknown): Filter<R>;\n}\n\n/** A filter callback — the same builder `readAllRows` callers write today, typed to the row. */\nexport type Where<R = any> = (q: Filter<R>) => Filter<R>;\n\nexport interface ReadOptions<C> {\n /** Only these columns (the result is typed to them). Default every column. */\n columns?: readonly C[];\n /** Include soft-deleted rows (default: excluded on tables with `deleted_at`). */\n includeDeleted?: boolean;\n}\n\nconst selectOf = (columns?: readonly string[]) => (columns && columns.length ? columns.join(\",\") : \"*\");\n\nfunction live(q: any, table: TableDescriptor<any, any, any>, includeDeleted?: boolean): any {\n return table.softDelete && !includeDeleted ? q.is(\"deleted_at\", null) : q;\n}\n\nfunction ordered(q: any, table: TableDescriptor<any, any, any>, orderBy?: string, ascending = true): any {\n // The FULL primary key, so paging is stable on composite keys.\n const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : [\"id\"];\n return cols.reduce((acc: any, c) => acc.order(c, { ascending }), q);\n}\n\n/** One row by primary key (a composite key takes `{ col: value }`), or null. */\nexport async function read<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: ReadOptions<C> = {},\n): Promise<Pick<R, C> | null> {\n assertDoor(db, table, \"read\");\n const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);\n return check(`read ${qualified(table)}`, await q.maybeSingle());\n}\n\n/** EVERY matching row, paged past the 1,000-row cap (ordered by the full key); throws on a short read. */\nexport async function listAll<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n where?: Where<R>,\n opts: ReadOptions<C> & { orderBy?: Col<R>; pageSize?: number; maxRows?: number } = {},\n): Promise<Pick<R, C>[]> {\n assertDoor(db, table, \"read\");\n return readAllRows<Pick<R, C>>(\n ({ from: lo, to: hi }) => {\n let q: any = live(from(db, table).select(selectOf(opts.columns), { count: \"exact\" }), table, opts.includeDeleted);\n if (where) q = where(q);\n // The database's own error (code, details) reaches the caller as a DatabaseError,\n // exactly like every other door — never a re-worded readAllRows message.\n return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);\n return res;\n });\n },\n {\n label: `listAll ${qualified(table)}`,\n ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}),\n ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}),\n },\n );\n}\n\n/** One explicit window (`from`..`to`, inclusive) plus the exact total. */\nexport async function page<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n window: ReadOptions<C> & { from: number; to: number; where?: Where<R>; orderBy?: Col<R>; ascending?: boolean },\n): Promise<{ rows: Pick<R, C>[]; total: number | null }> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(selectOf(window.columns), { count: \"exact\" }), table, window.includeDeleted);\n if (window.where) q = window.where(q);\n const res = await ordered(q, table, window.orderBy, window.ascending ?? true).range(window.from, window.to);\n return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };\n}\n\n/** The exact count of matching rows (no rows transferred). */\nexport async function count<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, where?: Where<R>, opts: { includeDeleted?: boolean } = {}): Promise<number> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(\"*\", { count: \"exact\", head: true }), table, opts.includeDeleted);\n if (where) q = where(q);\n const res = await q;\n check(`count ${qualified(table)}`, res);\n return res.count ?? 0;\n}\n\n// ─── writes ─────────────────────────────────────────────────────────────────\n\nfunction withOrganization(db: Db, table: TableDescriptor<any, any, any>, values: Record<string, unknown>): Record<string, unknown> {\n const door = `insert ${qualified(table)}`;\n const org = table.org;\n if (org.rule === \"parent\" && values[org.via] != null) {\n // A child row: its organization is its parent's. Never the active organization —\n // `inherit_org_from_parent` fills organization_id; an explicit one is checked by\n // `refuse_org_foreign_to_parent`. So adding to a record in another organization works.\n return values;\n }\n if (org.rule === \"binding\" || org.rule === \"parent\") {\n // An entity row, or a row whose optional parent is absent (a note outside any folder),\n // is saved in the named organization, else the active one.\n if (values.organization_id != null) return values;\n const active = db.activeOrganization();\n if (!active) {\n throw new DoorRefusedError(\n door,\n db.role === \"service\" ? \"a service insert names its organization_id\" : \"no organization: pass organization_id or bind createDb({ organization })\",\n );\n }\n return { ...values, organization_id: active };\n }\n return values;\n}\n\n/** Insert one row (or many) and return what the database stored. */\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: I): Promise<R>;\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: readonly I[]): Promise<R[]>;\nexport async function insert(db: Db, table: TableDescriptor<any, any, any>, values: unknown): Promise<unknown> {\n assertDoor(db, table, \"insert\");\n const many = Array.isArray(values);\n let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v as Record<string, unknown>));\n if (table.createdBy && db.role === \"user\" && rows.some((r) => r.created_by == null) && db.client.auth) {\n // Stamp the signed-in person (no session: sent as given, and row security answers).\n const { data } = await db.client.auth.getSession();\n const person = data.session?.user?.id;\n if (person) rows = rows.map((r) => (r.created_by == null ? { ...r, created_by: person } : r));\n }\n const res = await from(db, table).insert(rows).select(\"*\");\n const stored = check(`insert ${qualified(table)}`, res) as unknown[];\n return many ? stored : stored[0];\n}\n\n/** Every write on a versioned table is explicit: `{ expectedVersion }` (guarded) or `{ overwrite: true }`. */\nexport type UpdateGuard = { expectedVersion: number } | { overwrite: true };\n\nexport type WriteResult<R> =\n | { status: \"saved\"; row: R; rebasedFrom?: { expectedVersion: number; currentVersion: number } }\n | { status: \"conflict\"; currentRow: R; currentVersion: number }\n | { status: \"not_found\" };\n\n/** The one guarded write every update-shaped door goes through. */\nasync function writeRow<R>(db: Db, table: TableDescriptor<any, any, any>, id: unknown, values: Record<string, unknown>, door: string, guard?: UpdateGuard): Promise<WriteResult<R>> {\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) {\n throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned — nothing would guard it; pass no guard\");\n }\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard && guard.overwrite)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n if (expected !== undefined) {\n return (await guardedUpdate<any>({\n expectedVersion: expected,\n applyUpdate: ({ expectedVersion, nextVersion }) =>\n byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq(\"version\", expectedVersion).select(\"*\").maybeSingle(),\n fetchCurrent: () => byPk(from(db, table).select(\"*\"), table, id).maybeSingle(),\n })) as WriteResult<R>;\n }\n const row = check(door, await byPk(from(db, table).update(values), table, id).select(\"*\").maybeSingle());\n return row ? { status: \"saved\", row } : { status: \"not_found\" };\n}\n\n/**\n * Update one row by primary key. On a versioned table the guard is required:\n * `{ expectedVersion }` (a conflict comes back, never a silent overwrite) or\n * `{ overwrite: true }`; on an unversioned table `expectedVersion` is refused.\n */\nexport async function update<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, patch: U, guard?: UpdateGuard): Promise<WriteResult<R>> {\n assertDoor(db, table, \"update\");\n const door = `update ${qualified(table)}`;\n const values = patch as Record<string, unknown>;\n if (db.role === \"user\") {\n const blocked = table.readOnlyColumns.filter((c) => c in values);\n if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(\", \")}`);\n }\n return writeRow<R>(db, table, id, values, door, guard);\n}\n\n/**\n * Remove one row the way the table removes: soft delete (`deleted_at`) where it\n * has one; an archive-only table refuses (use `archive`); a hard delete only where\n * the table has neither. Guarded like `update` on a versioned table.\n */\nexport async function remove<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n guard?: UpdateGuard,\n): Promise<{ status: \"soft_deleted\" | \"deleted\" | \"not_found\" } | { status: \"conflict\"; currentVersion: number }> {\n assertDoor(db, table, \"remove\");\n const door = `remove ${qualified(table)}`;\n if (table.softDelete) {\n const res = await writeRow<R>(db, table, id, { deleted_at: new Date().toISOString() }, door, guard);\n return res.status === \"saved\" ? { status: \"soft_deleted\" } : res.status === \"conflict\" ? { status: \"conflict\", currentVersion: res.currentVersion } : res;\n }\n if (table.archive) throw new DoorRefusedError(door, \"this table archives instead of deleting — use archive()\");\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned\");\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n let q = byPk(from(db, table).delete(), table, id);\n if (expected !== undefined) q = q.eq(\"version\", expected);\n const rows = check(door, await q.select(\"*\")) as unknown[] | null;\n if (rows && rows.length) return { status: \"deleted\" };\n if (expected !== undefined) {\n const current = check(door, await byPk(from(db, table).select(\"version\"), table, id).maybeSingle()) as { version: number } | null;\n if (current) return { status: \"conflict\", currentVersion: current.version };\n }\n return { status: \"not_found\" };\n}\n\n/** Archive (or, with `restore`, bring back) one row by the table's archive column. Guarded like `update`. */\nexport async function archive<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: { restore?: boolean; guard?: UpdateGuard } = {},\n): Promise<WriteResult<R>> {\n assertDoor(db, table, \"archive\");\n const door = `${opts.restore ? \"restore\" : \"archive\"} ${qualified(table)}`;\n let values: Record<string, unknown>;\n if (table.archive === \"archived_at\") values = { archived_at: opts.restore ? null : new Date().toISOString() };\n else if (table.archive === \"is_archived\") values = { is_archived: !opts.restore };\n else if (table.softDelete && opts.restore) values = { deleted_at: null };\n else throw new DoorRefusedError(door, \"the table has no archive column\");\n return writeRow<R>(db, table, id, values, door, opts.guard);\n}\n\n/** Bring back an archived or soft-deleted row. */\nexport const restore = <R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, guard?: UpdateGuard) =>\n archive(db, table, id, guard ? { restore: true, guard } : { restore: true });\n\n// ─── functions ──────────────────────────────────────────────────────────────\n\n/** Call a database function through its generated door (PostgREST chooses an overload by argument names). */\nexport async function call<F extends FunctionDescriptor<any, any>>(db: Db, fn: F, args: ArgsOf<F>): Promise<ResultOf<F>> {\n const door = `call ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n return check(door, await db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>));\n}\n\n/** One ordering column of a set-returning function's rows (PostgREST `order`). */\nexport type CallOrder = string | { column: string; ascending?: boolean; nullsFirst?: boolean };\n\n/**\n * EVERY row a set-returning function answers, paged past the 1,000-row cap by\n * `range` over a stable `order` (name enough columns to make it total — end on a key);\n * throws a `DatabaseError` with the database's own error, or `IncompleteReadError` on a short read.\n */\nexport async function callAll<F extends FunctionDescriptor<any, any>>(\n db: Db,\n fn: F,\n args: ArgsOf<F>,\n opts: { order: readonly CallOrder[]; pageSize?: number; maxRows?: number },\n): Promise<ResultOf<F> extends readonly (infer Row)[] ? Row[] : ResultOf<F>[]> {\n const door = `callAll ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n if (!opts.order.length) throw new DoorRefusedError(door, \"a paged read needs an order (end on a key) — pages over no order can skip or repeat rows\");\n return readAllRows<any>(\n ({ from: lo, to: hi }) => {\n let q: any = db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>, { count: \"exact\" });\n for (const o of opts.order) {\n q = typeof o === \"string\" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...(o.nullsFirst !== undefined ? { nullsFirst: o.nullsFirst } : {}) });\n }\n return Promise.resolve(q.range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(door, res.error);\n return res;\n });\n },\n { label: door, ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}), ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}) },\n ) as never;\n}\n\n// ─── a table chosen at runtime ──────────────────────────────────────────────\n\n/** One schema's flags (`<schema>.flags.ts` from `matrx-data generate --flags-index`): \"schema.table\" → descriptor. */\nexport type FlagsIndex = Readonly<Record<string, TableDescriptor>>;\n/** Per-schema loaders (`flags-index.ts`): only the schema a runtime name names is loaded (pay-only). */\nexport type FlagLoaders = Readonly<Record<string, () => Promise<FlagsIndex>>>;\n\n/** The door for a table whose name arrives at runtime — loosely typed, the same rules. */\nexport function tableByName(index: FlagsIndex, name: string): TableDescriptor {\n const t = index[name];\n if (!t) throw new DoorRefusedError(`tableByName ${name}`, \"no such table in the database description (schema-qualify it: schema.table)\");\n return t;\n}\n\n/** `tableByName` over per-schema loaders: loads only the named schema's flags. */\nexport async function loadTableByName(loaders: FlagLoaders, name: string): Promise<TableDescriptor> {\n const schema = name.slice(0, name.indexOf(\".\"));\n const load = schema ? loaders[schema] : undefined;\n if (!load) throw new DoorRefusedError(`tableByName ${name}`, \"no such schema in the database description (schema-qualify it: schema.table)\");\n return tableByName(await load(), name);\n}\n\n// ─── realtime ───────────────────────────────────────────────────────────────\n\n/** The `postgres_changes` filter for a table — only published tables have one (an unpublished table delivers nothing). */\nexport function changesOf(table: TableDescriptor<any, any, any>): { schema: string; table: string } {\n if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, \"the table is not in the supabase_realtime publication\");\n return { schema: table.schema, table: table.name };\n}\n"],"mappings":";AAcA,SAAS,oBAAoB;;;AC+I7B,SAAS,cAAc,QAA2C;AAChE,SAAO;AAAA,IACL,MAAM,MAAM;AACV,UAAI,CAAC,OAAO,KAAM,QAAO;AACzB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK,WAAW;AAC9C,aAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,aAAa,KAAK,QAAQ,aAAa,IAAI;AAAA,IACnF;AAAA,IACA,SAAS,IAAI;AACX,UAAI,CAAC,OAAO,KAAM,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC;AACzD,aAAO,MAAM,KAAK,aAAa,YAAY;AAAA,IAC7C;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAA8B;AACrD,QAAM,EAAE,QAAQ,aAAa,IAAI;AACjC,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,oBAAoB,MAAM,eAAe,KAAK;AAAA,IAC9C,aAAa,cAAc,MAAM;AAAA,EACnC;AACF;AAMO,SAAS,gBAAgB,SAAuC;AACrE,MAAI,OAAQ,WAAoC,WAAW,aAAa;AACtE,UAAM,IAAI,iBAAiB,mBAAmB,0CAA0C;AAAA,EAC1F;AACA,SAAO,EAAE,QAAQ,QAAQ,QAAQ,MAAM,WAAW,oBAAoB,MAAM,MAAM,aAAa,cAAc,QAAQ,MAAM,EAAE;AAC/H;AAIO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACW,MACA,QACT;AACA,UAAM,GAAG,IAAI,KAAK,MAAM,EAAE;AAHjB;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;;;ADzLO,IAAM,kBAAkB,CAAC,4BAA4B,qBAAqB;AAE1E,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASA,SAAS,SAAS,KAAyC,OAAmD;AAC5G,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,IAAI,IAAI;AACtB,QAAI,MAAO,QAAO,CAAC,MAAM,KAAK;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,eAAe,UAA2B,CAAC,GAAc;AACvE,QAAM,MAAM,QAAQ,OAAQ,WAAyE,SAAS,OAAO,CAAC;AACtH,QAAM,MAAM,SAAS,KAAK,QAAQ,WAAW,eAAe;AAC5D,QAAM,MAAM,SAAS,KAAK,QAAQ,WAAW,eAAe;AAC5D,MAAI,CAAC,OAAO,CAAC,IAAK,QAAO;AACzB,QAAM,SAAS,aAAa,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG;AAAA,IAC1C,MAAM,EAAE,gBAAgB,OAAO,kBAAkB,MAAM;AAAA,EACzD,CAAC;AAED,SAAO,UAAU,KAAK,IAAI,CAAC,CAAC,IAAI,gBAAgB,EAAE,OAAO,CAAC,IAAI,SAAS,EAAE,OAAO,CAAC;AACnF;","names":[]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/short-link.ts
|
|
21
|
+
var short_link_exports = {};
|
|
22
|
+
__export(short_link_exports, {
|
|
23
|
+
mintShortLink: () => mintShortLink,
|
|
24
|
+
resolveShortLinkPath: () => resolveShortLinkPath,
|
|
25
|
+
shortLinkMinter: () => shortLinkMinter
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(short_link_exports);
|
|
28
|
+
var import_short_link = require("@ai-matrx/kit/short-link");
|
|
29
|
+
|
|
30
|
+
// src/db/doors.ts
|
|
31
|
+
function credentialsOf(client) {
|
|
32
|
+
return {
|
|
33
|
+
async get() {
|
|
34
|
+
if (!client.auth) return null;
|
|
35
|
+
const { data } = await client.auth.getSession();
|
|
36
|
+
return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
|
|
37
|
+
},
|
|
38
|
+
onChange(cb) {
|
|
39
|
+
if (!client.auth) return () => {
|
|
40
|
+
};
|
|
41
|
+
const { data } = client.auth.onAuthStateChange(() => cb());
|
|
42
|
+
return () => data.subscription.unsubscribe();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createDb(options) {
|
|
47
|
+
const { client, organization } = options;
|
|
48
|
+
return {
|
|
49
|
+
client,
|
|
50
|
+
role: "user",
|
|
51
|
+
activeOrganization: () => organization?.() ?? null,
|
|
52
|
+
credentials: credentialsOf(client)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var DoorRefusedError = class extends Error {
|
|
56
|
+
constructor(door, reason) {
|
|
57
|
+
super(`${door}: ${reason}`);
|
|
58
|
+
this.door = door;
|
|
59
|
+
this.reason = reason;
|
|
60
|
+
this.name = "DoorRefusedError";
|
|
61
|
+
}
|
|
62
|
+
door;
|
|
63
|
+
reason;
|
|
64
|
+
};
|
|
65
|
+
var DatabaseError = class extends Error {
|
|
66
|
+
constructor(door, cause) {
|
|
67
|
+
super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : ""}`);
|
|
68
|
+
this.door = door;
|
|
69
|
+
this.cause = cause;
|
|
70
|
+
this.name = "DatabaseError";
|
|
71
|
+
}
|
|
72
|
+
door;
|
|
73
|
+
cause;
|
|
74
|
+
};
|
|
75
|
+
function check(door, res) {
|
|
76
|
+
if (res.error) throw new DatabaseError(door, res.error);
|
|
77
|
+
return res.data;
|
|
78
|
+
}
|
|
79
|
+
async function call(db, fn, args) {
|
|
80
|
+
const door = `call ${fn.schema}.${fn.name}`;
|
|
81
|
+
const role = db.role === "service" ? "service_role" : "authenticated";
|
|
82
|
+
if (!fn.roles.includes(role) && !(role === "authenticated" && fn.roles.includes("anon"))) {
|
|
83
|
+
throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(", ") || "nobody"})`);
|
|
84
|
+
}
|
|
85
|
+
return check(door, await db.client.schema(fn.schema).rpc(fn.name, args));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/short-link-db/public.ts
|
|
89
|
+
var resolveShortLink = { "schema": "public", "name": "resolve_short_link", "roles": ["anon", "authenticated", "service_role"], "registered": true };
|
|
90
|
+
var shortenAppUrl = { "schema": "public", "name": "shorten_app_url", "roles": ["authenticated", "service_role"], "registered": true };
|
|
91
|
+
|
|
92
|
+
// src/short-link.ts
|
|
93
|
+
var dbs = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
function dbOf(client) {
|
|
95
|
+
let db = dbs.get(client);
|
|
96
|
+
if (!db) {
|
|
97
|
+
db = createDb({ client });
|
|
98
|
+
dbs.set(client, db);
|
|
99
|
+
}
|
|
100
|
+
return db;
|
|
101
|
+
}
|
|
102
|
+
function messageOf(e) {
|
|
103
|
+
const err = e;
|
|
104
|
+
if (err?.name === "DatabaseError" && err.cause?.message) return err.cause.message;
|
|
105
|
+
return err?.message ?? String(e);
|
|
106
|
+
}
|
|
107
|
+
async function mintShortLink(client, options) {
|
|
108
|
+
const ready = (0, import_short_link.prepareShortLinkMint)(options);
|
|
109
|
+
if (!ready.ok) return ready;
|
|
110
|
+
let data;
|
|
111
|
+
try {
|
|
112
|
+
data = await call(dbOf(client), shortenAppUrl, {
|
|
113
|
+
p_path: options.path,
|
|
114
|
+
p_organization_id: options.organizationId,
|
|
115
|
+
...options.expiresAt ? { p_expires_at: options.expiresAt } : {}
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { ok: false, error: messageOf(e) };
|
|
119
|
+
}
|
|
120
|
+
return (0, import_short_link.readShortLinkMintAnswer)(data, ready.origin);
|
|
121
|
+
}
|
|
122
|
+
function shortLinkMinter(client) {
|
|
123
|
+
return (options) => mintShortLink(client, options);
|
|
124
|
+
}
|
|
125
|
+
async function resolveShortLinkPath(client, token) {
|
|
126
|
+
const normalized = (0, import_short_link.normalizeShortLinkToken)(token);
|
|
127
|
+
if (!normalized) return { ok: false };
|
|
128
|
+
let data;
|
|
129
|
+
try {
|
|
130
|
+
data = await call(dbOf(client), resolveShortLink, { p_token: normalized });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { ok: false, transportError: messageOf(e) };
|
|
133
|
+
}
|
|
134
|
+
return (0, import_short_link.readShortLinkResolveAnswer)(data);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=short-link.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/short-link.ts","../src/db/doors.ts","../src/short-link-db/public.ts"],"sourcesContent":["/**\n * @ai-matrx/data/short-link — the platform short-link DATABASE calls.\n *\n * kit never talks to the database (its charter), so the two door calls live here:\n * `public.shorten_app_url` (signed-in members mint under their organization) and\n * `public.resolve_short_link` (anon; answers a PATH, never content). Everything\n * pure — the token contract, the URL shape, the path checks and the answer readers —\n * stays in `@ai-matrx/kit/short-link`; the React button/hook in\n * `@ai-matrx/kit/short-link-react` takes the minter this module builds:\n *\n * const mint = shortLinkMinter(supabase);\n * <CopyShortLinkButton mint={mint} path={`/notes/${id}`} organizationId={orgId} />\n * const r = await resolveShortLinkPath(supabase, token);\n *\n * Same calls, same arguments and the same answers as kit ≤ 0.41 (where they lived).\n */\nimport {\n normalizeShortLinkToken,\n prepareShortLinkMint,\n readShortLinkMintAnswer,\n readShortLinkResolveAnswer,\n type MintShortLinkOptions,\n type MintShortLinkResult,\n type ResolveShortLinkResult,\n type ShortLinkMinter,\n} from \"@ai-matrx/kit/short-link\";\n\nimport { call, createDb, type Db, type SupabaseLike } from \"./db/doors\";\nimport { resolveShortLink, shortenAppUrl } from \"./short-link-db/public\";\n\nexport type { MintShortLinkOptions, MintShortLinkResult, ResolveShortLinkResult, ShortLinkMinter };\n\nconst dbs = new WeakMap<object, Db>();\nfunction dbOf(client: SupabaseLike): Db {\n let db = dbs.get(client);\n if (!db) {\n db = createDb({ client });\n dbs.set(client, db);\n }\n return db;\n}\n\n/** The database's own message (a `DatabaseError` carries it as `cause`), else the error's. */\nfunction messageOf(e: unknown): string {\n const err = e as { name?: string; cause?: { message?: string }; message?: string } | null;\n if (err?.name === \"DatabaseError\" && err.cause?.message) return err.cause.message;\n return err?.message ?? String(e);\n}\n\n/** Mint a short link through the platform's authenticated mint door. */\nexport async function mintShortLink(\n client: SupabaseLike,\n options: MintShortLinkOptions,\n): Promise<MintShortLinkResult> {\n const ready = prepareShortLinkMint(options);\n if (!ready.ok) return ready;\n let data: unknown;\n try {\n data = await call(dbOf(client), shortenAppUrl, {\n p_path: options.path,\n p_organization_id: options.organizationId,\n ...(options.expiresAt ? { p_expires_at: options.expiresAt } : {}),\n });\n } catch (e) {\n return { ok: false, error: messageOf(e) };\n }\n return readShortLinkMintAnswer(data, ready.origin);\n}\n\n/** The minter `@ai-matrx/kit/short-link-react`'s button and hooks take. */\nexport function shortLinkMinter(client: SupabaseLike): ShortLinkMinter {\n return (options) => mintShortLink(client, options);\n}\n\n/**\n * Resolve a short token to its target path through the anon resolver.\n * `{ok:false}` covers invalid, unknown, and expired identically. A TRANSPORT\n * failure (the call itself errored) additionally carries `transportError`: a\n * gateway outage is not \"this link is gone\", and a caller rendering a 404 must\n * check for it first.\n */\nexport async function resolveShortLinkPath(\n client: SupabaseLike,\n token: string,\n): Promise<ResolveShortLinkResult> {\n const normalized = normalizeShortLinkToken(token);\n if (!normalized) return { ok: false };\n let data: unknown;\n try {\n data = await call(dbOf(client), resolveShortLink, { p_token: normalized });\n } catch (e) {\n return { ok: false, transportError: messageOf(e) };\n }\n return readShortLinkResolveAnswer(data);\n}\n","/**\n * @ai-matrx/data — THE doors: one way to talk to the database.\n *\n * `createDb` owns the connection (wrapping the app's existing Supabase client\n * during migration — data never opens a second auth client or socket), the\n * session (handed to `@ai-matrx/api` as its `user` credential through\n * `db.credentials`) and the organization binding. Every operation is a\n * standalone function over a DESCRIPTOR that each app generates locally from the\n * ONE database description (`matrx-data generate`), so the rules ride the\n * descriptor's flags — read from the live table registry — never a call site's\n * memory:\n *\n * - organization (the access ladder): an entity insert takes the explicit\n * `organization_id`, else the active organization; a CHILD insert\n * (`org.rule === \"parent\"`) never takes the active organization — the\n * database's `inherit_org_from_parent` fills it from the parent, so adding to\n * a record in another organization works; reads NEVER read the active\n * organization; service doors have no binding (organization is required);\n * - complete reads: `listAll` pages past PostgREST's 1,000-row cap\n * (`readAllRows`) and throws on a short read; `page` is the explicit window;\n * - versioned tables: `update` takes `{ expectedVersion }` (guarded, conflict\n * returned) or `{ overwrite: true }` — explicit either way;\n * - lifecycle: `remove` soft-deletes where the table has `deleted_at`; an\n * archive-only table refuses `remove` (use `archive`); a hard delete exists\n * only where the table has neither;\n * - the door set by registry type (entity/detail all · ledger read/list/insert ·\n * reference/system read/list · restricted service-only · deprecated none),\n * narrowed by `client_read_only` / `client_deletes_refused` — refused BEFORE\n * the wire with a sentence that names the rule;\n * - service role: `createServiceDb` throws in a browser bundle;\n * - a table chosen at runtime: `tableByName(index, \"schema.table\")` over the\n * generated flags index — loosely typed, same rules.\n */\nimport { readAllRows } from \"./read-all-rows\";\nimport { guardedUpdate } from \"./guarded-update\";\n\n// ─── descriptors (what `matrx-data generate` emits) ─────────────────────────\n\n/** Registry type (`platform.entity_types.type`); null = untyped or no registry row. */\nexport type TableType = \"entity\" | \"detail\" | \"ledger\" | \"reference\" | \"system\" | \"restricted\" | \"deprecated\";\n\nexport type OrgRule =\n | { readonly rule: \"binding\" }\n | { readonly rule: \"parent\"; readonly parent: string; readonly via: string }\n | { readonly rule: \"column\" }\n | { readonly rule: \"none\" };\n\n/** Phantom carrier for the generated row types — never present at runtime. */\ndeclare const ROW: unique symbol;\n\nexport interface TableDescriptor<Row = Record<string, unknown>, Insert = Record<string, unknown>, Update = Partial<Insert>> {\n readonly schema: string;\n readonly name: string;\n readonly kind: \"table\" | \"view\" | \"materialized_view\";\n readonly type: TableType | null;\n readonly pk: readonly string[];\n readonly org: OrgRule;\n readonly versioned: boolean;\n readonly softDelete: boolean;\n /** The archive column, when the table archives instead of (or as well as) soft-deleting. */\n readonly archive: \"archived_at\" | \"is_archived\" | null;\n readonly readOnly: boolean;\n readonly deletesRefused: boolean;\n readonly readOnlyColumns: readonly string[];\n readonly listScope: string | null;\n readonly realtime: boolean;\n /**\n * The table has a `created_by` column. A signed-in insert that omits it is stamped with the\n * session's person (row security on tables like `workbench.notes` refuses an insert whose\n * `created_by` is not `auth.uid()`). Optional: descriptors generated before 0.25.0 lack it.\n */\n readonly createdBy?: boolean;\n readonly [ROW]?: { row: Row; insert: Insert; update: Update };\n}\n\nexport interface FunctionDescriptor<Args = Record<string, unknown>, Result = unknown> {\n readonly schema: string;\n readonly name: string;\n readonly roles: readonly (\"anon\" | \"authenticated\" | \"service_role\")[];\n readonly registered: boolean;\n readonly [ROW]?: { args: Args; result: Result };\n}\n\nexport type RowOf<T> = T extends TableDescriptor<infer R, any, any> ? R : never;\nexport type InsertOf<T> = T extends TableDescriptor<any, infer I, any> ? I : never;\nexport type UpdateOf<T> = T extends TableDescriptor<any, any, infer U> ? U : never;\nexport type ArgsOf<F> = F extends FunctionDescriptor<infer A, any> ? A : never;\nexport type ResultOf<F> = F extends FunctionDescriptor<any, infer R> ? R : never;\n\n// ─── the connection ─────────────────────────────────────────────────────────\n\ninterface PostgrestError {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\ninterface Resp<T> {\n data: T | null;\n error: PostgrestError | null;\n count?: number | null;\n}\n/** The slice of a supabase-js query builder the doors use (structural — any supabase-js 2.x client). */\nexport interface QueryLike extends PromiseLike<Resp<any>> {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n eq(column: string, value: unknown): QueryLike;\n is(column: string, value: null | boolean): QueryLike;\n order(column: string, opts?: { ascending?: boolean }): QueryLike;\n range(from: number, to: number): QueryLike;\n maybeSingle(): PromiseLike<Resp<any>>;\n single(): PromiseLike<Resp<any>>;\n}\n/** The slice of a supabase-js table builder (`client.schema(s).from(t)`) the doors use. */\nexport interface TableLike {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n insert(values: any): QueryLike;\n update(values: any): QueryLike;\n delete(): QueryLike;\n}\n/** The slice of a supabase-js client the doors use (a real `SupabaseClient` is assignable — doors.types.test.ts). */\nexport interface SupabaseLike {\n schema(name: string): {\n /**\n * A table builder (read as `TableLike` inside the doors). Typed `unknown` on purpose: matching\n * supabase-js's deep builder generics structurally costs TS2589 in every consumer.\n */\n from(table: string): unknown;\n rpc(fn: string, args?: Record<string, unknown>, opts?: { count?: \"exact\" | \"planned\" | \"estimated\" }): PromiseLike<Resp<any>>;\n };\n auth?: {\n getSession(): Promise<{ data: { session: { access_token: string; user?: { id: string } | null } | null } }>;\n onAuthStateChange(cb: (event: string, session: unknown) => void): { data: { subscription: { unsubscribe(): void } } };\n };\n}\n\n/** The credential shape `@ai-matrx/api`'s `CredentialsPort` accepts (structural; data owns the session). */\nexport interface UserCredentialsPort {\n get(): Promise<{ kind: \"user\"; accessToken: string } | null>;\n onChange?(cb: () => void): () => void;\n}\n\nexport interface Db {\n readonly client: SupabaseLike;\n readonly role: \"user\" | \"service\";\n /** The active organization — where NEW entity rows are saved. Never a read filter. */\n activeOrganization(): string | null;\n /** The session as `@ai-matrx/api`'s `user` credential (one sign-in feeds both lanes). */\n readonly credentials: UserCredentialsPort;\n}\n\nexport interface CreateDbOptions {\n /** The app's existing Supabase client (browser, RSC, route or the host's chat client). */\n client: SupabaseLike;\n /** Where new entity rows are saved. Read lazily on every insert; never consulted by a read. */\n organization?: () => string | null | undefined;\n}\n\nfunction credentialsOf(client: SupabaseLike): UserCredentialsPort {\n return {\n async get() {\n if (!client.auth) return null;\n const { data } = await client.auth.getSession();\n return data.session ? { kind: \"user\", accessToken: data.session.access_token } : null;\n },\n onChange(cb) {\n if (!client.auth) return () => {};\n const { data } = client.auth.onAuthStateChange(() => cb());\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\n/** The one database connection for a signed-in (or anonymous) person. */\nexport function createDb(options: CreateDbOptions): Db {\n const { client, organization } = options;\n return {\n client,\n role: \"user\",\n activeOrganization: () => organization?.() ?? null,\n credentials: credentialsOf(client),\n };\n}\n\n/**\n * Service-role doors — servers only. No session, no binding: every entity insert\n * names its organization. Throws in a browser bundle.\n */\nexport function createServiceDb(options: { client: SupabaseLike }): Db {\n if (typeof (globalThis as { window?: unknown }).window !== \"undefined\") {\n throw new DoorRefusedError(\"createServiceDb\", \"the service role never runs in a browser\");\n }\n return { client: options.client, role: \"service\", activeOrganization: () => null, credentials: credentialsOf(options.client) };\n}\n\n// ─── refusals ───────────────────────────────────────────────────────────────\n\nexport class DoorRefusedError extends Error {\n constructor(\n readonly door: string,\n readonly reason: string,\n ) {\n super(`${door}: ${reason}`);\n this.name = \"DoorRefusedError\";\n }\n}\n\nexport class DatabaseError extends Error {\n constructor(\n readonly door: string,\n readonly cause: PostgrestError,\n ) {\n super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : \"\"}`);\n this.name = \"DatabaseError\";\n }\n}\n\ntype Op = \"read\" | \"insert\" | \"update\" | \"remove\" | \"archive\";\nconst CLIENT_DOORS: Record<TableType, readonly Op[]> = {\n entity: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n detail: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n ledger: [\"read\", \"insert\"],\n reference: [\"read\"],\n system: [\"read\"],\n restricted: [],\n deprecated: [],\n};\n\nconst qualified = (t: { schema: string; name: string }) => `${t.schema}.${t.name}`;\n\n/** Refuse before the wire when the descriptor's rules do not open this door. */\nexport function assertDoor(db: Db, table: TableDescriptor<any, any, any>, op: Op): void {\n const door = `${op} ${qualified(table)}`;\n if (table.type === \"deprecated\") throw new DoorRefusedError(door, \"the table is deprecated — no door opens on it\");\n if (table.kind !== \"table\" && op !== \"read\") throw new DoorRefusedError(door, `a ${table.kind.replace(\"_\", \" \")} is read-only`);\n if (db.role === \"service\") return;\n if (table.type && !CLIENT_DOORS[table.type].includes(op))\n throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(\", \") || \"service\"} doors to a client`);\n if (op !== \"read\" && table.readOnly) throw new DoorRefusedError(door, \"the table is read-only to clients (client_read_only)\");\n if (op === \"remove\" && table.deletesRefused) throw new DoorRefusedError(door, \"clients may not delete from this table (client_deletes_refused)\");\n}\n\nfunction check<T>(door: string, res: Resp<T>): T {\n if (res.error) throw new DatabaseError(door, res.error);\n return res.data as T;\n}\n\nconst from = (db: Db, t: TableDescriptor<any, any, any>) => db.client.schema(t.schema).from(t.name) as TableLike;\n\nfunction byPk(q: QueryLike, t: TableDescriptor<any, any, any>, id: unknown): QueryLike {\n if (t.pk.length === 1) return q.eq(t.pk[0] as string, id);\n const key = id as Record<string, unknown>;\n return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);\n}\n\n// ─── reads (never the active organization; soft-deleted rows excluded unless asked) ─\n\ntype Col<R> = Extract<keyof R, string>;\n\n/** The typed filter a `Where` callback receives — column names and values checked against the row. */\nexport interface Filter<R> {\n eq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n neq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n like(column: Col<R>, pattern: string): Filter<R>;\n ilike(column: Col<R>, pattern: string): Filter<R>;\n in<K extends Col<R>>(column: K, values: readonly NonNullable<R[K]>[]): Filter<R>;\n is(column: Col<R>, value: null | boolean): Filter<R>;\n contains(column: Col<R>, value: unknown): Filter<R>;\n overlaps(column: Col<R>, value: unknown): Filter<R>;\n textSearch(column: Col<R>, query: string, opts?: { type?: \"plain\" | \"phrase\" | \"websearch\"; config?: string }): Filter<R>;\n /** PostgREST's raw `or` grammar (`\"pinned.eq.true,starred.eq.true\"`). */\n or(filters: string, opts?: { referencedTable?: string }): Filter<R>;\n not(column: Col<R>, operator: string, value: unknown): Filter<R>;\n}\n\n/** A filter callback — the same builder `readAllRows` callers write today, typed to the row. */\nexport type Where<R = any> = (q: Filter<R>) => Filter<R>;\n\nexport interface ReadOptions<C> {\n /** Only these columns (the result is typed to them). Default every column. */\n columns?: readonly C[];\n /** Include soft-deleted rows (default: excluded on tables with `deleted_at`). */\n includeDeleted?: boolean;\n}\n\nconst selectOf = (columns?: readonly string[]) => (columns && columns.length ? columns.join(\",\") : \"*\");\n\nfunction live(q: any, table: TableDescriptor<any, any, any>, includeDeleted?: boolean): any {\n return table.softDelete && !includeDeleted ? q.is(\"deleted_at\", null) : q;\n}\n\nfunction ordered(q: any, table: TableDescriptor<any, any, any>, orderBy?: string, ascending = true): any {\n // The FULL primary key, so paging is stable on composite keys.\n const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : [\"id\"];\n return cols.reduce((acc: any, c) => acc.order(c, { ascending }), q);\n}\n\n/** One row by primary key (a composite key takes `{ col: value }`), or null. */\nexport async function read<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: ReadOptions<C> = {},\n): Promise<Pick<R, C> | null> {\n assertDoor(db, table, \"read\");\n const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);\n return check(`read ${qualified(table)}`, await q.maybeSingle());\n}\n\n/** EVERY matching row, paged past the 1,000-row cap (ordered by the full key); throws on a short read. */\nexport async function listAll<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n where?: Where<R>,\n opts: ReadOptions<C> & { orderBy?: Col<R>; pageSize?: number; maxRows?: number } = {},\n): Promise<Pick<R, C>[]> {\n assertDoor(db, table, \"read\");\n return readAllRows<Pick<R, C>>(\n ({ from: lo, to: hi }) => {\n let q: any = live(from(db, table).select(selectOf(opts.columns), { count: \"exact\" }), table, opts.includeDeleted);\n if (where) q = where(q);\n // The database's own error (code, details) reaches the caller as a DatabaseError,\n // exactly like every other door — never a re-worded readAllRows message.\n return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);\n return res;\n });\n },\n {\n label: `listAll ${qualified(table)}`,\n ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}),\n ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}),\n },\n );\n}\n\n/** One explicit window (`from`..`to`, inclusive) plus the exact total. */\nexport async function page<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n window: ReadOptions<C> & { from: number; to: number; where?: Where<R>; orderBy?: Col<R>; ascending?: boolean },\n): Promise<{ rows: Pick<R, C>[]; total: number | null }> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(selectOf(window.columns), { count: \"exact\" }), table, window.includeDeleted);\n if (window.where) q = window.where(q);\n const res = await ordered(q, table, window.orderBy, window.ascending ?? true).range(window.from, window.to);\n return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };\n}\n\n/** The exact count of matching rows (no rows transferred). */\nexport async function count<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, where?: Where<R>, opts: { includeDeleted?: boolean } = {}): Promise<number> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(\"*\", { count: \"exact\", head: true }), table, opts.includeDeleted);\n if (where) q = where(q);\n const res = await q;\n check(`count ${qualified(table)}`, res);\n return res.count ?? 0;\n}\n\n// ─── writes ─────────────────────────────────────────────────────────────────\n\nfunction withOrganization(db: Db, table: TableDescriptor<any, any, any>, values: Record<string, unknown>): Record<string, unknown> {\n const door = `insert ${qualified(table)}`;\n const org = table.org;\n if (org.rule === \"parent\" && values[org.via] != null) {\n // A child row: its organization is its parent's. Never the active organization —\n // `inherit_org_from_parent` fills organization_id; an explicit one is checked by\n // `refuse_org_foreign_to_parent`. So adding to a record in another organization works.\n return values;\n }\n if (org.rule === \"binding\" || org.rule === \"parent\") {\n // An entity row, or a row whose optional parent is absent (a note outside any folder),\n // is saved in the named organization, else the active one.\n if (values.organization_id != null) return values;\n const active = db.activeOrganization();\n if (!active) {\n throw new DoorRefusedError(\n door,\n db.role === \"service\" ? \"a service insert names its organization_id\" : \"no organization: pass organization_id or bind createDb({ organization })\",\n );\n }\n return { ...values, organization_id: active };\n }\n return values;\n}\n\n/** Insert one row (or many) and return what the database stored. */\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: I): Promise<R>;\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: readonly I[]): Promise<R[]>;\nexport async function insert(db: Db, table: TableDescriptor<any, any, any>, values: unknown): Promise<unknown> {\n assertDoor(db, table, \"insert\");\n const many = Array.isArray(values);\n let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v as Record<string, unknown>));\n if (table.createdBy && db.role === \"user\" && rows.some((r) => r.created_by == null) && db.client.auth) {\n // Stamp the signed-in person (no session: sent as given, and row security answers).\n const { data } = await db.client.auth.getSession();\n const person = data.session?.user?.id;\n if (person) rows = rows.map((r) => (r.created_by == null ? { ...r, created_by: person } : r));\n }\n const res = await from(db, table).insert(rows).select(\"*\");\n const stored = check(`insert ${qualified(table)}`, res) as unknown[];\n return many ? stored : stored[0];\n}\n\n/** Every write on a versioned table is explicit: `{ expectedVersion }` (guarded) or `{ overwrite: true }`. */\nexport type UpdateGuard = { expectedVersion: number } | { overwrite: true };\n\nexport type WriteResult<R> =\n | { status: \"saved\"; row: R; rebasedFrom?: { expectedVersion: number; currentVersion: number } }\n | { status: \"conflict\"; currentRow: R; currentVersion: number }\n | { status: \"not_found\" };\n\n/** The one guarded write every update-shaped door goes through. */\nasync function writeRow<R>(db: Db, table: TableDescriptor<any, any, any>, id: unknown, values: Record<string, unknown>, door: string, guard?: UpdateGuard): Promise<WriteResult<R>> {\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) {\n throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned — nothing would guard it; pass no guard\");\n }\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard && guard.overwrite)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n if (expected !== undefined) {\n return (await guardedUpdate<any>({\n expectedVersion: expected,\n applyUpdate: ({ expectedVersion, nextVersion }) =>\n byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq(\"version\", expectedVersion).select(\"*\").maybeSingle(),\n fetchCurrent: () => byPk(from(db, table).select(\"*\"), table, id).maybeSingle(),\n })) as WriteResult<R>;\n }\n const row = check(door, await byPk(from(db, table).update(values), table, id).select(\"*\").maybeSingle());\n return row ? { status: \"saved\", row } : { status: \"not_found\" };\n}\n\n/**\n * Update one row by primary key. On a versioned table the guard is required:\n * `{ expectedVersion }` (a conflict comes back, never a silent overwrite) or\n * `{ overwrite: true }`; on an unversioned table `expectedVersion` is refused.\n */\nexport async function update<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, patch: U, guard?: UpdateGuard): Promise<WriteResult<R>> {\n assertDoor(db, table, \"update\");\n const door = `update ${qualified(table)}`;\n const values = patch as Record<string, unknown>;\n if (db.role === \"user\") {\n const blocked = table.readOnlyColumns.filter((c) => c in values);\n if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(\", \")}`);\n }\n return writeRow<R>(db, table, id, values, door, guard);\n}\n\n/**\n * Remove one row the way the table removes: soft delete (`deleted_at`) where it\n * has one; an archive-only table refuses (use `archive`); a hard delete only where\n * the table has neither. Guarded like `update` on a versioned table.\n */\nexport async function remove<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n guard?: UpdateGuard,\n): Promise<{ status: \"soft_deleted\" | \"deleted\" | \"not_found\" } | { status: \"conflict\"; currentVersion: number }> {\n assertDoor(db, table, \"remove\");\n const door = `remove ${qualified(table)}`;\n if (table.softDelete) {\n const res = await writeRow<R>(db, table, id, { deleted_at: new Date().toISOString() }, door, guard);\n return res.status === \"saved\" ? { status: \"soft_deleted\" } : res.status === \"conflict\" ? { status: \"conflict\", currentVersion: res.currentVersion } : res;\n }\n if (table.archive) throw new DoorRefusedError(door, \"this table archives instead of deleting — use archive()\");\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned\");\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n let q = byPk(from(db, table).delete(), table, id);\n if (expected !== undefined) q = q.eq(\"version\", expected);\n const rows = check(door, await q.select(\"*\")) as unknown[] | null;\n if (rows && rows.length) return { status: \"deleted\" };\n if (expected !== undefined) {\n const current = check(door, await byPk(from(db, table).select(\"version\"), table, id).maybeSingle()) as { version: number } | null;\n if (current) return { status: \"conflict\", currentVersion: current.version };\n }\n return { status: \"not_found\" };\n}\n\n/** Archive (or, with `restore`, bring back) one row by the table's archive column. Guarded like `update`. */\nexport async function archive<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: { restore?: boolean; guard?: UpdateGuard } = {},\n): Promise<WriteResult<R>> {\n assertDoor(db, table, \"archive\");\n const door = `${opts.restore ? \"restore\" : \"archive\"} ${qualified(table)}`;\n let values: Record<string, unknown>;\n if (table.archive === \"archived_at\") values = { archived_at: opts.restore ? null : new Date().toISOString() };\n else if (table.archive === \"is_archived\") values = { is_archived: !opts.restore };\n else if (table.softDelete && opts.restore) values = { deleted_at: null };\n else throw new DoorRefusedError(door, \"the table has no archive column\");\n return writeRow<R>(db, table, id, values, door, opts.guard);\n}\n\n/** Bring back an archived or soft-deleted row. */\nexport const restore = <R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, guard?: UpdateGuard) =>\n archive(db, table, id, guard ? { restore: true, guard } : { restore: true });\n\n// ─── functions ──────────────────────────────────────────────────────────────\n\n/** Call a database function through its generated door (PostgREST chooses an overload by argument names). */\nexport async function call<F extends FunctionDescriptor<any, any>>(db: Db, fn: F, args: ArgsOf<F>): Promise<ResultOf<F>> {\n const door = `call ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n return check(door, await db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>));\n}\n\n/** One ordering column of a set-returning function's rows (PostgREST `order`). */\nexport type CallOrder = string | { column: string; ascending?: boolean; nullsFirst?: boolean };\n\n/**\n * EVERY row a set-returning function answers, paged past the 1,000-row cap by\n * `range` over a stable `order` (name enough columns to make it total — end on a key);\n * throws a `DatabaseError` with the database's own error, or `IncompleteReadError` on a short read.\n */\nexport async function callAll<F extends FunctionDescriptor<any, any>>(\n db: Db,\n fn: F,\n args: ArgsOf<F>,\n opts: { order: readonly CallOrder[]; pageSize?: number; maxRows?: number },\n): Promise<ResultOf<F> extends readonly (infer Row)[] ? Row[] : ResultOf<F>[]> {\n const door = `callAll ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n if (!opts.order.length) throw new DoorRefusedError(door, \"a paged read needs an order (end on a key) — pages over no order can skip or repeat rows\");\n return readAllRows<any>(\n ({ from: lo, to: hi }) => {\n let q: any = db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>, { count: \"exact\" });\n for (const o of opts.order) {\n q = typeof o === \"string\" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...(o.nullsFirst !== undefined ? { nullsFirst: o.nullsFirst } : {}) });\n }\n return Promise.resolve(q.range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(door, res.error);\n return res;\n });\n },\n { label: door, ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}), ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}) },\n ) as never;\n}\n\n// ─── a table chosen at runtime ──────────────────────────────────────────────\n\n/** One schema's flags (`<schema>.flags.ts` from `matrx-data generate --flags-index`): \"schema.table\" → descriptor. */\nexport type FlagsIndex = Readonly<Record<string, TableDescriptor>>;\n/** Per-schema loaders (`flags-index.ts`): only the schema a runtime name names is loaded (pay-only). */\nexport type FlagLoaders = Readonly<Record<string, () => Promise<FlagsIndex>>>;\n\n/** The door for a table whose name arrives at runtime — loosely typed, the same rules. */\nexport function tableByName(index: FlagsIndex, name: string): TableDescriptor {\n const t = index[name];\n if (!t) throw new DoorRefusedError(`tableByName ${name}`, \"no such table in the database description (schema-qualify it: schema.table)\");\n return t;\n}\n\n/** `tableByName` over per-schema loaders: loads only the named schema's flags. */\nexport async function loadTableByName(loaders: FlagLoaders, name: string): Promise<TableDescriptor> {\n const schema = name.slice(0, name.indexOf(\".\"));\n const load = schema ? loaders[schema] : undefined;\n if (!load) throw new DoorRefusedError(`tableByName ${name}`, \"no such schema in the database description (schema-qualify it: schema.table)\");\n return tableByName(await load(), name);\n}\n\n// ─── realtime ───────────────────────────────────────────────────────────────\n\n/** The `postgres_changes` filter for a table — only published tables have one (an unpublished table delivers nothing). */\nexport function changesOf(table: TableDescriptor<any, any, any>): { schema: string; table: string } {\n if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, \"the table is not in the supabase_realtime publication\");\n return { schema: table.schema, table: table.name };\n}\n","/**\n * GENERATED by `matrx-data generate` from the database description — never edit by hand.\n * schema: public · description eb4e70fa23e6\n * Regenerate: matrx-data generate (see @ai-matrx/data README).\n */\n/* eslint-disable */\nimport type { FunctionDescriptor, TableDescriptor } from \"@ai-matrx/data/db\";\n\ntype Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];\n\nexport type ResolveShortLinkArgs = { p_token: string };\nexport type ResolveShortLinkResult = Json;\nexport const resolveShortLink = {\"schema\":\"public\",\"name\":\"resolve_short_link\",\"roles\":[\"anon\",\"authenticated\",\"service_role\"],\"registered\":true} as unknown as FunctionDescriptor<ResolveShortLinkArgs, ResolveShortLinkResult>;\n\nexport type ShortenAppUrlArgs = { p_path: string; p_organization_id: string; p_expires_at?: string };\nexport type ShortenAppUrlResult = Json;\nexport const shortenAppUrl = {\"schema\":\"public\",\"name\":\"shorten_app_url\",\"roles\":[\"authenticated\",\"service_role\"],\"registered\":true} as unknown as FunctionDescriptor<ShortenAppUrlArgs, ShortenAppUrlResult>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,wBASO;;;ACoIP,SAAS,cAAc,QAA2C;AAChE,SAAO;AAAA,IACL,MAAM,MAAM;AACV,UAAI,CAAC,OAAO,KAAM,QAAO;AACzB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK,WAAW;AAC9C,aAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,aAAa,KAAK,QAAQ,aAAa,IAAI;AAAA,IACnF;AAAA,IACA,SAAS,IAAI;AACX,UAAI,CAAC,OAAO,KAAM,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC;AACzD,aAAO,MAAM,KAAK,aAAa,YAAY;AAAA,IAC7C;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAA8B;AACrD,QAAM,EAAE,QAAQ,aAAa,IAAI;AACjC,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,oBAAoB,MAAM,eAAe,KAAK;AAAA,IAC9C,aAAa,cAAc,MAAM;AAAA,EACnC;AACF;AAeO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACW,MACA,QACT;AACA,UAAM,GAAG,IAAI,KAAK,MAAM,EAAE;AAHjB;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;AAEO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YACW,MACA,OACT;AACA,UAAM,GAAG,IAAI,KAAK,MAAM,OAAO,GAAG,MAAM,OAAO,KAAK,MAAM,IAAI,MAAM,EAAE,EAAE;AAH/D;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;AA2BA,SAAS,MAAS,MAAc,KAAiB;AAC/C,MAAI,IAAI,MAAO,OAAM,IAAI,cAAc,MAAM,IAAI,KAAK;AACtD,SAAO,IAAI;AACb;AA0QA,eAAsB,KAA6C,IAAQ,IAAO,MAAuC;AACvH,QAAM,OAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,IAAI;AACzC,QAAM,OAAO,GAAG,SAAS,YAAY,iBAAiB;AACtD,MAAI,CAAC,GAAG,MAAM,SAAS,IAAI,KAAK,EAAE,SAAS,mBAAmB,GAAG,MAAM,SAAS,MAAM,IAAI;AACxF,UAAM,IAAI,iBAAiB,MAAM,qBAAqB,IAAI,oBAAoB,GAAG,MAAM,KAAK,IAAI,KAAK,QAAQ,GAAG;AAAA,EAClH;AACA,SAAO,MAAM,MAAM,MAAM,GAAG,OAAO,OAAO,GAAG,MAAM,EAAE,IAAI,GAAG,MAAM,IAA+B,CAAC;AACpG;;;ACzfO,IAAM,mBAAmB,EAAC,UAAS,UAAS,QAAO,sBAAqB,SAAQ,CAAC,QAAO,iBAAgB,cAAc,GAAE,cAAa,KAAI;AAIzI,IAAM,gBAAgB,EAAC,UAAS,UAAS,QAAO,mBAAkB,SAAQ,CAAC,iBAAgB,cAAc,GAAE,cAAa,KAAI;;;AFgBnI,IAAM,MAAM,oBAAI,QAAoB;AACpC,SAAS,KAAK,QAA0B;AACtC,MAAI,KAAK,IAAI,IAAI,MAAM;AACvB,MAAI,CAAC,IAAI;AACP,SAAK,SAAS,EAAE,OAAO,CAAC;AACxB,QAAI,IAAI,QAAQ,EAAE;AAAA,EACpB;AACA,SAAO;AACT;AAGA,SAAS,UAAU,GAAoB;AACrC,QAAM,MAAM;AACZ,MAAI,KAAK,SAAS,mBAAmB,IAAI,OAAO,QAAS,QAAO,IAAI,MAAM;AAC1E,SAAO,KAAK,WAAW,OAAO,CAAC;AACjC;AAGA,eAAsB,cACpB,QACA,SAC8B;AAC9B,QAAM,YAAQ,wCAAqB,OAAO;AAC1C,MAAI,CAAC,MAAM,GAAI,QAAO;AACtB,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,KAAK,KAAK,MAAM,GAAG,eAAe;AAAA,MAC7C,QAAQ,QAAQ;AAAA,MAChB,mBAAmB,QAAQ;AAAA,MAC3B,GAAI,QAAQ,YAAY,EAAE,cAAc,QAAQ,UAAU,IAAI,CAAC;AAAA,IACjE,CAAC;AAAA,EACH,SAAS,GAAG;AACV,WAAO,EAAE,IAAI,OAAO,OAAO,UAAU,CAAC,EAAE;AAAA,EAC1C;AACA,aAAO,2CAAwB,MAAM,MAAM,MAAM;AACnD;AAGO,SAAS,gBAAgB,QAAuC;AACrE,SAAO,CAAC,YAAY,cAAc,QAAQ,OAAO;AACnD;AASA,eAAsB,qBACpB,QACA,OACiC;AACjC,QAAM,iBAAa,2CAAwB,KAAK;AAChD,MAAI,CAAC,WAAY,QAAO,EAAE,IAAI,MAAM;AACpC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,KAAK,KAAK,MAAM,GAAG,kBAAkB,EAAE,SAAS,WAAW,CAAC;AAAA,EAC3E,SAAS,GAAG;AACV,WAAO,EAAE,IAAI,OAAO,gBAAgB,UAAU,CAAC,EAAE;AAAA,EACnD;AACA,aAAO,8CAA2B,IAAI;AACxC;","names":[]}
|