@ai-matrx/data 0.22.32 → 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.
- package/CHANGELOG.md +32 -0
- package/bin/direct-db.mjs +80 -0
- package/bin/emit.mjs +355 -0
- package/bin/generate.mjs +196 -0
- package/bin/matrx-data.mjs +47 -0
- package/dist/db.cjs +240 -19
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +220 -3
- package/dist/db.d.ts +220 -3
- package/dist/db.js +236 -15
- package/dist/db.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +128 -0
- package/dist/types.d.ts +128 -0
- package/package.json +6 -2
package/dist/db.js
CHANGED
|
@@ -66,18 +66,18 @@ async function readAllRows(query, opts) {
|
|
|
66
66
|
const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
|
|
67
67
|
const rows = [];
|
|
68
68
|
let total = null;
|
|
69
|
-
for (let
|
|
70
|
-
const res = await query({ from, to:
|
|
69
|
+
for (let from2 = 0; ; from2 += pageSize) {
|
|
70
|
+
const res = await query({ from: from2, to: from2 + pageSize - 1 });
|
|
71
71
|
if (res.error) {
|
|
72
72
|
throw new Error(
|
|
73
73
|
`readAllRows(${opts.label}): query failed \u2014 ${res.error.message}`
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
|
-
const
|
|
77
|
-
rows.push(...
|
|
76
|
+
const page2 = res.data ?? [];
|
|
77
|
+
rows.push(...page2);
|
|
78
78
|
if (typeof res.count === "number") total = res.count;
|
|
79
79
|
if (total === null) {
|
|
80
|
-
if (
|
|
80
|
+
if (page2.length < pageSize) return rows;
|
|
81
81
|
throw new IncompleteReadError(
|
|
82
82
|
opts.label,
|
|
83
83
|
rows.length,
|
|
@@ -86,7 +86,7 @@ async function readAllRows(query, opts) {
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
if (rows.length >= total) break;
|
|
89
|
-
if (
|
|
89
|
+
if (page2.length === 0) {
|
|
90
90
|
throw new IncompleteReadError(
|
|
91
91
|
opts.label,
|
|
92
92
|
rows.length,
|
|
@@ -129,7 +129,7 @@ async function readAllRowsRest(opts) {
|
|
|
129
129
|
const endpoint = `${opts.url.replace(/\/$/, "")}/rest/v1/${opts.path}`;
|
|
130
130
|
const rows = [];
|
|
131
131
|
let total = null;
|
|
132
|
-
for (let
|
|
132
|
+
for (let from2 = 0; ; from2 += pageSize) {
|
|
133
133
|
const res = await fetch(endpoint, {
|
|
134
134
|
headers: {
|
|
135
135
|
apikey: opts.key,
|
|
@@ -140,7 +140,7 @@ async function readAllRowsRest(opts) {
|
|
|
140
140
|
// no total, which is unverifiable and therefore refused below. This is
|
|
141
141
|
// the REST twin of supabase-js's `{ count: "exact" }`.
|
|
142
142
|
Prefer: "count=exact",
|
|
143
|
-
Range: `${
|
|
143
|
+
Range: `${from2}-${from2 + pageSize - 1}`,
|
|
144
144
|
"Range-Unit": "items"
|
|
145
145
|
}
|
|
146
146
|
});
|
|
@@ -149,12 +149,12 @@ async function readAllRowsRest(opts) {
|
|
|
149
149
|
`readAllRowsRest(${opts.label}): HTTP ${res.status} \u2014 ${await res.text()}`
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
|
-
const
|
|
153
|
-
rows.push(...
|
|
152
|
+
const page2 = await res.json();
|
|
153
|
+
rows.push(...page2);
|
|
154
154
|
const totalPart = res.headers.get("content-range")?.split("/")[1];
|
|
155
155
|
if (totalPart && totalPart !== "*") total = Number(totalPart);
|
|
156
156
|
if (total === null) {
|
|
157
|
-
if (
|
|
157
|
+
if (page2.length < pageSize) return rows;
|
|
158
158
|
throw new IncompleteReadError(
|
|
159
159
|
opts.label,
|
|
160
160
|
rows.length,
|
|
@@ -163,7 +163,7 @@ async function readAllRowsRest(opts) {
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
if (rows.length >= total) break;
|
|
166
|
-
if (
|
|
166
|
+
if (page2.length === 0) {
|
|
167
167
|
throw new IncompleteReadError(
|
|
168
168
|
opts.label,
|
|
169
169
|
rows.length,
|
|
@@ -299,9 +299,9 @@ function belongsToStorageKey(name, storageKey) {
|
|
|
299
299
|
if (!name.startsWith(`${storageKey}.`)) return false;
|
|
300
300
|
return /^(0|[1-9][0-9]*)$/.test(name.slice(storageKey.length + 1));
|
|
301
301
|
}
|
|
302
|
-
function renamedCookie(cookie,
|
|
303
|
-
if (cookie.name ===
|
|
304
|
-
const prefix = `${
|
|
302
|
+
function renamedCookie(cookie, from2, to) {
|
|
303
|
+
if (cookie.name === from2) return { ...cookie, name: to };
|
|
304
|
+
const prefix = `${from2}.`;
|
|
305
305
|
if (!cookie.name.startsWith(prefix)) return null;
|
|
306
306
|
const chunk = cookie.name.slice(prefix.length);
|
|
307
307
|
if (!/^(0|[1-9][0-9]*)$/.test(chunk)) return null;
|
|
@@ -894,40 +894,261 @@ function recordUnavailableMessage(entity, reason) {
|
|
|
894
894
|
function isRecordUnavailableError(value) {
|
|
895
895
|
return value instanceof RecordUnavailableError;
|
|
896
896
|
}
|
|
897
|
+
|
|
898
|
+
// src/db/doors.ts
|
|
899
|
+
function credentialsOf(client) {
|
|
900
|
+
return {
|
|
901
|
+
async get() {
|
|
902
|
+
if (!client.auth) return null;
|
|
903
|
+
const { data } = await client.auth.getSession();
|
|
904
|
+
return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
|
|
905
|
+
},
|
|
906
|
+
onChange(cb) {
|
|
907
|
+
if (!client.auth) return () => {
|
|
908
|
+
};
|
|
909
|
+
const { data } = client.auth.onAuthStateChange(() => cb());
|
|
910
|
+
return () => data.subscription.unsubscribe();
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function createDb(options) {
|
|
915
|
+
const { client, organization } = options;
|
|
916
|
+
return {
|
|
917
|
+
client,
|
|
918
|
+
role: "user",
|
|
919
|
+
activeOrganization: () => organization?.() ?? null,
|
|
920
|
+
credentials: credentialsOf(client)
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function createServiceDb(options) {
|
|
924
|
+
if (typeof globalThis.window !== "undefined") {
|
|
925
|
+
throw new DoorRefusedError("createServiceDb", "the service role never runs in a browser");
|
|
926
|
+
}
|
|
927
|
+
return { client: options.client, role: "service", activeOrganization: () => null, credentials: credentialsOf(options.client) };
|
|
928
|
+
}
|
|
929
|
+
var DoorRefusedError = class extends Error {
|
|
930
|
+
constructor(door, reason) {
|
|
931
|
+
super(`${door}: ${reason}`);
|
|
932
|
+
this.door = door;
|
|
933
|
+
this.reason = reason;
|
|
934
|
+
this.name = "DoorRefusedError";
|
|
935
|
+
}
|
|
936
|
+
door;
|
|
937
|
+
reason;
|
|
938
|
+
};
|
|
939
|
+
var DatabaseError = class extends Error {
|
|
940
|
+
constructor(door, cause) {
|
|
941
|
+
super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : ""}`);
|
|
942
|
+
this.door = door;
|
|
943
|
+
this.cause = cause;
|
|
944
|
+
this.name = "DatabaseError";
|
|
945
|
+
}
|
|
946
|
+
door;
|
|
947
|
+
cause;
|
|
948
|
+
};
|
|
949
|
+
var CLIENT_DOORS = {
|
|
950
|
+
entity: ["read", "insert", "update", "remove", "archive"],
|
|
951
|
+
detail: ["read", "insert", "update", "remove", "archive"],
|
|
952
|
+
ledger: ["read", "insert"],
|
|
953
|
+
reference: ["read"],
|
|
954
|
+
system: ["read"],
|
|
955
|
+
restricted: [],
|
|
956
|
+
deprecated: []
|
|
957
|
+
};
|
|
958
|
+
var qualified = (t) => `${t.schema}.${t.name}`;
|
|
959
|
+
function assertDoor(db, table, op) {
|
|
960
|
+
const door = `${op} ${qualified(table)}`;
|
|
961
|
+
if (table.type === "deprecated") throw new DoorRefusedError(door, "the table is deprecated \u2014 no door opens on it");
|
|
962
|
+
if (table.kind !== "table" && op !== "read") throw new DoorRefusedError(door, `a ${table.kind.replace("_", " ")} is read-only`);
|
|
963
|
+
if (db.role === "service") return;
|
|
964
|
+
if (table.type && !CLIENT_DOORS[table.type].includes(op))
|
|
965
|
+
throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(", ") || "service"} doors to a client`);
|
|
966
|
+
if (op !== "read" && table.readOnly) throw new DoorRefusedError(door, "the table is read-only to clients (client_read_only)");
|
|
967
|
+
if (op === "remove" && table.deletesRefused) throw new DoorRefusedError(door, "clients may not delete from this table (client_deletes_refused)");
|
|
968
|
+
}
|
|
969
|
+
function check(door, res) {
|
|
970
|
+
if (res.error) throw new DatabaseError(door, res.error);
|
|
971
|
+
return res.data;
|
|
972
|
+
}
|
|
973
|
+
var from = (db, t) => db.client.schema(t.schema).from(t.name);
|
|
974
|
+
function byPk(q, t, id) {
|
|
975
|
+
if (t.pk.length === 1) return q.eq(t.pk[0], id);
|
|
976
|
+
const key = id;
|
|
977
|
+
return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);
|
|
978
|
+
}
|
|
979
|
+
async function read(db, table, id, columns = "*") {
|
|
980
|
+
assertDoor(db, table, "read");
|
|
981
|
+
return check(`read ${qualified(table)}`, await byPk(from(db, table).select(columns), table, id).maybeSingle());
|
|
982
|
+
}
|
|
983
|
+
async function listAll(db, table, where, opts = {}) {
|
|
984
|
+
assertDoor(db, table, "read");
|
|
985
|
+
const order = opts.orderBy ?? table.pk[0] ?? "id";
|
|
986
|
+
return readAllRows(
|
|
987
|
+
({ from: lo, to: hi }) => {
|
|
988
|
+
let q = from(db, table).select(opts.columns ?? "*", { count: "exact" });
|
|
989
|
+
if (where) q = where(q);
|
|
990
|
+
return q.order(order, { ascending: true }).range(lo, hi);
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
label: `listAll ${qualified(table)}`,
|
|
994
|
+
...opts.pageSize !== void 0 ? { pageSize: opts.pageSize } : {},
|
|
995
|
+
...opts.maxRows !== void 0 ? { maxRows: opts.maxRows } : {}
|
|
996
|
+
}
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
async function page(db, table, window2) {
|
|
1000
|
+
assertDoor(db, table, "read");
|
|
1001
|
+
let q = from(db, table).select(window2.columns ?? "*", { count: "exact" });
|
|
1002
|
+
if (window2.where) q = window2.where(q);
|
|
1003
|
+
const res = await q.order(window2.orderBy ?? table.pk[0] ?? "id", { ascending: window2.ascending ?? true }).range(window2.from, window2.to);
|
|
1004
|
+
return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };
|
|
1005
|
+
}
|
|
1006
|
+
async function count(db, table, where) {
|
|
1007
|
+
assertDoor(db, table, "read");
|
|
1008
|
+
let q = from(db, table).select("*", { count: "exact", head: true });
|
|
1009
|
+
if (where) q = where(q);
|
|
1010
|
+
const res = await q;
|
|
1011
|
+
check(`count ${qualified(table)}`, res);
|
|
1012
|
+
return res.count ?? 0;
|
|
1013
|
+
}
|
|
1014
|
+
function withOrganization(db, table, values) {
|
|
1015
|
+
const door = `insert ${qualified(table)}`;
|
|
1016
|
+
const org = table.org;
|
|
1017
|
+
if (org.rule === "parent" && values[org.via] != null) {
|
|
1018
|
+
return values;
|
|
1019
|
+
}
|
|
1020
|
+
if (org.rule === "binding" || org.rule === "parent") {
|
|
1021
|
+
if (values.organization_id != null) return values;
|
|
1022
|
+
const active = db.activeOrganization();
|
|
1023
|
+
if (!active) {
|
|
1024
|
+
throw new DoorRefusedError(
|
|
1025
|
+
door,
|
|
1026
|
+
db.role === "service" ? "a service insert names its organization_id" : "no organization: pass organization_id or bind createDb({ organization })"
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return { ...values, organization_id: active };
|
|
1030
|
+
}
|
|
1031
|
+
return values;
|
|
1032
|
+
}
|
|
1033
|
+
async function insert(db, table, values) {
|
|
1034
|
+
assertDoor(db, table, "insert");
|
|
1035
|
+
const many = Array.isArray(values);
|
|
1036
|
+
const rows = (many ? values : [values]).map((v) => withOrganization(db, table, v));
|
|
1037
|
+
const res = await from(db, table).insert(rows).select("*");
|
|
1038
|
+
const stored = check(`insert ${qualified(table)}`, res);
|
|
1039
|
+
return many ? stored : stored[0];
|
|
1040
|
+
}
|
|
1041
|
+
async function update(db, table, id, patch, guard) {
|
|
1042
|
+
assertDoor(db, table, "update");
|
|
1043
|
+
const door = `update ${qualified(table)}`;
|
|
1044
|
+
const values = patch;
|
|
1045
|
+
if (db.role === "user") {
|
|
1046
|
+
const blocked = table.readOnlyColumns.filter((c) => c in values);
|
|
1047
|
+
if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(", ")}`);
|
|
1048
|
+
}
|
|
1049
|
+
if (table.versioned && guard && "expectedVersion" in guard) {
|
|
1050
|
+
return guardedUpdate({
|
|
1051
|
+
expectedVersion: guard.expectedVersion,
|
|
1052
|
+
applyUpdate: ({ expectedVersion, nextVersion }) => byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq("version", expectedVersion).select("*").maybeSingle(),
|
|
1053
|
+
fetchCurrent: () => byPk(from(db, table).select("*"), table, id).maybeSingle()
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
if (table.versioned && !(guard && "overwrite" in guard && guard.overwrite)) {
|
|
1057
|
+
throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
|
|
1058
|
+
}
|
|
1059
|
+
const row = check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
|
|
1060
|
+
return row ? { status: "saved", row } : { status: "not_found" };
|
|
1061
|
+
}
|
|
1062
|
+
async function remove(db, table, id) {
|
|
1063
|
+
assertDoor(db, table, "remove");
|
|
1064
|
+
const door = `remove ${qualified(table)}`;
|
|
1065
|
+
if (table.softDelete) {
|
|
1066
|
+
const row = check(door, await byPk(from(db, table).update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }), table, id).select("*").maybeSingle());
|
|
1067
|
+
return row ? "soft_deleted" : "not_found";
|
|
1068
|
+
}
|
|
1069
|
+
if (table.archive) throw new DoorRefusedError(door, "this table archives instead of deleting \u2014 use archive()");
|
|
1070
|
+
const rows = check(door, await byPk(from(db, table).delete(), table, id).select("*"));
|
|
1071
|
+
return rows && rows.length ? "deleted" : "not_found";
|
|
1072
|
+
}
|
|
1073
|
+
async function archive(db, table, id, opts = {}) {
|
|
1074
|
+
assertDoor(db, table, "archive");
|
|
1075
|
+
const door = `${opts.restore ? "restore" : "archive"} ${qualified(table)}`;
|
|
1076
|
+
let values;
|
|
1077
|
+
if (table.archive === "archived_at") values = { archived_at: opts.restore ? null : (/* @__PURE__ */ new Date()).toISOString() };
|
|
1078
|
+
else if (table.archive === "is_archived") values = { is_archived: !opts.restore };
|
|
1079
|
+
else if (table.softDelete && opts.restore) values = { deleted_at: null };
|
|
1080
|
+
else throw new DoorRefusedError(door, "the table has no archive column");
|
|
1081
|
+
return check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
|
|
1082
|
+
}
|
|
1083
|
+
var restore = (db, table, id) => archive(db, table, id, { restore: true });
|
|
1084
|
+
async function call(db, fn, args) {
|
|
1085
|
+
const door = `call ${fn.schema}.${fn.name}`;
|
|
1086
|
+
const role = db.role === "service" ? "service_role" : "authenticated";
|
|
1087
|
+
if (!fn.roles.includes(role) && !(role === "authenticated" && fn.roles.includes("anon"))) {
|
|
1088
|
+
throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(", ") || "nobody"})`);
|
|
1089
|
+
}
|
|
1090
|
+
return check(door, await db.client.schema(fn.schema).rpc(fn.name, args));
|
|
1091
|
+
}
|
|
1092
|
+
function tableByName(index, name) {
|
|
1093
|
+
const t = index[name];
|
|
1094
|
+
if (!t) throw new DoorRefusedError(`tableByName ${name}`, "no such table in the database description (schema-qualify it: schema.table)");
|
|
1095
|
+
return t;
|
|
1096
|
+
}
|
|
1097
|
+
function changesOf(table) {
|
|
1098
|
+
if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, "the table is not in the supabase_realtime publication");
|
|
1099
|
+
return { schema: table.schema, table: table.name };
|
|
1100
|
+
}
|
|
897
1101
|
export {
|
|
898
1102
|
BulkWriteError,
|
|
899
1103
|
DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE,
|
|
900
1104
|
DEFAULT_ACTIVE_ORG_COOKIE_NAME,
|
|
901
1105
|
DEFAULT_AUTH_COOKIE_NAME,
|
|
1106
|
+
DatabaseError,
|
|
1107
|
+
DoorRefusedError,
|
|
902
1108
|
IncompleteReadError,
|
|
903
1109
|
RecordUnavailableError,
|
|
904
1110
|
SessionUnavailableError,
|
|
905
1111
|
WriteDidNotLandError,
|
|
906
1112
|
WriteRefusedError,
|
|
1113
|
+
archive,
|
|
907
1114
|
asJsonObject,
|
|
1115
|
+
assertDoor,
|
|
908
1116
|
assertWriteLanded,
|
|
1117
|
+
call,
|
|
1118
|
+
changesOf,
|
|
1119
|
+
count,
|
|
909
1120
|
createActiveOrgCookie,
|
|
910
1121
|
createAuthCookie,
|
|
1122
|
+
createDb,
|
|
911
1123
|
createOAuthRefreshFetch,
|
|
1124
|
+
createServiceDb,
|
|
912
1125
|
createSessionRetry,
|
|
913
1126
|
decodeActiveOrgSelection,
|
|
914
1127
|
decodeOAuthRefreshClaims,
|
|
915
1128
|
describeWriteFailure,
|
|
916
1129
|
encodeActiveOrgSelection,
|
|
917
1130
|
guardedUpdate,
|
|
1131
|
+
insert,
|
|
918
1132
|
isMissingSessionError,
|
|
919
1133
|
isRecordUnavailableError,
|
|
1134
|
+
listAll,
|
|
920
1135
|
mergeJsonColumn,
|
|
1136
|
+
page,
|
|
921
1137
|
parseCookieHeader,
|
|
922
1138
|
personSentence,
|
|
1139
|
+
read,
|
|
923
1140
|
readAllRows,
|
|
924
1141
|
readAllRowsRest,
|
|
925
1142
|
recordUnavailableMessage,
|
|
1143
|
+
remove,
|
|
1144
|
+
restore,
|
|
926
1145
|
serverMessageFromBody,
|
|
1146
|
+
tableByName,
|
|
927
1147
|
tryReadAllRows,
|
|
928
1148
|
tryReadAllRowsRest,
|
|
929
1149
|
tryWriteOne,
|
|
930
1150
|
uniqueChannelTopic,
|
|
1151
|
+
update,
|
|
931
1152
|
writeFailureStatus,
|
|
932
1153
|
writeOne,
|
|
933
1154
|
writeOneRow
|