okmodel 0.5.1 → 0.7.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 +100 -1116
- package/dist/adapters/capabilities.d.ts +3 -0
- package/dist/adapters/failure.d.ts +13 -2
- package/dist/adapters/lazy-errors.d.ts +22 -0
- package/dist/adapters/pg/bunsql-extra.d.ts +54 -3
- package/dist/adapters/pg/bunsql.d.ts +14 -1
- package/dist/adapters/pg/cursor.d.ts +17 -0
- package/dist/adapters/pg/no-inflight-timeout.d.ts +20 -0
- package/dist/adapters/pg/nodepostgres-extra.d.ts +22 -1
- package/dist/adapters/pg/nodepostgres.d.ts +12 -0
- package/dist/adapters/pg/pglite-wire.d.ts +24 -0
- package/dist/adapters/pg/pglite.d.ts +10 -0
- package/dist/adapters/pg/postgres-extra.d.ts +41 -3
- package/dist/adapters/pg/reserved.d.ts +42 -0
- package/dist/adapters/pg/session.d.ts +0 -18
- package/dist/contracts/catalog/document.d.ts +19 -5
- package/dist/contracts/catalog/object.d.ts +2 -1
- package/dist/contracts/catalog/policy.d.ts +48 -0
- package/dist/contracts/catalog/types.d.ts +25 -1
- package/dist/contracts/catalog/view.d.ts +2 -0
- package/dist/contracts/connection.d.ts +7 -6
- package/dist/contracts/internal.js +1 -1
- package/dist/contracts/location.d.ts +10 -0
- package/dist/dialects/pg/column.d.ts +13 -2
- package/dist/dialects/pg/ddl.d.ts +37 -1
- package/dist/dialects/pg/emit.d.ts +19 -1
- package/dist/dialects/pg/errors.d.ts +32 -1
- package/dist/dialects/pg/ext/citext.js +1 -1
- package/dist/dialects/pg/ext/index.js +1 -1
- package/dist/dialects/pg/ext/pg-trgm.js +1 -1
- package/dist/dialects/pg/filter-parse.d.ts +5 -4
- package/dist/dialects/pg/floor.d.ts +68 -0
- package/dist/dialects/pg/fn/index.js +2 -2
- package/dist/dialects/pg/index.d.ts +8 -2
- package/dist/dialects/pg/index.js +67 -171
- package/dist/dialects/pg/introspect.d.ts +7 -0
- package/dist/dialects/pg/keys.d.ts +11 -8
- package/dist/dialects/pg/model.d.ts +47 -2
- package/dist/dialects/pg/morph.d.ts +43 -0
- package/dist/dialects/pg/operator-fit.d.ts +2 -2
- package/dist/dialects/pg/operators.d.ts +14 -7
- package/dist/dialects/pg/ops/bucket.d.ts +65 -0
- package/dist/dialects/pg/ops/inc.d.ts +2 -2
- package/dist/dialects/pg/ops/matches.d.ts +9 -12
- package/dist/dialects/pg/ops/relevance.d.ts +13 -0
- package/dist/dialects/pg/ops/search-query.d.ts +22 -0
- package/dist/dialects/pg/ops/window.d.ts +103 -0
- package/dist/dialects/pg/owner.d.ts +42 -0
- package/dist/dialects/pg/path-endpoint.d.ts +20 -0
- package/dist/dialects/pg/preset.d.ts +1 -1
- package/dist/dialects/pg/relations.d.ts +100 -3
- package/dist/dialects/pg/role/check.d.ts +3 -0
- package/dist/dialects/pg/role/index.d.ts +71 -6
- package/dist/dialects/pg/role/sql.d.ts +11 -0
- package/dist/dialects/pg/schema.d.ts +33 -3
- package/dist/dialects/pg/table.d.ts +25 -3
- package/dist/dialects/pg/tenancy.d.ts +45 -3
- package/dist/dialects/pg/view/client.d.ts +19 -1
- package/dist/dialects/pg/view/index.d.ts +4 -0
- package/dist/dialects/pg/view/index.js +21 -178
- package/dist/dialects/pg/view/refresh.d.ts +24 -0
- package/dist/runtime/aggregate.d.ts +8 -2
- package/dist/runtime/batch.d.ts +1 -1
- package/dist/runtime/bucket-group.d.ts +70 -0
- package/dist/runtime/conflict.d.ts +24 -6
- package/dist/runtime/drift.d.ts +9 -0
- package/dist/runtime/include.d.ts +0 -8
- package/dist/runtime/inflight.d.ts +14 -0
- package/dist/runtime/page.d.ts +4 -1
- package/dist/runtime/pg/bun.js +12 -5
- package/dist/runtime/pg/pg.js +5 -5
- package/dist/runtime/pg/pglite.d.ts +1 -0
- package/dist/runtime/pg/pglite.js +9 -8
- package/dist/runtime/pg/postgresjs.js +5 -5
- package/dist/runtime/plan.d.ts +113 -15
- package/dist/runtime/predicate.d.ts +19 -0
- package/dist/runtime/read-stream.d.ts +13 -16
- package/dist/runtime/result-name.d.ts +17 -0
- package/dist/runtime/safety/index.js +1 -1
- package/dist/runtime/server-check.d.ts +3 -3
- package/dist/runtime/sort-bind.d.ts +21 -0
- package/dist/runtime/sort.d.ts +56 -0
- package/dist/runtime/sync.d.ts +70 -0
- package/dist/runtime/tenancy/composite.d.ts +26 -0
- package/dist/runtime/tenancy/context.d.ts +16 -0
- package/dist/runtime/tenancy/index.d.ts +9 -5
- package/dist/runtime/tenancy/index.js +1511 -38
- package/dist/runtime/tenancy/path.d.ts +87 -0
- package/dist/runtime/tenancy/rls.d.ts +67 -0
- package/dist/runtime/tenancy/via.d.ts +22 -0
- package/dist/runtime/text-search.d.ts +20 -0
- package/dist/runtime/traits/index.d.ts +80 -0
- package/dist/runtime/traits/index.js +187 -17
- package/dist/runtime/tree.d.ts +38 -0
- package/dist/runtime/types.d.ts +231 -23
- package/dist/runtime/validate/index.js +4 -4
- package/dist/runtime/version.d.ts +39 -0
- package/dist/runtime/window.d.ts +21 -0
- package/dist/runtime/write.d.ts +55 -3
- package/dist/shared/{krxx77g0.js → 14vx95q4.js} +12 -18
- package/dist/shared/1est4ezg.js +313 -0
- package/dist/shared/{4az561fn.js → 1qa0qajz.js} +8 -23
- package/dist/shared/1w508jbr.js +1611 -0
- package/dist/shared/2c13q96h.js +18 -0
- package/dist/shared/2d3xq4ps.js +9 -0
- package/dist/shared/2shdbn8q.js +23 -0
- package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
- package/dist/shared/3v7t1yrv.js +208 -0
- package/dist/shared/5e3tf63j.js +220 -0
- package/dist/shared/5fe3kbsh.js +10 -0
- package/dist/shared/5v3xf80k.js +37 -0
- package/dist/shared/{51mvgj8w.js → 63qtkrca.js} +130 -3
- package/dist/shared/{gbtcc2d7.js → 67txcrcw.js} +62 -66
- package/dist/shared/79dt02vc.js +12 -0
- package/dist/shared/{v36c5g6p.js → 7k9grvf6.js} +407 -14
- package/dist/shared/7kms6fvj.js +20 -0
- package/dist/shared/7mnme5tn.js +8 -0
- package/dist/shared/7n7ddwt4.js +6 -0
- package/dist/shared/{87jvvhnt.js → 81ebdx9p.js} +48 -68
- package/dist/shared/{szm0cksa.js → 8skf0cam.js} +34 -6
- package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
- package/dist/shared/9a4ae4p8.js +31 -0
- package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
- package/dist/shared/9y3mmmh1.js +436 -0
- package/dist/shared/ab9gmbwy.js +213 -0
- package/dist/shared/{sycnx8gt.js → ax7qv55k.js} +18 -8
- package/dist/shared/{f5cw5q5r.js → aytqh5y2.js} +6 -4
- package/dist/shared/{fancm5pt.js → b5pftnn2.js} +138 -15
- package/dist/shared/{efmy2gcg.js → b76b8d95.js} +2 -2
- package/dist/shared/bdk4sty6.js +58 -0
- package/dist/shared/{32zqc1dw.js → bg3rym3g.js} +63 -32
- package/dist/shared/{0t22wwh1.js → bm22fvzr.js} +16 -14
- package/dist/shared/bz84tkzm.js +342 -0
- package/dist/shared/{31vm914h.js → cm3w0ywp.js} +15 -16
- package/dist/shared/cn957f8d.js +179 -0
- package/dist/shared/{pf0d8f77.js → cnkky55j.js} +25 -14
- package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
- package/dist/shared/{nkaxvt9q.js → dkxgzaqd.js} +224 -19
- package/dist/shared/dzj09cye.js +10 -0
- package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
- package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
- package/dist/shared/exn3sd90.js +359 -0
- package/dist/shared/g6evj1j3.js +66 -0
- package/dist/shared/ga9j92qj.js +25 -0
- package/dist/shared/{qv2ydck9.js → gs74dbj5.js} +651 -76
- package/dist/shared/gzaveazc.js +285 -0
- package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
- package/dist/shared/{dyj86cmj.js → hyh73qk6.js} +3 -3
- package/dist/shared/je4axnm8.js +22 -0
- package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
- package/dist/shared/{6qd27mx0.js → jvd0mqag.js} +5 -5
- package/dist/shared/jznfax0y.js +155 -0
- package/dist/shared/{cgbamx3e.js → kb2sdhfb.js} +7 -4
- package/dist/shared/{ps7a6h14.js → kr0jzbw2.js} +12 -8
- package/dist/shared/{janx09m5.js → ktz42yez.js} +20 -8
- package/dist/shared/{gde73es6.js → m6p12kbr.js} +56 -55
- package/dist/shared/{r6nwkxnt.js → m7n9h2a5.js} +4 -4
- package/dist/shared/{672t5vym.js → mss6ctgv.js} +2 -2
- package/dist/shared/{nxdp2ze3.js → mtaszv5a.js} +144 -12
- package/dist/shared/n816peh1.js +45 -0
- package/dist/shared/nbw1z4yd.js +18 -0
- package/dist/shared/{cctw17hj.js → p6qc5gcp.js} +38 -34
- package/dist/shared/{6rk2gen5.js → pbp1ss85.js} +20 -17
- package/dist/shared/pt94kz7f.js +6 -0
- package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
- package/dist/shared/q5c6ngn3.js +13 -0
- package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
- package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
- package/dist/shared/rhs3xe4k.js +7 -0
- package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
- package/dist/shared/{sdc7r2ww.js → rs73fca9.js} +8 -8
- package/dist/shared/{tm5ab8we.js → sfz1ksn6.js} +7 -4
- package/dist/shared/{6mnp99hx.js → svncdeh5.js} +16 -12
- package/dist/shared/{hf22e0wt.js → tbrvgw6z.js} +62 -42
- package/dist/shared/te5as7z7.js +622 -0
- package/dist/shared/{ga3sfy93.js → tsvww22q.js} +535 -200
- package/dist/shared/{47x8x9ze.js → ttsqgp4p.js} +20 -13
- package/dist/shared/{x72jb7ad.js → v8g1vgv8.js} +1 -1
- package/dist/shared/vb6qfyve.js +152 -0
- package/dist/shared/{r2sp8hrc.js → vczs8gd4.js} +22 -13
- package/dist/shared/{1g0drakv.js → vqrjpcqn.js} +145 -3
- package/dist/shared/{2pqx26h8.js → w72kgg4q.js} +31 -22
- package/dist/shared/{96qcj21c.js → w79hsckn.js} +16 -51
- package/dist/shared/wa07eas9.js +730 -0
- package/dist/shared/{k8v5v5wj.js → x933k9a3.js} +75 -34
- package/dist/shared/{cr74e96e.js → xhfsfkgk.js} +62 -28
- package/dist/shared/xjk7hnn5.js +272 -0
- package/dist/shared/{5enne4ma.js → xnxsed62.js} +66 -113
- package/dist/shared/xqve9598.js +53 -0
- package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
- package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
- package/dist/shared/y6njgyda.js +249 -0
- package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
- package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
- package/dist/shared/z7s6fpxv.js +62 -0
- package/dist/shared/{n7mcc1gk.js → zchaghkf.js} +4 -3
- package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
- package/dist/shared/zm0ptry0.js +40 -0
- package/dist/shared/{nsva2a6b.js → znm0bpp5.js} +3 -0
- package/dist/shared/zp9fmend.js +145 -0
- package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
- package/dist/tooling/cli.js +41 -15
- package/dist/tooling/migrate/apply.d.ts +14 -0
- package/dist/tooling/migrate/check.d.ts +3 -4
- package/dist/tooling/migrate/classify.d.ts +4 -0
- package/dist/tooling/migrate/config.d.ts +3 -2
- package/dist/tooling/migrate/engine.d.ts +3 -2
- package/dist/tooling/migrate/files.d.ts +11 -1
- package/dist/tooling/migrate/index.js +14 -14
- package/dist/tooling/migrate/lint.d.ts +1 -1
- package/dist/tooling/migrate/lost.d.ts +28 -0
- package/dist/tooling/migrate/paths.d.ts +14 -0
- package/dist/tooling/migrate/plan.d.ts +6 -3
- package/dist/tooling/migrate/runner.d.ts +25 -0
- package/dist/tooling/migrate/sandbox.d.ts +57 -0
- package/dist/tooling/migrate/sql-lex.d.ts +20 -3
- package/dist/tooling/migrate/text.d.ts +42 -0
- package/dist/tooling/node-version.d.ts +21 -0
- package/dist/tooling/testing/facts.d.ts +33 -5
- package/dist/tooling/testing/index.d.ts +1 -1
- package/dist/tooling/testing/index.js +3 -3
- package/dist/tooling/testing/isolation.d.ts +11 -5
- package/package.json +8 -3
- package/dist/shared/7m88x3e9.js +0 -45
- package/dist/shared/7zwwyh0v.js +0 -448
- package/dist/shared/aj2qxyhj.js +0 -90
- package/dist/shared/caw8b9d8.js +0 -204
- package/dist/shared/jwtxdj06.js +0 -76
- package/dist/shared/kfjp0gck.js +0 -212
- package/dist/shared/q58y17hy.js +0 -21
- package/dist/shared/qm7cdtvr.js +0 -912
- package/dist/shared/r8x5qhd7.js +0 -86
- package/dist/shared/zqrnhwsq.js +0 -31
|
@@ -1,17 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
markPathEndpoint
|
|
3
|
+
} from "../../shared/rhs3xe4k.js";
|
|
4
|
+
import {
|
|
5
|
+
tenantPolicyExpression,
|
|
6
|
+
unscopedPolicyExpression,
|
|
7
|
+
policyObject
|
|
8
|
+
} from "../../shared/bdk4sty6.js";
|
|
9
|
+
import {
|
|
10
|
+
isRelationCall
|
|
11
|
+
} from "../../shared/exn3sd90.js";
|
|
1
12
|
import {
|
|
2
13
|
uuid2
|
|
3
|
-
} from "../../shared/
|
|
14
|
+
} from "../../shared/hyh73qk6.js";
|
|
15
|
+
import {
|
|
16
|
+
index2,
|
|
17
|
+
snakeCase
|
|
18
|
+
} from "../../shared/rpycarcr.js";
|
|
4
19
|
import {
|
|
5
20
|
ColumnBuilder2,
|
|
6
21
|
retarget
|
|
7
|
-
} from "../../shared/
|
|
22
|
+
} from "../../shared/ywr8hmey.js";
|
|
8
23
|
import {
|
|
9
|
-
definition
|
|
10
|
-
unavailable
|
|
24
|
+
definition
|
|
11
25
|
} from "../../shared/tmbdywx0.js";
|
|
26
|
+
import {
|
|
27
|
+
quoteLiteral
|
|
28
|
+
} from "../../shared/94ez8kzh.js";
|
|
12
29
|
import {
|
|
13
30
|
quote
|
|
14
|
-
} from "../../shared/
|
|
31
|
+
} from "../../shared/67txcrcw.js";
|
|
15
32
|
import {
|
|
16
33
|
assertIdentifier2
|
|
17
34
|
} from "../../shared/nfzc5z0w.js";
|
|
@@ -19,6 +36,1439 @@ import {
|
|
|
19
36
|
OkmError2
|
|
20
37
|
} from "../../shared/52szzakm.js";
|
|
21
38
|
|
|
39
|
+
// src/runtime/tenancy/composite.ts
|
|
40
|
+
function compositeTenancy(input) {
|
|
41
|
+
if (input === undefined || typeof input !== "object") {
|
|
42
|
+
definition("compositeTenancy() needs { key, type }.");
|
|
43
|
+
}
|
|
44
|
+
if (input.type !== "uuid") {
|
|
45
|
+
definition(`compositeTenancy() type ${String(input.type)} must be uuid.`);
|
|
46
|
+
}
|
|
47
|
+
const keys = readKeys(input.key);
|
|
48
|
+
const key = keys[0] ?? "";
|
|
49
|
+
const columns = keys.map(() => uuid2().guarded());
|
|
50
|
+
const encodes = columns.map((column) => column.state.encode);
|
|
51
|
+
const tenants = new Set;
|
|
52
|
+
const globals = new Map;
|
|
53
|
+
const exemptions = new Map;
|
|
54
|
+
const encodeKey = (name, value) => {
|
|
55
|
+
const at = keys.indexOf(name);
|
|
56
|
+
const encode = at < 0 ? undefined : encodes[at];
|
|
57
|
+
return encode === undefined ? value : encode(value);
|
|
58
|
+
};
|
|
59
|
+
const api = {
|
|
60
|
+
key,
|
|
61
|
+
keys,
|
|
62
|
+
type: "uuid",
|
|
63
|
+
strategy: "composite",
|
|
64
|
+
rewrite(tables) {
|
|
65
|
+
tenants.clear();
|
|
66
|
+
globals.clear();
|
|
67
|
+
exemptions.clear();
|
|
68
|
+
return rewriteTables(keys, columns, encodes, api, tables, tenants, globals, exemptions);
|
|
69
|
+
},
|
|
70
|
+
column: () => columns[0] ?? uuid2().guarded(),
|
|
71
|
+
predicate(spec) {
|
|
72
|
+
return writePredicate(keys, tenants, spec, encodeKey);
|
|
73
|
+
},
|
|
74
|
+
guard(table, field, kind) {
|
|
75
|
+
if (!tenants.has(table) || !keys.includes(field))
|
|
76
|
+
return;
|
|
77
|
+
refuseKey(kind, table, field);
|
|
78
|
+
},
|
|
79
|
+
stamp(table, rows, scope) {
|
|
80
|
+
if (!tenants.has(table))
|
|
81
|
+
return;
|
|
82
|
+
stampRows(keys, table, rows, scope);
|
|
83
|
+
},
|
|
84
|
+
client(spec) {
|
|
85
|
+
const names = spec.scoped ? spec.names : spec.names.filter((name) => !tenants.has(name));
|
|
86
|
+
if (spec.scoped)
|
|
87
|
+
return { names };
|
|
88
|
+
return { names, ...bindClient(keys, encodes, spec.open) };
|
|
89
|
+
},
|
|
90
|
+
rules(table, source, scope) {
|
|
91
|
+
return tenancyRules(table, source, scope, tenants, globals, exemptions);
|
|
92
|
+
},
|
|
93
|
+
scopeView(name) {
|
|
94
|
+
tenants.add(name);
|
|
95
|
+
},
|
|
96
|
+
hook(target, ctx) {
|
|
97
|
+
if (ctx.tables === undefined || ctx.names === undefined || ctx.open === undefined || ctx.scoped === undefined) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const view = api.client({ names: ctx.names, scoped: ctx.scoped, open: ctx.open });
|
|
101
|
+
if (!ctx.scoped) {
|
|
102
|
+
for (const name of ctx.names) {
|
|
103
|
+
if (view.names.includes(name))
|
|
104
|
+
continue;
|
|
105
|
+
delete ctx.tables[name];
|
|
106
|
+
delete target[name];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (view.for !== undefined)
|
|
110
|
+
target.for = view.for;
|
|
111
|
+
if (view.unscoped !== undefined)
|
|
112
|
+
target.unscoped = view.unscoped;
|
|
113
|
+
},
|
|
114
|
+
missing(table) {
|
|
115
|
+
throw new OkmError2("OKM1701", `Table ${table} is tenant-scoped. Call for({ ${keys.join(", ")} }) or unscoped("reason").`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return api;
|
|
119
|
+
}
|
|
120
|
+
function rewriteTables(keys, columns, encodes, api, tables, tenants, globals, exemptions) {
|
|
121
|
+
const kind = new Map;
|
|
122
|
+
const swapped = new Map;
|
|
123
|
+
const extras = new Map;
|
|
124
|
+
for (const item of tables) {
|
|
125
|
+
const options = item.options;
|
|
126
|
+
const mark = options?.tenancy;
|
|
127
|
+
if (mark === undefined) {
|
|
128
|
+
kind.set(item.name, "tenant");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (isGlobal(mark)) {
|
|
132
|
+
kind.set(item.name, "global");
|
|
133
|
+
globals.set(item.name, mark.reason);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (isRecord(mark) && typeof mark.rewrite === "function") {
|
|
137
|
+
const ctx = { tables, keys, encodes, api, extras };
|
|
138
|
+
swapped.set(item.name, mark.rewrite(item, ctx));
|
|
139
|
+
kind.set(item.name, "path");
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
definition(`Table ${item.name} tenancy must be global("reason") or via().`);
|
|
143
|
+
}
|
|
144
|
+
return tables.map((item) => {
|
|
145
|
+
const next = swapped.get(item.name);
|
|
146
|
+
if (next !== undefined)
|
|
147
|
+
return next;
|
|
148
|
+
if (kind.get(item.name) === "global") {
|
|
149
|
+
refuseGlobalReference(item, kind);
|
|
150
|
+
return item;
|
|
151
|
+
}
|
|
152
|
+
return rewriteTenant(keys, columns, item, tables, kind, tenants, exemptions, extras);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function refuseGlobalReference(item, kind) {
|
|
156
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
157
|
+
if (!(builder instanceof ColumnBuilder2))
|
|
158
|
+
continue;
|
|
159
|
+
const reference = builder.state.references;
|
|
160
|
+
if (reference !== undefined && kind.get(reference.table) === "tenant") {
|
|
161
|
+
throw new OkmError2("OKM1705", `Table ${item.name}.${field} is global and references tenant table ${reference.table}. Drop the reference, or make ${item.name} a tenant table.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function rewriteTenant(keys, keyColumns, item, tables, kind, tenants, exemptions, extras) {
|
|
166
|
+
for (const key of keys) {
|
|
167
|
+
if (Object.hasOwn(item.columns, key)) {
|
|
168
|
+
throw new OkmError2("OKM1012", `Table ${item.name} already declares ${key}, which tenancy adds. Rename the field or drop it from the table.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const options = item.options;
|
|
172
|
+
const columns = {};
|
|
173
|
+
const unique = {};
|
|
174
|
+
const named = options?.unique;
|
|
175
|
+
if (named !== undefined) {
|
|
176
|
+
for (const [name, fields] of Object.entries(named)) {
|
|
177
|
+
unique[name] = covers(fields, keys) ? fields : [...keys, ...fields];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const notes = [];
|
|
181
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
182
|
+
if (!(builder instanceof ColumnBuilder2)) {
|
|
183
|
+
columns[field] = builder;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const patch = {};
|
|
187
|
+
if (builder.state.primaryKey === true)
|
|
188
|
+
patch.dropPrimary = true;
|
|
189
|
+
const flag = builder.state.unique;
|
|
190
|
+
if (flag !== undefined) {
|
|
191
|
+
if (flag.global === true) {
|
|
192
|
+
const reason = flag.reason?.trim() ?? "";
|
|
193
|
+
if (reason.length === 0) {
|
|
194
|
+
definition(`Unique ${item.name}.${field} sets global and needs a reason.`);
|
|
195
|
+
}
|
|
196
|
+
notes.push({ name: field, reason });
|
|
197
|
+
} else {
|
|
198
|
+
patch.dropUnique = true;
|
|
199
|
+
unique[field] = [...keys, field];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const reference = builder.state.references;
|
|
203
|
+
if (reference !== undefined) {
|
|
204
|
+
const widened = widenReference(keys, item.name, field, reference, tables, kind);
|
|
205
|
+
if (widened !== undefined)
|
|
206
|
+
patch.references = widened;
|
|
207
|
+
}
|
|
208
|
+
const changed = patch.dropUnique === true || patch.dropPrimary === true || patch.references !== undefined;
|
|
209
|
+
columns[field] = changed ? retarget(builder, patch) : builder;
|
|
210
|
+
}
|
|
211
|
+
const primary = primaryFields(item);
|
|
212
|
+
const primaryKey = primary.length > 0 && !covers(primary, keys) ? [...primary, ...keys] : undefined;
|
|
213
|
+
if (primaryKey !== undefined)
|
|
214
|
+
unique[`${primary.join("_")}_${keys.join("_")}`] = primaryKey;
|
|
215
|
+
const declared = extras.get(item.name);
|
|
216
|
+
if (declared !== undefined && declared.length > 0 && unique[declared.join("_")] === undefined) {
|
|
217
|
+
unique[declared.join("_")] = declared;
|
|
218
|
+
}
|
|
219
|
+
for (let index = 0;index < keys.length; index += 1) {
|
|
220
|
+
const name = keys[index];
|
|
221
|
+
const column = keyColumns[index];
|
|
222
|
+
if (name !== undefined && column !== undefined)
|
|
223
|
+
columns[name] = column;
|
|
224
|
+
}
|
|
225
|
+
tenants.add(item.name);
|
|
226
|
+
if (notes.length > 0)
|
|
227
|
+
exemptions.set(item.name, notes);
|
|
228
|
+
const indexes = wrapIndexes(keys[0] ?? "", item.name, options?.indexes);
|
|
229
|
+
const rewritten = {
|
|
230
|
+
...item,
|
|
231
|
+
columns,
|
|
232
|
+
options: {
|
|
233
|
+
...options,
|
|
234
|
+
...primaryKey !== undefined ? { primaryKey } : {},
|
|
235
|
+
...Object.keys(unique).length > 0 ? { unique } : {},
|
|
236
|
+
...indexes !== undefined ? { indexes } : {},
|
|
237
|
+
tenantKeys: keys
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return declared !== undefined && declared.length > 0 ? markPathEndpoint(rewritten) : rewritten;
|
|
241
|
+
}
|
|
242
|
+
function covers(fields, keys) {
|
|
243
|
+
for (const key of keys)
|
|
244
|
+
if (!fields.includes(key))
|
|
245
|
+
return false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
function widenReference(keys, table, field, reference, tables, kind) {
|
|
249
|
+
const targetKind = kind.get(reference.table);
|
|
250
|
+
if (targetKind !== "tenant")
|
|
251
|
+
return;
|
|
252
|
+
const action = [reference.onDelete, reference.onUpdate].find((item) => item === "set null" || item === "set default");
|
|
253
|
+
if (action !== undefined) {
|
|
254
|
+
definition(`Foreign key ${table}.${field} includes ${keys.join(", ")}, so ${action} cannot clear it. Accepted actions: cascade, restrict, no action.`);
|
|
255
|
+
}
|
|
256
|
+
const target = tables.find((item) => item.name === reference.table);
|
|
257
|
+
const remote = reference.columns ?? (target === undefined ? undefined : primaryFields(target));
|
|
258
|
+
if (remote === undefined || remote.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
const along = [...reference.along ?? []];
|
|
261
|
+
let columns = remote;
|
|
262
|
+
if (!covers(remote, keys))
|
|
263
|
+
columns = [...remote, ...keys];
|
|
264
|
+
for (const key of keys)
|
|
265
|
+
if (!along.includes(key))
|
|
266
|
+
along.push(key);
|
|
267
|
+
if (covers(remote, keys) && covers(reference.along ?? [], keys))
|
|
268
|
+
return;
|
|
269
|
+
return { ...reference, columns, along };
|
|
270
|
+
}
|
|
271
|
+
function primaryFields(item) {
|
|
272
|
+
const listed = item.options?.primaryKey;
|
|
273
|
+
if (listed !== undefined && listed.length > 0)
|
|
274
|
+
return [...listed];
|
|
275
|
+
const fields = [];
|
|
276
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
277
|
+
if (builder instanceof ColumnBuilder2 && builder.state.primaryKey === true)
|
|
278
|
+
fields.push(field);
|
|
279
|
+
}
|
|
280
|
+
return fields;
|
|
281
|
+
}
|
|
282
|
+
function wrapIndexes(key, table, build) {
|
|
283
|
+
if (build === undefined)
|
|
284
|
+
return;
|
|
285
|
+
return (columns) => {
|
|
286
|
+
const calls = build(columns);
|
|
287
|
+
const tenantSql = columns[key]?.name;
|
|
288
|
+
if (!Array.isArray(calls) || tenantSql === undefined)
|
|
289
|
+
return calls;
|
|
290
|
+
for (const call of calls) {
|
|
291
|
+
if (call.columns[0] === tenantSql)
|
|
292
|
+
continue;
|
|
293
|
+
throw new OkmError2("OKM1706", `Index on ${table} starts with ${call.columns[0] ?? "no column"}. Put ${tenantSql} first.`);
|
|
294
|
+
}
|
|
295
|
+
return calls;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function writePredicate(keys, tenants, input, encodeKey) {
|
|
299
|
+
if (!tenants.has(input.table))
|
|
300
|
+
return false;
|
|
301
|
+
const scope = input.scope;
|
|
302
|
+
if (scope === undefined) {
|
|
303
|
+
throw new OkmError2("OKM1701", `Table ${input.table} is tenant-scoped. Call for({ ${keys.join(", ")} }) or unscoped("reason").`);
|
|
304
|
+
}
|
|
305
|
+
if ("unscoped" in scope) {
|
|
306
|
+
input.sink.mark("unscoped|");
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
for (let index = 0;index < keys.length; index += 1) {
|
|
310
|
+
const key = keys[index];
|
|
311
|
+
if (key === undefined)
|
|
312
|
+
continue;
|
|
313
|
+
const sql = input.fieldSql(key);
|
|
314
|
+
if (sql === undefined) {
|
|
315
|
+
throw new OkmError2("OKM1701", `Table ${input.table} is missing its tenant key ${key}.`);
|
|
316
|
+
}
|
|
317
|
+
input.sink.text(index === 0 ? input.appended ? " and " : " where " : " and ");
|
|
318
|
+
input.sink.text(input.alias);
|
|
319
|
+
input.sink.text(".");
|
|
320
|
+
input.sink.text(quote(sql));
|
|
321
|
+
input.sink.text(" = ");
|
|
322
|
+
const raw = scope.values?.[key] ?? scope.value;
|
|
323
|
+
const encoded = keys.length === 1 && input.encode !== undefined ? input.encode(raw) : encodeKey(key, raw);
|
|
324
|
+
input.sink.param(encoded);
|
|
325
|
+
}
|
|
326
|
+
input.sink.mark("tenant|");
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function refuseKey(kind, table, field) {
|
|
330
|
+
if (kind === "insert") {
|
|
331
|
+
throw new OkmError2("OKM1190", `Field ${table}.${field} is the tenant key. The scope sets it. Input cannot.`);
|
|
332
|
+
}
|
|
333
|
+
if (kind === "update") {
|
|
334
|
+
throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. An update cannot change it.`);
|
|
335
|
+
}
|
|
336
|
+
throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. A filter cannot set it.`);
|
|
337
|
+
}
|
|
338
|
+
function stampRows(keys, table, rows, scope) {
|
|
339
|
+
requireScope(keys, table, scope);
|
|
340
|
+
if (scope === undefined || "unscoped" in scope)
|
|
341
|
+
return;
|
|
342
|
+
for (const row of rows) {
|
|
343
|
+
for (const key of keys)
|
|
344
|
+
row[key] = scope.values?.[key] ?? scope.value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function requireScope(keys, table, scope) {
|
|
348
|
+
if (scope === undefined || "unscoped" in scope) {
|
|
349
|
+
throw new OkmError2("OKM1701", `insert on ${table} has no tenant. Call for({ ${keys.join(", ")} }) so the scope can set it.`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function bindClient(keys, encodes, openScope) {
|
|
353
|
+
const list = keys.join(", ");
|
|
354
|
+
return {
|
|
355
|
+
for: (input) => {
|
|
356
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
357
|
+
throw new OkmError2("OKM1701", `for() needs { ${list} }.`);
|
|
358
|
+
}
|
|
359
|
+
const record = input;
|
|
360
|
+
const values = {};
|
|
361
|
+
for (let index = 0;index < keys.length; index += 1) {
|
|
362
|
+
const key = keys[index];
|
|
363
|
+
if (key === undefined)
|
|
364
|
+
continue;
|
|
365
|
+
const value = record[key];
|
|
366
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
367
|
+
throw new OkmError2("OKM1701", `for() needs { ${list} } as a uuid.`);
|
|
368
|
+
}
|
|
369
|
+
const encode = encodes[index];
|
|
370
|
+
if (encode !== undefined)
|
|
371
|
+
encode(value);
|
|
372
|
+
values[key] = value;
|
|
373
|
+
}
|
|
374
|
+
for (const name of Object.keys(record)) {
|
|
375
|
+
if (keys.includes(name))
|
|
376
|
+
continue;
|
|
377
|
+
throw new OkmError2("OKM1701", `for() accepts ${list}. ${name} is not the tenant key.`);
|
|
378
|
+
}
|
|
379
|
+
const first = keys[0] ?? "";
|
|
380
|
+
return openScope(keys.length === 1 ? { value: values[first] ?? "" } : { value: values[first] ?? "", values });
|
|
381
|
+
},
|
|
382
|
+
unscoped: (reason) => {
|
|
383
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
384
|
+
throw new OkmError2("OKM1701", "unscoped() needs a reason.");
|
|
385
|
+
}
|
|
386
|
+
return openScope({ unscoped: reason.trim() });
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function tenancyRules(table, source, scope, tenants, globals, exemptions) {
|
|
391
|
+
const rules = [];
|
|
392
|
+
const reason = globals.get(table);
|
|
393
|
+
if (reason !== undefined) {
|
|
394
|
+
rules.push({
|
|
395
|
+
rule: "tenancy",
|
|
396
|
+
contribution: `global ${reason}`,
|
|
397
|
+
provenance: "catalog",
|
|
398
|
+
...source !== undefined ? { source } : {}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const notes = exemptions.get(table);
|
|
402
|
+
if (notes !== undefined) {
|
|
403
|
+
for (const item of notes) {
|
|
404
|
+
rules.push({
|
|
405
|
+
rule: "tenancy",
|
|
406
|
+
contribution: `unique ${item.name} global ${item.reason}`,
|
|
407
|
+
provenance: "catalog",
|
|
408
|
+
...source !== undefined ? { source } : {}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (tenants.has(table) && scope !== undefined && "value" in scope) {
|
|
413
|
+
rules.push({ rule: "tenancy", contribution: "scoped", provenance: "planner" });
|
|
414
|
+
}
|
|
415
|
+
if (scope !== undefined && "unscoped" in scope) {
|
|
416
|
+
rules.push({
|
|
417
|
+
rule: "tenancy",
|
|
418
|
+
contribution: `unscoped ${scope.unscoped}`,
|
|
419
|
+
provenance: "caller"
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return rules;
|
|
423
|
+
}
|
|
424
|
+
function readKeys(key) {
|
|
425
|
+
if (!Array.isArray(key) || key.length === 0) {
|
|
426
|
+
definition('compositeTenancy() key must be a list of field names, such as ["organizationId", "workspaceId"].');
|
|
427
|
+
}
|
|
428
|
+
const keys = [];
|
|
429
|
+
for (const name of key) {
|
|
430
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
431
|
+
definition("compositeTenancy() key must be a field name.");
|
|
432
|
+
}
|
|
433
|
+
if (keys.includes(name))
|
|
434
|
+
definition(`compositeTenancy() key repeats ${name}.`);
|
|
435
|
+
assertIdentifier2(name, "tenancy key");
|
|
436
|
+
keys.push(name);
|
|
437
|
+
}
|
|
438
|
+
const first = keys[0];
|
|
439
|
+
if (first === undefined)
|
|
440
|
+
definition("compositeTenancy() key needs a field name.");
|
|
441
|
+
return keys;
|
|
442
|
+
}
|
|
443
|
+
function isGlobal(value) {
|
|
444
|
+
return isRecord(value) && value.kind === "global" && typeof value.reason === "string";
|
|
445
|
+
}
|
|
446
|
+
function isRecord(value) {
|
|
447
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
448
|
+
}
|
|
449
|
+
// src/runtime/tenancy/path.ts
|
|
450
|
+
var MAX_HOPS = 3;
|
|
451
|
+
function compilePaths(tables, kind) {
|
|
452
|
+
const chains = new Map;
|
|
453
|
+
const endpoints = new Map;
|
|
454
|
+
for (const item of tables) {
|
|
455
|
+
if (kind.get(item.name) !== "path")
|
|
456
|
+
continue;
|
|
457
|
+
const via = viaOf(item);
|
|
458
|
+
const hops = resolve(item, via, tables, kind, new Set([item.name]));
|
|
459
|
+
chains.set(item.name, { via, hops });
|
|
460
|
+
const last = hops[hops.length - 1];
|
|
461
|
+
if (last === undefined)
|
|
462
|
+
continue;
|
|
463
|
+
const parent = tables.find((table) => table.name === last.parent);
|
|
464
|
+
const declared = parent === undefined ? [] : declaredPrimary(parent);
|
|
465
|
+
if (declared.length === 0) {
|
|
466
|
+
pathError(`Table ${item.name} tenancy via "${via}" ends at ${last.parent}, which has no primary key.`);
|
|
467
|
+
}
|
|
468
|
+
endpoints.set(last.parent, declared);
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
endpoints,
|
|
472
|
+
isPath: (name) => chains.has(name),
|
|
473
|
+
hops: (name) => chains.get(name)?.hops,
|
|
474
|
+
adjust: (item) => adjust(item, chains.get(item.name), kind, tables),
|
|
475
|
+
writeExists: (input) => writeExists(chains.get(input.table), input)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function viaOf(item) {
|
|
479
|
+
const mark = item.options?.tenancy;
|
|
480
|
+
if (!isRecord2(mark) || typeof mark.via !== "string" || mark.via.trim().length === 0) {
|
|
481
|
+
pathError(`Table ${item.name} tenancy via needs a relation path.`);
|
|
482
|
+
}
|
|
483
|
+
return mark.via.trim();
|
|
484
|
+
}
|
|
485
|
+
function resolve(item, via, tables, kind, seen) {
|
|
486
|
+
const segments = via.split(".");
|
|
487
|
+
if (segments.length > MAX_HOPS) {
|
|
488
|
+
pathError(`Table ${item.name} tenancy via "${via}" is longer than 3 hops.`);
|
|
489
|
+
}
|
|
490
|
+
const hops = [];
|
|
491
|
+
let current = item;
|
|
492
|
+
for (const segment of segments) {
|
|
493
|
+
if (segment.length === 0) {
|
|
494
|
+
pathError(`Table ${item.name} tenancy via "${via}" has an empty step.`);
|
|
495
|
+
}
|
|
496
|
+
const relations = current.options?.relations;
|
|
497
|
+
const call = relations?.[segment];
|
|
498
|
+
if (!isRelationCall(call) || call.kind !== "one") {
|
|
499
|
+
pathError(`Table ${current.name} tenancy via "${via}" has no to-one relation ${segment}.`);
|
|
500
|
+
}
|
|
501
|
+
if (seen.has(call.table)) {
|
|
502
|
+
pathError(`Table ${item.name} tenancy via "${via}" cycles at ${call.table}.`);
|
|
503
|
+
}
|
|
504
|
+
const parent = tables.find((table) => table.name === call.table);
|
|
505
|
+
if (parent === undefined) {
|
|
506
|
+
pathError(`Table ${current.name} tenancy via "${via}" names ${call.table}, which is not in the schema.`);
|
|
507
|
+
}
|
|
508
|
+
const localField = localOf(current, call.field, parent.name, segment, via);
|
|
509
|
+
const remoteField = remoteOf(parent, localField, current, via);
|
|
510
|
+
seen.add(parent.name);
|
|
511
|
+
hops.push({
|
|
512
|
+
child: current.name,
|
|
513
|
+
parent: parent.name,
|
|
514
|
+
localField,
|
|
515
|
+
remoteField,
|
|
516
|
+
localSql: sqlNameOf(current, localField),
|
|
517
|
+
remoteSql: sqlNameOf(parent, remoteField),
|
|
518
|
+
parentSql: parent.options?.sqlName,
|
|
519
|
+
encode: encodeOf(current, localField)
|
|
520
|
+
});
|
|
521
|
+
current = parent;
|
|
522
|
+
}
|
|
523
|
+
const end = kind.get(current.name);
|
|
524
|
+
if (end !== "tenant") {
|
|
525
|
+
pathError(`Table ${item.name} tenancy via "${via}" ends at ${current.name}, which is not a tenant table.`);
|
|
526
|
+
}
|
|
527
|
+
return hops;
|
|
528
|
+
}
|
|
529
|
+
function localOf(table, field, parent, segment, via) {
|
|
530
|
+
if (field !== undefined) {
|
|
531
|
+
if (!Object.hasOwn(table.columns, field)) {
|
|
532
|
+
pathError(`Table ${table.name} relation ${segment} names ${field}, which is not a column.`);
|
|
533
|
+
}
|
|
534
|
+
return field;
|
|
535
|
+
}
|
|
536
|
+
const found = [];
|
|
537
|
+
for (const [name, builder] of Object.entries(table.columns)) {
|
|
538
|
+
if (!(builder instanceof ColumnBuilder2))
|
|
539
|
+
continue;
|
|
540
|
+
if (builder.state.references?.table === parent)
|
|
541
|
+
found.push(name);
|
|
542
|
+
}
|
|
543
|
+
const only = found.length === 1 ? found[0] : undefined;
|
|
544
|
+
if (only === undefined) {
|
|
545
|
+
pathError(`Table ${table.name} tenancy via "${via}" cannot tell which column points at ${parent}. Name it on ${segment}.`);
|
|
546
|
+
}
|
|
547
|
+
return only;
|
|
548
|
+
}
|
|
549
|
+
function remoteOf(parent, localField, child, via) {
|
|
550
|
+
const builder = child.columns[localField];
|
|
551
|
+
const named = builder instanceof ColumnBuilder2 ? builder.state.references?.columns : undefined;
|
|
552
|
+
if (named !== undefined && named.length === 1 && named[0] !== undefined)
|
|
553
|
+
return named[0];
|
|
554
|
+
if (named !== undefined && named.length !== 1) {
|
|
555
|
+
pathError(`Table ${child.name} tenancy via "${via}" points ${localField} at ${String(named.length)} columns on ${parent.name}.`);
|
|
556
|
+
}
|
|
557
|
+
const declared = declaredPrimary(parent);
|
|
558
|
+
if (declared.length !== 1 || declared[0] === undefined) {
|
|
559
|
+
pathError(`Table ${child.name} tenancy via "${via}" needs one primary column on ${parent.name}.`);
|
|
560
|
+
}
|
|
561
|
+
return declared[0];
|
|
562
|
+
}
|
|
563
|
+
function adjust(item, chain, kind, tables) {
|
|
564
|
+
if (chain === undefined)
|
|
565
|
+
return item;
|
|
566
|
+
const first = chain.hops[0];
|
|
567
|
+
const options = item.options;
|
|
568
|
+
const columns = { ...item.columns };
|
|
569
|
+
for (const hop of chain.hops) {
|
|
570
|
+
if (hop.child !== item.name)
|
|
571
|
+
continue;
|
|
572
|
+
if (kind.get(hop.parent) !== "tenant")
|
|
573
|
+
continue;
|
|
574
|
+
const builder = columns[hop.localField];
|
|
575
|
+
if (!(builder instanceof ColumnBuilder2) || builder.state.references === undefined)
|
|
576
|
+
continue;
|
|
577
|
+
const parent = tables.find((table) => table.name === hop.parent);
|
|
578
|
+
const declared = parent === undefined ? [hop.remoteField] : declaredPrimary(parent);
|
|
579
|
+
columns[hop.localField] = retarget(builder, {
|
|
580
|
+
references: { ...builder.state.references, columns: declared }
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const field = first?.localField;
|
|
584
|
+
const indexes = field === undefined ? options?.indexes : leadingIndex(item.name, field, options?.indexes);
|
|
585
|
+
return {
|
|
586
|
+
...item,
|
|
587
|
+
columns,
|
|
588
|
+
options: {
|
|
589
|
+
...options,
|
|
590
|
+
...indexes !== undefined ? { indexes } : {}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function leadingIndex(table, field, previous) {
|
|
595
|
+
return (columns) => {
|
|
596
|
+
const handle = columns[field];
|
|
597
|
+
if (handle === undefined) {
|
|
598
|
+
throw new OkmError2("OKM1706", `Index on ${table} does not lead with ${field}. Put ${field} first.`);
|
|
599
|
+
}
|
|
600
|
+
const calls = previous === undefined ? [] : previous(columns);
|
|
601
|
+
if (!Array.isArray(calls))
|
|
602
|
+
return calls;
|
|
603
|
+
if (calls.some((call) => call.columns[0] === handle.name))
|
|
604
|
+
return calls;
|
|
605
|
+
return [index2(handle), ...calls];
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function writeExists(chain, input) {
|
|
609
|
+
if (chain === undefined)
|
|
610
|
+
return false;
|
|
611
|
+
const scope = input.scope;
|
|
612
|
+
if (scope === undefined) {
|
|
613
|
+
throw new OkmError2("OKM1701", `Table ${input.table} is tenant-scoped. Call for({ ${input.keys.join(", ")} }) or unscoped("reason").`);
|
|
614
|
+
}
|
|
615
|
+
if ("unscoped" in scope) {
|
|
616
|
+
input.sink.mark("unscoped|");
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
input.sink.text(input.appended ? " and " : " where ");
|
|
620
|
+
const snake = snakeOn(chain, input.fieldSql);
|
|
621
|
+
emitHop(chain, 0, input, snake, scope);
|
|
622
|
+
input.sink.mark("tenant|");
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
function emitHop(chain, index, input, snake, scope) {
|
|
626
|
+
const hop = chain.hops[index];
|
|
627
|
+
if (hop === undefined)
|
|
628
|
+
return;
|
|
629
|
+
const parentAlias = `p${String(index)}`;
|
|
630
|
+
const last = index === chain.hops.length - 1;
|
|
631
|
+
input.sink.text("exists (select 1 from ");
|
|
632
|
+
input.sink.text(quote(tableSql(hop.parent, hop.parentSql, snake)));
|
|
633
|
+
input.sink.text(" ");
|
|
634
|
+
input.sink.text(parentAlias);
|
|
635
|
+
input.sink.text(" where ");
|
|
636
|
+
input.sink.text(parentAlias);
|
|
637
|
+
input.sink.text(".");
|
|
638
|
+
input.sink.text(quote(columnSql(hop.remoteField, hop.remoteSql, snake)));
|
|
639
|
+
input.sink.text(" = ");
|
|
640
|
+
const assigned = index === 0 ? input.assigned?.[hop.localField] : undefined;
|
|
641
|
+
if (assigned !== undefined) {
|
|
642
|
+
input.sink.param(assigned);
|
|
643
|
+
} else {
|
|
644
|
+
const qualifier = index === 0 ? input.alias : `p${String(index - 1)}`;
|
|
645
|
+
input.sink.text(qualifier);
|
|
646
|
+
input.sink.text(".");
|
|
647
|
+
input.sink.text(quote(columnSql(hop.localField, hop.localSql, snake)));
|
|
648
|
+
}
|
|
649
|
+
if (!last) {
|
|
650
|
+
input.sink.text(" and ");
|
|
651
|
+
emitHop(chain, index + 1, input, snake, scope);
|
|
652
|
+
} else {
|
|
653
|
+
for (const key of input.keys) {
|
|
654
|
+
input.sink.text(" and ");
|
|
655
|
+
input.sink.text(parentAlias);
|
|
656
|
+
input.sink.text(".");
|
|
657
|
+
input.sink.text(quote(columnSql(key, undefined, snake)));
|
|
658
|
+
input.sink.text(" = ");
|
|
659
|
+
const raw = scope.values?.[key] ?? scope.value;
|
|
660
|
+
input.sink.param(input.encodeKey(key, raw));
|
|
661
|
+
}
|
|
662
|
+
if (input.lock)
|
|
663
|
+
input.sink.text(" for share");
|
|
664
|
+
}
|
|
665
|
+
input.sink.text(")");
|
|
666
|
+
}
|
|
667
|
+
function snakeOn(chain, fieldSql) {
|
|
668
|
+
const hop = chain.hops[0];
|
|
669
|
+
if (hop === undefined || hop.localSql !== undefined)
|
|
670
|
+
return false;
|
|
671
|
+
const live = fieldSql(hop.localField);
|
|
672
|
+
const snake = snakeCase(hop.localField);
|
|
673
|
+
return live !== undefined && live === snake && live !== hop.localField;
|
|
674
|
+
}
|
|
675
|
+
function columnSql(field, explicit, snake) {
|
|
676
|
+
if (explicit !== undefined)
|
|
677
|
+
return explicit;
|
|
678
|
+
return snake ? snakeCase(field) : field;
|
|
679
|
+
}
|
|
680
|
+
function tableSql(name, explicit, snake) {
|
|
681
|
+
if (explicit !== undefined)
|
|
682
|
+
return explicit;
|
|
683
|
+
return snake ? snakeCase(name) : name;
|
|
684
|
+
}
|
|
685
|
+
function declaredPrimary(item) {
|
|
686
|
+
const listed = item.options?.primaryKey;
|
|
687
|
+
if (listed !== undefined && listed.length > 0)
|
|
688
|
+
return [...listed];
|
|
689
|
+
const fields = [];
|
|
690
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
691
|
+
if (builder instanceof ColumnBuilder2 && builder.state.primaryKey === true)
|
|
692
|
+
fields.push(field);
|
|
693
|
+
}
|
|
694
|
+
return fields;
|
|
695
|
+
}
|
|
696
|
+
function sqlNameOf(table, field) {
|
|
697
|
+
const builder = table.columns[field];
|
|
698
|
+
if (!(builder instanceof ColumnBuilder2))
|
|
699
|
+
return;
|
|
700
|
+
return builder.state.sqlName;
|
|
701
|
+
}
|
|
702
|
+
function encodeOf(table, field) {
|
|
703
|
+
const builder = table.columns[field];
|
|
704
|
+
if (!(builder instanceof ColumnBuilder2))
|
|
705
|
+
return (value) => value;
|
|
706
|
+
const encode = builder.state.encode;
|
|
707
|
+
return (value) => encode(value);
|
|
708
|
+
}
|
|
709
|
+
function pathError(message) {
|
|
710
|
+
throw new OkmError2("OKM1705", message);
|
|
711
|
+
}
|
|
712
|
+
function isRecord2(value) {
|
|
713
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/runtime/tenancy/via.ts
|
|
717
|
+
var slots = new WeakMap;
|
|
718
|
+
var compiled = new WeakMap;
|
|
719
|
+
function via(path) {
|
|
720
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
721
|
+
definition("via() needs a relation path.");
|
|
722
|
+
}
|
|
723
|
+
const text = path.trim();
|
|
724
|
+
return {
|
|
725
|
+
via: text,
|
|
726
|
+
rewrite(table, ctx) {
|
|
727
|
+
return planFor(ctx).adjust(table);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function planFor(ctx) {
|
|
732
|
+
const cached = compiled.get(ctx.extras);
|
|
733
|
+
if (cached !== undefined)
|
|
734
|
+
return cached;
|
|
735
|
+
const plan = compilePaths(ctx.tables, classify(ctx.tables));
|
|
736
|
+
for (const [name, declared] of plan.endpoints) {
|
|
737
|
+
if (declared.length > 0)
|
|
738
|
+
ctx.extras.set(name, declared);
|
|
739
|
+
}
|
|
740
|
+
const encodeKey = (name, value) => {
|
|
741
|
+
const at = ctx.keys.indexOf(name);
|
|
742
|
+
const encode = at < 0 ? undefined : ctx.encodes[at];
|
|
743
|
+
return encode === undefined ? value : encode(value);
|
|
744
|
+
};
|
|
745
|
+
let slot = slots.get(ctx.api);
|
|
746
|
+
if (slot === undefined) {
|
|
747
|
+
slot = { plan, keys: ctx.keys, encodeKey };
|
|
748
|
+
slots.set(ctx.api, slot);
|
|
749
|
+
install(ctx.api, slot);
|
|
750
|
+
} else {
|
|
751
|
+
slot.plan = plan;
|
|
752
|
+
slot.keys = ctx.keys;
|
|
753
|
+
slot.encodeKey = encodeKey;
|
|
754
|
+
}
|
|
755
|
+
compiled.set(ctx.extras, plan);
|
|
756
|
+
return plan;
|
|
757
|
+
}
|
|
758
|
+
function classify(tables) {
|
|
759
|
+
const kind = new Map;
|
|
760
|
+
for (const item of tables) {
|
|
761
|
+
const mark = item.options?.tenancy;
|
|
762
|
+
if (isGlobal2(mark)) {
|
|
763
|
+
kind.set(item.name, "global");
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (isRecord3(mark) && typeof mark.via === "string" && typeof mark.rewrite === "function") {
|
|
767
|
+
kind.set(item.name, "path");
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
kind.set(item.name, "tenant");
|
|
771
|
+
}
|
|
772
|
+
return kind;
|
|
773
|
+
}
|
|
774
|
+
function install(api, slot) {
|
|
775
|
+
const previousPredicate = api.predicate.bind(api);
|
|
776
|
+
const previousStamp = api.stamp.bind(api);
|
|
777
|
+
const previousClient = api.client.bind(api);
|
|
778
|
+
const previousRules = api.rules.bind(api);
|
|
779
|
+
const target = api;
|
|
780
|
+
target.predicate = (spec) => {
|
|
781
|
+
if (!slot.plan.isPath(spec.table))
|
|
782
|
+
return previousPredicate(spec);
|
|
783
|
+
return slot.plan.writeExists({
|
|
784
|
+
table: spec.table,
|
|
785
|
+
alias: spec.alias,
|
|
786
|
+
fieldSql: spec.fieldSql,
|
|
787
|
+
scope: spec.scope,
|
|
788
|
+
sink: spec.sink,
|
|
789
|
+
appended: spec.appended,
|
|
790
|
+
lock: false,
|
|
791
|
+
keys: slot.keys,
|
|
792
|
+
encodeKey: slot.encodeKey
|
|
793
|
+
}) === true;
|
|
794
|
+
};
|
|
795
|
+
target.isPath = (table) => slot.plan.isPath(table);
|
|
796
|
+
target.pathOf = (table) => slot.plan.hops(table)?.map((hop) => ({
|
|
797
|
+
child: hop.child,
|
|
798
|
+
parent: hop.parent,
|
|
799
|
+
localField: hop.localField,
|
|
800
|
+
remoteField: hop.remoteField
|
|
801
|
+
}));
|
|
802
|
+
target.lockPredicate = (spec) => {
|
|
803
|
+
if (!slot.plan.isPath(spec.table))
|
|
804
|
+
return false;
|
|
805
|
+
return slot.plan.writeExists({
|
|
806
|
+
table: spec.table,
|
|
807
|
+
alias: spec.alias,
|
|
808
|
+
fieldSql: spec.fieldSql,
|
|
809
|
+
scope: spec.scope,
|
|
810
|
+
sink: spec.sink,
|
|
811
|
+
appended: false,
|
|
812
|
+
lock: true,
|
|
813
|
+
keys: slot.keys,
|
|
814
|
+
encodeKey: slot.encodeKey
|
|
815
|
+
});
|
|
816
|
+
};
|
|
817
|
+
target.noteParent = (table, set, sink, scope, fieldSql) => {
|
|
818
|
+
const hop = slot.plan.hops(table)?.[0];
|
|
819
|
+
if (hop === undefined)
|
|
820
|
+
return;
|
|
821
|
+
if (scope === undefined || "unscoped" in scope)
|
|
822
|
+
return;
|
|
823
|
+
const assigned = {};
|
|
824
|
+
const next = set[hop.localField];
|
|
825
|
+
if (typeof next === "string")
|
|
826
|
+
assigned[hop.localField] = hop.encode(next);
|
|
827
|
+
slot.plan.writeExists({
|
|
828
|
+
table,
|
|
829
|
+
alias: "t",
|
|
830
|
+
fieldSql,
|
|
831
|
+
scope,
|
|
832
|
+
sink,
|
|
833
|
+
appended: true,
|
|
834
|
+
lock: true,
|
|
835
|
+
keys: slot.keys,
|
|
836
|
+
encodeKey: slot.encodeKey,
|
|
837
|
+
...Object.keys(assigned).length > 0 ? { assigned } : {}
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
target.stamp = (table, rows, scope) => {
|
|
841
|
+
if (slot.plan.isPath(table)) {
|
|
842
|
+
requireScope2(slot.keys, table, scope);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
previousStamp(table, rows, scope);
|
|
846
|
+
};
|
|
847
|
+
target.client = (spec) => {
|
|
848
|
+
const view = previousClient(spec);
|
|
849
|
+
if (spec.scoped)
|
|
850
|
+
return view;
|
|
851
|
+
return { ...view, names: view.names.filter((name) => !slot.plan.isPath(name)) };
|
|
852
|
+
};
|
|
853
|
+
target.rules = (table, source, scope) => pathRules(previousRules(table, source, scope), slot, table, source, scope);
|
|
854
|
+
}
|
|
855
|
+
function pathRules(rules, slot, table, source, scope) {
|
|
856
|
+
const hops = slot.plan.hops(table);
|
|
857
|
+
if (hops === undefined)
|
|
858
|
+
return rules;
|
|
859
|
+
const next = [...rules];
|
|
860
|
+
next.push({
|
|
861
|
+
rule: "tenancy",
|
|
862
|
+
contribution: `path ${hops.map((hop) => hop.parent).join(".")}`,
|
|
863
|
+
provenance: "catalog",
|
|
864
|
+
...source !== undefined ? { source } : {}
|
|
865
|
+
});
|
|
866
|
+
if (scope !== undefined && "value" in scope && !next.some((rule) => rule.contribution === "scoped")) {
|
|
867
|
+
next.push({ rule: "tenancy", contribution: "scoped", provenance: "planner" });
|
|
868
|
+
}
|
|
869
|
+
return next;
|
|
870
|
+
}
|
|
871
|
+
function requireScope2(keys, table, scope) {
|
|
872
|
+
if (scope === undefined || "unscoped" in scope) {
|
|
873
|
+
throw new OkmError2("OKM1701", `insert on ${table} has no tenant. Call for({ ${keys.join(", ")} }) so the scope can set it.`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function isGlobal2(value) {
|
|
877
|
+
return isRecord3(value) && value.kind === "global" && typeof value.reason === "string";
|
|
878
|
+
}
|
|
879
|
+
function isRecord3(value) {
|
|
880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
881
|
+
}
|
|
882
|
+
// src/runtime/tenancy/rls.ts
|
|
883
|
+
var ROLE_MESSAGE = {
|
|
884
|
+
owner: "The connected role owns the tenant tables, so row-level security would not apply.",
|
|
885
|
+
superuser: "The connected role is a superuser, so row-level security would not apply.",
|
|
886
|
+
bypassrls: "The connected role has BYPASSRLS, so row-level security would not apply."
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
class RlsRoleError extends OkmError2 {
|
|
890
|
+
reason;
|
|
891
|
+
constructor(reason, endpoint) {
|
|
892
|
+
const named = endpoint === undefined ? ROLE_MESSAGE[reason] : `${ROLE_MESSAGE[reason]} Endpoint ${endpoint}.`;
|
|
893
|
+
super("OKM1707", named, {
|
|
894
|
+
fix: {
|
|
895
|
+
summary: "Connect as a role that is not the table owner, a superuser, or BYPASSRLS. okm seed under rlsTenancy runs as the application role. The owner gets this error."
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
this.name = "OkmError";
|
|
899
|
+
this.reason = reason;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
var TRAIT = "okm.rls";
|
|
903
|
+
var WRAPPED = Symbol("okmodel.rls.wrapped");
|
|
904
|
+
var INNER = Symbol("okmodel.rls.inner");
|
|
905
|
+
var guardChecks = new WeakMap;
|
|
906
|
+
var scopeStates = new WeakMap;
|
|
907
|
+
function openColumn(key) {
|
|
908
|
+
return columnTenancy({
|
|
909
|
+
key,
|
|
910
|
+
type: "uuid"
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function statementOf(text, params) {
|
|
914
|
+
return params === undefined ? { text } : { text, params };
|
|
915
|
+
}
|
|
916
|
+
function rlsTenancy(input) {
|
|
917
|
+
if (input === undefined || typeof input !== "object") {
|
|
918
|
+
throw new OkmError2("OKM1060", "rlsTenancy() needs { key, type }.", {
|
|
919
|
+
fix: { summary: 'Pass rlsTenancy({ key: "tenantId", type: "uuid" }).' }
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (Array.isArray(input.key)) {
|
|
923
|
+
throw new OkmError2("OKM1060", "rlsTenancy() takes one key. Combining it with compositeTenancy or via() is not supported yet.", {
|
|
924
|
+
fix: {
|
|
925
|
+
summary: "Use rlsTenancy() on its own, with one key. compositeTenancy() and via() are separate strategies."
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
const base = openColumn(input.key);
|
|
930
|
+
const key = base.key;
|
|
931
|
+
let checker;
|
|
932
|
+
let armedFor;
|
|
933
|
+
const api = {
|
|
934
|
+
...base,
|
|
935
|
+
strategy: "rls",
|
|
936
|
+
rewrite(tables) {
|
|
937
|
+
refuseVia(tables);
|
|
938
|
+
const next = base.rewrite(tables);
|
|
939
|
+
for (const item of tables) {
|
|
940
|
+
if (isTenantTable(item))
|
|
941
|
+
attachTrait(item, key);
|
|
942
|
+
}
|
|
943
|
+
return next;
|
|
944
|
+
},
|
|
945
|
+
hook(target, ctx) {
|
|
946
|
+
if (ctx.table !== undefined) {
|
|
947
|
+
arm(ctx, key, (check, session) => {
|
|
948
|
+
checker = check;
|
|
949
|
+
armedFor = session;
|
|
950
|
+
}, armedFor);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
base.hook(target, ctx);
|
|
954
|
+
if (checker !== undefined) {
|
|
955
|
+
target["~rls"] = checker;
|
|
956
|
+
checker = undefined;
|
|
957
|
+
armedFor = undefined;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
return api;
|
|
962
|
+
}
|
|
963
|
+
async function runReadOnly(conn, text, params, prelude, options) {
|
|
964
|
+
await conn.execute("begin read only");
|
|
965
|
+
try {
|
|
966
|
+
await conn.execute(prelude.text, prelude.params);
|
|
967
|
+
const result = await conn.execute(text, params, options);
|
|
968
|
+
await conn.execute("commit");
|
|
969
|
+
return result;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
await conn.execute("rollback").catch(() => {
|
|
972
|
+
return;
|
|
973
|
+
});
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function* streamHeld(conn, text, params, prelude) {
|
|
978
|
+
await conn.execute("begin read only");
|
|
979
|
+
try {
|
|
980
|
+
await conn.execute(prelude.text, prelude.params);
|
|
981
|
+
await conn.execute(`declare okm_rls no scroll cursor for ${text}`, params);
|
|
982
|
+
for (;; ) {
|
|
983
|
+
const chunk = await conn.execute("fetch 64 from okm_rls");
|
|
984
|
+
if (chunk.rows.length === 0)
|
|
985
|
+
break;
|
|
986
|
+
yield chunk.rows;
|
|
987
|
+
}
|
|
988
|
+
await conn.execute("close okm_rls");
|
|
989
|
+
await conn.execute("commit");
|
|
990
|
+
} catch (error) {
|
|
991
|
+
await conn.execute("rollback").catch(() => {
|
|
992
|
+
return;
|
|
993
|
+
});
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function refuseVia(tables) {
|
|
998
|
+
for (const item of tables) {
|
|
999
|
+
const mark = item.options?.tenancy;
|
|
1000
|
+
if (isRecord4(mark) && typeof mark.rewrite === "function") {
|
|
1001
|
+
throw new OkmError2("OKM1060", `Table ${item.name} uses via() with rlsTenancy(). Combining them is not supported yet.`, {
|
|
1002
|
+
fix: {
|
|
1003
|
+
summary: "Use rlsTenancy() on its own, with one key. via() and compositeTenancy() are separate strategies."
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function isTenantTable(item) {
|
|
1010
|
+
const mark = item.options?.tenancy;
|
|
1011
|
+
return mark === undefined;
|
|
1012
|
+
}
|
|
1013
|
+
function attachTrait(item, key) {
|
|
1014
|
+
const trait = policyTrait(item.name, key);
|
|
1015
|
+
const record = item;
|
|
1016
|
+
if (record.options === undefined) {
|
|
1017
|
+
Object.assign(item, { options: { traits: [trait] } });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const traits = record.options.traits;
|
|
1021
|
+
if (!Array.isArray(traits)) {
|
|
1022
|
+
record.options.traits = [trait];
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (traits.some((entry) => isRecord4(entry) && entry.name === TRAIT))
|
|
1026
|
+
return;
|
|
1027
|
+
traits.push(trait);
|
|
1028
|
+
}
|
|
1029
|
+
function policyTrait(tsName, key) {
|
|
1030
|
+
return {
|
|
1031
|
+
name: TRAIT,
|
|
1032
|
+
fields: {},
|
|
1033
|
+
apply() {
|
|
1034
|
+
return;
|
|
1035
|
+
},
|
|
1036
|
+
contribute(_peers, built) {
|
|
1037
|
+
if (!Array.isArray(built))
|
|
1038
|
+
return [];
|
|
1039
|
+
const objects = built;
|
|
1040
|
+
const parent = objects.find((object) => object.kind === "table" && object.provenance.name === tsName);
|
|
1041
|
+
if (parent === undefined || parent.kind !== "table")
|
|
1042
|
+
return [];
|
|
1043
|
+
const ref = { namespace: parent.identity.namespace, name: parent.identity.name };
|
|
1044
|
+
if (objects.some((object) => object.kind === "policy" && object.identity.parent.name === ref.name)) {
|
|
1045
|
+
return [];
|
|
1046
|
+
}
|
|
1047
|
+
const column = objects.find((object) => object.kind === "column" && object.identity.parent.name === ref.name && (object.identity.name === key || object.identity.name === snakeCase(key)));
|
|
1048
|
+
if (column === undefined || column.kind !== "column") {
|
|
1049
|
+
throw new OkmError2("OKM1020", `Table ${tsName} is missing the ${key} column row-level security needs.`);
|
|
1050
|
+
}
|
|
1051
|
+
const provenance = { origin: "trait", name: "rls" };
|
|
1052
|
+
const expression = tenantPolicyExpression(column.identity.name, column.definition.dataType);
|
|
1053
|
+
return [
|
|
1054
|
+
policyObject({
|
|
1055
|
+
parent: ref,
|
|
1056
|
+
name: `${ref.name}_tenant`,
|
|
1057
|
+
command: "all",
|
|
1058
|
+
expression,
|
|
1059
|
+
force: true,
|
|
1060
|
+
provenance
|
|
1061
|
+
}),
|
|
1062
|
+
policyObject({
|
|
1063
|
+
parent: ref,
|
|
1064
|
+
name: `${ref.name}_unscoped_select`,
|
|
1065
|
+
command: "select",
|
|
1066
|
+
expression: unscopedPolicyExpression(),
|
|
1067
|
+
force: true,
|
|
1068
|
+
provenance
|
|
1069
|
+
})
|
|
1070
|
+
];
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function arm(ctx, key, remember, armedFor) {
|
|
1075
|
+
const session = ctx.session;
|
|
1076
|
+
if (!isSession(session) || session === armedFor)
|
|
1077
|
+
return;
|
|
1078
|
+
const raw = innerPool(session.pool);
|
|
1079
|
+
const tables = tenantTables(session.schema.model, key);
|
|
1080
|
+
remember(() => checkEndpoint(raw, "primary", 0, tables), session);
|
|
1081
|
+
if (session.scope === undefined || isWrapped(session.pool))
|
|
1082
|
+
return;
|
|
1083
|
+
session.pool = wrapPool(session.pool, session.scope, session.tx !== undefined, tables);
|
|
1084
|
+
}
|
|
1085
|
+
function isSession(value) {
|
|
1086
|
+
if (value === undefined)
|
|
1087
|
+
return false;
|
|
1088
|
+
const pool = value.pool;
|
|
1089
|
+
return typeof pool === "object" && pool !== null && "execute" in pool;
|
|
1090
|
+
}
|
|
1091
|
+
function tenantTables(model, key) {
|
|
1092
|
+
const snake = snakeCase(key);
|
|
1093
|
+
const tables = [];
|
|
1094
|
+
for (const table of Object.values(model)) {
|
|
1095
|
+
if (table.primary.length === 0)
|
|
1096
|
+
continue;
|
|
1097
|
+
const column = table.columns.find((item) => item.field === key || item.sql === snake);
|
|
1098
|
+
if (column?.dataType === undefined)
|
|
1099
|
+
continue;
|
|
1100
|
+
tables.push({ name: table.sql, column: column.sql, dataType: column.dataType });
|
|
1101
|
+
}
|
|
1102
|
+
return tables;
|
|
1103
|
+
}
|
|
1104
|
+
function isWrapped(pool) {
|
|
1105
|
+
return WRAPPED in pool;
|
|
1106
|
+
}
|
|
1107
|
+
function innerPool(pool) {
|
|
1108
|
+
const inner = pool[INNER];
|
|
1109
|
+
return inner ?? pool;
|
|
1110
|
+
}
|
|
1111
|
+
function rootPool(pool) {
|
|
1112
|
+
let current = pool;
|
|
1113
|
+
for (;; ) {
|
|
1114
|
+
const inner = current[INNER];
|
|
1115
|
+
if (inner === undefined)
|
|
1116
|
+
return current;
|
|
1117
|
+
current = inner;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function wrapPool(inner, scope, inside, tables) {
|
|
1121
|
+
const setting = settingOf(scope);
|
|
1122
|
+
const unscoped = "unscoped" in scope;
|
|
1123
|
+
installGuard(inner, tables);
|
|
1124
|
+
const ensure = () => inner.okmGuardInstalled === true ? Promise.resolve() : checkEndpoint(inner, "primary", 0, tables);
|
|
1125
|
+
const state = inside ? scopeState(rootPool(inner)) : undefined;
|
|
1126
|
+
if (state !== undefined)
|
|
1127
|
+
queueScope(inner, scope, state);
|
|
1128
|
+
const arm = () => state === undefined ? Promise.resolve() : ensureScope(inner, scope, state);
|
|
1129
|
+
const wrapped = {
|
|
1130
|
+
capabilities: inner.capabilities,
|
|
1131
|
+
execute: async (text, params, options) => {
|
|
1132
|
+
await ensure();
|
|
1133
|
+
if (unscoped && isWriteSql(text))
|
|
1134
|
+
unscopedWrite();
|
|
1135
|
+
if (inside) {
|
|
1136
|
+
if (state === undefined) {
|
|
1137
|
+
await arm();
|
|
1138
|
+
return inner.execute(text, params, options);
|
|
1139
|
+
}
|
|
1140
|
+
const release = await acquire(state);
|
|
1141
|
+
try {
|
|
1142
|
+
await ensureScope(inner, scope, state);
|
|
1143
|
+
return await inner.execute(text, params, options);
|
|
1144
|
+
} finally {
|
|
1145
|
+
release();
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (isReadSql(text))
|
|
1149
|
+
return runScopedRead(inner, text, params, setting, options);
|
|
1150
|
+
const results = await inner.batch([setting, statementOf(text, params)], options);
|
|
1151
|
+
const result = results[1];
|
|
1152
|
+
if (result === undefined) {
|
|
1153
|
+
throw new OkmError2("OKM1111", "The driver returned no result for the statement.", {
|
|
1154
|
+
fix: { summary: "Use a Postgres driver that returns one result per batched statement." }
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
return result;
|
|
1158
|
+
},
|
|
1159
|
+
batch: async (statements, options) => {
|
|
1160
|
+
await ensure();
|
|
1161
|
+
if (unscoped && statements.some((statement) => isWriteSql(statement.text)))
|
|
1162
|
+
unscopedWrite();
|
|
1163
|
+
if (inside) {
|
|
1164
|
+
if (state === undefined) {
|
|
1165
|
+
await arm();
|
|
1166
|
+
return inner.batch(statements, options);
|
|
1167
|
+
}
|
|
1168
|
+
const release = await acquire(state);
|
|
1169
|
+
try {
|
|
1170
|
+
await ensureScope(inner, scope, state);
|
|
1171
|
+
return await inner.batch(statements, options);
|
|
1172
|
+
} finally {
|
|
1173
|
+
release();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (statements.length > 0 && statements.every((statement) => isReadSql(statement.text))) {
|
|
1177
|
+
const results = [];
|
|
1178
|
+
for (const statement of statements) {
|
|
1179
|
+
results.push(await runScopedRead(inner, statement.text, statement.params, setting, options));
|
|
1180
|
+
}
|
|
1181
|
+
return results;
|
|
1182
|
+
}
|
|
1183
|
+
const results = await inner.batch([setting, ...statements.map((statement) => statementOf(statement.text, statement.params))], options);
|
|
1184
|
+
return results.slice(1);
|
|
1185
|
+
},
|
|
1186
|
+
stats: () => inner.stats(),
|
|
1187
|
+
close: () => inner.close()
|
|
1188
|
+
};
|
|
1189
|
+
if (state !== undefined)
|
|
1190
|
+
bindScopeHooks(inner, wrapped, state);
|
|
1191
|
+
if (inner.reserve !== undefined)
|
|
1192
|
+
wrapped.reserve = () => inner.reserve();
|
|
1193
|
+
if (inner.describe !== undefined) {
|
|
1194
|
+
wrapped.describe = (text, params) => inner.describe(text, params);
|
|
1195
|
+
}
|
|
1196
|
+
if (inner.listen !== undefined)
|
|
1197
|
+
wrapped.listen = (channel, onNotify) => inner.listen(channel, onNotify);
|
|
1198
|
+
if (inner.cancel !== undefined)
|
|
1199
|
+
wrapped.cancel = () => inner.cancel();
|
|
1200
|
+
if (inner.stream !== undefined || inner.okmReadOnlyStream !== undefined) {
|
|
1201
|
+
wrapped.stream = (text, params) => streamScoped(inner, text, params, setting, scope, inside, unscoped, ensure, arm, state);
|
|
1202
|
+
}
|
|
1203
|
+
Object.defineProperty(wrapped, WRAPPED, { value: true });
|
|
1204
|
+
Object.defineProperty(wrapped, INNER, { value: inner });
|
|
1205
|
+
return wrapped;
|
|
1206
|
+
}
|
|
1207
|
+
function settingOf(scope) {
|
|
1208
|
+
if ("unscoped" in scope)
|
|
1209
|
+
return { text: "select set_config('app.unscoped', 'on', true)" };
|
|
1210
|
+
return { text: "select set_config('app.tenant', $1, true)", params: [scope.value] };
|
|
1211
|
+
}
|
|
1212
|
+
async function runScopedRead(inner, text, params, setting, options) {
|
|
1213
|
+
const unit = inner.okmReadOnlyUnit;
|
|
1214
|
+
if (unit !== undefined)
|
|
1215
|
+
return unit(text, params, setting, options);
|
|
1216
|
+
if (inner.reserve === undefined) {
|
|
1217
|
+
throw new OkmError2("OKM1111", "Row-level security needs a driver that can reserve a connection.", {
|
|
1218
|
+
fix: { summary: "Use a Postgres driver that supports interactive transactions." }
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const conn = await inner.reserve();
|
|
1222
|
+
try {
|
|
1223
|
+
return await runReadOnly(conn, text, params, setting, options);
|
|
1224
|
+
} finally {
|
|
1225
|
+
await conn.release();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function streamScoped(inner, text, params, setting, scope, inside, unscoped, ensure, arm, state) {
|
|
1229
|
+
return async function* () {
|
|
1230
|
+
await ensure();
|
|
1231
|
+
if (unscoped && isWriteSql(text))
|
|
1232
|
+
unscopedWrite();
|
|
1233
|
+
const routed = inner.okmReadOnlyStream;
|
|
1234
|
+
if (!inside && isReadSql(text) && routed !== undefined) {
|
|
1235
|
+
yield* routed(text, params, setting, undefined);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (inside) {
|
|
1239
|
+
if (inner.stream === undefined) {
|
|
1240
|
+
throw new OkmError2("OKM1111", "stream() inside tx() needs a driver stream.", {
|
|
1241
|
+
fix: { summary: "Stream outside the transaction, or read with find()." }
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (state === undefined) {
|
|
1245
|
+
await arm();
|
|
1246
|
+
yield* inner.stream(text, params);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const release = await acquire(state);
|
|
1250
|
+
try {
|
|
1251
|
+
await ensureScope(inner, scope, state);
|
|
1252
|
+
yield* inner.stream(text, params);
|
|
1253
|
+
} finally {
|
|
1254
|
+
release();
|
|
1255
|
+
}
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (inner.reserve === undefined) {
|
|
1259
|
+
throw new OkmError2("OKM1111", "Row-level security needs a driver that can reserve a connection.", {
|
|
1260
|
+
fix: { summary: "Use a Postgres driver that supports interactive transactions." }
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
const conn = await inner.reserve();
|
|
1264
|
+
try {
|
|
1265
|
+
yield* streamHeld(conn, text, params, setting);
|
|
1266
|
+
} finally {
|
|
1267
|
+
await conn.release();
|
|
1268
|
+
}
|
|
1269
|
+
}();
|
|
1270
|
+
}
|
|
1271
|
+
function scopeState(pool) {
|
|
1272
|
+
let state = scopeStates.get(pool);
|
|
1273
|
+
if (state === undefined) {
|
|
1274
|
+
state = { last: undefined, pending: undefined, epoch: 0, chain: Promise.resolve() };
|
|
1275
|
+
scopeStates.set(pool, state);
|
|
1276
|
+
}
|
|
1277
|
+
return state;
|
|
1278
|
+
}
|
|
1279
|
+
function acquire(state) {
|
|
1280
|
+
const previous = state.chain;
|
|
1281
|
+
let release = () => {
|
|
1282
|
+
return;
|
|
1283
|
+
};
|
|
1284
|
+
const gate = new Promise((resolve) => {
|
|
1285
|
+
release = resolve;
|
|
1286
|
+
});
|
|
1287
|
+
state.chain = previous.then(() => gate, () => gate);
|
|
1288
|
+
return previous.then(() => release, () => release);
|
|
1289
|
+
}
|
|
1290
|
+
function scopeKey(scope) {
|
|
1291
|
+
return "unscoped" in scope ? `unscoped:${scope.unscoped}` : scope.value;
|
|
1292
|
+
}
|
|
1293
|
+
function queueScope(inner, scope, state) {
|
|
1294
|
+
const key = scopeKey(scope);
|
|
1295
|
+
if (state.last === key || state.pending !== undefined)
|
|
1296
|
+
return;
|
|
1297
|
+
const setting = settingOf(scope);
|
|
1298
|
+
const run = inner.execute(setting.text, setting.params).then(() => {
|
|
1299
|
+
if (state.pending === run)
|
|
1300
|
+
state.pending = undefined;
|
|
1301
|
+
});
|
|
1302
|
+
state.pending = run;
|
|
1303
|
+
run.catch(() => {
|
|
1304
|
+
if (state.pending === run)
|
|
1305
|
+
state.pending = undefined;
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
async function ensureScope(inner, scope, state) {
|
|
1309
|
+
const key = scopeKey(scope);
|
|
1310
|
+
const pending = state.pending;
|
|
1311
|
+
if (pending !== undefined)
|
|
1312
|
+
await pending;
|
|
1313
|
+
if (state.last === key)
|
|
1314
|
+
return;
|
|
1315
|
+
const epoch = state.epoch;
|
|
1316
|
+
const setting = settingOf(scope);
|
|
1317
|
+
await inner.execute(setting.text, setting.params);
|
|
1318
|
+
if (state.epoch !== epoch)
|
|
1319
|
+
return;
|
|
1320
|
+
state.last = key;
|
|
1321
|
+
}
|
|
1322
|
+
function bindScopeHooks(inner, wrapped, state) {
|
|
1323
|
+
const hooks = {
|
|
1324
|
+
okmForgetScope() {
|
|
1325
|
+
state.epoch += 1;
|
|
1326
|
+
state.last = undefined;
|
|
1327
|
+
state.pending = undefined;
|
|
1328
|
+
state.chain = Promise.resolve();
|
|
1329
|
+
},
|
|
1330
|
+
okmScopePending() {
|
|
1331
|
+
return state.pending;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
Object.assign(inner, hooks);
|
|
1335
|
+
Object.assign(wrapped, hooks);
|
|
1336
|
+
}
|
|
1337
|
+
function installGuard(inner, tables) {
|
|
1338
|
+
const routed = inner;
|
|
1339
|
+
if (routed.okmEndpoints === undefined || routed.okmGuardInstalled === true)
|
|
1340
|
+
return;
|
|
1341
|
+
const guard = (endpoint) => checkEndpoint(endpoint.pool, endpoint.name, endpoint.generation, tables);
|
|
1342
|
+
for (const endpoint of routed.okmEndpoints()) {
|
|
1343
|
+
endpoint.pool.okmGuard = guard;
|
|
1344
|
+
}
|
|
1345
|
+
routed.okmGuardInstalled = true;
|
|
1346
|
+
}
|
|
1347
|
+
function checkEndpoint(pool, endpoint, generation, tables) {
|
|
1348
|
+
let generations = guardChecks.get(pool);
|
|
1349
|
+
if (generations === undefined) {
|
|
1350
|
+
generations = new Map;
|
|
1351
|
+
guardChecks.set(pool, generations);
|
|
1352
|
+
}
|
|
1353
|
+
const existing = generations.get(generation);
|
|
1354
|
+
if (existing !== undefined)
|
|
1355
|
+
return existing;
|
|
1356
|
+
const pending = verifyEndpoint(pool, endpoint, tables);
|
|
1357
|
+
generations.set(generation, pending);
|
|
1358
|
+
pending.catch(() => {
|
|
1359
|
+
if (generations.get(generation) === pending)
|
|
1360
|
+
generations.delete(generation);
|
|
1361
|
+
});
|
|
1362
|
+
return pending;
|
|
1363
|
+
}
|
|
1364
|
+
async function verifyEndpoint(pool, endpoint, tables) {
|
|
1365
|
+
const idents = tables.length === 0 ? "" : tables.map((table) => quoteLiteral(quotedIdent(table.name))).join(", ");
|
|
1366
|
+
const names = idents.length === 0 ? "" : `array[${idents}]::text[]`;
|
|
1367
|
+
const owns = names.length === 0 ? "false" : `exists (
|
|
1368
|
+
select 1
|
|
1369
|
+
from unnest(${names}) as owned_name(relname)
|
|
1370
|
+
join pg_class owned on owned.oid = to_regclass(owned_name.relname)
|
|
1371
|
+
where owned.relkind = 'r'
|
|
1372
|
+
and owned.relowner = r.oid
|
|
1373
|
+
)`;
|
|
1374
|
+
const policies = names.length === 0 ? "" : `, c.relname, c.relrowsecurity::text, c.relforcerowsecurity::text, p.polname, coalesce(pg_get_expr(p.polqual, p.polrelid), ''), coalesce(pg_get_expr(p.polwithcheck, p.polrelid), '')`;
|
|
1375
|
+
const join = names.length === 0 ? "" : ` left join unnest(${names}) as listed(relname) on true left join pg_class c on c.oid = to_regclass(listed.relname) and c.relkind = 'r' left join pg_policy p on p.polrelid = c.oid`;
|
|
1376
|
+
const result = await pool.execute(`select r.rolsuper::text, r.rolbypassrls::text, (${owns})::text${policies} from pg_roles r${join} where r.rolname = current_user`);
|
|
1377
|
+
const row = result.rows[0];
|
|
1378
|
+
if (row === undefined) {
|
|
1379
|
+
throw new OkmError2("OKM1707", `The connected role is not visible in pg_roles. Endpoint ${endpoint}.`, {
|
|
1380
|
+
fix: {
|
|
1381
|
+
summary: "Connect as a role that is not the table owner, a superuser, or BYPASSRLS. okm seed under rlsTenancy runs as the application role. The owner gets this error."
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
if (row[0] === "true")
|
|
1386
|
+
throw new RlsRoleError("superuser", endpoint);
|
|
1387
|
+
if (row[1] === "true")
|
|
1388
|
+
throw new RlsRoleError("bypassrls", endpoint);
|
|
1389
|
+
if (row[2] === "true")
|
|
1390
|
+
throw new RlsRoleError("owner", endpoint);
|
|
1391
|
+
if (tables.length === 0)
|
|
1392
|
+
return;
|
|
1393
|
+
assertPolicyRows(result.rows, tables, endpoint);
|
|
1394
|
+
}
|
|
1395
|
+
function assertPolicyRows(rows, tables, endpoint) {
|
|
1396
|
+
const live = new Map;
|
|
1397
|
+
for (const row of rows) {
|
|
1398
|
+
const name = row[3];
|
|
1399
|
+
if (name === undefined || name === null || name.length === 0)
|
|
1400
|
+
continue;
|
|
1401
|
+
let table = live.get(name);
|
|
1402
|
+
if (table === undefined) {
|
|
1403
|
+
table = {
|
|
1404
|
+
rowsecurity: row[4] ?? "",
|
|
1405
|
+
force: row[5] ?? "",
|
|
1406
|
+
policies: new Map
|
|
1407
|
+
};
|
|
1408
|
+
live.set(name, table);
|
|
1409
|
+
}
|
|
1410
|
+
const policy = row[6];
|
|
1411
|
+
if (policy === undefined || policy === null || policy.length === 0)
|
|
1412
|
+
continue;
|
|
1413
|
+
table.policies.set(policy, { using: row[7] ?? "", check: row[8] ?? "" });
|
|
1414
|
+
}
|
|
1415
|
+
for (const table of tables) {
|
|
1416
|
+
const found = live.get(table.name);
|
|
1417
|
+
if (found === undefined || found.rowsecurity !== "true") {
|
|
1418
|
+
policyError(table.name, "is missing forced row-level security.", endpoint);
|
|
1419
|
+
}
|
|
1420
|
+
if (found.force !== "true") {
|
|
1421
|
+
policyError(table.name, "does not force row-level security.", endpoint);
|
|
1422
|
+
}
|
|
1423
|
+
const tenant = tenantPolicyExpression(table.column, table.dataType);
|
|
1424
|
+
const unscoped = unscopedPolicyExpression();
|
|
1425
|
+
const tenantPolicy = found.policies.get(`${table.name}_tenant`);
|
|
1426
|
+
const unscopedPolicy = found.policies.get(`${table.name}_unscoped_select`);
|
|
1427
|
+
if (tenantPolicy === undefined || tenantPolicy.using !== tenant || tenantPolicy.check !== tenant) {
|
|
1428
|
+
policyError(table.name, `policy ${table.name}_tenant does not match the tenant predicate.`, endpoint);
|
|
1429
|
+
}
|
|
1430
|
+
if (unscopedPolicy === undefined || unscopedPolicy.using !== unscoped || unscopedPolicy.check !== "") {
|
|
1431
|
+
policyError(table.name, `policy ${table.name}_unscoped_select does not match the unscoped predicate.`, endpoint);
|
|
1432
|
+
}
|
|
1433
|
+
if (found.policies.size !== 2) {
|
|
1434
|
+
policyError(table.name, "has a row-level security policy okm did not emit.", endpoint);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function policyError(table, detail, endpoint) {
|
|
1439
|
+
throw new OkmError2("OKM1708", `Table ${table} ${detail} Endpoint ${endpoint}.`, {
|
|
1440
|
+
table,
|
|
1441
|
+
fix: { summary: "Restore the policies okm generate emits for this rls schema." }
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
function quotedIdent(name) {
|
|
1445
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
1446
|
+
}
|
|
1447
|
+
function unscopedWrite() {
|
|
1448
|
+
throw new OkmError2("OKM1701", "unscoped() cannot write under row-level security.", {
|
|
1449
|
+
fix: {
|
|
1450
|
+
summary: "Cross-tenant writes belong to the migration role. Use for() to write as one tenant."
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function isWriteSql(text) {
|
|
1455
|
+
const sql = text.trimStart().toLowerCase().replace(/\s+/g, " ");
|
|
1456
|
+
if (/^(insert|update|delete|refresh|truncate|create|alter|drop|grant|revoke|copy)\b/.test(sql)) {
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
return sql.startsWith("with ") && /\b(insert|update|delete)\b/.test(sql);
|
|
1460
|
+
}
|
|
1461
|
+
function isReadSql(text) {
|
|
1462
|
+
const sql = text.trimStart().toLowerCase().replace(/\s+/g, " ");
|
|
1463
|
+
if (/^(select|values|table|show|explain)\b/.test(sql)) {
|
|
1464
|
+
return !/\sfor (no key update|key share|update|share)\b/.test(sql);
|
|
1465
|
+
}
|
|
1466
|
+
return sql.startsWith("with ") && !/\b(insert|update|delete)\b/.test(sql);
|
|
1467
|
+
}
|
|
1468
|
+
function isRecord4(value) {
|
|
1469
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
22
1472
|
// src/runtime/tenancy/index.ts
|
|
23
1473
|
function global(reason) {
|
|
24
1474
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
@@ -51,30 +1501,30 @@ function columnTenancy(input) {
|
|
|
51
1501
|
tenants.clear();
|
|
52
1502
|
globals.clear();
|
|
53
1503
|
exemptions.clear();
|
|
54
|
-
return
|
|
1504
|
+
return rewriteTables2(key, column, encode, api, tables, tenants, globals, exemptions);
|
|
55
1505
|
},
|
|
56
1506
|
column: () => column,
|
|
57
1507
|
predicate(spec) {
|
|
58
|
-
return
|
|
1508
|
+
return writePredicate2(key, tenants, spec);
|
|
59
1509
|
},
|
|
60
1510
|
guard(table, field, kind) {
|
|
61
1511
|
if (!tenants.has(table) || field !== key)
|
|
62
1512
|
return;
|
|
63
|
-
|
|
1513
|
+
refuseKey2(kind, table, field);
|
|
64
1514
|
},
|
|
65
1515
|
stamp(table, rows, scope) {
|
|
66
1516
|
if (!tenants.has(table))
|
|
67
1517
|
return;
|
|
68
|
-
|
|
1518
|
+
stampRows2(key, table, rows, scope);
|
|
69
1519
|
},
|
|
70
1520
|
client(spec) {
|
|
71
1521
|
const names = spec.scoped ? spec.names : spec.names.filter((name) => !tenants.has(name));
|
|
72
1522
|
if (spec.scoped)
|
|
73
1523
|
return { names };
|
|
74
|
-
return { names, ...
|
|
1524
|
+
return { names, ...bindClient2(key, encode, spec.open) };
|
|
75
1525
|
},
|
|
76
1526
|
rules(table, source, scope) {
|
|
77
|
-
return
|
|
1527
|
+
return tenancyRules2(table, source, scope, tenants, globals, exemptions);
|
|
78
1528
|
},
|
|
79
1529
|
scopeView(name) {
|
|
80
1530
|
tenants.add(name);
|
|
@@ -103,8 +1553,10 @@ function columnTenancy(input) {
|
|
|
103
1553
|
};
|
|
104
1554
|
return api;
|
|
105
1555
|
}
|
|
106
|
-
function
|
|
1556
|
+
function rewriteTables2(key, column, encode, api, tables, tenants, globals, exemptions) {
|
|
107
1557
|
const kind = new Map;
|
|
1558
|
+
const swapped = new Map;
|
|
1559
|
+
const extras = new Map;
|
|
108
1560
|
for (const item of tables) {
|
|
109
1561
|
const options = item.options;
|
|
110
1562
|
const mark = options?.tenancy;
|
|
@@ -112,26 +1564,37 @@ function rewriteTables(key, column, tables, tenants, globals, exemptions) {
|
|
|
112
1564
|
kind.set(item.name, "tenant");
|
|
113
1565
|
continue;
|
|
114
1566
|
}
|
|
115
|
-
if (
|
|
1567
|
+
if (isGlobal3(mark)) {
|
|
116
1568
|
kind.set(item.name, "global");
|
|
117
1569
|
globals.set(item.name, mark.reason);
|
|
118
1570
|
continue;
|
|
119
1571
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
1572
|
+
if (isRecord5(mark) && typeof mark.rewrite === "function") {
|
|
1573
|
+
swapped.set(item.name, mark.rewrite(item, {
|
|
1574
|
+
tables,
|
|
1575
|
+
keys: [key],
|
|
1576
|
+
encodes: [encode],
|
|
1577
|
+
api,
|
|
1578
|
+
extras
|
|
1579
|
+
}));
|
|
1580
|
+
kind.set(item.name, "path");
|
|
1581
|
+
continue;
|
|
122
1582
|
}
|
|
123
|
-
definition(`Table ${item.name} tenancy must be global("reason").`);
|
|
1583
|
+
definition(`Table ${item.name} tenancy must be global("reason") or via().`);
|
|
124
1584
|
}
|
|
125
1585
|
return tables.map((item) => {
|
|
1586
|
+
const next = swapped.get(item.name);
|
|
1587
|
+
if (next !== undefined)
|
|
1588
|
+
return next;
|
|
126
1589
|
if (kind.get(item.name) !== "tenant") {
|
|
127
1590
|
if (kind.get(item.name) === "global")
|
|
128
|
-
|
|
1591
|
+
refuseGlobalReference2(item, kind);
|
|
129
1592
|
return item;
|
|
130
1593
|
}
|
|
131
|
-
return
|
|
1594
|
+
return rewriteTenant2(key, column, item, tables, kind, tenants, exemptions, extras);
|
|
132
1595
|
});
|
|
133
1596
|
}
|
|
134
|
-
function
|
|
1597
|
+
function refuseGlobalReference2(item, kind) {
|
|
135
1598
|
for (const [field, builder] of Object.entries(item.columns)) {
|
|
136
1599
|
if (!(builder instanceof ColumnBuilder2))
|
|
137
1600
|
continue;
|
|
@@ -141,7 +1604,7 @@ function refuseGlobalReference(item, kind) {
|
|
|
141
1604
|
}
|
|
142
1605
|
}
|
|
143
1606
|
}
|
|
144
|
-
function
|
|
1607
|
+
function rewriteTenant2(key, column, item, tables, kind, tenants, exemptions, extras) {
|
|
145
1608
|
if (Object.hasOwn(item.columns, key)) {
|
|
146
1609
|
throw new OkmError2("OKM1012", `Table ${item.name} already declares ${key}, which tenancy adds. Rename the field or drop it from the table.`);
|
|
147
1610
|
}
|
|
@@ -178,34 +1641,40 @@ function rewriteTenant(key, column, item, tables, kind, tenants, exemptions) {
|
|
|
178
1641
|
}
|
|
179
1642
|
const reference = builder.state.references;
|
|
180
1643
|
if (reference !== undefined) {
|
|
181
|
-
const widened =
|
|
1644
|
+
const widened = widenReference2(key, item.name, field, reference, tables, kind);
|
|
182
1645
|
if (widened !== undefined)
|
|
183
1646
|
patch.references = widened;
|
|
184
1647
|
}
|
|
185
1648
|
const changed = patch.dropUnique === true || patch.dropPrimary === true || patch.references !== undefined;
|
|
186
1649
|
columns[field] = changed ? retarget(builder, patch) : builder;
|
|
187
1650
|
}
|
|
188
|
-
const primary =
|
|
1651
|
+
const primary = primaryFields2(item);
|
|
189
1652
|
const primaryKey = primary.length > 0 && !primary.includes(key) ? [...primary, key] : undefined;
|
|
190
1653
|
if (primaryKey !== undefined)
|
|
191
1654
|
unique[`${primary.join("_")}_${key}`] = primaryKey;
|
|
1655
|
+
const noted = extras.get(item.name);
|
|
1656
|
+
if (noted !== undefined && noted.length > 0 && unique[noted.join("_")] === undefined) {
|
|
1657
|
+
unique[noted.join("_")] = noted;
|
|
1658
|
+
}
|
|
192
1659
|
columns[key] = column;
|
|
193
1660
|
tenants.add(item.name);
|
|
194
1661
|
if (notes.length > 0)
|
|
195
1662
|
exemptions.set(item.name, notes);
|
|
196
|
-
const indexes =
|
|
197
|
-
|
|
1663
|
+
const indexes = wrapIndexes2(key, item.name, options?.indexes);
|
|
1664
|
+
const rewritten = {
|
|
198
1665
|
...item,
|
|
199
1666
|
columns,
|
|
200
1667
|
options: {
|
|
201
1668
|
...options,
|
|
202
1669
|
...primaryKey !== undefined ? { primaryKey } : {},
|
|
203
1670
|
...Object.keys(unique).length > 0 ? { unique } : {},
|
|
204
|
-
...indexes !== undefined ? { indexes } : {}
|
|
1671
|
+
...indexes !== undefined ? { indexes } : {},
|
|
1672
|
+
tenantKeys: [key]
|
|
205
1673
|
}
|
|
206
1674
|
};
|
|
1675
|
+
return noted !== undefined && noted.length > 0 ? markPathEndpoint(rewritten) : rewritten;
|
|
207
1676
|
}
|
|
208
|
-
function
|
|
1677
|
+
function widenReference2(key, table, field, reference, tables, kind) {
|
|
209
1678
|
const targetKind = kind.get(reference.table);
|
|
210
1679
|
if (targetKind !== "tenant")
|
|
211
1680
|
return;
|
|
@@ -214,7 +1683,7 @@ function widenReference(key, table, field, reference, tables, kind) {
|
|
|
214
1683
|
definition(`Foreign key ${table}.${field} includes ${key}, so ${action} cannot clear it. Accepted actions: cascade, restrict, no action.`);
|
|
215
1684
|
}
|
|
216
1685
|
const target = tables.find((item) => item.name === reference.table);
|
|
217
|
-
const remote = reference.columns ?? (target === undefined ? undefined :
|
|
1686
|
+
const remote = reference.columns ?? (target === undefined ? undefined : primaryFields2(target));
|
|
218
1687
|
if (remote === undefined || remote.length === 0)
|
|
219
1688
|
return;
|
|
220
1689
|
if (remote.includes(key)) {
|
|
@@ -228,7 +1697,7 @@ function widenReference(key, table, field, reference, tables, kind) {
|
|
|
228
1697
|
along: [...reference.along ?? [], key]
|
|
229
1698
|
};
|
|
230
1699
|
}
|
|
231
|
-
function
|
|
1700
|
+
function primaryFields2(item) {
|
|
232
1701
|
const listed = item.options?.primaryKey;
|
|
233
1702
|
if (listed !== undefined && listed.length > 0)
|
|
234
1703
|
return [...listed];
|
|
@@ -239,7 +1708,7 @@ function primaryFields(item) {
|
|
|
239
1708
|
}
|
|
240
1709
|
return fields;
|
|
241
1710
|
}
|
|
242
|
-
function
|
|
1711
|
+
function wrapIndexes2(key, table, build) {
|
|
243
1712
|
if (build === undefined)
|
|
244
1713
|
return;
|
|
245
1714
|
return (columns) => {
|
|
@@ -255,7 +1724,7 @@ function wrapIndexes(key, table, build) {
|
|
|
255
1724
|
return calls;
|
|
256
1725
|
};
|
|
257
1726
|
}
|
|
258
|
-
function
|
|
1727
|
+
function writePredicate2(key, tenants, input) {
|
|
259
1728
|
if (!tenants.has(input.table))
|
|
260
1729
|
return false;
|
|
261
1730
|
const scope = input.scope;
|
|
@@ -279,7 +1748,7 @@ function writePredicate(key, tenants, input) {
|
|
|
279
1748
|
input.sink.mark("tenant|");
|
|
280
1749
|
return true;
|
|
281
1750
|
}
|
|
282
|
-
function
|
|
1751
|
+
function refuseKey2(kind, table, field) {
|
|
283
1752
|
if (kind === "insert") {
|
|
284
1753
|
throw new OkmError2("OKM1190", `Field ${table}.${field} is the tenant key. The scope sets it. Input cannot.`);
|
|
285
1754
|
}
|
|
@@ -288,14 +1757,14 @@ function refuseKey(kind, table, field) {
|
|
|
288
1757
|
}
|
|
289
1758
|
throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. A filter cannot set it.`);
|
|
290
1759
|
}
|
|
291
|
-
function
|
|
1760
|
+
function stampRows2(key, table, rows, scope) {
|
|
292
1761
|
if (scope === undefined || "unscoped" in scope) {
|
|
293
1762
|
throw new OkmError2("OKM1701", `insert on ${table} has no tenant. Call for({ ${key} }) so the scope can set it.`);
|
|
294
1763
|
}
|
|
295
1764
|
for (const row of rows)
|
|
296
1765
|
row[key] = scope.value;
|
|
297
1766
|
}
|
|
298
|
-
function
|
|
1767
|
+
function bindClient2(key, encode, openScope) {
|
|
299
1768
|
return {
|
|
300
1769
|
for: (input) => {
|
|
301
1770
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
@@ -322,7 +1791,7 @@ function bindClient(key, encode, openScope) {
|
|
|
322
1791
|
}
|
|
323
1792
|
};
|
|
324
1793
|
}
|
|
325
|
-
function
|
|
1794
|
+
function tenancyRules2(table, source, scope, tenants, globals, exemptions) {
|
|
326
1795
|
const rules = [];
|
|
327
1796
|
const reason = globals.get(table);
|
|
328
1797
|
if (reason !== undefined) {
|
|
@@ -356,13 +1825,17 @@ function tenancyRules(table, source, scope, tenants, globals, exemptions) {
|
|
|
356
1825
|
}
|
|
357
1826
|
return rules;
|
|
358
1827
|
}
|
|
359
|
-
function
|
|
360
|
-
return
|
|
1828
|
+
function isGlobal3(value) {
|
|
1829
|
+
return isRecord5(value) && value.kind === "global" && typeof value.reason === "string";
|
|
361
1830
|
}
|
|
362
|
-
function
|
|
1831
|
+
function isRecord5(value) {
|
|
363
1832
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
364
1833
|
}
|
|
365
1834
|
export {
|
|
1835
|
+
RlsRoleError,
|
|
366
1836
|
columnTenancy,
|
|
367
|
-
|
|
1837
|
+
compositeTenancy,
|
|
1838
|
+
global,
|
|
1839
|
+
rlsTenancy,
|
|
1840
|
+
via
|
|
368
1841
|
};
|