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