dzql 0.8.0 → 0.10.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/README.md +29 -13
- package/docs/README.md +6 -0
- package/docs/for_ai.md +6 -0
- package/docs/project-setup.md +6 -0
- package/package.json +11 -7
- package/src/cli/index.ts +7 -0
- package/src/compile/cascades.ts +151 -0
- package/src/compile/columns.ts +144 -0
- package/src/compile/holders.ts +72 -0
- package/src/compile/holds.ts +91 -0
- package/src/compile/ledger.ts +73 -0
- package/src/compile/opens.ts +363 -0
- package/src/compile/resolve.ts +75 -0
- package/src/compile/root.ts +61 -0
- package/src/compile/rows.ts +122 -0
- package/src/compile/scope.ts +87 -0
- package/src/compile/told.ts +94 -0
- package/src/postgres/auth-jwt-sql.ts +1 -1
- package/src/postgres/auth-jwt.ts +2 -2
- package/src/postgres/bootstrap.ts +2 -2
- package/src/postgres/cli.ts +339 -0
- package/src/postgres/codegen.ts +14 -1
- package/src/postgres/index.ts +2 -2
- package/src/postgres/pglite.ts +1 -1
- package/src/postgres/registry.ts +11 -2
- package/src/sql/001a-delta-meta.sql +8 -1
- package/src/sql/001b-delta-scope.sql +32 -5
- package/src/sql/001c-delta-read.sql +10 -11
- package/src/sql/001d-delta-write.sql +212 -123
- package/src/sql/001g-delta-ledger.sql +67 -23
- package/src/sql/auth-jwt.sql +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled row SQL: the statements a write runs on a row, written for each
|
|
3
|
+
* collection. The framework builds each from the table's name at run time
|
|
4
|
+
* (`EXECUTE format(...)`), planned again on every call; written out, each is a
|
|
5
|
+
* statement whose plan the database keeps.
|
|
6
|
+
*
|
|
7
|
+
* `compileRows(schema)` writes, for every collection of the schema:
|
|
8
|
+
* - `_delta_row_lock`: the row as it stands (a temporal one's open version),
|
|
9
|
+
* locked for the write;
|
|
10
|
+
* - `_delta_row_put`: a replace's row stored -- a temporal one a new version
|
|
11
|
+
* (one this write made gives way to it, the standing one closes), else the
|
|
12
|
+
* row again -- answered as stored;
|
|
13
|
+
* - `_delta_next_id`: an add's id, from the collection's sequence;
|
|
14
|
+
* - `_delta_row_clear_moment`: a temporal version this write made and took
|
|
15
|
+
* back, so an add of its id takes its place;
|
|
16
|
+
* - `_delta_row_there`: is the row there now (a temporal one's open version)?
|
|
17
|
+
* - `_delta_row_read`: the row as it stands, without its validity;
|
|
18
|
+
* - `_delta_row_add`: an add's row stored. The framework's INSERT names the
|
|
19
|
+
* columns the row gives (`_delta_insert_sql`), so one it leaves out takes its
|
|
20
|
+
* database default: where the row gives exactly every column the schema
|
|
21
|
+
* declares (its id, parent key, columns and validity), that INSERT is one
|
|
22
|
+
* statement, written here; a row with any other keys is the framework's.
|
|
23
|
+
*
|
|
24
|
+
* Each answers what the framework's (`_framework`) answers, and sends any
|
|
25
|
+
* collection it was not compiled for there.
|
|
26
|
+
*/
|
|
27
|
+
import { q, type Schema } from "@blueshed/delta/backend";
|
|
28
|
+
import { lit, standingRows } from "./opens";
|
|
29
|
+
|
|
30
|
+
/** A CASE over the collections, each branch from `branch`, any other collection to `fallback`. */
|
|
31
|
+
function dispatch(schema: Schema, branch: (coll: string, table: Schema["tables"][string]) => string | null, fallback: string): string {
|
|
32
|
+
const branches = Object.entries(schema.tables).flatMap(([coll, table]) => {
|
|
33
|
+
const b = branch(coll, table);
|
|
34
|
+
return b === null ? [] : [` WHEN ${lit(coll)} THEN\n ${b}`];
|
|
35
|
+
});
|
|
36
|
+
return branches.length ? ` CASE p_coll\n${branches.join("\n")}\n ELSE ${fallback}\n END CASE;` : ` ${fallback}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The row SQL of every collection of `schema`, and the dispatch to the framework's for any other. */
|
|
40
|
+
export function compileRows(schema: Schema): string {
|
|
41
|
+
const lock = `CREATE OR REPLACE FUNCTION _delta_row_lock(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT)
|
|
42
|
+
RETURNS JSONB AS $$
|
|
43
|
+
DECLARE
|
|
44
|
+
v_row JSONB;
|
|
45
|
+
BEGIN
|
|
46
|
+
${dispatch(schema, (_, t) => `SELECT to_jsonb(t.*) INTO v_row FROM ${q(t.name)} t WHERE t.id = p_id${t.temporal ? " AND valid_to IS NULL" : ""} FOR UPDATE;
|
|
47
|
+
RETURN v_row;`, "RETURN _delta_row_lock_framework(p_coll, p_table, p_temporal, p_id);")}
|
|
48
|
+
END;
|
|
49
|
+
$$ LANGUAGE plpgsql;`;
|
|
50
|
+
|
|
51
|
+
const put = `CREATE OR REPLACE FUNCTION _delta_row_put(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT, p_row JSONB, p_ts TIMESTAMPTZ)
|
|
52
|
+
RETURNS JSONB AS $$
|
|
53
|
+
DECLARE
|
|
54
|
+
v_row JSONB;
|
|
55
|
+
BEGIN
|
|
56
|
+
${dispatch(schema, (_, t) => (t.temporal
|
|
57
|
+
? `DELETE FROM ${q(t.name)} WHERE id = p_id AND valid_to IS NULL AND valid_from = p_ts;
|
|
58
|
+
UPDATE ${q(t.name)} SET valid_to = p_ts WHERE id = p_id AND valid_to IS NULL;
|
|
59
|
+
INSERT INTO ${q(t.name)} AS t SELECT * FROM jsonb_populate_record(null::${q(t.name)}, p_row || jsonb_build_object('valid_from', p_ts, 'valid_to', NULL))
|
|
60
|
+
RETURNING to_jsonb(t.*) INTO v_row;
|
|
61
|
+
RETURN v_row;`
|
|
62
|
+
: `DELETE FROM ${q(t.name)} WHERE id = p_id;
|
|
63
|
+
INSERT INTO ${q(t.name)} AS t SELECT * FROM jsonb_populate_record(null::${q(t.name)}, p_row) RETURNING to_jsonb(t.*) INTO v_row;
|
|
64
|
+
RETURN v_row;`), "RETURN _delta_row_put_framework(p_coll, p_table, p_temporal, p_id, p_row, p_ts);")}
|
|
65
|
+
END;
|
|
66
|
+
$$ LANGUAGE plpgsql;`;
|
|
67
|
+
|
|
68
|
+
// the sequence named as the framework names it: nextval of the text 'seq_<table>'
|
|
69
|
+
const next = `CREATE OR REPLACE FUNCTION _delta_next_id(p_coll TEXT, p_table TEXT)
|
|
70
|
+
RETURNS BIGINT AS $$
|
|
71
|
+
BEGIN
|
|
72
|
+
${dispatch(schema, (_, t) => `RETURN nextval(${lit(`seq_${t.name}`)});`, "RETURN _delta_next_id_framework(p_coll, p_table);")}
|
|
73
|
+
END;
|
|
74
|
+
$$ LANGUAGE plpgsql;`;
|
|
75
|
+
|
|
76
|
+
const clear = `CREATE OR REPLACE FUNCTION _delta_row_clear_moment(p_coll TEXT, p_table TEXT, p_id BIGINT, p_ts TIMESTAMPTZ)
|
|
77
|
+
RETURNS void AS $$
|
|
78
|
+
BEGIN
|
|
79
|
+
${dispatch(schema, (_, t) => (t.temporal ? `DELETE FROM ${q(t.name)} WHERE id = p_id AND valid_from = p_ts AND valid_to = p_ts;
|
|
80
|
+
RETURN;` : null), "PERFORM _delta_row_clear_moment_framework(p_coll, p_table, p_id, p_ts);")}
|
|
81
|
+
END;
|
|
82
|
+
$$ LANGUAGE plpgsql;`;
|
|
83
|
+
|
|
84
|
+
const there = `CREATE OR REPLACE FUNCTION _delta_row_there(p_collection_key TEXT, p_id BIGINT)
|
|
85
|
+
RETURNS BOOLEAN AS $$
|
|
86
|
+
DECLARE
|
|
87
|
+
p_coll TEXT := p_collection_key;
|
|
88
|
+
BEGIN
|
|
89
|
+
${dispatch(schema, (_, t) => `RETURN p_id IS NOT NULL AND EXISTS (SELECT 1 FROM ${standingRows(t)} WHERE id = p_id);`,
|
|
90
|
+
"RETURN _delta_row_there_framework(p_collection_key, p_id);")}
|
|
91
|
+
END;
|
|
92
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
93
|
+
|
|
94
|
+
const read = `CREATE OR REPLACE FUNCTION _delta_row_read(p_coll TEXT, p_view TEXT, p_id BIGINT)
|
|
95
|
+
RETURNS JSONB AS $$
|
|
96
|
+
DECLARE
|
|
97
|
+
v_row JSONB;
|
|
98
|
+
BEGIN
|
|
99
|
+
${dispatch(schema, (_, t) => `SELECT _delta_strip_temporal(to_jsonb(t.*)) INTO v_row FROM ${standingRows(t)} t WHERE t.id = p_id;
|
|
100
|
+
RETURN v_row;`, "RETURN _delta_row_read_framework(p_coll, p_view, p_id);")}
|
|
101
|
+
END;
|
|
102
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
103
|
+
|
|
104
|
+
const add = `CREATE OR REPLACE FUNCTION _delta_row_add(p_coll TEXT, p_table TEXT, p_row JSONB)
|
|
105
|
+
RETURNS JSONB AS $$
|
|
106
|
+
DECLARE
|
|
107
|
+
v_row JSONB;
|
|
108
|
+
BEGIN
|
|
109
|
+
${dispatch(schema, (_, t) => {
|
|
110
|
+
const keys = [...new Set(["id", ...(t.parent ? [t.parent.fkColumn] : []), ...Object.keys(t.columns), ...(t.temporal ? ["valid_from", "valid_to"] : [])])];
|
|
111
|
+
const cols = keys.map((k) => q(k)).join(", ");
|
|
112
|
+
return `IF p_row ?& ARRAY[${keys.map(lit).join(", ")}] AND (SELECT count(*) FROM jsonb_object_keys(p_row)) = ${keys.length} THEN
|
|
113
|
+
INSERT INTO ${q(t.name)} AS t (${cols}) SELECT ${cols} FROM jsonb_populate_record(null::${q(t.name)}, p_row) RETURNING to_jsonb(t.*) INTO v_row;
|
|
114
|
+
RETURN v_row;
|
|
115
|
+
END IF;
|
|
116
|
+
RETURN _delta_row_add_framework(p_coll, p_table, p_row);`;
|
|
117
|
+
}, "RETURN _delta_row_add_framework(p_coll, p_table, p_row);")}
|
|
118
|
+
END;
|
|
119
|
+
$$ LANGUAGE plpgsql;`;
|
|
120
|
+
|
|
121
|
+
return [lock, put, next, clear, there, read, add, ""].join("\n\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled write scope: may a document write a row? "You may write what you
|
|
3
|
+
* may read": every row-addressed op in a write is asked first (`delta_apply`,
|
|
4
|
+
* `_delta_row_in_scope`), and a row it may not read is answered as one that is
|
|
5
|
+
* not there.
|
|
6
|
+
*
|
|
7
|
+
* `compileInScope(schema, docs)` writes `dzql_in_scope__<slug>(name,
|
|
8
|
+
* collection, id)` for each document, answering what the framework's
|
|
9
|
+
* `_delta_row_in_scope` answers, from its definition: a list document holds
|
|
10
|
+
* its included collections whole and its root rows by the name's conditions;
|
|
11
|
+
* a single document holds a row whose parent chain, walked up through the
|
|
12
|
+
* tables as they stand, reaches its root, which meets the rest of its name's
|
|
13
|
+
* scope. And `_delta_row_in_scope` itself, which sends each compiled document
|
|
14
|
+
* to its own and any other to the framework (`_delta_row_in_scope_framework`).
|
|
15
|
+
*/
|
|
16
|
+
import { q, type DocDef, type Schema } from "@blueshed/delta/backend";
|
|
17
|
+
import { lit, named, need, scopeOf , currentOnly } from "./opens";
|
|
18
|
+
|
|
19
|
+
/** A document's write-scope function: `dzql_in_scope__<slug>`. */
|
|
20
|
+
export const inScopeFunction = (prefix: string): string => named("in_scope", prefix);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/** One document's write-scope function. */
|
|
24
|
+
function inScopeOf(schema: Schema, doc: DocDef): string {
|
|
25
|
+
const root = need(schema, doc, doc.root);
|
|
26
|
+
const scope = scopeOf(schema, doc); // a value its columns cannot take: refused, as the framework's resolver refuses it
|
|
27
|
+
const conds = scope.conds ? ` AND ${scope.conds}` : "";
|
|
28
|
+
// the root row of a single document: its id, and (where the name scopes more than the id) the rest of its scope
|
|
29
|
+
const others = Object.keys(doc.scope ?? {}).some((k) => k !== "id");
|
|
30
|
+
const rootMeets = (id: string) => others
|
|
31
|
+
? `COALESCE(${id} = v_id, FALSE) AND EXISTS (SELECT 1 FROM ${q(root.name)} t WHERE t.id = v_id${currentOnly(root, "t")}${conds})`
|
|
32
|
+
: `COALESCE(${id} = v_id, FALSE)`;
|
|
33
|
+
const included = new Set(doc.include);
|
|
34
|
+
|
|
35
|
+
const singles = Object.entries(schema.tables).map(([coll, table]) => {
|
|
36
|
+
if (coll === doc.root) return ` WHEN ${lit(coll)} THEN RETURN ${rootMeets("p_id")};`;
|
|
37
|
+
// unparented: held whole where included; else it ends a chain short of the root
|
|
38
|
+
if (!table.parent) return ` WHEN ${lit(coll)} THEN RETURN ${included.has(coll) ? "TRUE" : "FALSE"};`;
|
|
39
|
+
let id = "p_id";
|
|
40
|
+
let t = table;
|
|
41
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
42
|
+
id = `(SELECT x.${q(t.parent!.fkColumn)} FROM ${q(t.name)} x WHERE x.id = ${id}${currentOnly(t, "x")})`;
|
|
43
|
+
const parentKey = t.parent!.collection;
|
|
44
|
+
if (parentKey === doc.root) return ` WHEN ${lit(coll)} THEN RETURN ${rootMeets(id)};`;
|
|
45
|
+
const parent = schema.tables[parentKey];
|
|
46
|
+
if (!parent?.parent) return ` WHEN ${lit(coll)} THEN RETURN FALSE;`; // the chain ends short of the root
|
|
47
|
+
t = parent;
|
|
48
|
+
}
|
|
49
|
+
return ` WHEN ${lit(coll)} THEN RETURN FALSE;`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return `CREATE OR REPLACE FUNCTION ${inScopeFunction(doc.prefix)}(p_name TEXT, p_coll TEXT, p_id BIGINT)
|
|
53
|
+
RETURNS BOOLEAN AS $$
|
|
54
|
+
DECLARE
|
|
55
|
+
p_at TIMESTAMPTZ;
|
|
56
|
+
v_single BOOLEAN;
|
|
57
|
+
v_id BIGINT;
|
|
58
|
+
${scope.declare.join("\n ")}
|
|
59
|
+
BEGIN
|
|
60
|
+
IF p_id IS NULL THEN RETURN FALSE; END IF;
|
|
61
|
+
${scope.body}
|
|
62
|
+
IF NOT v_single THEN
|
|
63
|
+
-- a list holds its included collections whole; its root rows by the name's conditions
|
|
64
|
+
IF p_coll IS DISTINCT FROM ${lit(doc.root)} THEN RETURN TRUE; END IF;
|
|
65
|
+
RETURN EXISTS (SELECT 1 FROM ${q(root.name)} t WHERE t.id = p_id${currentOnly(root, "t")}${conds});
|
|
66
|
+
END IF;
|
|
67
|
+
CASE p_coll
|
|
68
|
+
${singles.join("\n")}
|
|
69
|
+
ELSE RETURN FALSE;
|
|
70
|
+
END CASE;
|
|
71
|
+
END;
|
|
72
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The SQL that answers a write's scope for each of `docs`: a function per document, and `_delta_row_in_scope` sending each to its own. */
|
|
76
|
+
export function compileInScope(schema: Schema, docs: DocDef[]): string {
|
|
77
|
+
const dispatch = `CREATE OR REPLACE FUNCTION _delta_row_in_scope(p_def _delta_docs, p_doc_name TEXT, p_collection_key TEXT, p_id BIGINT)
|
|
78
|
+
RETURNS BOOLEAN AS $$
|
|
79
|
+
BEGIN
|
|
80
|
+
CASE p_def.prefix
|
|
81
|
+
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${inScopeFunction(d.prefix)}(p_doc_name, p_collection_key, p_id);`).join("\n")}
|
|
82
|
+
ELSE RETURN _delta_row_in_scope_framework(p_def, p_doc_name, p_collection_key, p_id);
|
|
83
|
+
END CASE;
|
|
84
|
+
END;
|
|
85
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
86
|
+
return [...docs.map((d) => inScopeOf(schema, d)), dispatch, ""].join("\n\n");
|
|
87
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled telling: what a document is told of a write. Asked by `_delta_tell`
|
|
3
|
+
* of the writer and of every document that held a row the write touched, or
|
|
4
|
+
* holds it now, with the write's touches and their holders.
|
|
5
|
+
*
|
|
6
|
+
* `compileTold(schema, docs)` writes `dzql_told__<slug>(name, rows)` for each
|
|
7
|
+
* document, answering what the framework's `_delta_told` answers, from its
|
|
8
|
+
* definition: whether the name makes it one row (`scopeOf`), its root, the
|
|
9
|
+
* columns it shows of each collection it names them for (`columns`, the row's
|
|
10
|
+
* id and parent key with them), and the empty root an implied one opens with
|
|
11
|
+
* when its root leaves. And `_delta_told` itself, which sends each compiled
|
|
12
|
+
* document to its own and any other to the framework (`_delta_told_framework`).
|
|
13
|
+
*/
|
|
14
|
+
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
15
|
+
import { lit, named, need, scopeOf } from "./opens";
|
|
16
|
+
|
|
17
|
+
/** A document's telling function: `dzql_told__<slug>`. */
|
|
18
|
+
export const toldFunction = (prefix: string): string => named("told", prefix);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A row (SQL `row`) of collection (SQL `coll`) as the document shows it, as
|
|
22
|
+
* `_delta_show` does: where it names the collection's columns, those, the id
|
|
23
|
+
* and the parent key; else whole.
|
|
24
|
+
*/
|
|
25
|
+
function shown(schema: Schema, doc: DocDef, coll: string, row: string): string {
|
|
26
|
+
const named = Object.entries(doc.columns ?? {});
|
|
27
|
+
if (named.length === 0) return row;
|
|
28
|
+
const branches = named.map(([c, cols]) => {
|
|
29
|
+
const fk = schema.tables[c]?.parent?.fkColumn;
|
|
30
|
+
const keep = [...new Set(["id", ...(fk ? [fk] : []), ...cols])].map(lit).join(", ");
|
|
31
|
+
return `WHEN ${lit(c)} THEN COALESCE((SELECT jsonb_object_agg(f, v) FROM jsonb_each(${row}) AS e(f, v) WHERE f IN (${keep})), '{}'::jsonb)`;
|
|
32
|
+
});
|
|
33
|
+
return `(CASE WHEN jsonb_typeof(${row}) IS DISTINCT FROM 'object' THEN ${row} ELSE CASE ${coll} ${branches.join(" ")} ELSE ${row} END END)`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One document's telling function. */
|
|
37
|
+
function toldOf(schema: Schema, doc: DocDef): string {
|
|
38
|
+
need(schema, doc, doc.root);
|
|
39
|
+
const scope = scopeOf(schema, doc); // a name its columns cannot take: refused, as the framework's resolver refuses it
|
|
40
|
+
const after = shown(schema, doc, "v_t->>'coll'", "v_t->'after'");
|
|
41
|
+
const gone = doc.implied
|
|
42
|
+
? shown(schema, doc, "v_t->>'coll'", `_delta_implied_root((SELECT d FROM _delta_docs d WHERE d.prefix = ${lit(doc.prefix)}), (v_t->>'id')::BIGINT)`)
|
|
43
|
+
: "'null'::jsonb";
|
|
44
|
+
return `CREATE OR REPLACE FUNCTION ${toldFunction(doc.prefix)}(p_name TEXT, p_rows JSONB)
|
|
45
|
+
RETURNS JSONB AS $$
|
|
46
|
+
DECLARE
|
|
47
|
+
p_at TIMESTAMPTZ;
|
|
48
|
+
v_single BOOLEAN;
|
|
49
|
+
v_id BIGINT;
|
|
50
|
+
v_t JSONB;
|
|
51
|
+
v_told JSONB := '[]'::jsonb;
|
|
52
|
+
v_was BOOLEAN;
|
|
53
|
+
v_is BOOLEAN;
|
|
54
|
+
v_root BOOLEAN;
|
|
55
|
+
v_path TEXT;
|
|
56
|
+
${scope.declare.join("\n ")}
|
|
57
|
+
BEGIN
|
|
58
|
+
${scope.body}
|
|
59
|
+
FOR v_t IN SELECT jsonb_array_elements(p_rows) LOOP
|
|
60
|
+
v_was := (v_t->'before') ? p_name;
|
|
61
|
+
v_is := (v_t->'holders') ? p_name;
|
|
62
|
+
CONTINUE WHEN NOT v_was AND NOT v_is;
|
|
63
|
+
CONTINUE WHEN v_was AND v_is AND (v_t ? 'carried'); -- carried along, and its either way: unchanged for it
|
|
64
|
+
v_root := v_single AND (v_t->>'coll') = ${lit(doc.root)};
|
|
65
|
+
v_path := CASE WHEN v_root THEN _delta_build_path(v_t->>'coll') ELSE _delta_build_path(v_t->>'coll', v_t->>'id') END;
|
|
66
|
+
IF v_was AND v_is THEN -- stayed: what changed
|
|
67
|
+
v_told := v_told || COALESCE((SELECT jsonb_agg(jsonb_build_object('op', 'replace', 'path', v_path || _delta_build_path(f), 'value', v) ORDER BY f COLLATE "C")
|
|
68
|
+
FROM jsonb_each(${after}) AS e(f, v) WHERE v IS DISTINCT FROM v_t->'was'->f), '[]'::jsonb);
|
|
69
|
+
ELSIF v_is THEN
|
|
70
|
+
v_told := v_told || jsonb_build_array(jsonb_build_object('op', CASE WHEN v_root THEN 'replace' ELSE 'add' END, 'path', v_path, 'value', ${after}));
|
|
71
|
+
ELSIF v_root THEN -- gone: ${doc.implied ? "the empty root it then opens with" : "null"}
|
|
72
|
+
v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'replace', 'path', v_path, 'value', ${gone}));
|
|
73
|
+
ELSE
|
|
74
|
+
v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'remove', 'path', v_path));
|
|
75
|
+
END IF;
|
|
76
|
+
END LOOP;
|
|
77
|
+
RETURN v_told;
|
|
78
|
+
END;
|
|
79
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The SQL that answers what each of `docs` is told of a write: a function per document, and `_delta_told` sending each to its own. */
|
|
83
|
+
export function compileTold(schema: Schema, docs: DocDef[]): string {
|
|
84
|
+
const dispatch = `CREATE OR REPLACE FUNCTION _delta_told(p_def _delta_docs, p_target TEXT, p_rows JSONB)
|
|
85
|
+
RETURNS JSONB AS $$
|
|
86
|
+
BEGIN
|
|
87
|
+
CASE p_def.prefix
|
|
88
|
+
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${toldFunction(d.prefix)}(p_target, p_rows);`).join("\n")}
|
|
89
|
+
ELSE RETURN _delta_told_framework(p_def, p_target, p_rows);
|
|
90
|
+
END CASE;
|
|
91
|
+
END;
|
|
92
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
93
|
+
return [...docs.map((d) => toldOf(schema, d)), dispatch, ""].join("\n\n");
|
|
94
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These are split out of `auth-jwt.ts` so the `delta init` CLI (which only
|
|
5
5
|
* needs the file path) can run in projects that haven't installed jose.
|
|
6
|
-
* Consumers of the
|
|
6
|
+
* Consumers of the `dzql/auth-jwt` subpath still get these via
|
|
7
7
|
* re-export from `auth-jwt.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Pool } from "pg";
|
package/src/postgres/auth-jwt.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Auth-JWT — reference `DeltaAuth` implementation using JWTs (jose) and
|
|
3
3
|
* Postgres stored functions for credential verification.
|
|
4
4
|
*
|
|
5
|
-
* Ship this as an opt-in subpath:
|
|
5
|
+
* Ship this as an opt-in subpath: `dzql/auth-jwt`. Users who want
|
|
6
6
|
* a different auth scheme (sessions, OAuth, magic links, none) implement
|
|
7
7
|
* their own `DeltaAuth` instead.
|
|
8
8
|
*
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Both are overridable via the `loginSql` / `registerSql` options.
|
|
15
15
|
*
|
|
16
|
-
* import { jwtAuth } from "
|
|
16
|
+
* import { jwtAuth } from "dzql/auth-jwt";
|
|
17
17
|
*
|
|
18
18
|
* const auth = jwtAuth({
|
|
19
19
|
* pool,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* consumer's `init_db/` directory.
|
|
5
5
|
*
|
|
6
6
|
* import { Pool } from "pg";
|
|
7
|
-
* import { applyFramework } from "
|
|
7
|
+
* import { applyFramework } from "dzql/postgres";
|
|
8
8
|
*
|
|
9
9
|
* const pool = new Pool({ connectionString: process.env.PG_URL });
|
|
10
10
|
* await applyFramework(pool);
|
|
@@ -23,7 +23,7 @@ const FRAMEWORK_SQL_DIR = join(import.meta.dir, "..", "sql");
|
|
|
23
23
|
|
|
24
24
|
/** Absolute paths to the framework SQL files, in apply-order.
|
|
25
25
|
* Filters to `001*-*.sql` — siblings like `auth-jwt.sql` are applied
|
|
26
|
-
* separately (see
|
|
26
|
+
* separately (see `dzql/auth-jwt` → `applyAuthJwtSchema`). */
|
|
27
27
|
export function frameworkSqlFiles(): string[] {
|
|
28
28
|
return readdirSync(FRAMEWORK_SQL_DIR)
|
|
29
29
|
.filter((f) => /^001[a-z]?-.+\.sql$/.test(f))
|