okmodel 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +29 -14
  2. package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
  3. package/dist/adapters/pg/postgres-extra.d.ts +22 -0
  4. package/dist/contracts/catalog/document.d.ts +19 -5
  5. package/dist/contracts/catalog/object.d.ts +2 -1
  6. package/dist/contracts/catalog/policy.d.ts +48 -0
  7. package/dist/contracts/catalog/types.d.ts +25 -1
  8. package/dist/contracts/catalog/view.d.ts +2 -0
  9. package/dist/contracts/internal.js +1 -1
  10. package/dist/contracts/location.d.ts +10 -0
  11. package/dist/dialects/pg/column.d.ts +13 -2
  12. package/dist/dialects/pg/ddl.d.ts +37 -1
  13. package/dist/dialects/pg/emit.d.ts +19 -1
  14. package/dist/dialects/pg/errors.d.ts +32 -1
  15. package/dist/dialects/pg/ext/citext.js +1 -1
  16. package/dist/dialects/pg/ext/index.js +1 -1
  17. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  18. package/dist/dialects/pg/filter-parse.d.ts +5 -4
  19. package/dist/dialects/pg/fn/index.js +2 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +23 -17
  22. package/dist/dialects/pg/introspect.d.ts +7 -0
  23. package/dist/dialects/pg/model.d.ts +7 -2
  24. package/dist/dialects/pg/owner.d.ts +42 -0
  25. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  26. package/dist/dialects/pg/role/check.d.ts +3 -0
  27. package/dist/dialects/pg/role/index.d.ts +71 -6
  28. package/dist/dialects/pg/role/sql.d.ts +11 -0
  29. package/dist/dialects/pg/schema.d.ts +2 -0
  30. package/dist/dialects/pg/table.d.ts +25 -3
  31. package/dist/dialects/pg/tenancy.d.ts +45 -3
  32. package/dist/dialects/pg/view/client.d.ts +19 -1
  33. package/dist/dialects/pg/view/index.d.ts +4 -0
  34. package/dist/dialects/pg/view/index.js +21 -178
  35. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  36. package/dist/runtime/batch.d.ts +1 -1
  37. package/dist/runtime/conflict.d.ts +2 -2
  38. package/dist/runtime/drift.d.ts +9 -0
  39. package/dist/runtime/inflight.d.ts +14 -0
  40. package/dist/runtime/pg/bun.js +4 -3
  41. package/dist/runtime/pg/pg.js +3 -3
  42. package/dist/runtime/pg/pglite.js +4 -3
  43. package/dist/runtime/pg/postgresjs.js +3 -3
  44. package/dist/runtime/plan.d.ts +18 -10
  45. package/dist/runtime/predicate.d.ts +19 -0
  46. package/dist/runtime/safety/index.js +1 -1
  47. package/dist/runtime/tenancy/composite.d.ts +26 -0
  48. package/dist/runtime/tenancy/context.d.ts +16 -0
  49. package/dist/runtime/tenancy/index.d.ts +9 -5
  50. package/dist/runtime/tenancy/index.js +1508 -37
  51. package/dist/runtime/tenancy/path.d.ts +87 -0
  52. package/dist/runtime/tenancy/rls.d.ts +67 -0
  53. package/dist/runtime/tenancy/via.d.ts +22 -0
  54. package/dist/runtime/traits/index.js +17 -11
  55. package/dist/runtime/types.d.ts +13 -1
  56. package/dist/runtime/validate/index.js +4 -4
  57. package/dist/runtime/write.d.ts +1 -1
  58. package/dist/shared/09p049xa.js +702 -0
  59. package/dist/shared/{6qd27mx0.js → 1f0xa3cb.js} +5 -5
  60. package/dist/shared/1nw1f3vs.js +23 -0
  61. package/dist/shared/{96qcj21c.js → 1z04fa85.js} +19 -9
  62. package/dist/shared/{qm7cdtvr.js → 2cg3wnxc.js} +296 -60
  63. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  64. package/dist/shared/{gbtcc2d7.js → 2z7wv5tj.js} +10 -35
  65. package/dist/shared/{k8v5v5wj.js → 32cnm7kn.js} +6 -1
  66. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  67. package/dist/shared/{2pqx26h8.js → 3fqwmr09.js} +31 -22
  68. package/dist/shared/{zqrnhwsq.js → 3qnhc7w5.js} +16 -9
  69. package/dist/shared/{r2sp8hrc.js → 5d8t84hg.js} +12 -3
  70. package/dist/shared/{87jvvhnt.js → 5j36z8dn.js} +16 -5
  71. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  72. package/dist/shared/8dqhth8c.js +12 -0
  73. package/dist/shared/8etn7d8w.js +62 -0
  74. package/dist/shared/{v36c5g6p.js → 8x72s86k.js} +400 -13
  75. package/dist/shared/{nxdp2ze3.js → 929h19xp.js} +107 -5
  76. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  77. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  78. package/dist/shared/{efmy2gcg.js → ag4c7516.js} +1 -1
  79. package/dist/shared/b3awxn7h.js +45 -0
  80. package/dist/shared/b4v57v3x.js +9 -0
  81. package/dist/shared/bdk4sty6.js +58 -0
  82. package/dist/shared/{51mvgj8w.js → c58secgj.js} +100 -7
  83. package/dist/shared/d5azex6s.js +6 -0
  84. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  85. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  86. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  87. package/dist/shared/{sycnx8gt.js → f0f1n281.js} +18 -8
  88. package/dist/shared/{f5cw5q5r.js → g90m9b0g.js} +2 -1
  89. package/dist/shared/{pf0d8f77.js → gwn6v667.js} +1 -1
  90. package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
  91. package/dist/shared/{5enne4ma.js → hazqqecq.js} +17 -93
  92. package/dist/shared/{jwtxdj06.js → j3rkk835.js} +32 -9
  93. package/dist/shared/{hf22e0wt.js → j60w0dvr.js} +19 -18
  94. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  95. package/dist/shared/k68eedmy.js +85 -0
  96. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  97. package/dist/shared/{0t22wwh1.js → mb2ycs1p.js} +4 -2
  98. package/dist/shared/{cr74e96e.js → n076ydsc.js} +1 -1
  99. package/dist/shared/{cctw17hj.js → nx9hh281.js} +31 -28
  100. package/dist/shared/{gde73es6.js → p248q79y.js} +56 -55
  101. package/dist/shared/{janx09m5.js → p6bjzq9q.js} +11 -7
  102. package/dist/shared/{6rk2gen5.js → pjr6xrm5.js} +2 -2
  103. package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
  104. package/dist/shared/{caw8b9d8.js → q3j1tq7a.js} +35 -22
  105. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  106. package/dist/shared/{tm5ab8we.js → r11n8grd.js} +2 -2
  107. package/dist/shared/r1fdd0f6.js +19 -0
  108. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  109. package/dist/shared/rhs3xe4k.js +7 -0
  110. package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
  111. package/dist/shared/{aj2qxyhj.js → sbdme3vy.js} +14 -2
  112. package/dist/shared/{qv2ydck9.js → t3g0k88x.js} +638 -67
  113. package/dist/shared/{ga3sfy93.js → ttmg09vd.js} +535 -200
  114. package/dist/shared/{kfjp0gck.js → venqrjna.js} +8 -4
  115. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  116. package/dist/shared/{32zqc1dw.js → vmfkhszw.js} +31 -7
  117. package/dist/shared/{krxx77g0.js → wkme6tep.js} +10 -8
  118. package/dist/shared/{ps7a6h14.js → wt5rzv1h.js} +12 -8
  119. package/dist/shared/wxr8cwns.js +6 -0
  120. package/dist/shared/{6mnp99hx.js → wynzeggs.js} +16 -12
  121. package/dist/shared/x3jc53kg.js +220 -0
  122. package/dist/shared/{sdc7r2ww.js → xq0mvhts.js} +8 -8
  123. package/dist/shared/xqve9598.js +53 -0
  124. package/dist/shared/{47x8x9ze.js → y2t3jktd.js} +2 -2
  125. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  126. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  127. package/dist/shared/{nkaxvt9q.js → ys0xzyaj.js} +1 -1
  128. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  129. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  130. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  131. package/dist/shared/{r8x5qhd7.js → zm0mafk4.js} +10 -4
  132. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  133. package/dist/tooling/cli.js +41 -15
  134. package/dist/tooling/migrate/apply.d.ts +14 -0
  135. package/dist/tooling/migrate/check.d.ts +3 -4
  136. package/dist/tooling/migrate/classify.d.ts +4 -0
  137. package/dist/tooling/migrate/config.d.ts +3 -2
  138. package/dist/tooling/migrate/files.d.ts +11 -1
  139. package/dist/tooling/migrate/index.js +14 -14
  140. package/dist/tooling/migrate/lint.d.ts +1 -1
  141. package/dist/tooling/migrate/lost.d.ts +28 -0
  142. package/dist/tooling/migrate/paths.d.ts +14 -0
  143. package/dist/tooling/migrate/plan.d.ts +6 -3
  144. package/dist/tooling/migrate/runner.d.ts +25 -0
  145. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  146. package/dist/tooling/migrate/sql-lex.d.ts +20 -3
  147. package/dist/tooling/migrate/text.d.ts +42 -0
  148. package/dist/tooling/node-version.d.ts +21 -0
  149. package/dist/tooling/testing/facts.d.ts +33 -5
  150. package/dist/tooling/testing/index.d.ts +1 -1
  151. package/dist/tooling/testing/index.js +3 -3
  152. package/dist/tooling/testing/isolation.d.ts +11 -5
  153. package/package.json +5 -2
  154. package/dist/shared/7zwwyh0v.js +0 -448
  155. package/dist/shared/q58y17hy.js +0 -21
@@ -1,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 "./hf22e0wt.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)) {
@@ -472,13 +511,17 @@ function refOf(value) {
472
511
  const table = value[REF];
473
512
  return typeof table === "string" ? table : undefined;
474
513
  }
475
- function openScope(db, key, tenant) {
514
+ function openScope(db, tenancy, tenant) {
476
515
  if (!("for" in db) || typeof db.for !== "function") {
477
516
  throw new OkmError2("invalid", "This schema has no for().", {
478
517
  fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
479
518
  });
480
519
  }
481
- 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);
482
525
  if (typeof opened !== "object" || opened === null) {
483
526
  throw new OkmError2("invalid", "for() did not return a client.", {
484
527
  fix: { summary: "The tenant key must be a uuid." }
@@ -509,6 +552,15 @@ async function isolation(host) {
509
552
  const saved = host.cache;
510
553
  host.cache = new Map;
511
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
+ }
512
564
  const checked = [];
513
565
  const skipped = [];
514
566
  const tenantA = crypto.randomUUID();
@@ -520,10 +572,19 @@ async function isolation(host) {
520
572
  }
521
573
  if (!facts.tenant)
522
574
  continue;
523
- await checkTable(host, facts, tenantA, tenantB);
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
+ }
524
584
  checked.push(facts.name);
525
585
  }
526
- return { checked, skipped };
586
+ const views = await checkViews(host, tenantA, tenantB, skipped);
587
+ return { checked, skipped, views };
527
588
  } finally {
528
589
  host.cache = saved;
529
590
  }
@@ -532,13 +593,14 @@ async function checkTable(host, facts, tenantA, tenantB) {
532
593
  const key = host.schema.tenancy?.key;
533
594
  if (key === undefined)
534
595
  return;
596
+ const keys = tenantKeys(host);
535
597
  if (host.definitions.size > 0 && !host.definitions.has(facts.name)) {
536
598
  throw new OkmError2("invalid", `Factory ${facts.name} is missing.`, {
537
599
  fix: { summary: `Add ${facts.name} to factories().` }
538
600
  });
539
601
  }
540
602
  const row = await insertRow(host, facts.name, { [key]: tenantB }, undefined, tenantB);
541
- const where = primaryWhere(facts, row, key);
603
+ const where = primaryWhere(facts, row, keys);
542
604
  const client = openTenant(host, tenantA);
543
605
  const handle = tableHandle2(client, facts.name);
544
606
  try {
@@ -649,10 +711,10 @@ function valueFrom(host, facts, field, row) {
649
711
  return host.rng.uuid();
650
712
  }
651
713
  }
652
- function primaryWhere(facts, row, tenantKey) {
714
+ function primaryWhere(facts, row, tenantKeys) {
653
715
  const where = {};
654
716
  for (const field of facts.primary) {
655
- if (field === tenantKey)
717
+ if (tenantKeys.includes(field))
656
718
  continue;
657
719
  where[field] = row[field];
658
720
  }
@@ -669,15 +731,340 @@ ${JSON.stringify(row)}`, {
669
731
  fix: { summary: "A query under tenant A returned a row inserted for tenant B." }
670
732
  });
671
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
+ }
672
1058
  function openTenant(host, tenant) {
673
- const key = host.schema.tenancy?.key ?? "tenantId";
1059
+ const keys = tenantKeys(host);
1060
+ const input = typeof tenant === "string" ? scopeFor(keys, tenant) : tenant;
674
1061
  const db = host.db;
675
1062
  if (!("for" in db) || typeof db.for !== "function") {
676
1063
  throw new OkmError2("invalid", "This schema has no for().", {
677
1064
  fix: { summary: "Pass columnTenancy() to schema({ tenancy })." }
678
1065
  });
679
1066
  }
680
- const opened = db.for({ [key]: tenant });
1067
+ const opened = db.for(input);
681
1068
  if (typeof opened !== "object" || opened === null) {
682
1069
  throw new OkmError2("invalid", "for() did not return a client.", {
683
1070
  fix: { summary: "The tenant key must be a uuid." }
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  isConnectionSqlstate,
3
3
  isConnectionErrno
4
- } from "./pf0d8f77.js";
4
+ } from "./gwn6v667.js";
5
5
  import {
6
6
  createClient
7
- } from "./hf22e0wt.js";
7
+ } from "./j60w0dvr.js";
8
8
  import {
9
9
  OkmError2
10
10
  } from "./52szzakm.js";
@@ -331,7 +331,8 @@ async function openEndpoint(open, url, max, defaults, name, role, weight, probeM
331
331
  timer: undefined,
332
332
  current: 0,
333
333
  inflight: 0,
334
- latencyMs: null
334
+ latencyMs: null,
335
+ generation: 0
335
336
  };
336
337
  }
337
338
  async function detectPosition(endpoint) {
@@ -345,8 +346,11 @@ async function detectPosition(endpoint) {
345
346
  async function sample(handle, endpoint) {
346
347
  try {
347
348
  const sampled = await readPosition(handle, endpoint);
349
+ const reopened = endpoint.circuit === "open";
348
350
  endpoint.failures = 0;
349
351
  endpoint.circuit = "closed";
352
+ if (reopened)
353
+ endpoint.generation += 1;
350
354
  applySample(endpoint, sampled);
351
355
  } catch {
352
356
  endpoint.failures += 1;
@@ -515,6 +519,7 @@ function routePool(handle) {
515
519
  replicaRefused();
516
520
  const reason = options?.route === "primary" ? "constraint:primary" : "primary-required";
517
521
  tell(handle, { op: "write", endpoint: primary.name, reason });
522
+ await callGuard(primary);
518
523
  const result = await primary.pool.batch(statements, strip(options));
519
524
  await noteWrite(handle, primary.pool);
520
525
  return result;
@@ -555,6 +560,14 @@ function routePool(handle) {
555
560
  endpoint.pool.cancel?.();
556
561
  };
557
562
  }
563
+ const routed = pool;
564
+ routed.okmReadOnlyUnit = (text, params, prelude, options) => readOnlyUnit(handle, text, params, prelude, options);
565
+ routed.okmReadOnlyStream = (text, params, prelude, options) => readOnlyStream(handle, text, params, prelude, options);
566
+ routed.okmEndpoints = () => handle.endpoints.map((endpoint) => ({
567
+ name: endpoint.name,
568
+ generation: endpoint.generation,
569
+ pool: endpoint.pool
570
+ }));
558
571
  return pool;
559
572
  }
560
573
  function derive(schema, handle, router, shared, route) {
@@ -601,8 +614,82 @@ function force(handle, router, route) {
601
614
  }
602
615
  if (router.cancel !== undefined)
603
616
  pool.cancel = () => router.cancel();
617
+ const unit = router.okmReadOnlyUnit;
618
+ const stream = router.okmReadOnlyStream;
619
+ const forced = pool;
620
+ if (unit !== undefined) {
621
+ forced.okmReadOnlyUnit = (text, params, prelude, options) => unit(text, params, prelude, { ...options, route });
622
+ }
623
+ if (stream !== undefined) {
624
+ forced.okmReadOnlyStream = (text, params, prelude, options) => stream(text, params, prelude, { ...options, route });
625
+ }
604
626
  return pool;
605
627
  }
628
+ async function readOnlyUnit(handle, text, params, prelude, options) {
629
+ if (handle.closed)
630
+ closed();
631
+ const choice = await choose(handle, text, options?.route);
632
+ return attempt(handle, choice, async (endpoint) => {
633
+ if (endpoint.pool.reserve === undefined)
634
+ replicaReserve();
635
+ const conn = await endpoint.pool.reserve();
636
+ try {
637
+ await conn.execute("begin read only");
638
+ try {
639
+ await conn.execute(prelude.text, prelude.params);
640
+ const result = await conn.execute(text, params, strip(options));
641
+ await conn.execute("commit");
642
+ return result;
643
+ } catch (error) {
644
+ await conn.execute("rollback").catch(() => {
645
+ return;
646
+ });
647
+ throw error;
648
+ }
649
+ } finally {
650
+ await conn.release();
651
+ }
652
+ });
653
+ }
654
+ function readOnlyStream(handle, text, params, prelude, options) {
655
+ return async function* () {
656
+ if (handle.closed)
657
+ closed();
658
+ const choice = await choose(handle, text, options?.route);
659
+ tell(handle, { op: choice.op, endpoint: choice.endpoint.name, reason: choice.reason });
660
+ await callGuard(choice.endpoint);
661
+ if (choice.endpoint.pool.reserve === undefined)
662
+ replicaReserve();
663
+ const conn = await choice.endpoint.pool.reserve();
664
+ try {
665
+ await conn.execute("begin read only");
666
+ try {
667
+ await conn.execute(prelude.text, prelude.params);
668
+ await conn.execute(`declare okm_rls no scroll cursor for ${text}`, params);
669
+ for (;; ) {
670
+ const chunk = await conn.execute("fetch 64 from okm_rls");
671
+ if (chunk.rows.length === 0)
672
+ break;
673
+ yield chunk.rows;
674
+ }
675
+ await conn.execute("close okm_rls");
676
+ await conn.execute("commit");
677
+ } catch (error) {
678
+ await conn.execute("rollback").catch(() => {
679
+ return;
680
+ });
681
+ throw error;
682
+ }
683
+ } finally {
684
+ await conn.release();
685
+ }
686
+ }();
687
+ }
688
+ function replicaReserve() {
689
+ throw new OkmError2("OKM1111", "Row-level security needs a driver that can reserve a connection.", {
690
+ fix: { summary: "Use a Postgres driver that supports interactive transactions." }
691
+ });
692
+ }
606
693
  function classOf(text) {
607
694
  const head = text.trimStart().toLowerCase().replace(/\s+/g, " ");
608
695
  if (/^(begin|start transaction|commit|rollback|savepoint|release|set|reset)\b/.test(head)) {
@@ -612,7 +699,7 @@ function classOf(text) {
612
699
  return "lock";
613
700
  if (/\sfor (no key update|key share|update|share)\b/.test(head))
614
701
  return "lock";
615
- if (/^(insert|update|delete|with|create|alter|drop|truncate|grant|revoke|comment|vacuum|analyze|reindex|copy)\b/.test(head)) {
702
+ if (/^(insert|update|delete|refresh|with|create|alter|drop|truncate|grant|revoke|comment|vacuum|analyze|reindex|copy)\b/.test(head)) {
616
703
  return "write";
617
704
  }
618
705
  return "read";
@@ -626,7 +713,7 @@ async function choose(handle, text, route) {
626
713
  if (primary === undefined)
627
714
  throw new OkmError2("OKM1120", "A topology needs one primary URL.");
628
715
  if (text.trimStart().toLowerCase().startsWith("select current_setting")) {
629
- return { endpoint: primary, op: "read", reason: "primary-required" };
716
+ return { endpoint: primary, op: "read", reason: "primary-required", skipGuard: true };
630
717
  }
631
718
  if (route === "replica" && op !== "read")
632
719
  replicaRefused();
@@ -673,17 +760,32 @@ async function* readStream(handle, text, route, params, fallback) {
673
760
  async function attempt(handle, choice, run) {
674
761
  tell(handle, { op: choice.op, endpoint: choice.endpoint.name, reason: choice.reason });
675
762
  try {
763
+ if (choice.skipGuard !== true)
764
+ await callGuard(choice.endpoint);
676
765
  return await tracked(choice.endpoint, choice.op === "read", () => run(choice.endpoint));
677
766
  } catch (error) {
678
767
  if (choice.op !== "read" || !isUnreachable(error))
679
768
  throw error;
769
+ choice.endpoint.generation += 1;
680
770
  const next = await retryOf(handle, choice);
681
771
  if (next === undefined)
682
772
  throw error;
683
773
  tell(handle, { op: "read", endpoint: next.endpoint.name, reason: next.reason });
774
+ if (choice.skipGuard !== true)
775
+ await callGuard(next.endpoint);
684
776
  return tracked(next.endpoint, true, () => run(next.endpoint));
685
777
  }
686
778
  }
779
+ function callGuard(endpoint) {
780
+ const guard = endpoint.pool.okmGuard;
781
+ if (guard === undefined)
782
+ return Promise.resolve();
783
+ return guard({
784
+ name: endpoint.name,
785
+ generation: endpoint.generation,
786
+ pool: endpoint.pool
787
+ });
788
+ }
687
789
  var CHAIN_DEPTH = 8;
688
790
  function isUnreachable(error) {
689
791
  const seen = new Set;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  required
3
- } from "./c9z5g0fm.js";
3
+ } from "./ywr8hmey.js";
4
4
  import {
5
5
  rejected
6
6
  } from "./tmbdywx0.js";