@ai-matrx/data 0.23.0 → 0.24.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 +20 -0
- package/README.md +34 -0
- package/bin/emit.mjs +32 -7
- package/bin/generate.mjs +37 -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 +62 -28
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +83 -33
- package/dist/db.d.ts +83 -33
- package/dist/db.js +62 -28
- package/dist/db.js.map +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 — doors hardened (independent review)
|
|
4
|
+
|
|
5
|
+
- **Every write on a versioned table is guarded.** `remove` (and `archive` / `restore`) take the same
|
|
6
|
+
`{ expectedVersion }` / `{ overwrite: true }` guard as `update`; `expectedVersion` on an UNversioned
|
|
7
|
+
table is refused (it used to be ignored silently). `remove` returns `{ status }` (or a conflict).
|
|
8
|
+
- **Reads exclude soft-deleted rows** by default (`read`, `listAll`, `page`, `count`); `includeDeleted: true` opts in.
|
|
9
|
+
- **`listAll` / `page` order by the full primary key** (stable paging on composite keys).
|
|
10
|
+
- **Types:** ops infer `TableDescriptor<Row, Insert, Update>` directly, so an insert error names the
|
|
11
|
+
table's Insert type; `read`/`listAll`/`page` take `columns: [...]` and type the result to them; the
|
|
12
|
+
`Where` callback receives a typed `Filter<Row>` (column names and value types checked).
|
|
13
|
+
- **Pay-only runtime tables:** `--flags-index` writes `<schema>.flags.ts` per schema plus
|
|
14
|
+
`flags-index.ts` loaders; `loadTableByName(flagLoaders, "schema.table")` loads only that schema.
|
|
15
|
+
- **The description ships in the package** (`description/*.json.gz`, 0.5 MB): `matrx-data generate`
|
|
16
|
+
needs no repository or database access (`--from` still takes a fresher one). README documents it.
|
|
17
|
+
- `matrx-data emit` accounts for every callable-door register row: `index.json` →
|
|
18
|
+
`report.registeredDoorsOutsideDescription` by reason, and the emit fails if any row is unexplained.
|
|
19
|
+
|
|
20
|
+
Consumer action: `remove(db, t, id)` on a versioned table now needs a guard and returns `{ status }`;
|
|
21
|
+
`read(db, t, id, "cols")` becomes `read(db, t, id, { columns: [...] })`. No consumer used 0.23.0's doors yet.
|
|
22
|
+
|
|
3
23
|
## 0.23.0 — the doors: one way to talk to the database
|
|
4
24
|
|
|
5
25
|
- **`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(
|
|
@@ -157,29 +159,56 @@ export function renderSchema(desc, schemaSha, importFrom) {
|
|
|
157
159
|
return out;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
162
|
+
/** One schema's flags (`<schema>.flags.ts`) — loaded only when a runtime name names that schema. */
|
|
163
|
+
export function renderSchemaFlags(desc, importFrom) {
|
|
161
164
|
const index = {};
|
|
162
|
-
for (const
|
|
163
|
-
return `/** GENERATED by \`matrx-data generate --flags-index\` —
|
|
165
|
+
for (const [name, t] of Object.entries({ ...desc.tables, ...desc.views })) index[`${desc.schema}.${name}`] = tableDescriptor(desc.schema, name, t);
|
|
166
|
+
return `/** GENERATED by \`matrx-data generate --flags-index\` — ${desc.schema}'s table flags for tableByName(). Never edit by hand. */
|
|
164
167
|
/* eslint-disable */
|
|
165
168
|
import type { FlagsIndex } from "${importFrom}";
|
|
166
169
|
|
|
167
|
-
export const
|
|
170
|
+
export const flags: FlagsIndex = ${JSON.stringify(index)} as unknown as FlagsIndex;
|
|
168
171
|
`;
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
/** Per-schema loaders: `loadTableByName(flagLoaders, "schema.table")` imports only that schema's flags. */
|
|
175
|
+
export function renderFlagLoaders(schemas, importFrom) {
|
|
176
|
+
return `/** GENERATED by \`matrx-data generate --flags-index\` — never edit by hand. */
|
|
177
|
+
/* eslint-disable */
|
|
178
|
+
import type { FlagLoaders } from "${importFrom}";
|
|
179
|
+
|
|
180
|
+
export const flagLoaders: FlagLoaders = {
|
|
181
|
+
${schemas.map((s) => ` ${key(s)}: () => import("./${s}.flags").then((m) => m.flags),`).join("\n")}
|
|
182
|
+
};
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The description ships inside @ai-matrx/data (gzipped); `--from` points at a fresher one. */
|
|
187
|
+
export const BUNDLED_DESCRIPTION = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "description");
|
|
188
|
+
|
|
189
|
+
function readDescriptionFile(dir, name) {
|
|
190
|
+
const plain = path.join(dir, name);
|
|
191
|
+
if (existsSync(plain)) return readFileSync(plain, "utf8");
|
|
192
|
+
const gz = `${plain}.gz`;
|
|
193
|
+
if (existsSync(gz)) return gunzipSync(readFileSync(gz)).toString("utf8");
|
|
194
|
+
throw new Error(`${name} not found in ${dir} (neither ${name} nor ${name}.gz)`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function generate({ from = BUNDLED_DESCRIPTION, out, schemas, check, importFrom, flagsIndex = process.argv.includes("--flags-index") }) {
|
|
198
|
+
const index = JSON.parse(readDescriptionFile(from, "index.json"));
|
|
173
199
|
const names = schemas?.length ? schemas : Object.keys(index.schemas).sort();
|
|
174
200
|
const files = {};
|
|
175
201
|
const descs = [];
|
|
176
202
|
for (const s of names) {
|
|
177
203
|
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(
|
|
204
|
+
const desc = JSON.parse(readDescriptionFile(from, `${s}.json`));
|
|
179
205
|
descs.push(desc);
|
|
180
206
|
files[`${s}.ts`] = renderSchema(desc, index.schemas[s].sha256, importFrom);
|
|
181
207
|
}
|
|
182
|
-
if (flagsIndex)
|
|
208
|
+
if (flagsIndex) {
|
|
209
|
+
for (const d of descs) files[`${d.schema}.flags.ts`] = renderSchemaFlags(d, importFrom);
|
|
210
|
+
files["flags-index.ts"] = renderFlagLoaders(descs.map((d) => d.schema), importFrom);
|
|
211
|
+
}
|
|
183
212
|
if (check) {
|
|
184
213
|
const stale = Object.entries(files).filter(([n, t]) => !existsSync(path.join(out, n)) || readFileSync(path.join(out, n), "utf8") !== t).map(([n]) => n);
|
|
185
214
|
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,26 @@ 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 q
|
|
1077
|
+
return ordered(q, table, opts.orderBy).range(lo, hi);
|
|
1069
1078
|
},
|
|
1070
1079
|
{
|
|
1071
1080
|
label: `listAll ${qualified(table)}`,
|
|
@@ -1076,14 +1085,14 @@ async function listAll(db, table, where, opts = {}) {
|
|
|
1076
1085
|
}
|
|
1077
1086
|
async function page(db, table, window2) {
|
|
1078
1087
|
assertDoor(db, table, "read");
|
|
1079
|
-
let q = from(db, table).select(window2.columns
|
|
1088
|
+
let q = live(from(db, table).select(selectOf(window2.columns), { count: "exact" }), table, window2.includeDeleted);
|
|
1080
1089
|
if (window2.where) q = window2.where(q);
|
|
1081
|
-
const res = await q
|
|
1090
|
+
const res = await ordered(q, table, window2.orderBy, window2.ascending ?? true).range(window2.from, window2.to);
|
|
1082
1091
|
return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };
|
|
1083
1092
|
}
|
|
1084
|
-
async function count(db, table, where) {
|
|
1093
|
+
async function count(db, table, where, opts = {}) {
|
|
1085
1094
|
assertDoor(db, table, "read");
|
|
1086
|
-
let q = from(db, table).select("*", { count: "exact", head: true });
|
|
1095
|
+
let q = live(from(db, table).select("*", { count: "exact", head: true }), table, opts.includeDeleted);
|
|
1087
1096
|
if (where) q = where(q);
|
|
1088
1097
|
const res = await q;
|
|
1089
1098
|
check(`count ${qualified(table)}`, res);
|
|
@@ -1116,6 +1125,24 @@ async function insert(db, table, values) {
|
|
|
1116
1125
|
const stored = check(`insert ${qualified(table)}`, res);
|
|
1117
1126
|
return many ? stored : stored[0];
|
|
1118
1127
|
}
|
|
1128
|
+
async function writeRow(db, table, id, values, door, guard) {
|
|
1129
|
+
const expected = guard && "expectedVersion" in guard ? guard.expectedVersion : void 0;
|
|
1130
|
+
if (expected !== void 0 && !table.versioned) {
|
|
1131
|
+
throw new DoorRefusedError(door, "expectedVersion on a table that is not versioned \u2014 nothing would guard it; pass no guard");
|
|
1132
|
+
}
|
|
1133
|
+
if (table.versioned && expected === void 0 && !(guard && "overwrite" in guard && guard.overwrite)) {
|
|
1134
|
+
throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
|
|
1135
|
+
}
|
|
1136
|
+
if (expected !== void 0) {
|
|
1137
|
+
return await guardedUpdate({
|
|
1138
|
+
expectedVersion: expected,
|
|
1139
|
+
applyUpdate: ({ expectedVersion, nextVersion }) => byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq("version", expectedVersion).select("*").maybeSingle(),
|
|
1140
|
+
fetchCurrent: () => byPk(from(db, table).select("*"), table, id).maybeSingle()
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const row = check(door, await byPk(from(db, table).update(values), table, id).select("*").maybeSingle());
|
|
1144
|
+
return row ? { status: "saved", row } : { status: "not_found" };
|
|
1145
|
+
}
|
|
1119
1146
|
async function update(db, table, id, patch, guard) {
|
|
1120
1147
|
assertDoor(db, table, "update");
|
|
1121
1148
|
const door = `update ${qualified(table)}`;
|
|
@@ -1124,29 +1151,30 @@ async function update(db, table, id, patch, guard) {
|
|
|
1124
1151
|
const blocked = table.readOnlyColumns.filter((c) => c in values);
|
|
1125
1152
|
if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(", ")}`);
|
|
1126
1153
|
}
|
|
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" };
|
|
1154
|
+
return writeRow(db, table, id, values, door, guard);
|
|
1139
1155
|
}
|
|
1140
|
-
async function remove(db, table, id) {
|
|
1156
|
+
async function remove(db, table, id, guard) {
|
|
1141
1157
|
assertDoor(db, table, "remove");
|
|
1142
1158
|
const door = `remove ${qualified(table)}`;
|
|
1143
1159
|
if (table.softDelete) {
|
|
1144
|
-
const
|
|
1145
|
-
return
|
|
1160
|
+
const res = await writeRow(db, table, id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() }, door, guard);
|
|
1161
|
+
return res.status === "saved" ? { status: "soft_deleted" } : res.status === "conflict" ? { status: "conflict", currentVersion: res.currentVersion } : res;
|
|
1146
1162
|
}
|
|
1147
1163
|
if (table.archive) throw new DoorRefusedError(door, "this table archives instead of deleting \u2014 use archive()");
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1164
|
+
const expected = guard && "expectedVersion" in guard ? guard.expectedVersion : void 0;
|
|
1165
|
+
if (expected !== void 0 && !table.versioned) throw new DoorRefusedError(door, "expectedVersion on a table that is not versioned");
|
|
1166
|
+
if (table.versioned && expected === void 0 && !(guard && "overwrite" in guard)) {
|
|
1167
|
+
throw new DoorRefusedError(door, "a versioned table takes { expectedVersion } or { overwrite: true }");
|
|
1168
|
+
}
|
|
1169
|
+
let q = byPk(from(db, table).delete(), table, id);
|
|
1170
|
+
if (expected !== void 0) q = q.eq("version", expected);
|
|
1171
|
+
const rows = check(door, await q.select("*"));
|
|
1172
|
+
if (rows && rows.length) return { status: "deleted" };
|
|
1173
|
+
if (expected !== void 0) {
|
|
1174
|
+
const current = check(door, await byPk(from(db, table).select("version"), table, id).maybeSingle());
|
|
1175
|
+
if (current) return { status: "conflict", currentVersion: current.version };
|
|
1176
|
+
}
|
|
1177
|
+
return { status: "not_found" };
|
|
1150
1178
|
}
|
|
1151
1179
|
async function archive(db, table, id, opts = {}) {
|
|
1152
1180
|
assertDoor(db, table, "archive");
|
|
@@ -1156,9 +1184,9 @@ async function archive(db, table, id, opts = {}) {
|
|
|
1156
1184
|
else if (table.archive === "is_archived") values = { is_archived: !opts.restore };
|
|
1157
1185
|
else if (table.softDelete && opts.restore) values = { deleted_at: null };
|
|
1158
1186
|
else throw new DoorRefusedError(door, "the table has no archive column");
|
|
1159
|
-
return
|
|
1187
|
+
return writeRow(db, table, id, values, door, opts.guard);
|
|
1160
1188
|
}
|
|
1161
|
-
var restore = (db, table, id) => archive(db, table, id, { restore: true });
|
|
1189
|
+
var restore = (db, table, id, guard) => archive(db, table, id, guard ? { restore: true, guard } : { restore: true });
|
|
1162
1190
|
async function call(db, fn, args) {
|
|
1163
1191
|
const door = `call ${fn.schema}.${fn.name}`;
|
|
1164
1192
|
const role = db.role === "service" ? "service_role" : "authenticated";
|
|
@@ -1172,6 +1200,12 @@ function tableByName(index, name) {
|
|
|
1172
1200
|
if (!t) throw new DoorRefusedError(`tableByName ${name}`, "no such table in the database description (schema-qualify it: schema.table)");
|
|
1173
1201
|
return t;
|
|
1174
1202
|
}
|
|
1203
|
+
async function loadTableByName(loaders, name) {
|
|
1204
|
+
const schema = name.slice(0, name.indexOf("."));
|
|
1205
|
+
const load = schema ? loaders[schema] : void 0;
|
|
1206
|
+
if (!load) throw new DoorRefusedError(`tableByName ${name}`, "no such schema in the database description (schema-qualify it: schema.table)");
|
|
1207
|
+
return tableByName(await load(), name);
|
|
1208
|
+
}
|
|
1175
1209
|
function changesOf(table) {
|
|
1176
1210
|
if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, "the table is not in the supabase_realtime publication");
|
|
1177
1211
|
return { schema: table.schema, table: table.name };
|