okmodel 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +36 -18
  2. package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
  3. package/dist/adapters/pg/postgres-extra.d.ts +22 -0
  4. package/dist/contracts/catalog/document.d.ts +19 -5
  5. package/dist/contracts/catalog/object.d.ts +2 -1
  6. package/dist/contracts/catalog/policy.d.ts +48 -0
  7. package/dist/contracts/catalog/types.d.ts +25 -1
  8. package/dist/contracts/catalog/view.d.ts +2 -0
  9. package/dist/contracts/internal.js +1 -1
  10. package/dist/contracts/location.d.ts +10 -0
  11. package/dist/dialects/pg/column.d.ts +13 -2
  12. package/dist/dialects/pg/ddl.d.ts +37 -1
  13. package/dist/dialects/pg/emit.d.ts +19 -1
  14. package/dist/dialects/pg/errors.d.ts +32 -1
  15. package/dist/dialects/pg/ext/citext.js +1 -1
  16. package/dist/dialects/pg/ext/index.js +1 -1
  17. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  18. package/dist/dialects/pg/filter-parse.d.ts +10 -3
  19. package/dist/dialects/pg/fn/index.js +10 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +29 -17
  22. package/dist/dialects/pg/introspect.d.ts +7 -0
  23. package/dist/dialects/pg/model.d.ts +7 -2
  24. package/dist/dialects/pg/ops/or.d.ts +1 -1
  25. package/dist/dialects/pg/owner.d.ts +42 -0
  26. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  27. package/dist/dialects/pg/role/check.d.ts +3 -0
  28. package/dist/dialects/pg/role/index.d.ts +71 -6
  29. package/dist/dialects/pg/role/sql.d.ts +11 -0
  30. package/dist/dialects/pg/schema.d.ts +2 -0
  31. package/dist/dialects/pg/table.d.ts +25 -3
  32. package/dist/dialects/pg/temporal.d.ts +5 -76
  33. package/dist/dialects/pg/tenancy.d.ts +45 -3
  34. package/dist/dialects/pg/view/client.d.ts +19 -1
  35. package/dist/dialects/pg/view/index.d.ts +4 -0
  36. package/dist/dialects/pg/view/index.js +21 -178
  37. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  38. package/dist/runtime/batch.d.ts +1 -1
  39. package/dist/runtime/client.d.ts +6 -1
  40. package/dist/runtime/conflict.d.ts +2 -1
  41. package/dist/runtime/drift.d.ts +9 -0
  42. package/dist/runtime/inflight.d.ts +14 -0
  43. package/dist/runtime/pg/bun.js +6 -5
  44. package/dist/runtime/pg/pg.js +5 -5
  45. package/dist/runtime/pg/pglite.js +6 -5
  46. package/dist/runtime/pg/postgresjs.js +5 -5
  47. package/dist/runtime/plan.d.ts +22 -1
  48. package/dist/runtime/predicate.d.ts +19 -0
  49. package/dist/runtime/safety/index.js +1 -1
  50. package/dist/runtime/tenancy/composite.d.ts +26 -0
  51. package/dist/runtime/tenancy/context.d.ts +16 -0
  52. package/dist/runtime/tenancy/index.d.ts +9 -5
  53. package/dist/runtime/tenancy/index.js +1508 -37
  54. package/dist/runtime/tenancy/path.d.ts +87 -0
  55. package/dist/runtime/tenancy/rls.d.ts +67 -0
  56. package/dist/runtime/tenancy/via.d.ts +22 -0
  57. package/dist/runtime/traits/index.js +17 -11
  58. package/dist/runtime/types.d.ts +13 -1
  59. package/dist/runtime/validate/index.js +4 -4
  60. package/dist/runtime/write.d.ts +1 -1
  61. package/dist/shared/09p049xa.js +702 -0
  62. package/dist/shared/{ykv7wh5b.js → 1f0xa3cb.js} +5 -5
  63. package/dist/shared/1nw1f3vs.js +23 -0
  64. package/dist/shared/{6p5k20sm.js → 1z04fa85.js} +21 -11
  65. package/dist/shared/{p7pfx624.js → 2cg3wnxc.js} +314 -65
  66. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  67. package/dist/shared/{qgvt8nx1.js → 2z7wv5tj.js} +21 -5
  68. package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
  69. package/dist/shared/{sf8m6tfy.js → 32cnm7kn.js} +8 -3
  70. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  71. package/dist/shared/{jdgqqx31.js → 3fqwmr09.js} +38 -14
  72. package/dist/shared/{922qbgnw.js → 3qnhc7w5.js} +16 -9
  73. package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
  74. package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
  75. package/dist/shared/{k7cc9nzk.js → 5d8t84hg.js} +15 -3
  76. package/dist/shared/{fa9vt245.js → 5j36z8dn.js} +17 -6
  77. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  78. package/dist/shared/8dqhth8c.js +12 -0
  79. package/dist/shared/8etn7d8w.js +62 -0
  80. package/dist/shared/{g516zdsb.js → 8x72s86k.js} +476 -30
  81. package/dist/shared/{nkc9sfqb.js → 929h19xp.js} +145 -7
  82. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  83. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  84. package/dist/shared/{b066g80h.js → ag4c7516.js} +2 -2
  85. package/dist/shared/b3awxn7h.js +45 -0
  86. package/dist/shared/b4v57v3x.js +9 -0
  87. package/dist/shared/bdk4sty6.js +58 -0
  88. package/dist/shared/{s6gqfn5j.js → c58secgj.js} +125 -8
  89. package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
  90. package/dist/shared/d5azex6s.js +6 -0
  91. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  92. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  93. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  94. package/dist/shared/{wxn4b7pp.js → f0f1n281.js} +59 -12
  95. package/dist/shared/{gdh9mrtp.js → g90m9b0g.js} +3 -2
  96. package/dist/shared/{tbszx8mz.js → gwn6v667.js} +2 -2
  97. package/dist/shared/{c8aghh05.js → h15jx9bc.js} +8 -2
  98. package/dist/shared/{3p4b1fg0.js → hazqqecq.js} +17 -93
  99. package/dist/shared/{4ef7mmqf.js → j3rkk835.js} +34 -11
  100. package/dist/shared/{bt9sfq0z.js → j60w0dvr.js} +56 -21
  101. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  102. package/dist/shared/k68eedmy.js +85 -0
  103. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  104. package/dist/shared/{ckjbyrt9.js → mb2ycs1p.js} +7 -9
  105. package/dist/shared/{tq9afp8d.js → n076ydsc.js} +1 -1
  106. package/dist/shared/{xhh1srw3.js → nx9hh281.js} +50 -32
  107. package/dist/shared/{kdkxh3kb.js → p248q79y.js} +58 -59
  108. package/dist/shared/{990a291j.js → p6bjzq9q.js} +14 -9
  109. package/dist/shared/{h4nqvjkp.js → pjr6xrm5.js} +2 -2
  110. package/dist/shared/pz6rj8ay.js +212 -0
  111. package/dist/shared/{5vdm4sgp.js → q3j1tq7a.js} +36 -23
  112. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  113. package/dist/shared/{dxjp14rk.js → r11n8grd.js} +2 -2
  114. package/dist/shared/r1fdd0f6.js +19 -0
  115. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  116. package/dist/shared/rhs3xe4k.js +7 -0
  117. package/dist/shared/{jsd07prz.js → rpycarcr.js} +6 -6
  118. package/dist/shared/{xsq7v6wp.js → sbdme3vy.js} +14 -2
  119. package/dist/shared/{xdpj5kkm.js → t3g0k88x.js} +775 -264
  120. package/dist/shared/{ayz64vzj.js → ttmg09vd.js} +708 -19
  121. package/dist/shared/{r6pj4fq2.js → venqrjna.js} +8 -4
  122. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  123. package/dist/shared/{9tgmr1gh.js → vmfkhszw.js} +44 -9
  124. package/dist/shared/{wqem4jed.js → wkme6tep.js} +11 -9
  125. package/dist/shared/{qy9rdq0g.js → wt5rzv1h.js} +15 -11
  126. package/dist/shared/wxr8cwns.js +6 -0
  127. package/dist/shared/{bzsjtd7c.js → wynzeggs.js} +19 -14
  128. package/dist/shared/x3jc53kg.js +220 -0
  129. package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
  130. package/dist/shared/{fyqkk1m0.js → xq0mvhts.js} +10 -9
  131. package/dist/shared/xqve9598.js +53 -0
  132. package/dist/shared/{47xzb9fc.js → y2t3jktd.js} +2 -2
  133. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  134. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  135. package/dist/shared/{h8ne16fq.js → ys0xzyaj.js} +1 -1
  136. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  137. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  138. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  139. package/dist/shared/{fb00y8fp.js → zm0mafk4.js} +12 -6
  140. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  141. package/dist/tooling/cli.js +97 -24
  142. package/dist/tooling/migrate/apply.d.ts +14 -0
  143. package/dist/tooling/migrate/check.d.ts +3 -4
  144. package/dist/tooling/migrate/classify.d.ts +4 -0
  145. package/dist/tooling/migrate/commands.d.ts +1 -2
  146. package/dist/tooling/migrate/config.d.ts +3 -2
  147. package/dist/tooling/migrate/drift.d.ts +1 -1
  148. package/dist/tooling/migrate/estimate.d.ts +14 -1
  149. package/dist/tooling/migrate/files.d.ts +11 -1
  150. package/dist/tooling/migrate/index.js +17 -16
  151. package/dist/tooling/migrate/lint.d.ts +4 -4
  152. package/dist/tooling/migrate/lost.d.ts +28 -0
  153. package/dist/tooling/migrate/paths.d.ts +14 -0
  154. package/dist/tooling/migrate/plan.d.ts +10 -3
  155. package/dist/tooling/migrate/policy.d.ts +11 -0
  156. package/dist/tooling/migrate/runner.d.ts +25 -0
  157. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  158. package/dist/tooling/migrate/sql-lex.d.ts +39 -0
  159. package/dist/tooling/migrate/text.d.ts +42 -0
  160. package/dist/tooling/node-version.d.ts +21 -0
  161. package/dist/tooling/testing/factories.d.ts +10 -0
  162. package/dist/tooling/testing/facts.d.ts +33 -5
  163. package/dist/tooling/testing/index.d.ts +1 -1
  164. package/dist/tooling/testing/index.js +3 -3
  165. package/dist/tooling/testing/isolation.d.ts +15 -8
  166. package/package.json +10 -6
  167. package/dist/shared/0v1g424p.js +0 -26
  168. package/dist/shared/r73pt594.js +0 -426
  169. package/dist/shared/rasvvzgz.js +0 -21
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  renderCatalog
3
- } from "./8xsbagad.js";
3
+ } from "./9s0ypp87.js";
4
+ import {
5
+ viewGlobalReason,
6
+ viewMentions
7
+ } from "./x3jc53kg.js";
4
8
  import {
5
9
  createClient
6
- } from "./bt9sfq0z.js";
10
+ } from "./j60w0dvr.js";
7
11
  import {
8
12
  OkmError2
9
13
  } from "./52szzakm.js";
@@ -36,6 +40,41 @@ function readFacts(schema) {
36
40
  }
37
41
  return facts;
38
42
  }
43
+ function readViews(schema) {
44
+ const tables = new Set;
45
+ const listed = new Map;
46
+ for (const object of schema.catalog.objects) {
47
+ if (object.kind === "table")
48
+ tables.add(object.identity.name);
49
+ if (object.kind !== "view" && object.kind !== "materializedView")
50
+ continue;
51
+ listed.set(object.identity.name, { kind: object.kind, query: object.definition.query });
52
+ }
53
+ const views = [];
54
+ for (const name of Object.keys(schema.model)) {
55
+ const model = schema.model[name];
56
+ if (model === undefined || tables.has(model.sql))
57
+ continue;
58
+ const meta = listed.get(model.sql);
59
+ if (meta === undefined)
60
+ continue;
61
+ const reason = viewGlobalReason(model);
62
+ views.push({
63
+ name,
64
+ sql: model.sql,
65
+ kind: meta.kind,
66
+ query: meta.query,
67
+ columns: model.columns.map((column) => ({
68
+ field: column.field,
69
+ dataType: column.dataType,
70
+ guarded: column.guarded
71
+ })),
72
+ tenant: reason === undefined && isTenant(schema.tenancy, model),
73
+ globalReason: reason
74
+ });
75
+ }
76
+ return views;
77
+ }
39
78
  function isRequired(column) {
40
79
  if (!column.writable || column.guarded || column.fill)
41
80
  return false;
@@ -113,7 +152,7 @@ function isTenant(tenancy, model) {
113
152
  try {
114
153
  return tenancy.predicate({
115
154
  table: model.name,
116
- fieldSql: (field) => field === tenancy.key ? key?.sql : undefined,
155
+ fieldSql: (field) => model.columns.find((column) => column.field === field)?.sql,
117
156
  encode: key?.encode,
118
157
  alias: "t",
119
158
  appended: false,
@@ -348,7 +387,7 @@ function paired(host, child, parent, childSql, parentSql, row) {
348
387
  return linked;
349
388
  }
350
389
  async function writeRow(host, facts, data, tenant) {
351
- const client = facts.tenant && tenant !== undefined ? openScope(host.db, host.schema.tenancy?.key ?? "tenantId", tenant) : host.db;
390
+ const client = facts.tenant && tenant !== undefined ? openScope(host.db, host.schema.tenancy, tenant) : host.db;
352
391
  const handle = tableHandle(client, facts.name);
353
392
  const inserted = await handle.insert(data);
354
393
  if (!isRow(inserted)) {
@@ -463,19 +502,26 @@ function fieldOf(facts, sql) {
463
502
  function cacheKey(table, tenant) {
464
503
  return `${tenant ?? "*"}:${table}`;
465
504
  }
505
+ function forgetCached(host, table, tenant) {
506
+ host.cache.delete(cacheKey(table, tenant));
507
+ }
466
508
  function refOf(value) {
467
509
  if (typeof value !== "object" || value === null || !(REF in value))
468
510
  return;
469
511
  const table = value[REF];
470
512
  return typeof table === "string" ? table : undefined;
471
513
  }
472
- function openScope(db, key, tenant) {
514
+ function openScope(db, tenancy, tenant) {
473
515
  if (!("for" in db) || typeof db.for !== "function") {
474
516
  throw new OkmError2("invalid", "This schema has no for().", {
475
517
  fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
476
518
  });
477
519
  }
478
- const opened = db.for({ [key]: tenant });
520
+ const keys = tenancy?.keys ?? [tenancy?.key ?? "tenantId"];
521
+ const input = {};
522
+ for (const key of keys)
523
+ input[key] = tenant;
524
+ const opened = db.for(input);
479
525
  if (typeof opened !== "object" || opened === null) {
480
526
  throw new OkmError2("invalid", "for() did not return a client.", {
481
527
  fix: { summary: "The tenant key must be a uuid." }
@@ -502,14 +548,23 @@ function isRow(value) {
502
548
  }
503
549
 
504
550
  // src/tooling/testing/isolation.ts
505
- var TENANT_A = "00000000-0000-4000-8000-00000000000a";
506
- var TENANT_B = "00000000-0000-4000-8000-00000000000b";
507
551
  async function isolation(host) {
508
552
  const saved = host.cache;
509
553
  host.cache = new Map;
510
554
  try {
555
+ if (host.schema.tenancy?.strategy === "rls") {
556
+ const check = host.db["~rls"];
557
+ if (check === undefined) {
558
+ throw new OkmError2("invalid", "Row-level security has no policy check on this client.", {
559
+ fix: { summary: "Open the client from a schema that uses rlsTenancy()." }
560
+ });
561
+ }
562
+ await check();
563
+ }
511
564
  const checked = [];
512
565
  const skipped = [];
566
+ const tenantA = crypto.randomUUID();
567
+ const tenantB = crypto.randomUUID();
513
568
  for (const facts of host.facts.values()) {
514
569
  if (facts.globalReason !== undefined) {
515
570
  skipped.push({ table: facts.name, reason: facts.globalReason });
@@ -517,33 +572,86 @@ async function isolation(host) {
517
572
  }
518
573
  if (!facts.tenant)
519
574
  continue;
520
- await checkTable(host, facts);
575
+ const hops = host.schema.tenancy?.pathOf?.(facts.name);
576
+ if (hops !== undefined && hops.length > 0) {
577
+ await checkPath(host, facts, hops, tenantA, tenantB);
578
+ } else {
579
+ await checkTable(host, facts, tenantA, tenantB);
580
+ const keys = tenantKeys(host);
581
+ if (keys.length > 1)
582
+ await checkFlips(host, facts, keys, tenantA, tenantB);
583
+ }
521
584
  checked.push(facts.name);
522
585
  }
523
- return { checked, skipped };
586
+ const views = await checkViews(host, tenantA, tenantB, skipped);
587
+ return { checked, skipped, views };
524
588
  } finally {
525
589
  host.cache = saved;
526
590
  }
527
591
  }
528
- async function checkTable(host, facts) {
592
+ async function checkTable(host, facts, tenantA, tenantB) {
529
593
  const key = host.schema.tenancy?.key;
530
594
  if (key === undefined)
531
595
  return;
532
- const row = await insertRow(host, facts.name, { [key]: TENANT_B }, undefined, TENANT_B);
533
- const where = primaryWhere(facts, row, key);
534
- const client = openTenant(host, TENANT_A);
596
+ const keys = tenantKeys(host);
597
+ if (host.definitions.size > 0 && !host.definitions.has(facts.name)) {
598
+ throw new OkmError2("invalid", `Factory ${facts.name} is missing.`, {
599
+ fix: { summary: `Add ${facts.name} to factories().` }
600
+ });
601
+ }
602
+ const row = await insertRow(host, facts.name, { [key]: tenantB }, undefined, tenantB);
603
+ const where = primaryWhere(facts, row, keys);
604
+ const client = openTenant(host, tenantA);
535
605
  const handle = tableHandle2(client, facts.name);
536
- const found = await handle.find({ where, limit: 1 });
537
- const rows = asRows(found);
606
+ try {
607
+ await readTenant(facts, handle, where);
608
+ const set = updateSet(host, facts, row);
609
+ if (set !== undefined) {
610
+ const updated = asCount(await handle.update({ where, set }));
611
+ if (updated > 0)
612
+ leak(facts.name, "update", { count: updated });
613
+ if (typeof client.batch === "function") {
614
+ const batched = await client.batch([handle.update({ where, set })]);
615
+ if (Array.isArray(batched) && batched.some((item) => asCount(item) > 0)) {
616
+ leak(facts.name, "batch", batched);
617
+ }
618
+ }
619
+ }
620
+ const removed = asCount(await handle.delete({ where }));
621
+ if (removed > 0)
622
+ leak(facts.name, "delete", { count: removed });
623
+ } finally {
624
+ const owner = tableHandle2(openTenant(host, tenantB), facts.name);
625
+ await owner.delete({ where }).catch(() => {
626
+ return;
627
+ });
628
+ forgetCached(host, facts.name, tenantB);
629
+ }
630
+ }
631
+ async function readTenant(facts, handle, where) {
632
+ const query = handle.find({ where, limit: 1 });
633
+ const rows = asRows(await query);
538
634
  if (rows.length > 0)
539
635
  leak(facts.name, "find", rows[0]);
636
+ const streamed = handle.find({ where, limit: 1 });
637
+ if (typeof streamed.stream === "function") {
638
+ try {
639
+ for await (const item of streamed.stream()) {
640
+ if (isRow2(item))
641
+ leak(facts.name, "stream", item);
642
+ }
643
+ } catch (error) {
644
+ if (!(error instanceof OkmError2) || error.code !== "OKM1111")
645
+ throw error;
646
+ }
647
+ }
540
648
  const one = await handle.one({ where });
541
649
  if (isRow2(one))
542
650
  leak(facts.name, "one", one);
543
- const counted = asCount(await handle.count());
651
+ const counted = asCount(await handle.count({ where }));
544
652
  if (counted > 0)
545
653
  leak(facts.name, "count", { count: counted });
546
- const exists = await handle.exists();
654
+ const exists = await handle.exists({ where });
547
655
  if (exists === true)
548
656
  leak(facts.name, "exists", { exists: true });
549
657
  const relation = facts.relations[0];
@@ -552,15 +660,28 @@ async function checkTable(host, facts) {
552
660
  if (isRow2(included))
553
661
  leak(facts.name, "include", included);
554
662
  }
555
- const set = updateSet(host, facts, row);
556
- if (set !== undefined) {
557
- const updated = asCount(await handle.update({ where, set }));
558
- if (updated > 0)
559
- leak(facts.name, "update", { count: updated });
663
+ if (typeof handle.aggregate === "function") {
664
+ const aggregated = await handle.aggregate({ where, count: true });
665
+ if (Array.isArray(aggregated)) {
666
+ for (const item of aggregated) {
667
+ if (isRow2(item) && asCount(item) > 0)
668
+ leak(facts.name, "aggregate", item);
669
+ }
670
+ }
671
+ }
672
+ if (typeof handle.page === "function") {
673
+ const orderField = Object.keys(where)[0];
674
+ if (orderField !== undefined) {
675
+ const paged = await handle.page({
676
+ where,
677
+ orderBy: { [orderField]: "asc" },
678
+ limit: 5
679
+ });
680
+ if (isRow2(paged) && Array.isArray(paged.items) && paged.items.length > 0) {
681
+ leak(facts.name, "page", paged.items[0]);
682
+ }
683
+ }
560
684
  }
561
- const deleted = asCount(await handle.delete({ where }));
562
- if (deleted > 0)
563
- leak(facts.name, "delete", { count: deleted });
564
685
  }
565
686
  function includeOf(relation) {
566
687
  if (relation.kind === "many")
@@ -590,10 +711,10 @@ function valueFrom(host, facts, field, row) {
590
711
  return host.rng.uuid();
591
712
  }
592
713
  }
593
- function primaryWhere(facts, row, tenantKey) {
714
+ function primaryWhere(facts, row, tenantKeys) {
594
715
  const where = {};
595
716
  for (const field of facts.primary) {
596
- if (field === tenantKey)
717
+ if (tenantKeys.includes(field))
597
718
  continue;
598
719
  where[field] = row[field];
599
720
  }
@@ -610,15 +731,340 @@ ${JSON.stringify(row)}`, {
610
731
  fix: { summary: "A query under tenant A returned a row inserted for tenant B." }
611
732
  });
612
733
  }
734
+ function tenantKeys(host) {
735
+ const tenancy = host.schema.tenancy;
736
+ if (tenancy === undefined)
737
+ return [];
738
+ if (tenancy.keys !== undefined && tenancy.keys.length > 0)
739
+ return tenancy.keys;
740
+ return [tenancy.key];
741
+ }
742
+ function scopeFor(keys, tenant) {
743
+ const input = {};
744
+ for (const key of keys)
745
+ input[key] = tenant;
746
+ return input;
747
+ }
748
+ async function checkFlips(host, facts, keys, tenantA, tenantB) {
749
+ const template = await insertRow(host, facts.name, undefined, undefined, tenantB);
750
+ const data = { ...template };
751
+ for (const key of keys)
752
+ delete data[key];
753
+ const clientA = openTenant(host, scopeFor(keys, tenantA));
754
+ try {
755
+ for (let index = 0;index < keys.length; index += 1) {
756
+ const values = scopeFor(keys, tenantA);
757
+ const flipped = keys[index];
758
+ if (flipped === undefined)
759
+ continue;
760
+ values[flipped] = tenantB;
761
+ const client = openTenant(host, values);
762
+ const stored = await tableHandle2(client, facts.name).insert(data);
763
+ if (!isRow2(stored))
764
+ continue;
765
+ const where = primaryWhere(facts, stored, keys);
766
+ await readTenant(facts, tableHandle2(clientA, facts.name), where);
767
+ await tableHandle2(client, facts.name).delete({ where }).catch(() => {
768
+ return;
769
+ });
770
+ }
771
+ } finally {
772
+ const where = primaryWhere(facts, template, keys);
773
+ await tableHandle2(openTenant(host, tenantB), facts.name).delete({ where }).catch(() => {
774
+ return;
775
+ });
776
+ forgetCached(host, facts.name, tenantB);
777
+ }
778
+ }
779
+ async function checkPath(host, facts, hops, tenantA, tenantB) {
780
+ const keys = tenantKeys(host);
781
+ const created = [];
782
+ const last = hops[hops.length - 1];
783
+ if (last === undefined)
784
+ return;
785
+ let parent = await insertRow(host, last.parent, undefined, undefined, tenantB);
786
+ created.push({
787
+ table: last.parent,
788
+ where: primaryWhere(requireFacts(host, last.parent), parent, keys)
789
+ });
790
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
791
+ const hop = hops[index];
792
+ if (hop === undefined)
793
+ continue;
794
+ const row = await insertRow(host, hop.child, { [hop.localField]: parent[hop.remoteField] }, undefined, tenantB);
795
+ parent = row;
796
+ created.push({
797
+ table: hop.child,
798
+ where: primaryWhere(requireFacts(host, hop.child), row, keys)
799
+ });
800
+ }
801
+ const where = primaryWhere(facts, parent, keys);
802
+ const client = openTenant(host, tenantA);
803
+ try {
804
+ await readTenant(facts, tableHandle2(client, facts.name), where);
805
+ const set = updateSet(host, facts, parent);
806
+ if (set !== undefined) {
807
+ const updated = asCount(await tableHandle2(client, facts.name).update({ where, set }));
808
+ if (updated > 0)
809
+ leak(facts.name, "update", { count: updated });
810
+ }
811
+ const removed = asCount(await tableHandle2(client, facts.name).delete({ where }));
812
+ if (removed > 0)
813
+ leak(facts.name, "delete", { count: removed });
814
+ } finally {
815
+ for (let index = created.length - 1;index >= 0; index -= 1) {
816
+ const item = created[index];
817
+ if (item === undefined)
818
+ continue;
819
+ await tableHandle2(openTenant(host, tenantB), item.table).delete({ where: item.where }).catch(() => {
820
+ return;
821
+ });
822
+ forgetCached(host, item.table, tenantB);
823
+ }
824
+ }
825
+ }
826
+ var SCAN_LIMIT = 100;
827
+ async function checkViews(host, tenantA, tenantB, skipped) {
828
+ const checked = [];
829
+ const keys = tenantKeys(host);
830
+ for (const view of readViews(host.schema)) {
831
+ if (view.globalReason !== undefined) {
832
+ skipped.push({ table: view.name, reason: view.globalReason });
833
+ continue;
834
+ }
835
+ if (!view.tenant)
836
+ continue;
837
+ await checkView(host, view, keys, tenantA, tenantB);
838
+ checked.push(view.name);
839
+ }
840
+ return checked;
841
+ }
842
+ async function checkView(host, view, keys, tenantA, tenantB) {
843
+ const sources = orderSources(host, sourceNames(host, view.query));
844
+ if (sources.length === 0) {
845
+ throw new OkmError2("invalid", `View ${view.name} names no table isolation can seed.`, {
846
+ fieldReason: "no-source",
847
+ fix: { summary: 'Point the view at a table in this schema, or mark it global("reason").' }
848
+ });
849
+ }
850
+ for (const name of sources) {
851
+ if (host.definitions.size > 0 && !host.definitions.has(name)) {
852
+ throw new OkmError2("invalid", `Factory ${name} is missing.`, {
853
+ fix: { summary: `Add ${name} to factories().` }
854
+ });
855
+ }
856
+ }
857
+ const before = new Map(host.cache);
858
+ const seeded = [];
859
+ try {
860
+ for (const tenant of [tenantA, tenantB]) {
861
+ for (const name of sources) {
862
+ const facts = requireFacts(host, name);
863
+ const key = keys[0];
864
+ const overrides = markerFor(view, facts, keys, tenant);
865
+ if (key !== undefined)
866
+ overrides[key] = tenant;
867
+ const row = await insertRow(host, name, overrides, undefined, tenant);
868
+ if (tenant === tenantB)
869
+ seeded.push(row);
870
+ }
871
+ }
872
+ const handle = viewHandle(openTenant(host, tenantA), view.name);
873
+ if (view.kind === "materializedView") {
874
+ if (handle.refresh === undefined) {
875
+ throw new OkmError2("invalid", `View ${view.name} has no refresh().`, {
876
+ fieldReason: "no-refresh",
877
+ fix: { summary: "A materialized view refreshes before the isolation read." }
878
+ });
879
+ }
880
+ await handle.refresh();
881
+ }
882
+ await readView(view, handle, keys, tenantB, seeded);
883
+ } finally {
884
+ await dropNewRows(host, before, keys);
885
+ }
886
+ }
887
+ function sourceNames(host, query) {
888
+ const names = [];
889
+ for (const facts of host.facts.values()) {
890
+ if (viewMentions(query, facts.sql) || viewMentions(query, facts.name))
891
+ names.push(facts.name);
892
+ }
893
+ return names;
894
+ }
895
+ function orderSources(host, names) {
896
+ const wanted = new Set(names);
897
+ const ordered = [];
898
+ const pending = [...names];
899
+ const guard = pending.length * pending.length + 1;
900
+ let spins = 0;
901
+ while (pending.length > 0 && spins < guard) {
902
+ spins += 1;
903
+ const name = pending.shift();
904
+ if (name === undefined)
905
+ break;
906
+ const facts = host.facts.get(name);
907
+ const waiting = facts?.foreignKeys.some((key) => {
908
+ let parent;
909
+ for (const item of host.facts.values()) {
910
+ if (item.sql === key.target)
911
+ parent = item.name;
912
+ }
913
+ return parent !== undefined && wanted.has(parent) && !ordered.includes(parent);
914
+ });
915
+ if (waiting)
916
+ pending.push(name);
917
+ else
918
+ ordered.push(name);
919
+ }
920
+ return ordered.length === names.length ? ordered : [...names];
921
+ }
922
+ function markerFor(view, facts, keys, tenant) {
923
+ const value = `isolation-${tenant}`;
924
+ const out = {};
925
+ for (const column of view.columns) {
926
+ if (column.guarded || keys.includes(column.field))
927
+ continue;
928
+ if (column.dataType.toLowerCase() !== "text")
929
+ continue;
930
+ const source = facts.columns.find((item) => item.field === column.field);
931
+ if (source === undefined || !source.writable || source.guarded)
932
+ continue;
933
+ if (keys.includes(source.field))
934
+ continue;
935
+ if (facts.foreignKeys.some((key) => key.columns.includes(source.sql)))
936
+ continue;
937
+ out[source.field] = value;
938
+ }
939
+ return out;
940
+ }
941
+ async function readView(view, handle, keys, tenantB, seeded) {
942
+ const exposed = view.columns.filter((column) => keys.includes(column.field));
943
+ const where = markerWhere(view, keys, seeded);
944
+ if (where === undefined && exposed.length === 0) {
945
+ throw new OkmError2("invalid", `View ${view.name} exposes no tenant key and no text column isolation can match.`, {
946
+ fieldReason: "no-marker",
947
+ fix: { summary: "Expose the tenant key, or a text column isolation can match." }
948
+ });
949
+ }
950
+ if (where !== undefined)
951
+ await readFiltered(view.name, handle, where);
952
+ if (exposed.length === 0)
953
+ return;
954
+ const rows = asRows(await handle.find({ limit: SCAN_LIMIT }));
955
+ const needle = tenantB.toLowerCase();
956
+ for (const row of rows) {
957
+ for (const column of exposed) {
958
+ const value = row[column.field];
959
+ if (typeof value === "string" && value.toLowerCase() === needle)
960
+ leak(view.name, "find", row);
961
+ }
962
+ }
963
+ }
964
+ function markerWhere(view, keys, rows) {
965
+ for (const column of view.columns) {
966
+ if (column.guarded || keys.includes(column.field))
967
+ continue;
968
+ if (column.dataType.toLowerCase() !== "text")
969
+ continue;
970
+ for (const row of rows) {
971
+ const value = row[column.field];
972
+ if (typeof value === "string")
973
+ return { [column.field]: value };
974
+ }
975
+ }
976
+ return;
977
+ }
978
+ async function readFiltered(name, handle, where) {
979
+ const query = handle.find({ where, limit: 1 });
980
+ const rows = asRows(await query);
981
+ if (rows.length > 0)
982
+ leak(name, "find", rows[0]);
983
+ const streamed = handle.find({ where, limit: 1 });
984
+ if (typeof streamed.stream === "function") {
985
+ try {
986
+ for await (const item of streamed.stream()) {
987
+ if (isRow2(item))
988
+ leak(name, "stream", item);
989
+ }
990
+ } catch (error) {
991
+ if (!(error instanceof OkmError2) || error.code !== "OKM1111")
992
+ throw error;
993
+ }
994
+ }
995
+ const one = await handle.one({ where });
996
+ if (isRow2(one))
997
+ leak(name, "one", one);
998
+ if (asCount(await handle.count({ where })) > 0)
999
+ leak(name, "count", { count: 1 });
1000
+ if (await handle.exists({ where }) === true)
1001
+ leak(name, "exists", { exists: true });
1002
+ }
1003
+ async function dropNewRows(host, before, keys) {
1004
+ const added = [];
1005
+ for (const [key, row] of host.cache) {
1006
+ if (!before.has(key))
1007
+ added.push({ key, row });
1008
+ }
1009
+ for (let index = added.length - 1;index >= 0; index -= 1) {
1010
+ const item = added[index];
1011
+ if (item === undefined)
1012
+ continue;
1013
+ const colon = item.key.indexOf(":");
1014
+ const scope = item.key.slice(0, colon);
1015
+ const table = item.key.slice(colon + 1);
1016
+ const facts = host.facts.get(table);
1017
+ const tenant = scope === "*" ? undefined : scope;
1018
+ if (facts !== undefined) {
1019
+ try {
1020
+ const where = primaryWhere(facts, item.row, keys);
1021
+ const client = tenant === undefined ? host.db : openTenant(host, tenant);
1022
+ await tableHandle2(client, table).delete({ where });
1023
+ } catch {}
1024
+ }
1025
+ forgetCached(host, table, tenant);
1026
+ }
1027
+ }
1028
+ function viewHandle(client, name) {
1029
+ const views = "views" in client ? client.views : undefined;
1030
+ if (typeof views !== "object" || views === null) {
1031
+ throw new OkmError2("invalid", "The client has no views.", {
1032
+ fix: { summary: "Declare the view on schema({ views }) and connect with that schema." }
1033
+ });
1034
+ }
1035
+ const handle = views[name];
1036
+ if (!isViewQuery(handle)) {
1037
+ throw new OkmError2("invalid", `View ${name} is not on this client.`, {
1038
+ fix: { summary: "A tenant view is read through for().views." }
1039
+ });
1040
+ }
1041
+ return handle;
1042
+ }
1043
+ function isViewQuery(value) {
1044
+ if (typeof value !== "object" || value === null)
1045
+ return false;
1046
+ const record = value;
1047
+ return typeof record.find === "function" && typeof record.one === "function";
1048
+ }
1049
+ function requireFacts(host, name) {
1050
+ const facts = host.facts.get(name);
1051
+ if (facts === undefined) {
1052
+ throw new OkmError2("invalid", `Table ${name} is not in the schema.`, {
1053
+ fix: { summary: "The path must name a table in this schema." }
1054
+ });
1055
+ }
1056
+ return facts;
1057
+ }
613
1058
  function openTenant(host, tenant) {
614
- const key = host.schema.tenancy?.key ?? "tenantId";
1059
+ const keys = tenantKeys(host);
1060
+ const input = typeof tenant === "string" ? scopeFor(keys, tenant) : tenant;
615
1061
  const db = host.db;
616
1062
  if (!("for" in db) || typeof db.for !== "function") {
617
1063
  throw new OkmError2("invalid", "This schema has no for().", {
618
1064
  fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
619
1065
  });
620
1066
  }
621
- const opened = db.for({ [key]: tenant });
1067
+ const opened = db.for(input);
622
1068
  if (typeof opened !== "object" || opened === null) {
623
1069
  throw new OkmError2("invalid", "for() did not return a client.", {
624
1070
  fix: { summary: "The tenant key must be a uuid." }