@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.
- package/CHANGELOG.md +32 -0
- package/README.md +34 -0
- package/bin/emit.mjs +32 -7
- package/bin/generate.mjs +38 -8
- package/bin/matrx-data.mjs +6 -4
- package/description/admin.json.gz +0 -0
- package/description/agent.json.gz +0 -0
- package/description/ai.json.gz +0 -0
- package/description/api.json.gz +0 -0
- package/description/app.json.gz +0 -0
- package/description/assignment.json.gz +0 -0
- package/description/audit.json.gz +0 -0
- package/description/batch.json.gz +0 -0
- package/description/billing.json.gz +0 -0
- package/description/browser.json.gz +0 -0
- package/description/canvas.json.gz +0 -0
- package/description/chat.json.gz +0 -0
- package/description/code.json.gz +0 -0
- package/description/commerce.json.gz +0 -0
- package/description/communication.json.gz +0 -0
- package/description/content.json.gz +0 -0
- package/description/content_ir.json.gz +0 -0
- package/description/context.json.gz +0 -0
- package/description/crm.json.gz +0 -0
- package/description/custom.json.gz +0 -0
- package/description/dictionary.json.gz +0 -0
- package/description/docproc.json.gz +0 -0
- package/description/education.json.gz +0 -0
- package/description/esign.json.gz +0 -0
- package/description/extend.json.gz +0 -0
- package/description/files.json.gz +0 -0
- package/description/growth.json.gz +0 -0
- package/description/hindsight.json.gz +0 -0
- package/description/history.json.gz +0 -0
- package/description/iam.json.gz +0 -0
- package/description/index.json.gz +0 -0
- package/description/interview.json.gz +0 -0
- package/description/knowledge.json.gz +0 -0
- package/description/legal.json.gz +0 -0
- package/description/mandate.json.gz +0 -0
- package/description/marketing.json.gz +0 -0
- package/description/media.json.gz +0 -0
- package/description/meta.json.gz +0 -0
- package/description/ops.json.gz +0 -0
- package/description/pdf.json.gz +0 -0
- package/description/plan.json.gz +0 -0
- package/description/platform.json.gz +0 -0
- package/description/podcast.json.gz +0 -0
- package/description/projects.json.gz +0 -0
- package/description/public.json.gz +0 -0
- package/description/rag.json.gz +0 -0
- package/description/research.json.gz +0 -0
- package/description/runtime.json.gz +0 -0
- package/description/scheduler.json.gz +0 -0
- package/description/scraper.json.gz +0 -0
- package/description/seo.json.gz +0 -0
- package/description/skill.json.gz +0 -0
- package/description/social.json.gz +0 -0
- package/description/tool.json.gz +0 -0
- package/description/transcripts.json.gz +0 -0
- package/description/ui.json.gz +0 -0
- package/description/users.json.gz +0 -0
- package/description/web.json.gz +0 -0
- package/description/work.json.gz +0 -0
- package/description/workbench.json.gz +0 -0
- package/description/workflow.json.gz +0 -0
- package/dist/db.cjs +71 -29
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +92 -33
- package/dist/db.d.ts +92 -33
- package/dist/db.js +71 -29
- package/dist/db.js.map +1 -1
- 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
|
-
|
|
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:
|
|
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
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
return `/** GENERATED by \`matrx-data generate --flags-index\` —
|
|
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
|
|
171
|
+
export const flags: FlagsIndex = ${JSON.stringify(index)} as unknown as FlagsIndex;
|
|
168
172
|
`;
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
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)
|
|
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) {
|
package/bin/matrx-data.mjs
CHANGED
|
@@ -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 --
|
|
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 (!
|
|
29
|
-
console.error("matrx-data generate: --
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1145
|
-
return
|
|
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
|
|
1149
|
-
|
|
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
|
|
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 };
|