okmodel 0.5.1 → 0.7.0

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