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.
Files changed (155) hide show
  1. package/README.md +29 -14
  2. package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
  3. package/dist/adapters/pg/postgres-extra.d.ts +22 -0
  4. package/dist/contracts/catalog/document.d.ts +19 -5
  5. package/dist/contracts/catalog/object.d.ts +2 -1
  6. package/dist/contracts/catalog/policy.d.ts +48 -0
  7. package/dist/contracts/catalog/types.d.ts +25 -1
  8. package/dist/contracts/catalog/view.d.ts +2 -0
  9. package/dist/contracts/internal.js +1 -1
  10. package/dist/contracts/location.d.ts +10 -0
  11. package/dist/dialects/pg/column.d.ts +13 -2
  12. package/dist/dialects/pg/ddl.d.ts +37 -1
  13. package/dist/dialects/pg/emit.d.ts +19 -1
  14. package/dist/dialects/pg/errors.d.ts +32 -1
  15. package/dist/dialects/pg/ext/citext.js +1 -1
  16. package/dist/dialects/pg/ext/index.js +1 -1
  17. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  18. package/dist/dialects/pg/filter-parse.d.ts +5 -4
  19. package/dist/dialects/pg/fn/index.js +2 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +23 -17
  22. package/dist/dialects/pg/introspect.d.ts +7 -0
  23. package/dist/dialects/pg/model.d.ts +7 -2
  24. package/dist/dialects/pg/owner.d.ts +42 -0
  25. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  26. package/dist/dialects/pg/role/check.d.ts +3 -0
  27. package/dist/dialects/pg/role/index.d.ts +71 -6
  28. package/dist/dialects/pg/role/sql.d.ts +11 -0
  29. package/dist/dialects/pg/schema.d.ts +2 -0
  30. package/dist/dialects/pg/table.d.ts +25 -3
  31. package/dist/dialects/pg/tenancy.d.ts +45 -3
  32. package/dist/dialects/pg/view/client.d.ts +19 -1
  33. package/dist/dialects/pg/view/index.d.ts +4 -0
  34. package/dist/dialects/pg/view/index.js +21 -178
  35. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  36. package/dist/runtime/batch.d.ts +1 -1
  37. package/dist/runtime/conflict.d.ts +2 -2
  38. package/dist/runtime/drift.d.ts +9 -0
  39. package/dist/runtime/inflight.d.ts +14 -0
  40. package/dist/runtime/pg/bun.js +4 -3
  41. package/dist/runtime/pg/pg.js +3 -3
  42. package/dist/runtime/pg/pglite.js +4 -3
  43. package/dist/runtime/pg/postgresjs.js +3 -3
  44. package/dist/runtime/plan.d.ts +18 -10
  45. package/dist/runtime/predicate.d.ts +19 -0
  46. package/dist/runtime/safety/index.js +1 -1
  47. package/dist/runtime/tenancy/composite.d.ts +26 -0
  48. package/dist/runtime/tenancy/context.d.ts +16 -0
  49. package/dist/runtime/tenancy/index.d.ts +9 -5
  50. package/dist/runtime/tenancy/index.js +1508 -37
  51. package/dist/runtime/tenancy/path.d.ts +87 -0
  52. package/dist/runtime/tenancy/rls.d.ts +67 -0
  53. package/dist/runtime/tenancy/via.d.ts +22 -0
  54. package/dist/runtime/traits/index.js +17 -11
  55. package/dist/runtime/types.d.ts +13 -1
  56. package/dist/runtime/validate/index.js +4 -4
  57. package/dist/runtime/write.d.ts +1 -1
  58. package/dist/shared/09p049xa.js +702 -0
  59. package/dist/shared/{6qd27mx0.js → 1f0xa3cb.js} +5 -5
  60. package/dist/shared/1nw1f3vs.js +23 -0
  61. package/dist/shared/{96qcj21c.js → 1z04fa85.js} +19 -9
  62. package/dist/shared/{qm7cdtvr.js → 2cg3wnxc.js} +296 -60
  63. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  64. package/dist/shared/{gbtcc2d7.js → 2z7wv5tj.js} +10 -35
  65. package/dist/shared/{k8v5v5wj.js → 32cnm7kn.js} +6 -1
  66. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  67. package/dist/shared/{2pqx26h8.js → 3fqwmr09.js} +31 -22
  68. package/dist/shared/{zqrnhwsq.js → 3qnhc7w5.js} +16 -9
  69. package/dist/shared/{r2sp8hrc.js → 5d8t84hg.js} +12 -3
  70. package/dist/shared/{87jvvhnt.js → 5j36z8dn.js} +16 -5
  71. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  72. package/dist/shared/8dqhth8c.js +12 -0
  73. package/dist/shared/8etn7d8w.js +62 -0
  74. package/dist/shared/{v36c5g6p.js → 8x72s86k.js} +400 -13
  75. package/dist/shared/{nxdp2ze3.js → 929h19xp.js} +107 -5
  76. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  77. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  78. package/dist/shared/{efmy2gcg.js → ag4c7516.js} +1 -1
  79. package/dist/shared/b3awxn7h.js +45 -0
  80. package/dist/shared/b4v57v3x.js +9 -0
  81. package/dist/shared/bdk4sty6.js +58 -0
  82. package/dist/shared/{51mvgj8w.js → c58secgj.js} +100 -7
  83. package/dist/shared/d5azex6s.js +6 -0
  84. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  85. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  86. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  87. package/dist/shared/{sycnx8gt.js → f0f1n281.js} +18 -8
  88. package/dist/shared/{f5cw5q5r.js → g90m9b0g.js} +2 -1
  89. package/dist/shared/{pf0d8f77.js → gwn6v667.js} +1 -1
  90. package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
  91. package/dist/shared/{5enne4ma.js → hazqqecq.js} +17 -93
  92. package/dist/shared/{jwtxdj06.js → j3rkk835.js} +32 -9
  93. package/dist/shared/{hf22e0wt.js → j60w0dvr.js} +19 -18
  94. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  95. package/dist/shared/k68eedmy.js +85 -0
  96. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  97. package/dist/shared/{0t22wwh1.js → mb2ycs1p.js} +4 -2
  98. package/dist/shared/{cr74e96e.js → n076ydsc.js} +1 -1
  99. package/dist/shared/{cctw17hj.js → nx9hh281.js} +31 -28
  100. package/dist/shared/{gde73es6.js → p248q79y.js} +56 -55
  101. package/dist/shared/{janx09m5.js → p6bjzq9q.js} +11 -7
  102. package/dist/shared/{6rk2gen5.js → pjr6xrm5.js} +2 -2
  103. package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
  104. package/dist/shared/{caw8b9d8.js → q3j1tq7a.js} +35 -22
  105. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  106. package/dist/shared/{tm5ab8we.js → r11n8grd.js} +2 -2
  107. package/dist/shared/r1fdd0f6.js +19 -0
  108. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  109. package/dist/shared/rhs3xe4k.js +7 -0
  110. package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
  111. package/dist/shared/{aj2qxyhj.js → sbdme3vy.js} +14 -2
  112. package/dist/shared/{qv2ydck9.js → t3g0k88x.js} +638 -67
  113. package/dist/shared/{ga3sfy93.js → ttmg09vd.js} +535 -200
  114. package/dist/shared/{kfjp0gck.js → venqrjna.js} +8 -4
  115. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  116. package/dist/shared/{32zqc1dw.js → vmfkhszw.js} +31 -7
  117. package/dist/shared/{krxx77g0.js → wkme6tep.js} +10 -8
  118. package/dist/shared/{ps7a6h14.js → wt5rzv1h.js} +12 -8
  119. package/dist/shared/wxr8cwns.js +6 -0
  120. package/dist/shared/{6mnp99hx.js → wynzeggs.js} +16 -12
  121. package/dist/shared/x3jc53kg.js +220 -0
  122. package/dist/shared/{sdc7r2ww.js → xq0mvhts.js} +8 -8
  123. package/dist/shared/xqve9598.js +53 -0
  124. package/dist/shared/{47x8x9ze.js → y2t3jktd.js} +2 -2
  125. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  126. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  127. package/dist/shared/{nkaxvt9q.js → ys0xzyaj.js} +1 -1
  128. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  129. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  130. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  131. package/dist/shared/{r8x5qhd7.js → zm0mafk4.js} +10 -4
  132. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  133. package/dist/tooling/cli.js +41 -15
  134. package/dist/tooling/migrate/apply.d.ts +14 -0
  135. package/dist/tooling/migrate/check.d.ts +3 -4
  136. package/dist/tooling/migrate/classify.d.ts +4 -0
  137. package/dist/tooling/migrate/config.d.ts +3 -2
  138. package/dist/tooling/migrate/files.d.ts +11 -1
  139. package/dist/tooling/migrate/index.js +14 -14
  140. package/dist/tooling/migrate/lint.d.ts +1 -1
  141. package/dist/tooling/migrate/lost.d.ts +28 -0
  142. package/dist/tooling/migrate/paths.d.ts +14 -0
  143. package/dist/tooling/migrate/plan.d.ts +6 -3
  144. package/dist/tooling/migrate/runner.d.ts +25 -0
  145. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  146. package/dist/tooling/migrate/sql-lex.d.ts +20 -3
  147. package/dist/tooling/migrate/text.d.ts +42 -0
  148. package/dist/tooling/node-version.d.ts +21 -0
  149. package/dist/tooling/testing/facts.d.ts +33 -5
  150. package/dist/tooling/testing/index.d.ts +1 -1
  151. package/dist/tooling/testing/index.js +3 -3
  152. package/dist/tooling/testing/isolation.d.ts +11 -5
  153. package/package.json +5 -2
  154. package/dist/shared/7zwwyh0v.js +0 -448
  155. 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/dyj86cmj.js";
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/c9z5g0fm.js";
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/gbtcc2d7.js";
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 rewriteTables(key, column, tables, tenants, globals, exemptions);
1503
+ return rewriteTables2(key, column, encode, api, tables, tenants, globals, exemptions);
55
1504
  },
56
1505
  column: () => column,
57
1506
  predicate(spec) {
58
- return writePredicate(key, tenants, spec);
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
- refuseKey(kind, table, field);
1512
+ refuseKey2(kind, table, field);
64
1513
  },
65
1514
  stamp(table, rows, scope) {
66
1515
  if (!tenants.has(table))
67
1516
  return;
68
- stampRows(key, table, rows, scope);
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, ...bindClient(key, encode, spec.open) };
1523
+ return { names, ...bindClient2(key, encode, spec.open) };
75
1524
  },
76
1525
  rules(table, source, scope) {
77
- return tenancyRules(table, source, scope, tenants, globals, exemptions);
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 rewriteTables(key, column, tables, tenants, globals, exemptions) {
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 (isGlobal(mark)) {
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 (isRecord(mark) && "via" in mark) {
121
- unavailable(`Table ${item.name} tenancy via is not available yet. It arrives later.`);
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
- refuseGlobalReference(item, kind);
1590
+ refuseGlobalReference2(item, kind);
129
1591
  return item;
130
1592
  }
131
- return rewriteTenant(key, column, item, tables, kind, tenants, exemptions);
1593
+ return rewriteTenant2(key, column, item, tables, kind, tenants, exemptions, extras);
132
1594
  });
133
1595
  }
134
- function refuseGlobalReference(item, kind) {
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 rewriteTenant(key, column, item, tables, kind, tenants, exemptions) {
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 = widenReference(key, item.name, field, reference, tables, kind);
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 = primaryFields(item);
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 = wrapIndexes(key, item.name, options?.indexes);
197
- return {
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 widenReference(key, table, field, reference, tables, kind) {
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 : primaryFields(target));
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 primaryFields(item) {
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 wrapIndexes(key, table, build) {
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 writePredicate(key, tenants, input) {
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 refuseKey(kind, table, field) {
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 stampRows(key, table, rows, scope) {
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 bindClient(key, encode, openScope) {
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 tenancyRules(table, source, scope, tenants, globals, exemptions) {
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 isGlobal(value) {
360
- return isRecord(value) && value.kind === "global" && typeof value.reason === "string";
1826
+ function isGlobal3(value) {
1827
+ return isRecord5(value) && value.kind === "global" && typeof value.reason === "string";
361
1828
  }
362
- function isRecord(value) {
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
- global
1835
+ compositeTenancy,
1836
+ global,
1837
+ rlsTenancy,
1838
+ via
368
1839
  };