@ai-matrx/data 0.22.33 → 0.23.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.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * matrx-data — @ai-matrx/data's build-time CLI.
4
+ *
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
7
+ *
8
+ * The package ships the runtime and this CLI; each app generates its own doors
9
+ * locally (Prisma's model), so a new column never waits on a package release.
10
+ */
11
+ import path from "node:path";
12
+
13
+ const [command, ...rest] = process.argv.slice(2);
14
+ const flag = (name) => {
15
+ const i = rest.indexOf(name);
16
+ return i >= 0 ? rest[i + 1] : undefined;
17
+ };
18
+
19
+ async function main() {
20
+ if (command === "emit") {
21
+ const { emit } = await import("./emit.mjs");
22
+ return emit({ out: path.resolve(flag("--out") ?? "db-contract"), check: rest.includes("--check") });
23
+ }
24
+ if (command === "generate") {
25
+ const { generate } = await import("./generate.mjs");
26
+ const from = flag("--from");
27
+ const out = flag("--out");
28
+ if (!from || !out) {
29
+ console.error("matrx-data generate: --from <description dir> and --out <dir> are required");
30
+ return 2;
31
+ }
32
+ return generate({
33
+ from: path.resolve(from),
34
+ out: path.resolve(out),
35
+ schemas: flag("--schemas")?.split(",").map((s) => s.trim()).filter(Boolean),
36
+ check: rest.includes("--check"),
37
+ importFrom: flag("--import-from") ?? "@ai-matrx/data/db",
38
+ });
39
+ }
40
+ console.error("usage: matrx-data emit [--out <dir>] [--check] | matrx-data generate --from <dir> --out <dir> [--schemas a,b] [--check]");
41
+ return 2;
42
+ }
43
+
44
+ main().then((code) => process.exit(code ?? 0), (err) => {
45
+ console.error(`matrx-data ${command ?? ""}: ${err?.message ?? err}`);
46
+ process.exit(1);
47
+ });
package/dist/db.cjs CHANGED
@@ -7,11 +7,11 @@ var __export = (target, all) => {
7
7
  for (var name in all)
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
10
+ var __copyProps = (to, from2, except, desc) => {
11
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
12
+ for (let key of __getOwnPropNames(from2))
13
13
  if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
15
15
  }
16
16
  return to;
17
17
  };
@@ -24,35 +24,52 @@ __export(db_exports, {
24
24
  DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE: () => DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE,
25
25
  DEFAULT_ACTIVE_ORG_COOKIE_NAME: () => DEFAULT_ACTIVE_ORG_COOKIE_NAME,
26
26
  DEFAULT_AUTH_COOKIE_NAME: () => DEFAULT_AUTH_COOKIE_NAME,
27
+ DatabaseError: () => DatabaseError,
28
+ DoorRefusedError: () => DoorRefusedError,
27
29
  IncompleteReadError: () => IncompleteReadError,
28
30
  RecordUnavailableError: () => RecordUnavailableError,
29
31
  SessionUnavailableError: () => SessionUnavailableError,
30
32
  WriteDidNotLandError: () => WriteDidNotLandError,
31
33
  WriteRefusedError: () => WriteRefusedError,
34
+ archive: () => archive,
32
35
  asJsonObject: () => asJsonObject,
36
+ assertDoor: () => assertDoor,
33
37
  assertWriteLanded: () => assertWriteLanded,
38
+ call: () => call,
39
+ changesOf: () => changesOf,
40
+ count: () => count,
34
41
  createActiveOrgCookie: () => createActiveOrgCookie,
35
42
  createAuthCookie: () => createAuthCookie,
43
+ createDb: () => createDb,
36
44
  createOAuthRefreshFetch: () => createOAuthRefreshFetch,
45
+ createServiceDb: () => createServiceDb,
37
46
  createSessionRetry: () => createSessionRetry,
38
47
  decodeActiveOrgSelection: () => decodeActiveOrgSelection,
39
48
  decodeOAuthRefreshClaims: () => decodeOAuthRefreshClaims,
40
49
  describeWriteFailure: () => describeWriteFailure,
41
50
  encodeActiveOrgSelection: () => encodeActiveOrgSelection,
42
51
  guardedUpdate: () => guardedUpdate,
52
+ insert: () => insert,
43
53
  isMissingSessionError: () => isMissingSessionError,
44
54
  isRecordUnavailableError: () => isRecordUnavailableError,
55
+ listAll: () => listAll,
45
56
  mergeJsonColumn: () => mergeJsonColumn,
57
+ page: () => page,
46
58
  parseCookieHeader: () => parseCookieHeader,
47
59
  personSentence: () => personSentence,
60
+ read: () => read,
48
61
  readAllRows: () => readAllRows,
49
62
  readAllRowsRest: () => readAllRowsRest,
50
63
  recordUnavailableMessage: () => recordUnavailableMessage,
64
+ remove: () => remove,
65
+ restore: () => restore,
51
66
  serverMessageFromBody: () => serverMessageFromBody,
67
+ tableByName: () => tableByName,
52
68
  tryReadAllRows: () => tryReadAllRows,
53
69
  tryReadAllRowsRest: () => tryReadAllRowsRest,
54
70
  tryWriteOne: () => tryWriteOne,
55
71
  uniqueChannelTopic: () => import_realtime.uniqueChannelTopic,
72
+ update: () => update,
56
73
  writeFailureStatus: () => writeFailureStatus,
57
74
  writeOne: () => writeOne,
58
75
  writeOneRow: () => writeOneRow
@@ -127,18 +144,18 @@ async function readAllRows(query, opts) {
127
144
  const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
128
145
  const rows = [];
129
146
  let total = null;
130
- for (let from = 0; ; from += pageSize) {
131
- const res = await query({ from, to: from + pageSize - 1 });
147
+ for (let from2 = 0; ; from2 += pageSize) {
148
+ const res = await query({ from: from2, to: from2 + pageSize - 1 });
132
149
  if (res.error) {
133
150
  throw new Error(
134
151
  `readAllRows(${opts.label}): query failed \u2014 ${res.error.message}`
135
152
  );
136
153
  }
137
- const page = res.data ?? [];
138
- rows.push(...page);
154
+ const page2 = res.data ?? [];
155
+ rows.push(...page2);
139
156
  if (typeof res.count === "number") total = res.count;
140
157
  if (total === null) {
141
- if (page.length < pageSize) return rows;
158
+ if (page2.length < pageSize) return rows;
142
159
  throw new IncompleteReadError(
143
160
  opts.label,
144
161
  rows.length,
@@ -147,7 +164,7 @@ async function readAllRows(query, opts) {
147
164
  );
148
165
  }
149
166
  if (rows.length >= total) break;
150
- if (page.length === 0) {
167
+ if (page2.length === 0) {
151
168
  throw new IncompleteReadError(
152
169
  opts.label,
153
170
  rows.length,
@@ -190,7 +207,7 @@ async function readAllRowsRest(opts) {
190
207
  const endpoint = `${opts.url.replace(/\/$/, "")}/rest/v1/${opts.path}`;
191
208
  const rows = [];
192
209
  let total = null;
193
- for (let from = 0; ; from += pageSize) {
210
+ for (let from2 = 0; ; from2 += pageSize) {
194
211
  const res = await fetch(endpoint, {
195
212
  headers: {
196
213
  apikey: opts.key,
@@ -201,7 +218,7 @@ async function readAllRowsRest(opts) {
201
218
  // no total, which is unverifiable and therefore refused below. This is
202
219
  // the REST twin of supabase-js's `{ count: "exact" }`.
203
220
  Prefer: "count=exact",
204
- Range: `${from}-${from + pageSize - 1}`,
221
+ Range: `${from2}-${from2 + pageSize - 1}`,
205
222
  "Range-Unit": "items"
206
223
  }
207
224
  });
@@ -210,12 +227,12 @@ async function readAllRowsRest(opts) {
210
227
  `readAllRowsRest(${opts.label}): HTTP ${res.status} \u2014 ${await res.text()}`
211
228
  );
212
229
  }
213
- const page = await res.json();
214
- rows.push(...page);
230
+ const page2 = await res.json();
231
+ rows.push(...page2);
215
232
  const totalPart = res.headers.get("content-range")?.split("/")[1];
216
233
  if (totalPart && totalPart !== "*") total = Number(totalPart);
217
234
  if (total === null) {
218
- if (page.length < pageSize) return rows;
235
+ if (page2.length < pageSize) return rows;
219
236
  throw new IncompleteReadError(
220
237
  opts.label,
221
238
  rows.length,
@@ -224,7 +241,7 @@ async function readAllRowsRest(opts) {
224
241
  );
225
242
  }
226
243
  if (rows.length >= total) break;
227
- if (page.length === 0) {
244
+ if (page2.length === 0) {
228
245
  throw new IncompleteReadError(
229
246
  opts.label,
230
247
  rows.length,
@@ -360,9 +377,9 @@ function belongsToStorageKey(name, storageKey) {
360
377
  if (!name.startsWith(`${storageKey}.`)) return false;
361
378
  return /^(0|[1-9][0-9]*)$/.test(name.slice(storageKey.length + 1));
362
379
  }
363
- function renamedCookie(cookie, from, to) {
364
- if (cookie.name === from) return { ...cookie, name: to };
365
- const prefix = `${from}.`;
380
+ function renamedCookie(cookie, from2, to) {
381
+ if (cookie.name === from2) return { ...cookie, name: to };
382
+ const prefix = `${from2}.`;
366
383
  if (!cookie.name.startsWith(prefix)) return null;
367
384
  const chunk = cookie.name.slice(prefix.length);
368
385
  if (!/^(0|[1-9][0-9]*)$/.test(chunk)) return null;
@@ -955,4 +972,208 @@ function recordUnavailableMessage(entity, reason) {
955
972
  function isRecordUnavailableError(value) {
956
973
  return value instanceof RecordUnavailableError;
957
974
  }
975
+
976
+ // src/db/doors.ts
977
+ function credentialsOf(client) {
978
+ return {
979
+ async get() {
980
+ if (!client.auth) return null;
981
+ const { data } = await client.auth.getSession();
982
+ return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
983
+ },
984
+ onChange(cb) {
985
+ if (!client.auth) return () => {
986
+ };
987
+ const { data } = client.auth.onAuthStateChange(() => cb());
988
+ return () => data.subscription.unsubscribe();
989
+ }
990
+ };
991
+ }
992
+ function createDb(options) {
993
+ const { client, organization } = options;
994
+ return {
995
+ client,
996
+ role: "user",
997
+ activeOrganization: () => organization?.() ?? null,
998
+ credentials: credentialsOf(client)
999
+ };
1000
+ }
1001
+ function createServiceDb(options) {
1002
+ if (typeof globalThis.window !== "undefined") {
1003
+ throw new DoorRefusedError("createServiceDb", "the service role never runs in a browser");
1004
+ }
1005
+ return { client: options.client, role: "service", activeOrganization: () => null, credentials: credentialsOf(options.client) };
1006
+ }
1007
+ var DoorRefusedError = class extends Error {
1008
+ constructor(door, reason) {
1009
+ super(`${door}: ${reason}`);
1010
+ this.door = door;
1011
+ this.reason = reason;
1012
+ this.name = "DoorRefusedError";
1013
+ }
1014
+ door;
1015
+ reason;
1016
+ };
1017
+ var DatabaseError = class extends Error {
1018
+ constructor(door, cause) {
1019
+ super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : ""}`);
1020
+ this.door = door;
1021
+ this.cause = cause;
1022
+ this.name = "DatabaseError";
1023
+ }
1024
+ door;
1025
+ cause;
1026
+ };
1027
+ var CLIENT_DOORS = {
1028
+ entity: ["read", "insert", "update", "remove", "archive"],
1029
+ detail: ["read", "insert", "update", "remove", "archive"],
1030
+ ledger: ["read", "insert"],
1031
+ reference: ["read"],
1032
+ system: ["read"],
1033
+ restricted: [],
1034
+ deprecated: []
1035
+ };
1036
+ var qualified = (t) => `${t.schema}.${t.name}`;
1037
+ function assertDoor(db, table, op) {
1038
+ const door = `${op} ${qualified(table)}`;
1039
+ if (table.type === "deprecated") throw new DoorRefusedError(door, "the table is deprecated \u2014 no door opens on it");
1040
+ if (table.kind !== "table" && op !== "read") throw new DoorRefusedError(door, `a ${table.kind.replace("_", " ")} is read-only`);
1041
+ if (db.role === "service") return;
1042
+ if (table.type && !CLIENT_DOORS[table.type].includes(op))
1043
+ throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(", ") || "service"} doors to a client`);
1044
+ if (op !== "read" && table.readOnly) throw new DoorRefusedError(door, "the table is read-only to clients (client_read_only)");
1045
+ if (op === "remove" && table.deletesRefused) throw new DoorRefusedError(door, "clients may not delete from this table (client_deletes_refused)");
1046
+ }
1047
+ function check(door, res) {
1048
+ if (res.error) throw new DatabaseError(door, res.error);
1049
+ return res.data;
1050
+ }
1051
+ var from = (db, t) => db.client.schema(t.schema).from(t.name);
1052
+ function byPk(q, t, id) {
1053
+ if (t.pk.length === 1) return q.eq(t.pk[0], id);
1054
+ const key = id;
1055
+ return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);
1056
+ }
1057
+ async function read(db, table, id, columns = "*") {
1058
+ assertDoor(db, table, "read");
1059
+ return check(`read ${qualified(table)}`, await byPk(from(db, table).select(columns), table, id).maybeSingle());
1060
+ }
1061
+ async function listAll(db, table, where, opts = {}) {
1062
+ assertDoor(db, table, "read");
1063
+ const order = opts.orderBy ?? table.pk[0] ?? "id";
1064
+ return readAllRows(
1065
+ ({ from: lo, to: hi }) => {
1066
+ let q = from(db, table).select(opts.columns ?? "*", { count: "exact" });
1067
+ if (where) q = where(q);
1068
+ return q.order(order, { ascending: true }).range(lo, hi);
1069
+ },
1070
+ {
1071
+ label: `listAll ${qualified(table)}`,
1072
+ ...opts.pageSize !== void 0 ? { pageSize: opts.pageSize } : {},
1073
+ ...opts.maxRows !== void 0 ? { maxRows: opts.maxRows } : {}
1074
+ }
1075
+ );
1076
+ }
1077
+ async function page(db, table, window2) {
1078
+ assertDoor(db, table, "read");
1079
+ let q = from(db, table).select(window2.columns ?? "*", { count: "exact" });
1080
+ 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);
1082
+ return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };
1083
+ }
1084
+ async function count(db, table, where) {
1085
+ assertDoor(db, table, "read");
1086
+ let q = from(db, table).select("*", { count: "exact", head: true });
1087
+ if (where) q = where(q);
1088
+ const res = await q;
1089
+ check(`count ${qualified(table)}`, res);
1090
+ return res.count ?? 0;
1091
+ }
1092
+ function withOrganization(db, table, values) {
1093
+ const door = `insert ${qualified(table)}`;
1094
+ const org = table.org;
1095
+ if (org.rule === "parent" && values[org.via] != null) {
1096
+ return values;
1097
+ }
1098
+ if (org.rule === "binding" || org.rule === "parent") {
1099
+ if (values.organization_id != null) return values;
1100
+ const active = db.activeOrganization();
1101
+ if (!active) {
1102
+ throw new DoorRefusedError(
1103
+ door,
1104
+ db.role === "service" ? "a service insert names its organization_id" : "no organization: pass organization_id or bind createDb({ organization })"
1105
+ );
1106
+ }
1107
+ return { ...values, organization_id: active };
1108
+ }
1109
+ return values;
1110
+ }
1111
+ async function insert(db, table, values) {
1112
+ assertDoor(db, table, "insert");
1113
+ const many = Array.isArray(values);
1114
+ const rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
1115
+ const res = await from(db, table).insert(rows).select("*");
1116
+ const stored = check(`insert ${qualified(table)}`, res);
1117
+ return many ? stored : stored[0];
1118
+ }
1119
+ async function update(db, table, id, patch, guard) {
1120
+ assertDoor(db, table, "update");
1121
+ const door = `update ${qualified(table)}`;
1122
+ const values = patch;
1123
+ if (db.role === "user") {
1124
+ const blocked = table.readOnlyColumns.filter((c) => c in values);
1125
+ if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(", ")}`);
1126
+ }
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" };
1139
+ }
1140
+ async function remove(db, table, id) {
1141
+ assertDoor(db, table, "remove");
1142
+ const door = `remove ${qualified(table)}`;
1143
+ 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";
1146
+ }
1147
+ 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";
1150
+ }
1151
+ async function archive(db, table, id, opts = {}) {
1152
+ assertDoor(db, table, "archive");
1153
+ const door = `${opts.restore ? "restore" : "archive"} ${qualified(table)}`;
1154
+ let values;
1155
+ if (table.archive === "archived_at") values = { archived_at: opts.restore ? null : (/* @__PURE__ */ new Date()).toISOString() };
1156
+ else if (table.archive === "is_archived") values = { is_archived: !opts.restore };
1157
+ else if (table.softDelete && opts.restore) values = { deleted_at: null };
1158
+ 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());
1160
+ }
1161
+ var restore = (db, table, id) => archive(db, table, id, { restore: true });
1162
+ async function call(db, fn, args) {
1163
+ const door = `call ${fn.schema}.${fn.name}`;
1164
+ const role = db.role === "service" ? "service_role" : "authenticated";
1165
+ if (!fn.roles.includes(role) && !(role === "authenticated" && fn.roles.includes("anon"))) {
1166
+ throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(", ") || "nobody"})`);
1167
+ }
1168
+ return check(door, await db.client.schema(fn.schema).rpc(fn.name, args));
1169
+ }
1170
+ function tableByName(index, name) {
1171
+ const t = index[name];
1172
+ if (!t) throw new DoorRefusedError(`tableByName ${name}`, "no such table in the database description (schema-qualify it: schema.table)");
1173
+ return t;
1174
+ }
1175
+ function changesOf(table) {
1176
+ if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, "the table is not in the supabase_realtime publication");
1177
+ return { schema: table.schema, table: table.name };
1178
+ }
958
1179
  //# sourceMappingURL=db.cjs.map