@ai-matrx/data 0.24.0 → 0.26.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0 — callAll: every row of a set-returning function
4
+
5
+ - **`callAll(db, fn, args, { order })`** — the complete read of a set-returning database function:
6
+ `rpc(…, { count: "exact" })` paged by `range` over the named `order` (`"id"` or
7
+ `{ column, ascending, nullsFirst }`; end on a key), past the 1,000-row cap; a `DatabaseError` keeps the
8
+ database's own error (code kept); a short read throws `IncompleteReadError`; an empty order is refused.
9
+ - `SupabaseLike.rpc` takes PostgREST's optional third argument (`{ count }`).
10
+
11
+ Consumer action: none (additive). Packages that paged `.rpc(...).order().range()` by hand move to `callAll`.
12
+
13
+ ## 0.25.0 — created_by stamped; listAll keeps the database's error
14
+
15
+ - **A signed-in `insert` stamps `created_by`** with the session's person when the table has a
16
+ `created_by` column and the row omits it (row security on `workbench.notes` / `note_folders` and
17
+ similar tables refuses an insert whose `created_by` is not `auth.uid()`). An explicit value is kept;
18
+ the service role never stamps. Descriptors carry `createdBy: boolean` (generated from the columns).
19
+ - **`listAll` throws a `DatabaseError`** carrying the database's own error (message, code, details) —
20
+ it used to surface as a re-worded `readAllRows(...)` `Error` without the code.
21
+
22
+ Consumer action: run `matrx-data generate` again (`pnpm db:generate`) so descriptors carry `createdBy`;
23
+ code that matched `listAll` failures by the `readAllRows(` message should check `instanceof DatabaseError`.
24
+
3
25
  ## 0.24.0 — doors hardened (independent review)
4
26
 
5
27
  - **Every write on a versioned table is guarded.** `remove` (and `archive` / `restore`) take the same
package/bin/generate.mjs CHANGED
@@ -94,6 +94,7 @@ function tableDescriptor(schema, name, t) {
94
94
  readOnlyColumns: reg?.clientReadOnlyColumns ?? [],
95
95
  listScope: reg?.listScope ?? null,
96
96
  realtime: (t.realtime ?? []).includes("supabase_realtime"),
97
+ createdBy: t.columns.some((c) => c.name === "created_by"),
97
98
  };
98
99
  return flags;
99
100
  }
package/dist/db.cjs CHANGED
@@ -36,6 +36,7 @@ __export(db_exports, {
36
36
  assertDoor: () => assertDoor,
37
37
  assertWriteLanded: () => assertWriteLanded,
38
38
  call: () => call,
39
+ callAll: () => callAll,
39
40
  changesOf: () => changesOf,
40
41
  count: () => count,
41
42
  createActiveOrgCookie: () => createActiveOrgCookie,
@@ -1074,7 +1075,10 @@ async function listAll(db, table, where, opts = {}) {
1074
1075
  ({ from: lo, to: hi }) => {
1075
1076
  let q = live(from(db, table).select(selectOf(opts.columns), { count: "exact" }), table, opts.includeDeleted);
1076
1077
  if (where) q = where(q);
1077
- return ordered(q, table, opts.orderBy).range(lo, hi);
1078
+ return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res) => {
1079
+ if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);
1080
+ return res;
1081
+ });
1078
1082
  },
1079
1083
  {
1080
1084
  label: `listAll ${qualified(table)}`,
@@ -1120,7 +1124,12 @@ function withOrganization(db, table, values) {
1120
1124
  async function insert(db, table, values) {
1121
1125
  assertDoor(db, table, "insert");
1122
1126
  const many = Array.isArray(values);
1123
- const rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
1127
+ let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
1128
+ if (table.createdBy && db.role === "user" && rows.some((r) => r.created_by == null) && db.client.auth) {
1129
+ const { data } = await db.client.auth.getSession();
1130
+ const person = data.session?.user?.id;
1131
+ if (person) rows = rows.map((r) => r.created_by == null ? { ...r, created_by: person } : r);
1132
+ }
1124
1133
  const res = await from(db, table).insert(rows).select("*");
1125
1134
  const stored = check(`insert ${qualified(table)}`, res);
1126
1135
  return many ? stored : stored[0];
@@ -1195,6 +1204,27 @@ async function call(db, fn, args) {
1195
1204
  }
1196
1205
  return check(door, await db.client.schema(fn.schema).rpc(fn.name, args));
1197
1206
  }
1207
+ async function callAll(db, fn, args, opts) {
1208
+ const door = `callAll ${fn.schema}.${fn.name}`;
1209
+ const role = db.role === "service" ? "service_role" : "authenticated";
1210
+ if (!fn.roles.includes(role) && !(role === "authenticated" && fn.roles.includes("anon"))) {
1211
+ throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(", ") || "nobody"})`);
1212
+ }
1213
+ if (!opts.order.length) throw new DoorRefusedError(door, "a paged read needs an order (end on a key) \u2014 pages over no order can skip or repeat rows");
1214
+ return readAllRows(
1215
+ ({ from: lo, to: hi }) => {
1216
+ let q = db.client.schema(fn.schema).rpc(fn.name, args, { count: "exact" });
1217
+ for (const o of opts.order) {
1218
+ q = typeof o === "string" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...o.nullsFirst !== void 0 ? { nullsFirst: o.nullsFirst } : {} });
1219
+ }
1220
+ return Promise.resolve(q.range(lo, hi)).then((res) => {
1221
+ if (res.error) throw new DatabaseError(door, res.error);
1222
+ return res;
1223
+ });
1224
+ },
1225
+ { label: door, ...opts.pageSize !== void 0 ? { pageSize: opts.pageSize } : {}, ...opts.maxRows !== void 0 ? { maxRows: opts.maxRows } : {} }
1226
+ );
1227
+ }
1198
1228
  function tableByName(index, name) {
1199
1229
  const t = index[name];
1200
1230
  if (!t) throw new DoorRefusedError(`tableByName ${name}`, "no such table in the database description (schema-qualify it: schema.table)");