okmodel 0.2.1 → 0.4.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 (148) hide show
  1. package/README.md +882 -48
  2. package/dist/contracts/catalog/document.d.ts +6 -2
  3. package/dist/contracts/catalog/enum.d.ts +31 -4
  4. package/dist/contracts/catalog/extension.d.ts +61 -0
  5. package/dist/contracts/catalog/privilege.d.ts +64 -0
  6. package/dist/contracts/catalog/routine.d.ts +56 -0
  7. package/dist/contracts/catalog/types.d.ts +164 -10
  8. package/dist/contracts/catalog/view.d.ts +73 -0
  9. package/dist/dialects/pg/column.d.ts +3 -1
  10. package/dist/dialects/pg/ddl.d.ts +65 -1
  11. package/dist/dialects/pg/enum.d.ts +10 -8
  12. package/dist/dialects/pg/ext/citext.d.ts +14 -0
  13. package/dist/dialects/pg/ext/citext.js +11 -0
  14. package/dist/dialects/pg/ext/index.d.ts +52 -0
  15. package/dist/dialects/pg/ext/index.js +6 -0
  16. package/dist/dialects/pg/ext/pg-trgm.d.ts +53 -0
  17. package/dist/dialects/pg/ext/pg-trgm.js +42 -0
  18. package/dist/dialects/pg/fn/index.d.ts +107 -0
  19. package/dist/dialects/pg/fn/index.js +175 -0
  20. package/dist/dialects/pg/fn/touch.d.ts +14 -0
  21. package/dist/dialects/pg/index.js +104 -19
  22. package/dist/dialects/pg/introspect.d.ts +10 -1
  23. package/dist/dialects/pg/operators.d.ts +8 -1
  24. package/dist/dialects/pg/role/check.d.ts +45 -0
  25. package/dist/dialects/pg/role/index.d.ts +50 -0
  26. package/dist/dialects/pg/role/sql.d.ts +51 -0
  27. package/dist/dialects/pg/table.d.ts +24 -1
  28. package/dist/dialects/pg/tenancy.d.ts +9 -0
  29. package/dist/dialects/pg/view/client.d.ts +30 -0
  30. package/dist/dialects/pg/view/depend.d.ts +35 -0
  31. package/dist/dialects/pg/view/index.d.ts +114 -0
  32. package/dist/dialects/pg/view/index.js +289 -0
  33. package/dist/dialects/pg/view/scratch.d.ts +22 -0
  34. package/dist/dialects/pg/where.d.ts +2 -2
  35. package/dist/runtime/catalog-cache.d.ts +17 -0
  36. package/dist/runtime/compat-words.d.ts +23 -0
  37. package/dist/runtime/drift-detail.d.ts +20 -4
  38. package/dist/runtime/drift.d.ts +4 -2
  39. package/dist/runtime/pg/bun.js +3 -3
  40. package/dist/runtime/pg/pg.js +3 -3
  41. package/dist/runtime/pg/pglite.js +3 -3
  42. package/dist/runtime/pg/postgresjs.js +3 -3
  43. package/dist/runtime/tenancy/index.js +6 -3
  44. package/dist/runtime/traits/index.d.ts +7 -2
  45. package/dist/runtime/traits/index.js +151 -13
  46. package/dist/runtime/types.d.ts +2 -0
  47. package/dist/runtime/validate/index.js +4 -4
  48. package/dist/shared/{xkz8g6by.js → 1g0drakv.js} +2 -2
  49. package/dist/shared/2321avmn.js +418 -0
  50. package/dist/shared/{7mht49yd.js → 3p4b1fg0.js} +57 -18
  51. package/dist/shared/41kv2xdm.js +318 -0
  52. package/dist/shared/{97g2jnrw.js → 4ef7mmqf.js} +3 -3
  53. package/dist/shared/{sacqha1y.js → 4m9fn52z.js} +2 -2
  54. package/dist/shared/5mtf6cj3.js +94 -0
  55. package/dist/shared/{1dp5knt7.js → 5vdm4sgp.js} +2 -2
  56. package/dist/shared/{a1gdp7j9.js → 6693j9rs.js} +1 -1
  57. package/dist/shared/{38crxbgn.js → 6p5k20sm.js} +3 -3
  58. package/dist/shared/7nk68fj8.js +751 -0
  59. package/dist/shared/84ptn35q.js +71 -0
  60. package/dist/shared/{d2whjz4k.js → 8jb4ymh2.js} +581 -28
  61. package/dist/shared/8ktwd3rh.js +97 -0
  62. package/dist/shared/94ez8kzh.js +19 -0
  63. package/dist/shared/{3a5ne750.js → 9m80mkac.js} +2 -2
  64. package/dist/shared/9x25fxhc.js +150 -0
  65. package/dist/shared/9yn6a2vd.js +11 -0
  66. package/dist/shared/ady1ms74.js +72 -0
  67. package/dist/shared/{4jzzghr6.js → awrsqrn7.js} +6 -6
  68. package/dist/shared/{4b9da9d7.js → axhk06q1.js} +19 -1
  69. package/dist/shared/{d04j53v8.js → c5g81ngb.js} +82 -14
  70. package/dist/shared/{n47rd821.js → c8aghh05.js} +1 -1
  71. package/dist/shared/{dd4f8mnx.js → c9z5g0fm.js} +4 -2
  72. package/dist/shared/{ps43134h.js → dyj86cmj.js} +4 -4
  73. package/dist/shared/{0qkxbm1f.js → fa9vt245.js} +1 -1
  74. package/dist/shared/{qa5dsc7z.js → fb00y8fp.js} +3 -3
  75. package/dist/shared/{qxa0yb9j.js → g7ez901n.js} +30 -1
  76. package/dist/shared/{py3nh7jy.js → gdh9mrtp.js} +2 -2
  77. package/dist/shared/{yw5c610b.js → gjthds9j.js} +2 -2
  78. package/dist/shared/hcmdtwgh.js +102 -0
  79. package/dist/shared/hqjsxnz7.js +13 -0
  80. package/dist/shared/htnaj77v.js +17 -0
  81. package/dist/shared/{v18jva5b.js → jgxzjzcz.js} +1 -1
  82. package/dist/shared/{x9v1qgeb.js → jsd07prz.js} +3 -3
  83. package/dist/shared/{g9f0zqce.js → kafaa5zv.js} +1 -1
  84. package/dist/shared/kj680nxv.js +19 -0
  85. package/dist/shared/{mxmcdsh5.js → kkzwp3d2.js} +9 -9
  86. package/dist/shared/{g1b5zzj9.js → n0dgs0pb.js} +1 -1
  87. package/dist/shared/n5y9zyj1.js +2431 -0
  88. package/dist/shared/{zt8y0wzb.js → n7mcc1gk.js} +1 -1
  89. package/dist/shared/{ge03bfca.js → nxb42q7h.js} +2 -2
  90. package/dist/shared/q6zsyh31.js +2192 -0
  91. package/dist/shared/{sfh4pk75.js → q795afwa.js} +26 -21
  92. package/dist/shared/{10tjenmd.js → qn0srwwq.js} +1 -1
  93. package/dist/shared/s4mb6a4b.js +545 -0
  94. package/dist/shared/{ytg3xf9p.js → sf8m6tfy.js} +3 -3
  95. package/dist/shared/{zd93g3rq.js → sr5ms7b3.js} +1 -1
  96. package/dist/shared/{xatfg0gn.js → tmbdywx0.js} +4 -19
  97. package/dist/shared/tny2t3sm.js +100 -0
  98. package/dist/shared/{e7a268fy.js → v7tpnhnz.js} +1 -1
  99. package/dist/shared/{ecdzej21.js → wqem4jed.js} +7 -7
  100. package/dist/shared/xghkt7a9.js +1144 -0
  101. package/dist/shared/y005b761.js +112 -0
  102. package/dist/shared/ycatkxmq.js +84 -0
  103. package/dist/shared/ygem90rn.js +900 -0
  104. package/dist/shared/{eaxwrgby.js → yjw02enp.js} +6 -3
  105. package/dist/shared/{mrq77sgw.js → zg24x0hm.js} +1 -1
  106. package/dist/shared/{cstgkzdx.js → zvbjc949.js} +1 -1
  107. package/dist/tooling/cli.js +66 -13
  108. package/dist/tooling/migrate/apply.d.ts +75 -5
  109. package/dist/tooling/migrate/backfill.d.ts +136 -0
  110. package/dist/tooling/migrate/check.d.ts +65 -0
  111. package/dist/tooling/migrate/classify.d.ts +108 -0
  112. package/dist/tooling/migrate/commands.d.ts +2 -1
  113. package/dist/tooling/migrate/config.d.ts +30 -0
  114. package/dist/tooling/migrate/doctor.d.ts +28 -0
  115. package/dist/tooling/migrate/drift.d.ts +35 -0
  116. package/dist/tooling/migrate/estimate.d.ts +50 -0
  117. package/dist/tooling/migrate/ext.d.ts +17 -0
  118. package/dist/tooling/migrate/extensions.d.ts +53 -0
  119. package/dist/tooling/migrate/files.d.ts +12 -0
  120. package/dist/tooling/migrate/index.d.ts +3 -2
  121. package/dist/tooling/migrate/index.js +67 -1
  122. package/dist/tooling/migrate/lint-rules.d.ts +51 -0
  123. package/dist/tooling/migrate/lint.d.ts +71 -0
  124. package/dist/tooling/migrate/managed.d.ts +17 -0
  125. package/dist/tooling/migrate/plan.d.ts +78 -10
  126. package/dist/tooling/migrate/project.d.ts +64 -5
  127. package/dist/tooling/migrate/provision.d.ts +18 -0
  128. package/dist/tooling/migrate/reference.d.ts +40 -0
  129. package/dist/tooling/migrate/runner.d.ts +108 -0
  130. package/dist/tooling/migrate/safe.d.ts +77 -0
  131. package/dist/tooling/migrate/seed.d.ts +20 -0
  132. package/dist/tooling/migrate/snapshot.d.ts +78 -0
  133. package/dist/tooling/migrate/status.d.ts +25 -6
  134. package/dist/tooling/migrate/type-name.d.ts +19 -0
  135. package/dist/tooling/testing/factories.d.ts +117 -0
  136. package/dist/tooling/testing/facts.d.ts +72 -0
  137. package/dist/tooling/testing/index.d.ts +89 -0
  138. package/dist/tooling/testing/index.js +8 -0
  139. package/dist/tooling/testing/isolation.d.ts +30 -0
  140. package/dist/tooling/testing/random.d.ts +40 -0
  141. package/dist/tooling/testing/record.d.ts +38 -0
  142. package/package.json +26 -2
  143. package/dist/shared/5z4q2zda.js +0 -2073
  144. package/dist/shared/eqmtn1bj.js +0 -399
  145. package/dist/shared/rtnaqre0.js +0 -37
  146. package/dist/shared/zwmqxb0g.js +0 -79
  147. package/dist/shared/{m9tv7xn7.js → radd3z4g.js} +3 -3
  148. /package/dist/shared/{9k6vetvf.js → rrstp3kb.js} +0 -0
@@ -0,0 +1,900 @@
1
+ import {
2
+ renderCatalog
3
+ } from "./41kv2xdm.js";
4
+ import {
5
+ createClient
6
+ } from "./kkzwp3d2.js";
7
+ import {
8
+ OkmError2
9
+ } from "./52szzakm.js";
10
+
11
+ // src/tooling/testing/facts.ts
12
+ var tenantProbe = "00000000-0000-4000-8000-000000000001";
13
+ function readFacts(schema) {
14
+ const foreign = foreignKeys(schema.catalog);
15
+ const facts = [];
16
+ for (const name of Object.keys(schema.model)) {
17
+ const model = schema.model[name];
18
+ if (model === undefined)
19
+ continue;
20
+ const keys = foreign.get(model.sql) ?? [];
21
+ facts.push({
22
+ name,
23
+ sql: model.sql,
24
+ primary: model.primary,
25
+ columns: columnsOf(model, schema.catalog),
26
+ relations: model.relations,
27
+ foreignKeys: keys,
28
+ tenant: isTenant(schema.tenancy, model),
29
+ globalReason: globalReason(schema.tenancy, name)
30
+ });
31
+ }
32
+ return facts;
33
+ }
34
+ function isRequired(column) {
35
+ if (!column.writable || column.guarded || column.fill)
36
+ return false;
37
+ if (column.nullable || column.databaseDefault || column.identity || column.generated) {
38
+ return false;
39
+ }
40
+ return true;
41
+ }
42
+ function columnsOf(model, catalog) {
43
+ const bySql = new Map;
44
+ for (const object of catalog.objects) {
45
+ if (object.kind !== "column")
46
+ continue;
47
+ if (object.identity.parent.name !== model.sql)
48
+ continue;
49
+ bySql.set(object.identity.name, object.definition);
50
+ }
51
+ return model.columns.map((column) => {
52
+ const definition = bySql.get(column.sql);
53
+ return {
54
+ field: column.field,
55
+ sql: column.sql,
56
+ dataType: column.dataType,
57
+ nullable: definition?.nullable === true,
58
+ databaseDefault: definition?.defaultExpression !== undefined,
59
+ identity: definition?.identity !== undefined,
60
+ generated: definition?.generated !== undefined,
61
+ guarded: column.guarded,
62
+ writable: column.writable,
63
+ fill: column.fill !== undefined,
64
+ accepts: column.accepts,
65
+ encode: column.encode
66
+ };
67
+ });
68
+ }
69
+ function foreignKeys(catalog) {
70
+ const map = new Map;
71
+ for (const object of catalog.objects) {
72
+ if (object.kind !== "constraint")
73
+ continue;
74
+ if (object.definition.constraintKind !== "foreignKey")
75
+ continue;
76
+ const references = object.definition.references;
77
+ if (references === undefined)
78
+ continue;
79
+ const table = object.identity.parent.name;
80
+ const list = map.get(table);
81
+ const key = {
82
+ columns: object.definition.columns,
83
+ target: references.parent.name,
84
+ targetColumns: references.columns
85
+ };
86
+ if (list === undefined)
87
+ map.set(table, [key]);
88
+ else
89
+ list.push(key);
90
+ }
91
+ return map;
92
+ }
93
+ function globalReason(tenancy, table) {
94
+ if (tenancy === undefined)
95
+ return;
96
+ for (const rule of tenancy.rules(table, undefined, undefined)) {
97
+ if (rule.contribution.startsWith("global "))
98
+ return rule.contribution.slice("global ".length);
99
+ }
100
+ return;
101
+ }
102
+ function isTenant(tenancy, model) {
103
+ if (tenancy === undefined)
104
+ return false;
105
+ if (globalReason(tenancy, model.name) !== undefined)
106
+ return false;
107
+ const key = model.columns.find((column) => column.field === tenancy.key);
108
+ try {
109
+ return tenancy.predicate({
110
+ table: model.name,
111
+ fieldSql: (field) => field === tenancy.key ? key?.sql : undefined,
112
+ encode: key?.encode,
113
+ alias: "t",
114
+ appended: false,
115
+ scope: { value: tenantProbe },
116
+ sink: { text() {}, param() {}, mark() {} }
117
+ }) === true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ // src/tooling/testing/factories.ts
124
+ var REF = Symbol("okm.factory.ref");
125
+ var hosts = new WeakMap;
126
+ function attachHost(harness, host) {
127
+ hosts.set(harness, host);
128
+ }
129
+ function createdOf(harness) {
130
+ const created = hosts.get(harness)?.created ?? new Map;
131
+ return [...created.entries()].map(([table, count]) => ({ table, count })).sort((left, right) => left.table < right.table ? -1 : left.table > right.table ? 1 : 0);
132
+ }
133
+ function bindFactories(host, definitions) {
134
+ const bound = {};
135
+ for (const name of Object.keys(definitions)) {
136
+ if (!host.facts.has(name)) {
137
+ throw new OkmError2("invalid", `Factory ${name} is not a table in the schema.`, {
138
+ fix: { summary: `Use a table name: ${[...host.facts.keys()].join(", ")}.` }
139
+ });
140
+ }
141
+ const define = definitions[name];
142
+ if (define !== undefined)
143
+ host.definitions.set(name, define);
144
+ bound[name] = factoryFor(host, name, undefined);
145
+ }
146
+ return bound;
147
+ }
148
+ async function insertRow(host, table, overrides, children, inherited) {
149
+ if (host.creating.has(table)) {
150
+ throw new OkmError2("invalid", `Factory cycle at ${table}.`, {
151
+ fix: { summary: "A ref() points back at a row that is still being created." }
152
+ });
153
+ }
154
+ host.creating.add(table);
155
+ try {
156
+ return await insertPrepared(host, table, overrides, children, inherited);
157
+ } finally {
158
+ host.creating.delete(table);
159
+ }
160
+ }
161
+ async function insertPrepared(host, table, overrides, children, inherited) {
162
+ const facts = requireTable(host, table);
163
+ const defined = host.definitions.get(table)?.(context(host)) ?? {};
164
+ const merged = { ...defined, ...overrides };
165
+ const key = host.schema.tenancy?.key;
166
+ const taken = takeTenant(merged, facts.tenant ? key : undefined);
167
+ const requested = taken.tenant ?? inherited;
168
+ const tenant = facts.tenant ? requested ?? host.rng.uuid() : undefined;
169
+ const resolved = await resolveRefs(host, taken.rest, tenant);
170
+ await fillRequired(host, facts, resolved, tenant);
171
+ const row = await writeRow(host, facts, resolved, tenant);
172
+ host.cache.set(cacheKey(table, facts.tenant ? tenant : undefined), row);
173
+ host.created.set(table, (host.created.get(table) ?? 0) + 1);
174
+ if (children !== undefined) {
175
+ for (const child of Object.keys(children)) {
176
+ const count = children[child];
177
+ if (count === undefined)
178
+ continue;
179
+ if (!Number.isInteger(count) || count < 0) {
180
+ throw new OkmError2("invalid", `with({ ${child}: ${String(count)} }) needs a count.`, {
181
+ fix: { summary: "Pass a non-negative integer." }
182
+ });
183
+ }
184
+ const link = linkFields(host, facts, requireTable(host, child), row);
185
+ for (let index = 0;index < count; index += 1) {
186
+ await insertRow(host, child, link, undefined, tenant);
187
+ }
188
+ }
189
+ }
190
+ return row;
191
+ }
192
+ function factoryFor(host, table, children) {
193
+ return {
194
+ create(overrides) {
195
+ return insertRow(host, table, overrides, children, undefined);
196
+ },
197
+ async createMany(count, overrides) {
198
+ if (!Number.isInteger(count) || count < 0) {
199
+ throw new OkmError2("invalid", `createMany needs a non-negative integer, got ${String(count)}.`, {
200
+ fix: { summary: "Pass how many rows to insert." }
201
+ });
202
+ }
203
+ const rows = [];
204
+ for (let index = 0;index < count; index += 1) {
205
+ rows.push(await insertRow(host, table, overrides, children, undefined));
206
+ }
207
+ return rows;
208
+ },
209
+ with(extra) {
210
+ return factoryFor(host, table, { ...children, ...extra });
211
+ }
212
+ };
213
+ }
214
+ function context(host) {
215
+ return {
216
+ email: () => host.rng.email(),
217
+ name: () => host.rng.name(),
218
+ words: (count) => host.rng.words(count),
219
+ int: (bound) => host.rng.int(bound ?? 1e4),
220
+ float: () => host.rng.float(),
221
+ boolean: () => host.rng.boolean(),
222
+ date: () => host.rng.date(),
223
+ uuid: () => host.rng.uuid(),
224
+ pick: (list) => host.rng.pick(list),
225
+ ref(table) {
226
+ if (!host.facts.has(table)) {
227
+ throw new OkmError2("invalid", `ref(${table}) is not a table in the schema.`, {
228
+ fix: { summary: `Use a table name: ${[...host.facts.keys()].join(", ")}.` }
229
+ });
230
+ }
231
+ return { [REF]: table };
232
+ }
233
+ };
234
+ }
235
+ function takeTenant(row, key) {
236
+ if (key === undefined || !Object.hasOwn(row, key))
237
+ return { tenant: undefined, rest: row };
238
+ const value = row[key];
239
+ const rest = { ...row };
240
+ delete rest[key];
241
+ if (typeof value !== "string" || value.length === 0) {
242
+ throw new OkmError2("invalid", `${key} must be a uuid string.`, {
243
+ fix: { summary: `Pass create({ ${key} }) the tenant uuid.` }
244
+ });
245
+ }
246
+ return { tenant: value, rest };
247
+ }
248
+ async function resolveRefs(host, row, tenant) {
249
+ const resolved = {};
250
+ for (const field of Object.keys(row)) {
251
+ const value = row[field];
252
+ const target = refOf(value);
253
+ if (target === undefined) {
254
+ resolved[field] = value;
255
+ continue;
256
+ }
257
+ resolved[field] = primaryValue(await referenced(host, target, tenant), requireTable(host, target), host.schema.tenancy?.key);
258
+ }
259
+ return resolved;
260
+ }
261
+ async function fillRequired(host, facts, row, tenant) {
262
+ for (const column of facts.columns) {
263
+ if (!isRequired(column) || Object.hasOwn(row, column.field))
264
+ continue;
265
+ const foreign = facts.foreignKeys.find((key) => key.columns.includes(column.sql));
266
+ if (foreign !== undefined) {
267
+ const target = factsBySql(host, foreign.target);
268
+ const parent = await referenced(host, target.name, tenant);
269
+ assignForeign(host, facts, foreign, parent, row);
270
+ if (Object.hasOwn(row, column.field))
271
+ continue;
272
+ }
273
+ row[column.field] = valueFor(host, column);
274
+ }
275
+ }
276
+ function assignForeign(host, child, foreign, parent, row) {
277
+ const target = factsBySql(host, foreign.target);
278
+ const key = host.schema.tenancy?.key;
279
+ for (let index = 0;index < foreign.columns.length; index += 1) {
280
+ const childSql = foreign.columns[index];
281
+ const parentSql = foreign.targetColumns[index];
282
+ if (childSql === undefined || parentSql === undefined)
283
+ continue;
284
+ const field = fieldOf(child, childSql);
285
+ const source = fieldOf(target, parentSql);
286
+ if (field === undefined || source === undefined || field === key)
287
+ continue;
288
+ if (!Object.hasOwn(row, field))
289
+ row[field] = parent[source];
290
+ }
291
+ }
292
+ async function referenced(host, table, tenant) {
293
+ const facts = requireTable(host, table);
294
+ const scope = facts.tenant ? tenant : undefined;
295
+ const cached = host.cache.get(cacheKey(table, scope));
296
+ if (cached !== undefined)
297
+ return cached;
298
+ return insertRow(host, table, scope === undefined ? undefined : tenantOverride(host, scope), undefined, scope);
299
+ }
300
+ function tenantOverride(host, tenant) {
301
+ const key = host.schema.tenancy?.key;
302
+ if (key === undefined)
303
+ return;
304
+ return { [key]: tenant };
305
+ }
306
+ function linkFields(host, parent, child, row) {
307
+ const many = parent.relations.find((relation) => relation.kind === "many" && relation.table === child.name);
308
+ if (many !== undefined)
309
+ return paired(host, child, parent, many.remote, many.local, row);
310
+ const one = child.relations.find((relation) => relation.kind === "one" && relation.table === parent.name);
311
+ if (one !== undefined)
312
+ return paired(host, child, parent, one.local, one.remote, row);
313
+ const foreign = child.foreignKeys.find((key) => key.target === parent.sql);
314
+ if (foreign !== undefined) {
315
+ const linked = {};
316
+ assignForeign(host, child, foreign, row, linked);
317
+ if (Object.keys(linked).length > 0)
318
+ return linked;
319
+ }
320
+ throw new OkmError2("invalid", `with() cannot link ${child.name} to ${parent.name}.`, {
321
+ fix: { summary: "Add a foreign key from the child table to the parent, or a many() relation." }
322
+ });
323
+ }
324
+ function paired(host, child, parent, childSql, parentSql, row) {
325
+ const key = host.schema.tenancy?.key;
326
+ const linked = {};
327
+ for (let index = 0;index < childSql.length; index += 1) {
328
+ const childName = childSql[index];
329
+ const parentName = parentSql[index];
330
+ if (childName === undefined || parentName === undefined)
331
+ continue;
332
+ const field = fieldOf(child, childName);
333
+ const source = fieldOf(parent, parentName);
334
+ if (field === undefined || source === undefined || field === key)
335
+ continue;
336
+ linked[field] = row[source];
337
+ }
338
+ if (Object.keys(linked).length === 0) {
339
+ throw new OkmError2("invalid", `with() cannot link ${child.name} to ${parent.name}.`, {
340
+ fix: { summary: "The relation has no column to set on the child." }
341
+ });
342
+ }
343
+ return linked;
344
+ }
345
+ async function writeRow(host, facts, data, tenant) {
346
+ const client = facts.tenant && tenant !== undefined ? openScope(host.db, host.schema.tenancy?.key ?? "tenantId", tenant) : host.db;
347
+ const handle = tableHandle(client, facts.name);
348
+ const inserted = await handle.insert(data);
349
+ if (!isRow(inserted)) {
350
+ throw new OkmError2("invalid", `Insert on ${facts.name} did not return a row.`, {
351
+ fix: { summary: "The insert returns the row. Check the table's primary key." }
352
+ });
353
+ }
354
+ return inserted;
355
+ }
356
+ function valueFor(host, column) {
357
+ const candidates = candidatesFor(host, column);
358
+ for (const value of candidates) {
359
+ try {
360
+ column.encode(value);
361
+ return value;
362
+ } catch {}
363
+ }
364
+ throw new OkmError2("invalid", `No factory default for ${column.field} (${column.dataType}).`, {
365
+ fix: { summary: "Set the column in the factory." }
366
+ });
367
+ }
368
+ function candidatesFor(host, column) {
369
+ const type = column.dataType.toLowerCase().replace(/\[\]$/, "");
370
+ const accepts = column.accepts ?? [];
371
+ const n = host.rng.counter();
372
+ const values = [];
373
+ if (accepts.includes("Temporal.Instant")) {
374
+ values.push(Temporal.Instant.from(`2020-01-01T00:00:${String(n % 60).padStart(2, "0")}Z`));
375
+ }
376
+ if (accepts.includes("Temporal.PlainDate")) {
377
+ values.push(Temporal.PlainDate.from(`2020-01-${String(n % 28 + 1).padStart(2, "0")}`));
378
+ }
379
+ if (accepts.includes("Temporal.PlainDateTime")) {
380
+ values.push(Temporal.PlainDateTime.from(`2020-01-01T00:00:${String(n % 60).padStart(2, "0")}`));
381
+ }
382
+ if (accepts.includes("Temporal.PlainTime")) {
383
+ values.push(Temporal.PlainTime.from(`00:00:${String(n % 60).padStart(2, "0")}`));
384
+ }
385
+ if (accepts.includes("Temporal.Duration"))
386
+ values.push(Temporal.Duration.from({ seconds: n }));
387
+ if (type === "json" || type === "jsonb" || accepts.includes("Object") || accepts.includes("Array")) {
388
+ values.push({});
389
+ }
390
+ if (type === "uuid")
391
+ values.push(host.rng.uuid());
392
+ if (type === "bool" || type === "boolean")
393
+ values.push(host.rng.boolean());
394
+ if (type === "smallint" || type === "integer" || type === "int" || type === "int2" || type === "int4") {
395
+ values.push(n);
396
+ }
397
+ if (type === "bigint" || type === "int8") {
398
+ values.push(String(n), n, BigInt(n));
399
+ }
400
+ if (type.startsWith("numeric") || type.startsWith("decimal")) {
401
+ values.push(`${String(n)}.00`, n);
402
+ }
403
+ if (type === "real" || type === "double precision" || type === "float4" || type === "float8") {
404
+ values.push(n);
405
+ }
406
+ const labels = enumLabels(host.schema.catalog, column.dataType);
407
+ if (labels !== undefined && labels.length > 0)
408
+ values.push(labels[0]);
409
+ values.push(`${host.rng.words(2)} ${String(n)}`);
410
+ return values;
411
+ }
412
+ function enumLabels(catalog, dataType) {
413
+ for (const object of catalog.objects) {
414
+ if (object.kind !== "type" || object.identity.name !== dataType)
415
+ continue;
416
+ const definition = object.definition;
417
+ if ("labels" in definition && Array.isArray(definition.labels))
418
+ return definition.labels;
419
+ }
420
+ return;
421
+ }
422
+ function primaryValue(row, facts, tenantKey) {
423
+ const fields = facts.primary.filter((field) => field !== tenantKey);
424
+ if (fields.length !== 1) {
425
+ throw new OkmError2("invalid", `ref(${facts.name}) needs a single primary key.`, {
426
+ fix: { summary: "Set the foreign-key columns in the factory." }
427
+ });
428
+ }
429
+ const field = fields[0];
430
+ if (field === undefined) {
431
+ throw new OkmError2("invalid", `ref(${facts.name}) needs a primary key.`, {
432
+ fix: { summary: "Give the table a primary key." }
433
+ });
434
+ }
435
+ return row[field];
436
+ }
437
+ function requireTable(host, name) {
438
+ const facts = host.facts.get(name);
439
+ if (facts === undefined) {
440
+ throw new OkmError2("invalid", `Table ${name} is not in the schema.`, {
441
+ fix: { summary: `Use a table name: ${[...host.facts.keys()].join(", ")}.` }
442
+ });
443
+ }
444
+ return facts;
445
+ }
446
+ function factsBySql(host, sql) {
447
+ for (const facts of host.facts.values()) {
448
+ if (facts.sql === sql)
449
+ return facts;
450
+ }
451
+ throw new OkmError2("invalid", `No table uses SQL name ${sql}.`, {
452
+ fix: { summary: "The foreign key target is not in this schema." }
453
+ });
454
+ }
455
+ function fieldOf(facts, sql) {
456
+ return facts.columns.find((column) => column.sql === sql)?.field;
457
+ }
458
+ function cacheKey(table, tenant) {
459
+ return `${tenant ?? "*"}:${table}`;
460
+ }
461
+ function refOf(value) {
462
+ if (typeof value !== "object" || value === null || !(REF in value))
463
+ return;
464
+ const table = value[REF];
465
+ return typeof table === "string" ? table : undefined;
466
+ }
467
+ function openScope(db, key, tenant) {
468
+ if (!("for" in db) || typeof db.for !== "function") {
469
+ throw new OkmError2("invalid", "This schema has no for().", {
470
+ fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
471
+ });
472
+ }
473
+ const opened = db.for({ [key]: tenant });
474
+ if (typeof opened !== "object" || opened === null) {
475
+ throw new OkmError2("invalid", "for() did not return a client.", {
476
+ fix: { summary: "The tenant key must be a uuid." }
477
+ });
478
+ }
479
+ return opened;
480
+ }
481
+ function tableHandle(client, name) {
482
+ if (!("table" in client) || typeof client.table !== "function") {
483
+ throw new OkmError2("invalid", "The client has no table().", {
484
+ fix: { summary: "Connect with the schema before inserting." }
485
+ });
486
+ }
487
+ const handle = client.table(name);
488
+ if (typeof handle !== "object" || handle === null || !("insert" in handle) || typeof handle.insert !== "function") {
489
+ throw new OkmError2("invalid", `Table ${name} is not on this client.`, {
490
+ fix: { summary: "A tenant table is reached with for({ tenantId })." }
491
+ });
492
+ }
493
+ return handle;
494
+ }
495
+ function isRow(value) {
496
+ return typeof value === "object" && value !== null && !Array.isArray(value);
497
+ }
498
+
499
+ // src/tooling/testing/isolation.ts
500
+ var TENANT_A = "00000000-0000-4000-8000-00000000000a";
501
+ var TENANT_B = "00000000-0000-4000-8000-00000000000b";
502
+ async function isolation(host) {
503
+ const saved = host.cache;
504
+ host.cache = new Map;
505
+ try {
506
+ const checked = [];
507
+ const skipped = [];
508
+ for (const facts of host.facts.values()) {
509
+ if (facts.globalReason !== undefined) {
510
+ skipped.push({ table: facts.name, reason: facts.globalReason });
511
+ continue;
512
+ }
513
+ if (!facts.tenant)
514
+ continue;
515
+ await checkTable(host, facts);
516
+ checked.push(facts.name);
517
+ }
518
+ return { checked, skipped };
519
+ } finally {
520
+ host.cache = saved;
521
+ }
522
+ }
523
+ async function checkTable(host, facts) {
524
+ const key = host.schema.tenancy?.key;
525
+ if (key === undefined)
526
+ return;
527
+ const row = await insertRow(host, facts.name, { [key]: TENANT_B }, undefined, TENANT_B);
528
+ const where = primaryWhere(facts, row, key);
529
+ const client = openTenant(host, TENANT_A);
530
+ const handle = tableHandle2(client, facts.name);
531
+ const found = await handle.find({ where, limit: 1 });
532
+ const rows = asRows(found);
533
+ if (rows.length > 0)
534
+ leak(facts.name, "find", rows[0]);
535
+ const one = await handle.one({ where });
536
+ if (isRow2(one))
537
+ leak(facts.name, "one", one);
538
+ const counted = asCount(await handle.count());
539
+ if (counted > 0)
540
+ leak(facts.name, "count", { count: counted });
541
+ const exists = await handle.exists();
542
+ if (exists === true)
543
+ leak(facts.name, "exists", { exists: true });
544
+ const relation = facts.relations[0];
545
+ if (relation !== undefined) {
546
+ const included = await handle.one({ where, include: includeOf(relation) });
547
+ if (isRow2(included))
548
+ leak(facts.name, "include", included);
549
+ }
550
+ const set = updateSet(host, facts, row);
551
+ if (set !== undefined) {
552
+ const updated = asCount(await handle.update({ where, set }));
553
+ if (updated > 0)
554
+ leak(facts.name, "update", { count: updated });
555
+ }
556
+ const deleted = asCount(await handle.delete({ where }));
557
+ if (deleted > 0)
558
+ leak(facts.name, "delete", { count: deleted });
559
+ }
560
+ function includeOf(relation) {
561
+ if (relation.kind === "many")
562
+ return { [relation.name]: { limit: 1 } };
563
+ return { [relation.name]: true };
564
+ }
565
+ function updateSet(host, facts, row) {
566
+ const column = facts.columns.find((item) => item.writable && !item.guarded && !facts.primary.includes(item.field));
567
+ if (column === undefined)
568
+ return;
569
+ const current = row[column.field];
570
+ if (current !== undefined && !facts.foreignKeys.some((key) => key.columns.includes(column.sql))) {
571
+ return { [column.field]: current };
572
+ }
573
+ return { [column.field]: valueFrom(host, facts, column.field, row) };
574
+ }
575
+ function valueFrom(host, facts, field, row) {
576
+ const column = facts.columns.find((item) => item.field === field);
577
+ if (column === undefined)
578
+ return row[field];
579
+ try {
580
+ if (column.dataType === "uuid")
581
+ return host.rng.uuid();
582
+ column.encode(row[field]);
583
+ return row[field];
584
+ } catch {
585
+ return host.rng.uuid();
586
+ }
587
+ }
588
+ function primaryWhere(facts, row, tenantKey) {
589
+ const where = {};
590
+ for (const field of facts.primary) {
591
+ if (field === tenantKey)
592
+ continue;
593
+ where[field] = row[field];
594
+ }
595
+ if (Object.keys(where).length === 0) {
596
+ throw new OkmError2("invalid", `Table ${facts.name} has no primary key to look up.`, {
597
+ fix: { summary: "Give the tenant table a primary key so isolation can address one row." }
598
+ });
599
+ }
600
+ return where;
601
+ }
602
+ function leak(table, query, row) {
603
+ throw new OkmError2("invalid", `Isolation leak on ${table}.${query}.
604
+ ${JSON.stringify(row)}`, {
605
+ fix: { summary: "A query under tenant A returned a row inserted for tenant B." }
606
+ });
607
+ }
608
+ function openTenant(host, tenant) {
609
+ const key = host.schema.tenancy?.key ?? "tenantId";
610
+ const db = host.db;
611
+ if (!("for" in db) || typeof db.for !== "function") {
612
+ throw new OkmError2("invalid", "This schema has no for().", {
613
+ fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
614
+ });
615
+ }
616
+ const opened = db.for({ [key]: tenant });
617
+ if (typeof opened !== "object" || opened === null) {
618
+ throw new OkmError2("invalid", "for() did not return a client.", {
619
+ fix: { summary: "The tenant key must be a uuid." }
620
+ });
621
+ }
622
+ return opened;
623
+ }
624
+ function tableHandle2(client, name) {
625
+ if (!("table" in client) || typeof client.table !== "function") {
626
+ throw new OkmError2("invalid", "The client has no table().", {
627
+ fix: { summary: "Connect with the schema before isolation()." }
628
+ });
629
+ }
630
+ const handle = client.table(name);
631
+ if (!isQueryHandle(handle)) {
632
+ throw new OkmError2("invalid", `Table ${name} is not on this client.`, {
633
+ fix: { summary: "A tenant table is reached with for({ tenantId })." }
634
+ });
635
+ }
636
+ return handle;
637
+ }
638
+ function isQueryHandle(value) {
639
+ if (typeof value !== "object" || value === null)
640
+ return false;
641
+ const record = value;
642
+ return typeof record.find === "function" && typeof record.one === "function";
643
+ }
644
+ function asRows(value) {
645
+ if (!Array.isArray(value))
646
+ return [];
647
+ return value.filter(isRow2);
648
+ }
649
+ function asCount(value) {
650
+ if (typeof value === "number")
651
+ return value;
652
+ if (typeof value === "object" && value !== null && "count" in value) {
653
+ const count = value.count;
654
+ if (typeof count === "number")
655
+ return count;
656
+ }
657
+ return 0;
658
+ }
659
+ function isRow2(value) {
660
+ return typeof value === "object" && value !== null && !Array.isArray(value);
661
+ }
662
+
663
+ // src/tooling/testing/random.ts
664
+ var WORDS = [
665
+ "amber",
666
+ "basin",
667
+ "cedar",
668
+ "delta",
669
+ "ember",
670
+ "flint",
671
+ "grove",
672
+ "harbor",
673
+ "ivory",
674
+ "juniper",
675
+ "kelp",
676
+ "lattice",
677
+ "maple",
678
+ "north",
679
+ "olive",
680
+ "pebble",
681
+ "quartz",
682
+ "river",
683
+ "spruce",
684
+ "tide",
685
+ "umbra",
686
+ "violet",
687
+ "willow",
688
+ "yellow"
689
+ ];
690
+ var FIRST = ["Ada", "Grace", "Lin", "Edsger", "Barbara", "Alan", "Margaret"];
691
+ var LAST = ["Lovelace", "Hopper", "Torvalds", "Dijkstra", "Liskov", "Kay", "Hamilton"];
692
+ function rng(seed) {
693
+ if (!Number.isInteger(seed)) {
694
+ throw new OkmError2("invalid", `Factory seed must be an integer, got ${String(seed)}.`, {
695
+ fix: { summary: "Pass testing(schema, { driver, seed }) an integer." }
696
+ });
697
+ }
698
+ let state = (seed === 0 ? 2654435769 : seed) >>> 0;
699
+ let count = 0;
700
+ const next = () => {
701
+ state = state + 1831565813 >>> 0;
702
+ let t = state;
703
+ t = Math.imul(t ^ t >>> 15, t | 1);
704
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
705
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
706
+ };
707
+ const int = (bound) => {
708
+ if (!Number.isInteger(bound) || bound <= 0) {
709
+ throw new OkmError2("invalid", `int bound must be a positive integer, got ${String(bound)}.`, {
710
+ fix: { summary: "Pass a positive integer to int()." }
711
+ });
712
+ }
713
+ return Math.floor(next() * bound);
714
+ };
715
+ const api = {
716
+ next,
717
+ int,
718
+ uuid() {
719
+ const bytes = [];
720
+ for (let index = 0;index < 16; index += 1)
721
+ bytes.push(Math.floor(next() * 256));
722
+ const sixth = bytes[6] ?? 0;
723
+ const eighth = bytes[8] ?? 0;
724
+ bytes[6] = sixth & 15 | 64;
725
+ bytes[8] = eighth & 63 | 128;
726
+ const hex = bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
727
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
728
+ },
729
+ email() {
730
+ count += 1;
731
+ return `user${String(count)}@example.com`;
732
+ },
733
+ name() {
734
+ const first = FIRST[int(FIRST.length)] ?? FIRST[0];
735
+ const last = LAST[int(LAST.length)] ?? LAST[0];
736
+ return `${first} ${last}`;
737
+ },
738
+ words(amount) {
739
+ if (!Number.isInteger(amount) || amount < 0) {
740
+ throw new OkmError2("invalid", `words() needs a non-negative integer, got ${String(amount)}.`, { fix: { summary: "Pass words(n) with n greater than or equal to 0." } });
741
+ }
742
+ const picked = [];
743
+ for (let index = 0;index < amount; index += 1) {
744
+ picked.push(WORDS[int(WORDS.length)] ?? WORDS[0]);
745
+ }
746
+ return picked.join(" ");
747
+ },
748
+ boolean() {
749
+ return next() < 0.5;
750
+ },
751
+ float() {
752
+ return next();
753
+ },
754
+ date() {
755
+ const day = 1 + int(28);
756
+ return `2020-01-${String(day).padStart(2, "0")}`;
757
+ },
758
+ pick(list) {
759
+ if (list.length === 0) {
760
+ throw new OkmError2("invalid", "pick() needs a list with one value.", {
761
+ fix: { summary: "Pass a non-empty list to pick()." }
762
+ });
763
+ }
764
+ const value = list[int(list.length)];
765
+ if (value === undefined) {
766
+ throw new OkmError2("invalid", "pick() needs a list with one value.", {
767
+ fix: { summary: "Pass a non-empty list to pick()." }
768
+ });
769
+ }
770
+ return value;
771
+ },
772
+ counter() {
773
+ count += 1;
774
+ return count;
775
+ }
776
+ };
777
+ return api;
778
+ }
779
+
780
+ // src/tooling/testing/record.ts
781
+ function isTransactionControl(text) {
782
+ const command = text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ").trim().replace(/;\s*$/, "").trim().toLowerCase();
783
+ if (/^(?:begin|start\s+transaction)(?:\s|$)/.test(command))
784
+ return true;
785
+ if (command === "commit" || command === "commit work" || command === "end")
786
+ return true;
787
+ if (command === "rollback" || command === "rollback work")
788
+ return true;
789
+ if (command.startsWith("rollback to"))
790
+ return true;
791
+ if (command.startsWith("savepoint ") || command === "savepoint")
792
+ return true;
793
+ if (command.startsWith("release savepoint ") || command.startsWith("release "))
794
+ return true;
795
+ if (/^set\s+transaction\b/.test(command))
796
+ return true;
797
+ if (/^set\s+local\b/.test(command))
798
+ return true;
799
+ if (/^set\s+session\s+characteristics\b/.test(command))
800
+ return true;
801
+ return false;
802
+ }
803
+ function record(pool) {
804
+ const log = [];
805
+ const note = (statement) => {
806
+ log.push({ text: statement.text, params: statement.params ?? [] });
807
+ };
808
+ const wrapConnection = (connection) => ({
809
+ ...connection,
810
+ execute(text, params, options) {
811
+ note({ text, ...params !== undefined ? { params } : {} });
812
+ return connection.execute(text, params, options);
813
+ },
814
+ batch(statements, options) {
815
+ for (const statement of statements)
816
+ note(statement);
817
+ return connection.batch(statements, options);
818
+ },
819
+ release: () => connection.release(),
820
+ ...connection.cancel !== undefined ? { cancel: () => connection.cancel?.() } : {}
821
+ });
822
+ const wrapped = {
823
+ capabilities: pool.capabilities,
824
+ execute(text, params, options) {
825
+ note({ text, ...params !== undefined ? { params } : {} });
826
+ return pool.execute(text, params, options);
827
+ },
828
+ batch(statements, options) {
829
+ for (const statement of statements)
830
+ note(statement);
831
+ return pool.batch(statements, options);
832
+ },
833
+ stats: () => pool.stats(),
834
+ close: () => pool.close(),
835
+ ...pool.reserve !== undefined ? { reserve: async () => wrapConnection(await pool.reserve()) } : {},
836
+ ...pool.describe !== undefined ? { describe: (text, params) => pool.describe(text, params) } : {},
837
+ ...pool.cancel !== undefined ? { cancel: () => pool.cancel?.() } : {}
838
+ };
839
+ return { pool: wrapped, log };
840
+ }
841
+
842
+ // src/tooling/testing/index.ts
843
+ async function testing2(schema, options) {
844
+ const seed = options.seed ?? 1;
845
+ const pool = await options.driver;
846
+ if (options.migrate !== false) {
847
+ for (const statement of renderCatalog(schema.catalog, "public")) {
848
+ if (statement.trim().length === 0)
849
+ continue;
850
+ await pool.execute(statement);
851
+ }
852
+ }
853
+ const recording = record(pool);
854
+ const db = createClient(schema, recording.pool, { ownsPool: true });
855
+ await db.connected;
856
+ const facts = new Map(readFacts(schema).map((item) => [item.name, item]));
857
+ const host = {
858
+ schema,
859
+ db,
860
+ facts,
861
+ rng: rng(seed),
862
+ cache: new Map,
863
+ creating: new Set,
864
+ created: new Map,
865
+ definitions: new Map
866
+ };
867
+ const harness = {
868
+ db,
869
+ factories(definitions) {
870
+ return bindFactories(host, definitions);
871
+ },
872
+ async expectQueries(count, run) {
873
+ if (!Number.isInteger(count) || count < 0) {
874
+ throw new OkmError2("invalid", `expectQueries needs a non-negative integer, got ${String(count)}.`, {
875
+ fix: { summary: "Pass the number of statements the call should send." }
876
+ });
877
+ }
878
+ const from = recording.log.length;
879
+ await run();
880
+ const counted = recording.log.slice(from).filter((item) => !isTransactionControl(item.text));
881
+ if (counted.length === count)
882
+ return;
883
+ const noun = count === 1 ? "query" : "queries";
884
+ const text = counted.map((item) => item.text).join(`
885
+ `);
886
+ throw new OkmError2("invalid", `expected ${String(count)} ${noun}, ran ${String(counted.length)}
887
+ ${text}`, {
888
+ fix: {
889
+ summary: "The statements above were counted. Transaction control is not counted."
890
+ }
891
+ });
892
+ },
893
+ isolation: () => isolation(host),
894
+ close: () => db.close()
895
+ };
896
+ attachHost(harness, host);
897
+ return harness;
898
+ }
899
+
900
+ export { createdOf, testing2 };