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.
- package/README.md +29 -14
- package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
- package/dist/adapters/pg/postgres-extra.d.ts +22 -0
- package/dist/contracts/catalog/document.d.ts +19 -5
- package/dist/contracts/catalog/object.d.ts +2 -1
- package/dist/contracts/catalog/policy.d.ts +48 -0
- package/dist/contracts/catalog/types.d.ts +25 -1
- package/dist/contracts/catalog/view.d.ts +2 -0
- package/dist/contracts/internal.js +1 -1
- package/dist/contracts/location.d.ts +10 -0
- package/dist/dialects/pg/column.d.ts +13 -2
- package/dist/dialects/pg/ddl.d.ts +37 -1
- package/dist/dialects/pg/emit.d.ts +19 -1
- package/dist/dialects/pg/errors.d.ts +32 -1
- package/dist/dialects/pg/ext/citext.js +1 -1
- package/dist/dialects/pg/ext/index.js +1 -1
- package/dist/dialects/pg/ext/pg-trgm.js +1 -1
- package/dist/dialects/pg/filter-parse.d.ts +5 -4
- package/dist/dialects/pg/fn/index.js +2 -2
- package/dist/dialects/pg/index.d.ts +2 -1
- package/dist/dialects/pg/index.js +23 -17
- package/dist/dialects/pg/introspect.d.ts +7 -0
- package/dist/dialects/pg/model.d.ts +7 -2
- package/dist/dialects/pg/owner.d.ts +42 -0
- package/dist/dialects/pg/path-endpoint.d.ts +20 -0
- package/dist/dialects/pg/role/check.d.ts +3 -0
- package/dist/dialects/pg/role/index.d.ts +71 -6
- package/dist/dialects/pg/role/sql.d.ts +11 -0
- package/dist/dialects/pg/schema.d.ts +2 -0
- package/dist/dialects/pg/table.d.ts +25 -3
- package/dist/dialects/pg/tenancy.d.ts +45 -3
- package/dist/dialects/pg/view/client.d.ts +19 -1
- package/dist/dialects/pg/view/index.d.ts +4 -0
- package/dist/dialects/pg/view/index.js +21 -178
- package/dist/dialects/pg/view/refresh.d.ts +24 -0
- package/dist/runtime/batch.d.ts +1 -1
- package/dist/runtime/conflict.d.ts +2 -2
- package/dist/runtime/drift.d.ts +9 -0
- package/dist/runtime/inflight.d.ts +14 -0
- package/dist/runtime/pg/bun.js +4 -3
- package/dist/runtime/pg/pg.js +3 -3
- package/dist/runtime/pg/pglite.js +4 -3
- package/dist/runtime/pg/postgresjs.js +3 -3
- package/dist/runtime/plan.d.ts +18 -10
- package/dist/runtime/predicate.d.ts +19 -0
- package/dist/runtime/safety/index.js +1 -1
- package/dist/runtime/tenancy/composite.d.ts +26 -0
- package/dist/runtime/tenancy/context.d.ts +16 -0
- package/dist/runtime/tenancy/index.d.ts +9 -5
- package/dist/runtime/tenancy/index.js +1508 -37
- package/dist/runtime/tenancy/path.d.ts +87 -0
- package/dist/runtime/tenancy/rls.d.ts +67 -0
- package/dist/runtime/tenancy/via.d.ts +22 -0
- package/dist/runtime/traits/index.js +17 -11
- package/dist/runtime/types.d.ts +13 -1
- package/dist/runtime/validate/index.js +4 -4
- package/dist/runtime/write.d.ts +1 -1
- package/dist/shared/09p049xa.js +702 -0
- package/dist/shared/{6qd27mx0.js → 1f0xa3cb.js} +5 -5
- package/dist/shared/1nw1f3vs.js +23 -0
- package/dist/shared/{96qcj21c.js → 1z04fa85.js} +19 -9
- package/dist/shared/{qm7cdtvr.js → 2cg3wnxc.js} +296 -60
- package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
- package/dist/shared/{gbtcc2d7.js → 2z7wv5tj.js} +10 -35
- package/dist/shared/{k8v5v5wj.js → 32cnm7kn.js} +6 -1
- package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
- package/dist/shared/{2pqx26h8.js → 3fqwmr09.js} +31 -22
- package/dist/shared/{zqrnhwsq.js → 3qnhc7w5.js} +16 -9
- package/dist/shared/{r2sp8hrc.js → 5d8t84hg.js} +12 -3
- package/dist/shared/{87jvvhnt.js → 5j36z8dn.js} +16 -5
- package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
- package/dist/shared/8dqhth8c.js +12 -0
- package/dist/shared/8etn7d8w.js +62 -0
- package/dist/shared/{v36c5g6p.js → 8x72s86k.js} +400 -13
- package/dist/shared/{nxdp2ze3.js → 929h19xp.js} +107 -5
- package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
- package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
- package/dist/shared/{efmy2gcg.js → ag4c7516.js} +1 -1
- package/dist/shared/b3awxn7h.js +45 -0
- package/dist/shared/b4v57v3x.js +9 -0
- package/dist/shared/bdk4sty6.js +58 -0
- package/dist/shared/{51mvgj8w.js → c58secgj.js} +100 -7
- package/dist/shared/d5azex6s.js +6 -0
- package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
- package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
- package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
- package/dist/shared/{sycnx8gt.js → f0f1n281.js} +18 -8
- package/dist/shared/{f5cw5q5r.js → g90m9b0g.js} +2 -1
- package/dist/shared/{pf0d8f77.js → gwn6v667.js} +1 -1
- package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
- package/dist/shared/{5enne4ma.js → hazqqecq.js} +17 -93
- package/dist/shared/{jwtxdj06.js → j3rkk835.js} +32 -9
- package/dist/shared/{hf22e0wt.js → j60w0dvr.js} +19 -18
- package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
- package/dist/shared/k68eedmy.js +85 -0
- package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
- package/dist/shared/{0t22wwh1.js → mb2ycs1p.js} +4 -2
- package/dist/shared/{cr74e96e.js → n076ydsc.js} +1 -1
- package/dist/shared/{cctw17hj.js → nx9hh281.js} +31 -28
- package/dist/shared/{gde73es6.js → p248q79y.js} +56 -55
- package/dist/shared/{janx09m5.js → p6bjzq9q.js} +11 -7
- package/dist/shared/{6rk2gen5.js → pjr6xrm5.js} +2 -2
- package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
- package/dist/shared/{caw8b9d8.js → q3j1tq7a.js} +35 -22
- package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
- package/dist/shared/{tm5ab8we.js → r11n8grd.js} +2 -2
- package/dist/shared/r1fdd0f6.js +19 -0
- package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
- package/dist/shared/rhs3xe4k.js +7 -0
- package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
- package/dist/shared/{aj2qxyhj.js → sbdme3vy.js} +14 -2
- package/dist/shared/{qv2ydck9.js → t3g0k88x.js} +638 -67
- package/dist/shared/{ga3sfy93.js → ttmg09vd.js} +535 -200
- package/dist/shared/{kfjp0gck.js → venqrjna.js} +8 -4
- package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
- package/dist/shared/{32zqc1dw.js → vmfkhszw.js} +31 -7
- package/dist/shared/{krxx77g0.js → wkme6tep.js} +10 -8
- package/dist/shared/{ps7a6h14.js → wt5rzv1h.js} +12 -8
- package/dist/shared/wxr8cwns.js +6 -0
- package/dist/shared/{6mnp99hx.js → wynzeggs.js} +16 -12
- package/dist/shared/x3jc53kg.js +220 -0
- package/dist/shared/{sdc7r2ww.js → xq0mvhts.js} +8 -8
- package/dist/shared/xqve9598.js +53 -0
- package/dist/shared/{47x8x9ze.js → y2t3jktd.js} +2 -2
- package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
- package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
- package/dist/shared/{nkaxvt9q.js → ys0xzyaj.js} +1 -1
- package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
- package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
- package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
- package/dist/shared/{r8x5qhd7.js → zm0mafk4.js} +10 -4
- package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
- package/dist/tooling/cli.js +41 -15
- package/dist/tooling/migrate/apply.d.ts +14 -0
- package/dist/tooling/migrate/check.d.ts +3 -4
- package/dist/tooling/migrate/classify.d.ts +4 -0
- package/dist/tooling/migrate/config.d.ts +3 -2
- package/dist/tooling/migrate/files.d.ts +11 -1
- package/dist/tooling/migrate/index.js +14 -14
- package/dist/tooling/migrate/lint.d.ts +1 -1
- package/dist/tooling/migrate/lost.d.ts +28 -0
- package/dist/tooling/migrate/paths.d.ts +14 -0
- package/dist/tooling/migrate/plan.d.ts +6 -3
- package/dist/tooling/migrate/runner.d.ts +25 -0
- package/dist/tooling/migrate/sandbox.d.ts +57 -0
- package/dist/tooling/migrate/sql-lex.d.ts +20 -3
- package/dist/tooling/migrate/text.d.ts +42 -0
- package/dist/tooling/node-version.d.ts +21 -0
- package/dist/tooling/testing/facts.d.ts +33 -5
- package/dist/tooling/testing/index.d.ts +1 -1
- package/dist/tooling/testing/index.js +3 -3
- package/dist/tooling/testing/isolation.d.ts +11 -5
- package/package.json +5 -2
- package/dist/shared/7zwwyh0v.js +0 -448
- package/dist/shared/q58y17hy.js +0 -21
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
renderCatalog
|
|
3
|
-
} from "./
|
|
3
|
+
} from "./9s0ypp87.js";
|
|
4
|
+
import {
|
|
5
|
+
viewGlobalReason,
|
|
6
|
+
viewMentions
|
|
7
|
+
} from "./x3jc53kg.js";
|
|
4
8
|
import {
|
|
5
9
|
createClient
|
|
6
|
-
} from "./
|
|
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) =>
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
714
|
+
function primaryWhere(facts, row, tenantKeys) {
|
|
653
715
|
const where = {};
|
|
654
716
|
for (const field of facts.primary) {
|
|
655
|
-
if (field
|
|
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
|
|
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(
|
|
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 "./
|
|
4
|
+
} from "./gwn6v667.js";
|
|
5
5
|
import {
|
|
6
6
|
createClient
|
|
7
|
-
} from "./
|
|
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;
|