okmodel 0.1.0 → 0.2.1

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 (181) hide show
  1. package/README.md +237 -19
  2. package/dist/adapters/capabilities.d.ts +34 -0
  3. package/dist/adapters/pg/bunsql-extra.d.ts +17 -0
  4. package/dist/adapters/pg/bunsql.d.ts +60 -0
  5. package/dist/adapters/pg/call.d.ts +2 -12
  6. package/dist/adapters/pg/nodepostgres-extra.d.ts +18 -0
  7. package/dist/adapters/pg/nodepostgres.d.ts +57 -0
  8. package/dist/adapters/pg/postgres-extra.d.ts +50 -1
  9. package/dist/adapters/pg/postgresjs.d.ts +0 -4
  10. package/dist/adapters/pg/session.d.ts +94 -0
  11. package/dist/adapters/pg/watch.d.ts +18 -0
  12. package/dist/contracts/catalog/object.d.ts +1 -0
  13. package/dist/contracts/catalog/types.d.ts +15 -8
  14. package/dist/contracts/connection.d.ts +13 -0
  15. package/dist/contracts/driver.d.ts +18 -5
  16. package/dist/contracts/error.d.ts +17 -1
  17. package/dist/contracts/generator.d.ts +63 -0
  18. package/dist/contracts/index.d.ts +2 -2
  19. package/dist/contracts/index.js +1 -1
  20. package/dist/contracts/internal.js +11 -9
  21. package/dist/contracts/location.d.ts +24 -0
  22. package/dist/contracts/rows.d.ts +15 -0
  23. package/dist/dialects/pg/archive-bind.d.ts +22 -0
  24. package/dist/dialects/pg/column.d.ts +126 -9
  25. package/dist/dialects/pg/compile.d.ts +1 -0
  26. package/dist/dialects/pg/custom.d.ts +6 -0
  27. package/dist/dialects/pg/filter-parse.d.ts +19 -0
  28. package/dist/dialects/pg/filters.d.ts +19 -0
  29. package/dist/dialects/pg/index.d.ts +11 -2
  30. package/dist/dialects/pg/index.js +159 -1063
  31. package/dist/dialects/pg/keys.d.ts +19 -3
  32. package/dist/dialects/pg/model.d.ts +223 -2
  33. package/dist/dialects/pg/operator-fit.d.ts +86 -0
  34. package/dist/dialects/pg/operators.d.ts +39 -1
  35. package/dist/dialects/pg/ops/arr-append.d.ts +9 -0
  36. package/dist/dialects/pg/ops/arr-ns.d.ts +7 -0
  37. package/dist/dialects/pg/ops/arr-remove.d.ts +9 -0
  38. package/dist/dialects/pg/ops/containedBy.d.ts +9 -0
  39. package/dist/dialects/pg/ops/contains.d.ts +8 -4
  40. package/dist/dialects/pg/ops/hasAnyKey.d.ts +8 -0
  41. package/dist/dialects/pg/ops/hasKey.d.ts +8 -0
  42. package/dist/dialects/pg/ops/json-ns.d.ts +6 -0
  43. package/dist/dialects/pg/ops/json-set.d.ts +12 -0
  44. package/dist/dialects/pg/ops/matches.d.ts +16 -0
  45. package/dist/dialects/pg/ops/overlaps.d.ts +9 -0
  46. package/dist/dialects/pg/ops/path.d.ts +13 -0
  47. package/dist/dialects/pg/preset.d.ts +76 -0
  48. package/dist/dialects/pg/relations.d.ts +59 -2
  49. package/dist/dialects/pg/schema.d.ts +109 -5
  50. package/dist/dialects/pg/table.d.ts +130 -11
  51. package/dist/dialects/pg/temporal.d.ts +9 -7
  52. package/dist/dialects/pg/tenancy.d.ts +94 -0
  53. package/dist/dialects/pg/through.d.ts +21 -0
  54. package/dist/dialects/pg/trait.d.ts +141 -0
  55. package/dist/dialects/pg/where.d.ts +37 -0
  56. package/dist/runtime/aggregate.d.ts +25 -0
  57. package/dist/runtime/archive-handle.d.ts +18 -0
  58. package/dist/runtime/archive-rules.d.ts +12 -0
  59. package/dist/runtime/archive.d.ts +73 -0
  60. package/dist/runtime/batch.d.ts +20 -0
  61. package/dist/runtime/client.d.ts +114 -5
  62. package/dist/runtime/conflict.d.ts +2 -1
  63. package/dist/runtime/exposure.d.ts +27 -0
  64. package/dist/runtime/hookm.d.ts +39 -0
  65. package/dist/runtime/ids/index.d.ts +11 -0
  66. package/dist/runtime/ids/index.js +193 -0
  67. package/dist/runtime/ids/okid-extended.d.ts +19 -0
  68. package/dist/runtime/ids/okid-generator.d.ts +21 -0
  69. package/dist/runtime/ids/okid-shared.d.ts +89 -0
  70. package/dist/runtime/ids/okid.d.ts +24 -0
  71. package/dist/runtime/ids/uuid.d.ts +21 -0
  72. package/dist/runtime/lock.d.ts +22 -0
  73. package/dist/runtime/operator-sql.d.ts +8 -0
  74. package/dist/runtime/page.d.ts +25 -0
  75. package/dist/runtime/pg/bun.d.ts +30 -0
  76. package/dist/runtime/pg/bun.js +52 -0
  77. package/dist/runtime/pg/pg.d.ts +29 -0
  78. package/dist/runtime/pg/pg.js +52 -0
  79. package/dist/runtime/pg/pglite.js +16 -17
  80. package/dist/runtime/pg/postgresjs.js +14 -24
  81. package/dist/runtime/plan.d.ts +146 -7
  82. package/dist/runtime/preset-stack.d.ts +17 -0
  83. package/dist/runtime/presets.d.ts +48 -0
  84. package/dist/runtime/read-stream.d.ts +26 -0
  85. package/dist/runtime/safety/index.d.ts +159 -0
  86. package/dist/runtime/safety/index.js +294 -0
  87. package/dist/runtime/safety-hook.d.ts +42 -0
  88. package/dist/runtime/server-check.d.ts +34 -0
  89. package/dist/runtime/tenancy/index.d.ts +37 -0
  90. package/dist/runtime/tenancy/index.js +365 -0
  91. package/dist/runtime/trait-read.d.ts +30 -0
  92. package/dist/runtime/traits/index.d.ts +128 -0
  93. package/dist/runtime/traits/index.js +509 -0
  94. package/dist/runtime/transaction.d.ts +19 -0
  95. package/dist/runtime/tx.d.ts +22 -7
  96. package/dist/runtime/types.d.ts +390 -25
  97. package/dist/runtime/validate/bind.d.ts +11 -0
  98. package/dist/runtime/validate/closed.d.ts +16 -0
  99. package/dist/runtime/validate/engine.d.ts +41 -0
  100. package/dist/runtime/validate/hook.d.ts +17 -0
  101. package/dist/runtime/validate/index.d.ts +21 -0
  102. package/dist/runtime/validate/index.js +175 -0
  103. package/dist/runtime/validate/load.d.ts +13 -0
  104. package/dist/runtime/validate/places.d.ts +41 -0
  105. package/dist/runtime/validate/rules.d.ts +108 -0
  106. package/dist/runtime/validate/surface.d.ts +102 -0
  107. package/dist/runtime/write.d.ts +30 -5
  108. package/dist/shared/{fgqgsb5z.js → 0qkxbm1f.js} +2 -2
  109. package/dist/shared/0v1g424p.js +26 -0
  110. package/dist/shared/10tjenmd.js +210 -0
  111. package/dist/shared/{tb4ze1tr.js → 1dp5knt7.js} +95 -10
  112. package/dist/shared/38crxbgn.js +229 -0
  113. package/dist/shared/3a5ne750.js +158 -0
  114. package/dist/shared/4b9da9d7.js +138 -0
  115. package/dist/shared/4e4c6vq2.js +62 -0
  116. package/dist/shared/{ckb1p0a4.js → 4jzzghr6.js} +416 -83
  117. package/dist/shared/{ycwz42az.js → 52szzakm.js} +6 -26
  118. package/dist/shared/{kynx7vbm.js → 5z4q2zda.js} +604 -21
  119. package/dist/shared/672t5vym.js +12 -0
  120. package/dist/shared/7m88x3e9.js +45 -0
  121. package/dist/shared/7mht49yd.js +1091 -0
  122. package/dist/shared/{mtkwv8wx.js → 7p2wbz7f.js} +1 -1
  123. package/dist/shared/83eaefwr.js +10 -0
  124. package/dist/shared/96ps4g1z.js +32 -0
  125. package/dist/shared/97g2jnrw.js +76 -0
  126. package/dist/shared/{xy79j5hq.js → 9k6vetvf.js} +25 -36
  127. package/dist/shared/{w8samvk1.js → a1gdp7j9.js} +1 -1
  128. package/dist/shared/a5sxz52y.js +13 -0
  129. package/dist/shared/{dynq16vb.js → b066g80h.js} +8 -6
  130. package/dist/shared/cstgkzdx.js +154 -0
  131. package/dist/shared/{ax16zpvm.js → d04j53v8.js} +6 -5
  132. package/dist/shared/{y9m15dfv.js → d2whjz4k.js} +15 -9
  133. package/dist/shared/dd4f8mnx.js +307 -0
  134. package/dist/shared/e6wg00wd.js +164 -0
  135. package/dist/shared/e7a268fy.js +195 -0
  136. package/dist/shared/{vkm3q4x1.js → eaxwrgby.js} +4 -3
  137. package/dist/shared/{7ck0dtry.js → ecdzej21.js} +160 -81
  138. package/dist/shared/{px90pf9p.js → edwqqryd.js} +21 -16
  139. package/dist/shared/{g2v6gnc8.js → eqmtn1bj.js} +57 -5
  140. package/dist/shared/g1b5zzj9.js +75 -0
  141. package/dist/shared/g9f0zqce.js +376 -0
  142. package/dist/shared/ge03bfca.js +64 -0
  143. package/dist/shared/{79ggp945.js → jns2wx4t.js} +55 -12
  144. package/dist/shared/k3ted12y.js +33 -0
  145. package/dist/shared/k7dwkhvq.js +31 -0
  146. package/dist/shared/{hd293j9g.js → kyxb62mc.js} +14 -2
  147. package/dist/shared/m9tv7xn7.js +404 -0
  148. package/dist/shared/mrq77sgw.js +55 -0
  149. package/dist/shared/mxmcdsh5.js +450 -0
  150. package/dist/shared/n442h54c.js +14 -0
  151. package/dist/shared/n47rd821.js +442 -0
  152. package/dist/shared/n4jqhpqz.js +27 -0
  153. package/dist/shared/{mkjmqjv4.js → nfzc5z0w.js} +2 -104
  154. package/dist/shared/nsva2a6b.js +42 -0
  155. package/dist/shared/ps43134h.js +69 -0
  156. package/dist/shared/{8b04d28c.js → py3nh7jy.js} +2 -2
  157. package/dist/shared/qa5dsc7z.js +86 -0
  158. package/dist/shared/{g5fhqmwe.js → qxa0yb9j.js} +6 -4
  159. package/dist/shared/r6nwkxnt.js +86 -0
  160. package/dist/shared/{4j72449d.js → rtnaqre0.js} +3 -3
  161. package/dist/shared/rwmrejjz.js +26 -0
  162. package/dist/shared/{zhc5vmgf.js → s6gqfn5j.js} +2 -2
  163. package/dist/shared/sacqha1y.js +70 -0
  164. package/dist/shared/{qk3hxfja.js → sfh4pk75.js} +5 -5
  165. package/dist/shared/{j5cn2963.js → t4tbsy9h.js} +13 -64
  166. package/dist/shared/{ngk8gj61.js → tbszx8mz.js} +15 -1
  167. package/dist/shared/v18jva5b.js +90 -0
  168. package/dist/shared/x9v1qgeb.js +201 -0
  169. package/dist/shared/xatfg0gn.js +193 -0
  170. package/dist/shared/xkz8g6by.js +225 -0
  171. package/dist/shared/{f5thc1s0.js → xrfnq5k8.js} +237 -418
  172. package/dist/shared/ytg3xf9p.js +149 -0
  173. package/dist/shared/yw5c610b.js +48 -0
  174. package/dist/shared/zd93g3rq.js +31 -0
  175. package/dist/shared/zt8y0wzb.js +132 -0
  176. package/dist/shared/{p0cbr3g2.js → zwmqxb0g.js} +1 -1
  177. package/dist/tooling/cli.js +14 -10
  178. package/dist/tooling/migrate/commands.d.ts +10 -0
  179. package/dist/tooling/migrate/engine.d.ts +36 -0
  180. package/package.json +56 -4
  181. package/dist/shared/gk7cs7sp.js +0 -566
@@ -0,0 +1,365 @@
1
+ import {
2
+ uuid2
3
+ } from "../../shared/ps43134h.js";
4
+ import {
5
+ ColumnBuilder2,
6
+ retarget
7
+ } from "../../shared/dd4f8mnx.js";
8
+ import {
9
+ definition,
10
+ unavailable
11
+ } from "../../shared/xatfg0gn.js";
12
+ import {
13
+ quote
14
+ } from "../../shared/xrfnq5k8.js";
15
+ import {
16
+ assertIdentifier2
17
+ } from "../../shared/nfzc5z0w.js";
18
+ import {
19
+ OkmError2
20
+ } from "../../shared/52szzakm.js";
21
+
22
+ // src/runtime/tenancy/index.ts
23
+ function global(reason) {
24
+ if (typeof reason !== "string" || reason.trim().length === 0) {
25
+ definition("global() needs a reason.");
26
+ }
27
+ return { kind: "global", reason: reason.trim() };
28
+ }
29
+ function columnTenancy(input) {
30
+ if (input === undefined || typeof input !== "object") {
31
+ definition("columnTenancy() needs { key, type }.");
32
+ }
33
+ if (input.type !== "uuid") {
34
+ definition(`columnTenancy() type ${String(input.type)} must be uuid.`);
35
+ }
36
+ if (typeof input.key !== "string" || input.key.length === 0) {
37
+ definition("columnTenancy() key must be a field name.");
38
+ }
39
+ assertIdentifier2(input.key, "tenancy key");
40
+ const key = input.key;
41
+ const column = uuid2().guarded();
42
+ const encode = column.state.encode;
43
+ const tenants = new Set;
44
+ const globals = new Map;
45
+ const exemptions = new Map;
46
+ const api = {
47
+ key,
48
+ type: "uuid",
49
+ strategy: "column",
50
+ rewrite(tables) {
51
+ tenants.clear();
52
+ globals.clear();
53
+ exemptions.clear();
54
+ return rewriteTables(key, column, tables, tenants, globals, exemptions);
55
+ },
56
+ column: () => column,
57
+ predicate(spec) {
58
+ return writePredicate(key, tenants, spec);
59
+ },
60
+ guard(table, field, kind) {
61
+ if (!tenants.has(table) || field !== key)
62
+ return;
63
+ refuseKey(kind, table, field);
64
+ },
65
+ stamp(table, rows, scope) {
66
+ if (!tenants.has(table))
67
+ return;
68
+ stampRows(key, table, rows, scope);
69
+ },
70
+ client(spec) {
71
+ const names = spec.scoped ? spec.names : spec.names.filter((name) => !tenants.has(name));
72
+ if (spec.scoped)
73
+ return { names };
74
+ return { names, ...bindClient(key, encode, spec.open) };
75
+ },
76
+ rules(table, source, scope) {
77
+ return tenancyRules(table, source, scope, tenants, globals, exemptions);
78
+ },
79
+ hook(target, ctx) {
80
+ if (ctx.tables === undefined || ctx.names === undefined || ctx.open === undefined || ctx.scoped === undefined) {
81
+ return;
82
+ }
83
+ const view = api.client({ names: ctx.names, scoped: ctx.scoped, open: ctx.open });
84
+ if (!ctx.scoped) {
85
+ for (const name of ctx.names) {
86
+ if (view.names.includes(name))
87
+ continue;
88
+ delete ctx.tables[name];
89
+ delete target[name];
90
+ }
91
+ }
92
+ if (view.for !== undefined)
93
+ target.for = view.for;
94
+ if (view.unscoped !== undefined)
95
+ target.unscoped = view.unscoped;
96
+ },
97
+ missing(table) {
98
+ throw new OkmError2("OKM1701", `Table ${table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
99
+ }
100
+ };
101
+ return api;
102
+ }
103
+ function rewriteTables(key, column, tables, tenants, globals, exemptions) {
104
+ const kind = new Map;
105
+ for (const item of tables) {
106
+ const options = item.options;
107
+ const mark = options?.tenancy;
108
+ if (mark === undefined) {
109
+ kind.set(item.name, "tenant");
110
+ continue;
111
+ }
112
+ if (isGlobal(mark)) {
113
+ kind.set(item.name, "global");
114
+ globals.set(item.name, mark.reason);
115
+ continue;
116
+ }
117
+ if (isRecord(mark) && "via" in mark) {
118
+ unavailable(`Table ${item.name} tenancy via is not available yet. It arrives later.`);
119
+ }
120
+ definition(`Table ${item.name} tenancy must be global("reason").`);
121
+ }
122
+ return tables.map((item) => {
123
+ if (kind.get(item.name) !== "tenant") {
124
+ if (kind.get(item.name) === "global")
125
+ refuseGlobalReference(item, kind);
126
+ return item;
127
+ }
128
+ return rewriteTenant(key, column, item, tables, kind, tenants, exemptions);
129
+ });
130
+ }
131
+ function refuseGlobalReference(item, kind) {
132
+ for (const [field, builder] of Object.entries(item.columns)) {
133
+ if (!(builder instanceof ColumnBuilder2))
134
+ continue;
135
+ const reference = builder.state.references;
136
+ if (reference !== undefined && kind.get(reference.table) === "tenant") {
137
+ 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.`);
138
+ }
139
+ }
140
+ }
141
+ function rewriteTenant(key, column, item, tables, kind, tenants, exemptions) {
142
+ if (Object.hasOwn(item.columns, key)) {
143
+ throw new OkmError2("OKM1012", `Table ${item.name} already declares ${key}, which tenancy adds. Rename the field or drop it from the table.`);
144
+ }
145
+ const options = item.options;
146
+ const columns = {};
147
+ const unique = {};
148
+ const named = options?.unique;
149
+ if (named !== undefined) {
150
+ for (const [name, fields] of Object.entries(named)) {
151
+ unique[name] = fields.includes(key) ? fields : [key, ...fields];
152
+ }
153
+ }
154
+ const notes = [];
155
+ for (const [field, builder] of Object.entries(item.columns)) {
156
+ if (!(builder instanceof ColumnBuilder2)) {
157
+ columns[field] = builder;
158
+ continue;
159
+ }
160
+ const patch = {};
161
+ if (builder.state.primaryKey === true)
162
+ patch.dropPrimary = true;
163
+ const flag = builder.state.unique;
164
+ if (flag !== undefined) {
165
+ if (flag.global === true) {
166
+ const reason = flag.reason?.trim() ?? "";
167
+ if (reason.length === 0) {
168
+ definition(`Unique ${item.name}.${field} sets global and needs a reason.`);
169
+ }
170
+ notes.push({ name: field, reason });
171
+ } else {
172
+ patch.dropUnique = true;
173
+ unique[field] = [key, field];
174
+ }
175
+ }
176
+ const reference = builder.state.references;
177
+ if (reference !== undefined) {
178
+ const widened = widenReference(key, item.name, field, reference, tables, kind);
179
+ if (widened !== undefined)
180
+ patch.references = widened;
181
+ }
182
+ const changed = patch.dropUnique === true || patch.dropPrimary === true || patch.references !== undefined;
183
+ columns[field] = changed ? retarget(builder, patch) : builder;
184
+ }
185
+ const primary = primaryFields(item);
186
+ const primaryKey = primary.length > 0 && !primary.includes(key) ? [...primary, key] : undefined;
187
+ if (primaryKey !== undefined)
188
+ unique[`${primary.join("_")}_${key}`] = primaryKey;
189
+ columns[key] = column;
190
+ tenants.add(item.name);
191
+ if (notes.length > 0)
192
+ exemptions.set(item.name, notes);
193
+ const indexes = wrapIndexes(key, item.name, options?.indexes);
194
+ return {
195
+ ...item,
196
+ columns,
197
+ options: {
198
+ ...options,
199
+ ...primaryKey !== undefined ? { primaryKey } : {},
200
+ ...Object.keys(unique).length > 0 ? { unique } : {},
201
+ ...indexes !== undefined ? { indexes } : {}
202
+ }
203
+ };
204
+ }
205
+ function widenReference(key, table, field, reference, tables, kind) {
206
+ const targetKind = kind.get(reference.table);
207
+ if (targetKind !== "tenant")
208
+ return;
209
+ const action = [reference.onDelete, reference.onUpdate].find((item) => item === "set null" || item === "set default");
210
+ if (action !== undefined) {
211
+ definition(`Foreign key ${table}.${field} includes ${key}, so ${action} cannot clear it. Accepted actions: cascade, restrict, no action.`);
212
+ }
213
+ const target = tables.find((item) => item.name === reference.table);
214
+ const remote = reference.columns ?? (target === undefined ? undefined : primaryFields(target));
215
+ if (remote === undefined || remote.length === 0)
216
+ return;
217
+ if (remote.includes(key)) {
218
+ if (reference.along?.includes(key))
219
+ return;
220
+ return { ...reference, along: [...reference.along ?? [], key] };
221
+ }
222
+ return {
223
+ ...reference,
224
+ columns: [...remote, key],
225
+ along: [...reference.along ?? [], key]
226
+ };
227
+ }
228
+ function primaryFields(item) {
229
+ const listed = item.options?.primaryKey;
230
+ if (listed !== undefined && listed.length > 0)
231
+ return [...listed];
232
+ const fields = [];
233
+ for (const [field, builder] of Object.entries(item.columns)) {
234
+ if (builder instanceof ColumnBuilder2 && builder.state.primaryKey === true)
235
+ fields.push(field);
236
+ }
237
+ return fields;
238
+ }
239
+ function wrapIndexes(key, table, build) {
240
+ if (build === undefined)
241
+ return;
242
+ return (columns) => {
243
+ const calls = build(columns);
244
+ const tenantSql = columns[key]?.name;
245
+ if (!Array.isArray(calls) || tenantSql === undefined)
246
+ return calls;
247
+ for (const call of calls) {
248
+ if (call.columns[0] === tenantSql)
249
+ continue;
250
+ throw new OkmError2("OKM1706", `Index on ${table} starts with ${call.columns[0] ?? "no column"}. Put ${tenantSql} first.`);
251
+ }
252
+ return calls;
253
+ };
254
+ }
255
+ function writePredicate(key, tenants, input) {
256
+ if (!tenants.has(input.table))
257
+ return false;
258
+ const scope = input.scope;
259
+ if (scope === undefined) {
260
+ throw new OkmError2("OKM1701", `Table ${input.table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
261
+ }
262
+ if ("unscoped" in scope) {
263
+ input.sink.mark("unscoped|");
264
+ return false;
265
+ }
266
+ const sql = input.fieldSql(key);
267
+ if (sql === undefined) {
268
+ throw new OkmError2("OKM1701", `Table ${input.table} is missing its tenant key ${key}.`);
269
+ }
270
+ input.sink.text(input.appended ? " and " : " where ");
271
+ input.sink.text(input.alias);
272
+ input.sink.text(".");
273
+ input.sink.text(quote(sql));
274
+ input.sink.text(" = ");
275
+ input.sink.param(input.encode === undefined ? scope.value : input.encode(scope.value));
276
+ input.sink.mark("tenant|");
277
+ return true;
278
+ }
279
+ function refuseKey(kind, table, field) {
280
+ if (kind === "insert") {
281
+ throw new OkmError2("OKM1190", `Field ${table}.${field} is the tenant key. The scope sets it. Input cannot.`);
282
+ }
283
+ if (kind === "update") {
284
+ throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. An update cannot change it.`);
285
+ }
286
+ throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. A filter cannot set it.`);
287
+ }
288
+ function stampRows(key, table, rows, scope) {
289
+ if (scope === undefined || "unscoped" in scope) {
290
+ throw new OkmError2("OKM1701", `insert on ${table} has no tenant. Call for({ ${key} }) so the scope can set it.`);
291
+ }
292
+ for (const row of rows)
293
+ row[key] = scope.value;
294
+ }
295
+ function bindClient(key, encode, openScope) {
296
+ return {
297
+ for: (input) => {
298
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
299
+ throw new OkmError2("OKM1701", `for() needs { ${key} }.`);
300
+ }
301
+ const record = input;
302
+ const value = record[key];
303
+ if (typeof value !== "string" || value.length === 0) {
304
+ throw new OkmError2("OKM1701", `for() needs { ${key} } as a uuid.`);
305
+ }
306
+ for (const name of Object.keys(record)) {
307
+ if (name !== key) {
308
+ throw new OkmError2("OKM1701", `for() accepts ${key}. ${name} is not the tenant key.`);
309
+ }
310
+ }
311
+ encode(value);
312
+ return openScope({ value });
313
+ },
314
+ unscoped: (reason) => {
315
+ if (typeof reason !== "string" || reason.trim().length === 0) {
316
+ throw new OkmError2("OKM1701", "unscoped() needs a reason.");
317
+ }
318
+ return openScope({ unscoped: reason.trim() });
319
+ }
320
+ };
321
+ }
322
+ function tenancyRules(table, source, scope, tenants, globals, exemptions) {
323
+ const rules = [];
324
+ const reason = globals.get(table);
325
+ if (reason !== undefined) {
326
+ rules.push({
327
+ rule: "tenancy",
328
+ contribution: `global ${reason}`,
329
+ provenance: "catalog",
330
+ ...source !== undefined ? { source } : {}
331
+ });
332
+ }
333
+ const notes = exemptions.get(table);
334
+ if (notes !== undefined) {
335
+ for (const item of notes) {
336
+ rules.push({
337
+ rule: "tenancy",
338
+ contribution: `unique ${item.name} global ${item.reason}`,
339
+ provenance: "catalog",
340
+ ...source !== undefined ? { source } : {}
341
+ });
342
+ }
343
+ }
344
+ if (tenants.has(table) && scope !== undefined && "value" in scope) {
345
+ rules.push({ rule: "tenancy", contribution: "scoped", provenance: "planner" });
346
+ }
347
+ if (scope !== undefined && "unscoped" in scope) {
348
+ rules.push({
349
+ rule: "tenancy",
350
+ contribution: `unscoped ${scope.unscoped}`,
351
+ provenance: "caller"
352
+ });
353
+ }
354
+ return rules;
355
+ }
356
+ function isGlobal(value) {
357
+ return isRecord(value) && value.kind === "global" && typeof value.reason === "string";
358
+ }
359
+ function isRecord(value) {
360
+ return typeof value === "object" && value !== null && !Array.isArray(value);
361
+ }
362
+ export {
363
+ columnTenancy,
364
+ global
365
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Reads sealed and touch columns off the traits stored on a table.
3
+ *
4
+ * `schema()` does not copy those lists onto the model. This module is
5
+ * imported by the write path, which loads on the first write.
6
+ */
7
+ import type { TableModel } from "../dialects/pg/model.js";
8
+ /**
9
+ * Fields a trait sets to `now()` on update.
10
+ *
11
+ * @param model - Table model
12
+ * @returns Field names, or `undefined` when no trait touches a column
13
+ */
14
+ export declare function touchFields(model: TableModel): readonly string[] | undefined;
15
+ /**
16
+ * Whether a trait owns this field, so input cannot set it.
17
+ *
18
+ * @param model - Table model
19
+ * @param field - Field name
20
+ * @returns `true` when a trait seals it
21
+ */
22
+ export declare function fieldSealed(model: TableModel, field: string): boolean;
23
+ /**
24
+ * The trait that seals a field, when one does.
25
+ *
26
+ * @param model - Table model
27
+ * @param field - Field name
28
+ * @returns The trait name, or `undefined`
29
+ */
30
+ export declare function sealingTrait(model: TableModel, field: string): string | undefined;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Traits (`okmodel/traits`).
3
+ *
4
+ * Importing this module is what puts a trait on a schema. An application that
5
+ * does not import it does not load these columns.
6
+ */
7
+ import { type PresetMap } from "../../dialects/pg/preset.js";
8
+ import type { AnyTable, RowFrom } from "../../dialects/pg/table.js";
9
+ import { attachArchive } from "../archive-handle.js";
10
+ import { type Trait, type TraitContext, type TraitModel } from "../../dialects/pg/trait.js";
11
+ export type { Trait };
12
+ /**
13
+ * Options `trait()` accepts.
14
+ *
15
+ * `methods` and `requires` are named here and rejected until the prompt that
16
+ * implements them.
17
+ *
18
+ * @typeParam TFields - Columns the trait adds
19
+ * @typeParam TPresets - Presets the trait adds to each table it applies to
20
+ */
21
+ export type TraitDefinition<TFields extends Readonly<Record<string, object>>, TPresets extends PresetMap<RowFrom<TFields>> = PresetMap<RowFrom<TFields>>> = {
22
+ readonly fields: TFields;
23
+ /** Columns set to `now()` on update. Each one is sealed. */
24
+ readonly touch?: readonly (keyof TFields & string)[];
25
+ /** Columns input cannot set, including `{ allow }`. */
26
+ readonly sealed?: readonly (keyof TFields & string)[];
27
+ /**
28
+ * Named query refinements added to every table the trait applies to.
29
+ *
30
+ * They see the trait's columns, not the table's. A name a table or another
31
+ * trait also defines is OKM1040, and so is a reserved name.
32
+ */
33
+ readonly presets?: TPresets & PresetMap<RowFrom<TFields>>;
34
+ readonly methods?: unknown;
35
+ readonly requires?: unknown;
36
+ };
37
+ /**
38
+ * Builds a trait.
39
+ *
40
+ * The same function the built-in traits use. `schema()` copies `fields` onto
41
+ * each table and rejects a field the table already has (OKM1012).
42
+ *
43
+ * @typeParam TFields - Columns the trait adds
44
+ * @param name - Provenance name stored on those columns
45
+ * @param definition - Fields and, when needed, which ones the write path seals
46
+ * @returns The trait
47
+ */
48
+ export declare function trait<const TFields extends Readonly<Record<string, object>>, const TPresets extends PresetMap<RowFrom<TFields>> = Record<never, never>>(name: string, input: TraitDefinition<TFields, TPresets>): {
49
+ readonly name: string;
50
+ readonly fields: TFields;
51
+ readonly touch?: readonly (keyof TFields & string)[];
52
+ readonly sealed?: readonly (keyof TFields & string)[];
53
+ apply(model: TraitModel, ctx: TraitContext): void;
54
+ } & ([keyof TPresets] extends [never] ? unknown : {
55
+ readonly presets: TPresets;
56
+ });
57
+ /**
58
+ * Options for {@link timestamps}.
59
+ *
60
+ * `enforce: "trigger"` is the database trigger. It arrives in 0.3.
61
+ */
62
+ export type TimestampsOptions = {
63
+ readonly enforce?: "trigger";
64
+ };
65
+ /**
66
+ * Adds `createdAt` and `updatedAt`.
67
+ *
68
+ * Both are `timestamptz not null default now()`, and input cannot set either
69
+ * of them. Insert leaves them to the default, so one statement stamps both
70
+ * with the same time. Update sets `updatedAt` to `now()` and leaves `createdAt`.
71
+ *
72
+ * @param options - Pass `{ enforce: "trigger" }` only when the trigger ships
73
+ * @returns The timestamps trait
74
+ */
75
+ export declare function timestamps(options?: TimestampsOptions): {
76
+ readonly name: string;
77
+ readonly fields: {
78
+ readonly createdAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "hasDefault">, "guarded">>;
79
+ readonly updatedAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "hasDefault">, "guarded">>;
80
+ };
81
+ readonly touch?: readonly ("createdAt" | "updatedAt")[];
82
+ readonly sealed?: readonly ("createdAt" | "updatedAt")[];
83
+ apply(model: TraitModel, ctx: TraitContext): void;
84
+ };
85
+ /**
86
+ * Options for {@link archivable}.
87
+ *
88
+ * `strategy: "column"` keeps archived rows in the same table. `strategy: "table"`
89
+ * is deferred. `cascade` names the child tables archived and restored with the row.
90
+ */
91
+ export type ArchivableOptions = {
92
+ /** `"table"` is reserved and throws OKM1061. */
93
+ readonly strategy?: "column" | "table";
94
+ readonly cascade?: readonly string[];
95
+ };
96
+ /**
97
+ * Adds `archivedAt` and `archiveId`, and enables `archive()` and `restore()`.
98
+ *
99
+ * Reads, updates, and deletes target the active set. `withArchived()` and
100
+ * `onlyArchived()` widen or switch it. Uniques on the table become partial.
101
+ * `cascade` names children that share the call's `archiveId`.
102
+ *
103
+ * @param options - Column strategy and the child tables to cascade
104
+ * @returns The archivable trait
105
+ */
106
+ export declare function archivable(options?: ArchivableOptions): {
107
+ name: string;
108
+ fields: {
109
+ readonly archivedAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "nullable">, "guarded">>;
110
+ readonly archiveId: import("../../dialects/pg/column.js").ColumnBuilder<string, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "nullable">, "guarded">>;
111
+ };
112
+ touch?: readonly ("archiveId" | "archivedAt")[];
113
+ sealed?: readonly ("archiveId" | "archivedAt")[];
114
+ "~archive": true;
115
+ apply(model: TraitModel, ctx: TraitContext): void;
116
+ /**
117
+ * Turns uniques into partial indexes and checks cascade.
118
+ *
119
+ * Runs in the schema rewrite, after tenancy widens uniques.
120
+ *
121
+ * @param tables - Tables after the tenancy rewrite
122
+ * @param casing - Schema casing
123
+ * @param schemaTraits - Traits from `schema()`, when any were passed
124
+ * @returns The rewritten tables
125
+ */
126
+ rewrite(tables: readonly AnyTable[], casing: "snake" | undefined, schemaTraits: readonly object[] | undefined): readonly AnyTable[];
127
+ hook: typeof attachArchive;
128
+ };