@ai-matrx/data 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +34 -0
  3. package/bin/emit.mjs +32 -7
  4. package/bin/generate.mjs +38 -8
  5. package/bin/matrx-data.mjs +6 -4
  6. package/description/admin.json.gz +0 -0
  7. package/description/agent.json.gz +0 -0
  8. package/description/ai.json.gz +0 -0
  9. package/description/api.json.gz +0 -0
  10. package/description/app.json.gz +0 -0
  11. package/description/assignment.json.gz +0 -0
  12. package/description/audit.json.gz +0 -0
  13. package/description/batch.json.gz +0 -0
  14. package/description/billing.json.gz +0 -0
  15. package/description/browser.json.gz +0 -0
  16. package/description/canvas.json.gz +0 -0
  17. package/description/chat.json.gz +0 -0
  18. package/description/code.json.gz +0 -0
  19. package/description/commerce.json.gz +0 -0
  20. package/description/communication.json.gz +0 -0
  21. package/description/content.json.gz +0 -0
  22. package/description/content_ir.json.gz +0 -0
  23. package/description/context.json.gz +0 -0
  24. package/description/crm.json.gz +0 -0
  25. package/description/custom.json.gz +0 -0
  26. package/description/dictionary.json.gz +0 -0
  27. package/description/docproc.json.gz +0 -0
  28. package/description/education.json.gz +0 -0
  29. package/description/esign.json.gz +0 -0
  30. package/description/extend.json.gz +0 -0
  31. package/description/files.json.gz +0 -0
  32. package/description/growth.json.gz +0 -0
  33. package/description/hindsight.json.gz +0 -0
  34. package/description/history.json.gz +0 -0
  35. package/description/iam.json.gz +0 -0
  36. package/description/index.json.gz +0 -0
  37. package/description/interview.json.gz +0 -0
  38. package/description/knowledge.json.gz +0 -0
  39. package/description/legal.json.gz +0 -0
  40. package/description/mandate.json.gz +0 -0
  41. package/description/marketing.json.gz +0 -0
  42. package/description/media.json.gz +0 -0
  43. package/description/meta.json.gz +0 -0
  44. package/description/ops.json.gz +0 -0
  45. package/description/pdf.json.gz +0 -0
  46. package/description/plan.json.gz +0 -0
  47. package/description/platform.json.gz +0 -0
  48. package/description/podcast.json.gz +0 -0
  49. package/description/projects.json.gz +0 -0
  50. package/description/public.json.gz +0 -0
  51. package/description/rag.json.gz +0 -0
  52. package/description/research.json.gz +0 -0
  53. package/description/runtime.json.gz +0 -0
  54. package/description/scheduler.json.gz +0 -0
  55. package/description/scraper.json.gz +0 -0
  56. package/description/seo.json.gz +0 -0
  57. package/description/skill.json.gz +0 -0
  58. package/description/social.json.gz +0 -0
  59. package/description/tool.json.gz +0 -0
  60. package/description/transcripts.json.gz +0 -0
  61. package/description/ui.json.gz +0 -0
  62. package/description/users.json.gz +0 -0
  63. package/description/web.json.gz +0 -0
  64. package/description/work.json.gz +0 -0
  65. package/description/workbench.json.gz +0 -0
  66. package/description/workflow.json.gz +0 -0
  67. package/dist/db.cjs +71 -29
  68. package/dist/db.cjs.map +1 -1
  69. package/dist/db.d.cts +92 -33
  70. package/dist/db.d.ts +92 -33
  71. package/dist/db.js +71 -29
  72. package/dist/db.js.map +1 -1
  73. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0 — created_by stamped; listAll keeps the database's error
4
+
5
+ - **A signed-in `insert` stamps `created_by`** with the session's person when the table has a
6
+ `created_by` column and the row omits it (row security on `workbench.notes` / `note_folders` and
7
+ similar tables refuses an insert whose `created_by` is not `auth.uid()`). An explicit value is kept;
8
+ the service role never stamps. Descriptors carry `createdBy: boolean` (generated from the columns).
9
+ - **`listAll` throws a `DatabaseError`** carrying the database's own error (message, code, details) —
10
+ it used to surface as a re-worded `readAllRows(...)` `Error` without the code.
11
+
12
+ Consumer action: run `matrx-data generate` again (`pnpm db:generate`) so descriptors carry `createdBy`;
13
+ code that matched `listAll` failures by the `readAllRows(` message should check `instanceof DatabaseError`.
14
+
15
+ ## 0.24.0 — doors hardened (independent review)
16
+
17
+ - **Every write on a versioned table is guarded.** `remove` (and `archive` / `restore`) take the same
18
+ `{ expectedVersion }` / `{ overwrite: true }` guard as `update`; `expectedVersion` on an UNversioned
19
+ table is refused (it used to be ignored silently). `remove` returns `{ status }` (or a conflict).
20
+ - **Reads exclude soft-deleted rows** by default (`read`, `listAll`, `page`, `count`); `includeDeleted: true` opts in.
21
+ - **`listAll` / `page` order by the full primary key** (stable paging on composite keys).
22
+ - **Types:** ops infer `TableDescriptor<Row, Insert, Update>` directly, so an insert error names the
23
+ table's Insert type; `read`/`listAll`/`page` take `columns: [...]` and type the result to them; the
24
+ `Where` callback receives a typed `Filter<Row>` (column names and value types checked).
25
+ - **Pay-only runtime tables:** `--flags-index` writes `<schema>.flags.ts` per schema plus
26
+ `flags-index.ts` loaders; `loadTableByName(flagLoaders, "schema.table")` loads only that schema.
27
+ - **The description ships in the package** (`description/*.json.gz`, 0.5 MB): `matrx-data generate`
28
+ needs no repository or database access (`--from` still takes a fresher one). README documents it.
29
+ - `matrx-data emit` accounts for every callable-door register row: `index.json` →
30
+ `report.registeredDoorsOutsideDescription` by reason, and the emit fails if any row is unexplained.
31
+
32
+ Consumer action: `remove(db, t, id)` on a versioned table now needs a guard and returns `{ status }`;
33
+ `read(db, t, id, "cols")` becomes `read(db, t, id, { columns: [...] })`. No consumer used 0.23.0's doors yet.
34
+
3
35
  ## 0.23.0 — the doors: one way to talk to the database
4
36
 
5
37
  - **`matrx-data` CLI** (`bin`): `matrx-data emit [--out db-contract] [--check]` reads the LIVE database
package/README.md CHANGED
@@ -32,6 +32,40 @@ npm install @ai-matrx/data
32
32
  | `createActiveOrgCookie` | ONE browser memory of the organization a person is acting in, shared by every Matrx surface on the apex (`matrx-active-org`, `Domain=.aimatrx.com`): identity-keyed (`<userId>:<orgId>`), domain-wide only on a listed apex, host-only elsewhere. Stores the stored-selection rung of the canonical order; never decides. |
33
33
  | `uniqueChannelTopic` | Realtime channel hygiene: unique per-mount topics so React 19 double-invoked effects and Fast Refresh can never collide with a still-joined channel. |
34
34
 
35
+ ## The doors — one way to talk to the database
36
+
37
+ Every app talks to the database through doors over descriptors it **generates locally** from the
38
+ ONE database description. The description ships inside this package (`description/`, gzipped, the one
39
+ this version was built with), so no repository or database access is needed.
40
+
41
+ ```bash
42
+ # in your app (package.json: "db:generate": "matrx-data generate --out src/db/generated --schemas workbench,chat")
43
+ npx matrx-data generate --out src/db/generated --schemas workbench # one file per schema
44
+ npx matrx-data generate --out src/db/generated --schemas workbench --check # CI: fail when stale
45
+ npx matrx-data generate --out src/db/generated --flags-index # + per-schema flags for tableByName
46
+ # fresher than this package version? With the direct-DB variables (SUPABASE_MATRIX_*):
47
+ npx matrx-data emit --out db-contract && npx matrx-data generate --from db-contract --out src/db/generated
48
+ ```
49
+
50
+ ```ts
51
+ import { createDb, listAll, read, insert, update, remove, call } from "@ai-matrx/data/db";
52
+ import { noteFolders, notes, noteFolderGetOrCreate } from "./db/generated/workbench";
53
+
54
+ const db = createDb({ client: supabase, organization: () => activeOrgId }); // wraps YOUR client
55
+ const pinned = await listAll(db, notes, (q) => q.eq("is_pinned", true)); // complete; typed filter; live rows
56
+ const row = await read(db, notes, id, { columns: ["id", "label"] }); // typed to those columns
57
+ await insert(db, notes, { folder_id, label }); // child: the folder's organization, never the active one
58
+ await update(db, notes, id, { label }, { expectedVersion: row.version }); // or { overwrite: true }
59
+ await remove(db, notes, id, { expectedVersion: v }); // soft delete; archive-only tables refuse
60
+ await call(db, noteFolderGetOrCreate, { p_name: "Clients" });
61
+ // @ai-matrx/api: pass db.credentials as its `credentials` option — one sign-in feeds both lanes
62
+ ```
63
+
64
+ Rules come from the description (the live table registry), never the call site: organization by the
65
+ access ladder; reads never filter by the active organization and exclude soft-deleted rows unless
66
+ `includeDeleted`; `listAll` is complete and ordered by the full primary key; every write on a versioned
67
+ table is guarded; the door set follows the registry type; `createServiceDb` is server-only.
68
+
35
69
  ## `@ai-matrx/data/net`
36
70
 
37
71
  | Export | What it does |
package/bin/emit.mjs CHANGED
@@ -137,12 +137,32 @@ function collect(schemas) {
137
137
  and (${ROLES.map((r) => `has_function_privilege('${r}', p.oid, 'EXECUTE')`).join(" or ")})
138
138
  ) f`);
139
139
 
140
+ // Every register row, matched to its live function (schema-qualified type names normalized, so
141
+ // "p_row block_states" matches "p_row platform.block_states"), and when it is not in the
142
+ // description, WHY — so no register row is ever unaccounted for.
140
143
  const doors = readJson(`
141
144
  select coalesce(json_agg(json_build_object(
142
- 'schema', d.schema_name, 'name', d.function_name, 'identity', d.identity_args,
145
+ 'schema', d.schema_name, 'name', d.function_name, 'identity', coalesce(p.ident, d.identity_args),
143
146
  'gatePredicate', d.gate_predicate, 'anonymousCallers', d.anonymous_callers, 'signedInCallers', d.signed_in_callers,
144
- 'nonClientLane', d.non_client_lane, 'argumentRules', d.argument_rules, 'refusalOnly', d.refusal_only
145
- )), '[]') from platform.client_callable_door d`);
147
+ 'nonClientLane', d.non_client_lane, 'argumentRules', d.argument_rules, 'refusalOnly', d.refusal_only,
148
+ 'outside', case
149
+ when p.oid is null and not exists (select 1 from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
150
+ where n2.nspname = d.schema_name and p2.proname = d.function_name) then 'function no longer exists'
151
+ when p.oid is null then 'no live overload has the registered signature'
152
+ when not (d.schema_name = any(${S})) then 'schema not exposed to PostgREST (server-only)'
153
+ when not (${ROLES.map((r) => `has_function_privilege('${r}', p.oid, 'EXECUTE')`).join(" or ")}) then 'no client role may execute it'
154
+ when p.prorettype in ('trigger'::regtype, 'event_trigger'::regtype) then 'trigger function'
155
+ when exists (select 1 from pg_depend dd where dd.classid = 'pg_proc'::regclass and dd.objid = p.oid and dd.deptype = 'e') then 'extension-owned'
156
+ end
157
+ )), '[]')
158
+ from platform.client_callable_door d
159
+ left join lateral (
160
+ select p.oid, p.prorettype, pg_get_function_identity_arguments(p.oid) as ident
161
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
162
+ where n.nspname = d.schema_name and p.proname = d.function_name
163
+ and regexp_replace(pg_get_function_identity_arguments(p.oid), '\\m[a-z_][a-z0-9_]*\\.', '', 'g')
164
+ = regexp_replace(d.identity_args, '\\m[a-z_][a-z0-9_]*\\.', '', 'g')
165
+ limit 1) p on true`);
146
166
 
147
167
  const enums = readJson(`
148
168
  select coalesce(json_object_agg(n.nspname || '.' || t.typname,
@@ -262,7 +282,7 @@ export function buildDescription() {
262
282
  const doorsByKey = new Map(doors.map((d) => [doorKey(d.schema, d.name, d.identity), d]));
263
283
 
264
284
  const files = {};
265
- const index = { format: FORMAT, schemas: {}, publications: {}, report: { tablesWithoutRegistryRow: [], untypedRegistryRows: [], registeredDoorsOutsideDescription: 0 } };
285
+ const index = { format: FORMAT, schemas: {}, publications: {}, report: { tablesWithoutRegistryRow: [], untypedRegistryRows: [], registeredDoorsOutsideDescription: {} } };
266
286
  const publications = {};
267
287
  for (const { pub, table } of published) {
268
288
  const name = table.replace(/^realtime\.messages_\d{4}_\d{2}_\d{2}$/, "realtime.messages");
@@ -313,9 +333,14 @@ export function buildDescription() {
313
333
  }
314
334
  for (const k of Object.keys(publications)) publications[k].sort();
315
335
  index.publications = publications;
316
- // Register rows whose function is outside the exposed schemas, not executable by any client role,
317
- // or whose identity no longer matches a live signature.
318
- index.report.registeredDoorsOutsideDescription = doors.length - matchedDoors;
336
+ // Every register row not in the description, by reason (each named; none unaccounted for).
337
+ const outside = {};
338
+ for (const d of doors) if (d.outside) (outside[d.outside] ??= []).push(`${d.schema}.${d.name}(${d.identity})`);
339
+ for (const k of Object.keys(outside)) outside[k].sort();
340
+ index.report.registeredDoorsOutsideDescription = outside;
341
+ index.report.registeredDoors = { total: doors.length, inDescription: matchedDoors, outside: doors.length - matchedDoors };
342
+ const unexplained = doors.length - matchedDoors - Object.values(outside).reduce((n, l) => n + l.length, 0);
343
+ if (unexplained !== 0) throw new Error(`${unexplained} register row(s) neither in the description nor explained — the emitter's matching is wrong`);
319
344
  index.report.tablesWithoutRegistryRow.sort();
320
345
  index.report.untypedRegistryRows.sort();
321
346
  index.contractSha256 = sha256(Object.keys(index.schemas).sort().map((s) => `${s}:${index.schemas[s].sha256}`).join("\n"));
package/bin/generate.mjs CHANGED
@@ -17,6 +17,8 @@
17
17
  */
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { gunzipSync } from "node:zlib";
20
22
 
21
23
  const RESERVED = new Set(
22
24
  "break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(
@@ -92,6 +94,7 @@ function tableDescriptor(schema, name, t) {
92
94
  readOnlyColumns: reg?.clientReadOnlyColumns ?? [],
93
95
  listScope: reg?.listScope ?? null,
94
96
  realtime: (t.realtime ?? []).includes("supabase_realtime"),
97
+ createdBy: t.columns.some((c) => c.name === "created_by"),
95
98
  };
96
99
  return flags;
97
100
  }
@@ -157,29 +160,56 @@ export function renderSchema(desc, schemaSha, importFrom) {
157
160
  return out;
158
161
  }
159
162
 
160
- export function renderFlagsIndex(descs, importFrom) {
163
+ /** One schema's flags (`<schema>.flags.ts`) — loaded only when a runtime name names that schema. */
164
+ export function renderSchemaFlags(desc, importFrom) {
161
165
  const index = {};
162
- for (const d of descs) for (const [name, t] of Object.entries({ ...d.tables, ...d.views })) index[`${d.schema}.${name}`] = tableDescriptor(d.schema, name, t);
163
- return `/** GENERATED by \`matrx-data generate --flags-index\` — the flags of every table, for tableByName(). Never edit by hand. */
166
+ for (const [name, t] of Object.entries({ ...desc.tables, ...desc.views })) index[`${desc.schema}.${name}`] = tableDescriptor(desc.schema, name, t);
167
+ return `/** GENERATED by \`matrx-data generate --flags-index\` — ${desc.schema}'s table flags for tableByName(). Never edit by hand. */
164
168
  /* eslint-disable */
165
169
  import type { FlagsIndex } from "${importFrom}";
166
170
 
167
- export const flagsIndex: FlagsIndex = ${JSON.stringify(index)} as unknown as FlagsIndex;
171
+ export const flags: FlagsIndex = ${JSON.stringify(index)} as unknown as FlagsIndex;
168
172
  `;
169
173
  }
170
174
 
171
- export function generate({ from, out, schemas, check, importFrom, flagsIndex = process.argv.includes("--flags-index") }) {
172
- const index = JSON.parse(readFileSync(path.join(from, "index.json"), "utf8"));
175
+ /** Per-schema loaders: `loadTableByName(flagLoaders, "schema.table")` imports only that schema's flags. */
176
+ export function renderFlagLoaders(schemas, importFrom) {
177
+ return `/** GENERATED by \`matrx-data generate --flags-index\` — never edit by hand. */
178
+ /* eslint-disable */
179
+ import type { FlagLoaders } from "${importFrom}";
180
+
181
+ export const flagLoaders: FlagLoaders = {
182
+ ${schemas.map((s) => ` ${key(s)}: () => import("./${s}.flags").then((m) => m.flags),`).join("\n")}
183
+ };
184
+ `;
185
+ }
186
+
187
+ /** The description ships inside @ai-matrx/data (gzipped); `--from` points at a fresher one. */
188
+ export const BUNDLED_DESCRIPTION = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "description");
189
+
190
+ function readDescriptionFile(dir, name) {
191
+ const plain = path.join(dir, name);
192
+ if (existsSync(plain)) return readFileSync(plain, "utf8");
193
+ const gz = `${plain}.gz`;
194
+ if (existsSync(gz)) return gunzipSync(readFileSync(gz)).toString("utf8");
195
+ throw new Error(`${name} not found in ${dir} (neither ${name} nor ${name}.gz)`);
196
+ }
197
+
198
+ export function generate({ from = BUNDLED_DESCRIPTION, out, schemas, check, importFrom, flagsIndex = process.argv.includes("--flags-index") }) {
199
+ const index = JSON.parse(readDescriptionFile(from, "index.json"));
173
200
  const names = schemas?.length ? schemas : Object.keys(index.schemas).sort();
174
201
  const files = {};
175
202
  const descs = [];
176
203
  for (const s of names) {
177
204
  if (!index.schemas[s]) throw new Error(`schema "${s}" is not in the description (${Object.keys(index.schemas).length} schemas)`);
178
- const desc = JSON.parse(readFileSync(path.join(from, `${s}.json`), "utf8"));
205
+ const desc = JSON.parse(readDescriptionFile(from, `${s}.json`));
179
206
  descs.push(desc);
180
207
  files[`${s}.ts`] = renderSchema(desc, index.schemas[s].sha256, importFrom);
181
208
  }
182
- if (flagsIndex) files["flags-index.ts"] = renderFlagsIndex(descs, importFrom);
209
+ if (flagsIndex) {
210
+ for (const d of descs) files[`${d.schema}.flags.ts`] = renderSchemaFlags(d, importFrom);
211
+ files["flags-index.ts"] = renderFlagLoaders(descs.map((d) => d.schema), importFrom);
212
+ }
183
213
  if (check) {
184
214
  const stale = Object.entries(files).filter(([n, t]) => !existsSync(path.join(out, n)) || readFileSync(path.join(out, n), "utf8") !== t).map(([n]) => n);
185
215
  if (stale.length) {
@@ -3,7 +3,9 @@
3
3
  * matrx-data — @ai-matrx/data's build-time CLI.
4
4
  *
5
5
  * matrx-data emit [--out <dir>] [--check] live database → the database description (read-only)
6
- * matrx-data generate --from <dir> --out <dir> [--schemas a,b] description → typed doors in THIS app
6
+ * matrx-data generate --out <dir> [--from <dir>] [--schemas a,b] [--flags-index] [--check]
7
+ * description → typed doors in THIS app (--from defaults to the description
8
+ * shipped inside this package version)
7
9
  *
8
10
  * The package ships the runtime and this CLI; each app generates its own doors
9
11
  * locally (Prisma's model), so a new column never waits on a package release.
@@ -25,12 +27,12 @@ async function main() {
25
27
  const { generate } = await import("./generate.mjs");
26
28
  const from = flag("--from");
27
29
  const out = flag("--out");
28
- if (!from || !out) {
29
- console.error("matrx-data generate: --from <description dir> and --out <dir> are required");
30
+ if (!out) {
31
+ console.error("matrx-data generate: --out <dir> is required (--from defaults to the description shipped in this package)");
30
32
  return 2;
31
33
  }
32
34
  return generate({
33
- from: path.resolve(from),
35
+ ...(from ? { from: path.resolve(from) } : {}),
34
36
  out: path.resolve(out),
35
37
  schemas: flag("--schemas")?.split(",").map((s) => s.trim()).filter(Boolean),
36
38
  check: rest.includes("--check"),
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/db.cjs CHANGED
@@ -53,6 +53,7 @@ __export(db_exports, {
53
53
  isMissingSessionError: () => isMissingSessionError,
54
54
  isRecordUnavailableError: () => isRecordUnavailableError,
55
55
  listAll: () => listAll,
56
+ loadTableByName: () => loadTableByName,
56
57
  mergeJsonColumn: () => mergeJsonColumn,
57
58
  page: () => page,
58
59
  parseCookieHeader: () => parseCookieHeader,
@@ -1054,18 +1055,29 @@ function byPk(q, t, id) {
1054
1055
  const key = id;
1055
1056
  return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);
1056
1057
  }
1057
- async function read(db, table, id, columns = "*") {
1058
+ var selectOf = (columns) => columns && columns.length ? columns.join(",") : "*";
1059
+ function live(q, table, includeDeleted) {
1060
+ return table.softDelete && !includeDeleted ? q.is("deleted_at", null) : q;
1061
+ }
1062
+ function ordered(q, table, orderBy, ascending = true) {
1063
+ const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : ["id"];
1064
+ return cols.reduce((acc, c) => acc.order(c, { ascending }), q);
1065
+ }
1066
+ async function read(db, table, id, opts = {}) {
1058
1067
  assertDoor(db, table, "read");
1059
- return check(`read ${qualified(table)}`, await byPk(from(db, table).select(columns), table, id).maybeSingle());
1068
+ const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);
1069
+ return check(`read ${qualified(table)}`, await q.maybeSingle());
1060
1070
  }
1061
1071
  async function listAll(db, table, where, opts = {}) {
1062
1072
  assertDoor(db, table, "read");
1063
- const order = opts.orderBy ?? table.pk[0] ?? "id";
1064
1073
  return readAllRows(
1065
1074
  ({ from: lo, to: hi }) => {
1066
- let q = from(db, table).select(opts.columns ?? "*", { count: "exact" });
1075
+ let q = live(from(db, table).select(selectOf(opts.columns), { count: "exact" }), table, opts.includeDeleted);
1067
1076
  if (where) q = where(q);
1068
- return q.order(order, { ascending: true }).range(lo, hi);
1077
+ return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res) => {
1078
+ if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);
1079
+ return res;
1080
+ });
1069
1081
  },
1070
1082
  {
1071
1083
  label: `listAll ${qualified(table)}`,
@@ -1076,14 +1088,14 @@ async function listAll(db, table, where, opts = {}) {
1076
1088
  }
1077
1089
  async function page(db, table, window2) {
1078
1090
  assertDoor(db, table, "read");
1079
- let q = from(db, table).select(window2.columns ?? "*", { count: "exact" });
1091
+ let q = live(from(db, table).select(selectOf(window2.columns), { count: "exact" }), table, window2.includeDeleted);
1080
1092
  if (window2.where) q = window2.where(q);
1081
- const res = await q.order(window2.orderBy ?? table.pk[0] ?? "id", { ascending: window2.ascending ?? true }).range(window2.from, window2.to);
1093
+ const res = await ordered(q, table, window2.orderBy, window2.ascending ?? true).range(window2.from, window2.to);
1082
1094
  return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };
1083
1095
  }
1084
- async function count(db, table, where) {
1096
+ async function count(db, table, where, opts = {}) {
1085
1097
  assertDoor(db, table, "read");
1086
- let q = from(db, table).select("*", { count: "exact", head: true });
1098
+ let q = live(from(db, table).select("*", { count: "exact", head: true }), table, opts.includeDeleted);
1087
1099
  if (where) q = where(q);
1088
1100
  const res = await q;
1089
1101
  check(`count ${qualified(table)}`, res);
@@ -1111,11 +1123,34 @@ function withOrganization(db, table, values) {
1111
1123
  async function insert(db, table, values) {
1112
1124
  assertDoor(db, table, "insert");
1113
1125
  const many = Array.isArray(values);
1114
- const rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
1126
+ let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
1127
+ if (table.createdBy && db.role === "user" && rows.some((r) => r.created_by == null) && db.client.auth) {
1128
+ const { data } = await db.client.auth.getSession();
1129
+ const person = data.session?.user?.id;
1130
+ if (person) rows = rows.map((r) => r.created_by == null ? { ...r, created_by: person } : r);
1131
+ }
1115
1132
  const res = await from(db, table).insert(rows).select("*");
1116
1133
  const stored = check(`insert ${qualified(table)}`, res);
1117
1134
  return many ? stored : stored[0];
1118
1135
  }
1136
+ async function writeRow(db, table, id, values, door, guard) {
1137
+ const expected = guard && "expectedVersion" in guard ? guard.expectedVersion : void 0;
1138
+ if (expected !== void 0 && !table.versioned) {
1139
+ throw new DoorRefusedError(door, "expectedVersion on a table that is not versioned \u2014 nothing would guard it; pass no guard");
1140
+ }
1141
+ if (table.versioned && expected === void 0 && !(guard && "overwrite" in guard && guard.overwrite)) {
1142
+ throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
1143
+ }
1144
+ if (expected !== void 0) {
1145
+ return await guardedUpdate({
1146
+ expectedVersion: expected,
1147
+ applyUpdate: ({ expectedVersion, nextVersion }) => byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq("version", expectedVersion).select("*").maybeSingle(),
1148
+ fetchCurrent: () => byPk(from(db, table).select("*"), table, id).maybeSingle()
1149
+ });
1150
+ }
1151
+ const row = check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
1152
+ return row ? { status: "saved", row } : { status: "not_found" };
1153
+ }
1119
1154
  async function update(db, table, id, patch, guard) {
1120
1155
  assertDoor(db, table, "update");
1121
1156
  const door = `update ${qualified(table)}`;
@@ -1124,29 +1159,30 @@ async function update(db, table, id, patch, guard) {
1124
1159
  const blocked = table.readOnlyColumns.filter((c) => c in values);
1125
1160
  if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(", ")}`);
1126
1161
  }
1127
- if (table.versioned && guard && "expectedVersion" in guard) {
1128
- return guardedUpdate({
1129
- expectedVersion: guard.expectedVersion,
1130
- applyUpdate: ({ expectedVersion, nextVersion }) => byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq("version", expectedVersion).select("*").maybeSingle(),
1131
- fetchCurrent: () => byPk(from(db, table).select("*"), table, id).maybeSingle()
1132
- });
1133
- }
1134
- if (table.versioned && !(guard && "overwrite" in guard && guard.overwrite)) {
1135
- throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
1136
- }
1137
- const row = check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
1138
- return row ? { status: "saved", row } : { status: "not_found" };
1162
+ return writeRow(db, table, id, values, door, guard);
1139
1163
  }
1140
- async function remove(db, table, id) {
1164
+ async function remove(db, table, id, guard) {
1141
1165
  assertDoor(db, table, "remove");
1142
1166
  const door = `remove ${qualified(table)}`;
1143
1167
  if (table.softDelete) {
1144
- const row = check(door, await byPk(from(db, table).update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }), table, id).select("*").maybeSingle());
1145
- return row ? "soft_deleted" : "not_found";
1168
+ const res = await writeRow(db, table, id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() }, door, guard);
1169
+ return res.status === "saved" ? { status: "soft_deleted" } : res.status === "conflict" ? { status: "conflict", currentVersion: res.currentVersion } : res;
1146
1170
  }
1147
1171
  if (table.archive) throw new DoorRefusedError(door, "this table archives instead of deleting \u2014 use archive()");
1148
- const rows = check(door, await byPk(from(db, table).delete(), table, id).select("*"));
1149
- return rows && rows.length ? "deleted" : "not_found";
1172
+ const expected = guard && "expectedVersion" in guard ? guard.expectedVersion : void 0;
1173
+ if (expected !== void 0 && !table.versioned) throw new DoorRefusedError(door, "expectedVersion on a table that is not versioned");
1174
+ if (table.versioned && expected === void 0 && !(guard && "overwrite" in guard)) {
1175
+ throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
1176
+ }
1177
+ let q = byPk(from(db, table).delete(), table, id);
1178
+ if (expected !== void 0) q = q.eq("version", expected);
1179
+ const rows = check(door, await q.select("*"));
1180
+ if (rows && rows.length) return { status: "deleted" };
1181
+ if (expected !== void 0) {
1182
+ const current = check(door, await byPk(from(db, table).select("version"), table, id).maybeSingle());
1183
+ if (current) return { status: "conflict", currentVersion: current.version };
1184
+ }
1185
+ return { status: "not_found" };
1150
1186
  }
1151
1187
  async function archive(db, table, id, opts = {}) {
1152
1188
  assertDoor(db, table, "archive");
@@ -1156,9 +1192,9 @@ async function archive(db, table, id, opts = {}) {
1156
1192
  else if (table.archive === "is_archived") values = { is_archived: !opts.restore };
1157
1193
  else if (table.softDelete && opts.restore) values = { deleted_at: null };
1158
1194
  else throw new DoorRefusedError(door, "the table has no archive column");
1159
- return check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
1195
+ return writeRow(db, table, id, values, door, opts.guard);
1160
1196
  }
1161
- var restore = (db, table, id) => archive(db, table, id, { restore: true });
1197
+ var restore = (db, table, id, guard) => archive(db, table, id, guard ? { restore: true, guard } : { restore: true });
1162
1198
  async function call(db, fn, args) {
1163
1199
  const door = `call ${fn.schema}.${fn.name}`;
1164
1200
  const role = db.role === "service" ? "service_role" : "authenticated";
@@ -1172,6 +1208,12 @@ function tableByName(index, name) {
1172
1208
  if (!t) throw new DoorRefusedError(`tableByName ${name}`, "no such table in the database description (schema-qualify it: schema.table)");
1173
1209
  return t;
1174
1210
  }
1211
+ async function loadTableByName(loaders, name) {
1212
+ const schema = name.slice(0, name.indexOf("."));
1213
+ const load = schema ? loaders[schema] : void 0;
1214
+ if (!load) throw new DoorRefusedError(`tableByName ${name}`, "no such schema in the database description (schema-qualify it: schema.table)");
1215
+ return tableByName(await load(), name);
1216
+ }
1175
1217
  function changesOf(table) {
1176
1218
  if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, "the table is not in the supabase_realtime publication");
1177
1219
  return { schema: table.schema, table: table.name };