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.
- package/README.md +36 -18
- 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 +10 -3
- package/dist/dialects/pg/fn/index.js +10 -2
- package/dist/dialects/pg/index.d.ts +2 -1
- package/dist/dialects/pg/index.js +29 -17
- package/dist/dialects/pg/introspect.d.ts +7 -0
- package/dist/dialects/pg/model.d.ts +7 -2
- package/dist/dialects/pg/ops/or.d.ts +1 -1
- 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/temporal.d.ts +5 -76
- 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/client.d.ts +6 -1
- package/dist/runtime/conflict.d.ts +2 -1
- package/dist/runtime/drift.d.ts +9 -0
- package/dist/runtime/inflight.d.ts +14 -0
- package/dist/runtime/pg/bun.js +6 -5
- package/dist/runtime/pg/pg.js +5 -5
- package/dist/runtime/pg/pglite.js +6 -5
- package/dist/runtime/pg/postgresjs.js +5 -5
- package/dist/runtime/plan.d.ts +22 -1
- 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/{ykv7wh5b.js → 1f0xa3cb.js} +5 -5
- package/dist/shared/1nw1f3vs.js +23 -0
- package/dist/shared/{6p5k20sm.js → 1z04fa85.js} +21 -11
- package/dist/shared/{p7pfx624.js → 2cg3wnxc.js} +314 -65
- package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
- package/dist/shared/{qgvt8nx1.js → 2z7wv5tj.js} +21 -5
- package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
- package/dist/shared/{sf8m6tfy.js → 32cnm7kn.js} +8 -3
- package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
- package/dist/shared/{jdgqqx31.js → 3fqwmr09.js} +38 -14
- package/dist/shared/{922qbgnw.js → 3qnhc7w5.js} +16 -9
- package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
- package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
- package/dist/shared/{k7cc9nzk.js → 5d8t84hg.js} +15 -3
- package/dist/shared/{fa9vt245.js → 5j36z8dn.js} +17 -6
- 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/{g516zdsb.js → 8x72s86k.js} +476 -30
- package/dist/shared/{nkc9sfqb.js → 929h19xp.js} +145 -7
- package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
- package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
- package/dist/shared/{b066g80h.js → ag4c7516.js} +2 -2
- package/dist/shared/b3awxn7h.js +45 -0
- package/dist/shared/b4v57v3x.js +9 -0
- package/dist/shared/bdk4sty6.js +58 -0
- package/dist/shared/{s6gqfn5j.js → c58secgj.js} +125 -8
- package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
- 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/{wxn4b7pp.js → f0f1n281.js} +59 -12
- package/dist/shared/{gdh9mrtp.js → g90m9b0g.js} +3 -2
- package/dist/shared/{tbszx8mz.js → gwn6v667.js} +2 -2
- package/dist/shared/{c8aghh05.js → h15jx9bc.js} +8 -2
- package/dist/shared/{3p4b1fg0.js → hazqqecq.js} +17 -93
- package/dist/shared/{4ef7mmqf.js → j3rkk835.js} +34 -11
- package/dist/shared/{bt9sfq0z.js → j60w0dvr.js} +56 -21
- 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/{ckjbyrt9.js → mb2ycs1p.js} +7 -9
- package/dist/shared/{tq9afp8d.js → n076ydsc.js} +1 -1
- package/dist/shared/{xhh1srw3.js → nx9hh281.js} +50 -32
- package/dist/shared/{kdkxh3kb.js → p248q79y.js} +58 -59
- package/dist/shared/{990a291j.js → p6bjzq9q.js} +14 -9
- package/dist/shared/{h4nqvjkp.js → pjr6xrm5.js} +2 -2
- package/dist/shared/pz6rj8ay.js +212 -0
- package/dist/shared/{5vdm4sgp.js → q3j1tq7a.js} +36 -23
- package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
- package/dist/shared/{dxjp14rk.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/{jsd07prz.js → rpycarcr.js} +6 -6
- package/dist/shared/{xsq7v6wp.js → sbdme3vy.js} +14 -2
- package/dist/shared/{xdpj5kkm.js → t3g0k88x.js} +775 -264
- package/dist/shared/{ayz64vzj.js → ttmg09vd.js} +708 -19
- package/dist/shared/{r6pj4fq2.js → venqrjna.js} +8 -4
- package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
- package/dist/shared/{9tgmr1gh.js → vmfkhszw.js} +44 -9
- package/dist/shared/{wqem4jed.js → wkme6tep.js} +11 -9
- package/dist/shared/{qy9rdq0g.js → wt5rzv1h.js} +15 -11
- package/dist/shared/wxr8cwns.js +6 -0
- package/dist/shared/{bzsjtd7c.js → wynzeggs.js} +19 -14
- package/dist/shared/x3jc53kg.js +220 -0
- package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
- package/dist/shared/{fyqkk1m0.js → xq0mvhts.js} +10 -9
- package/dist/shared/xqve9598.js +53 -0
- package/dist/shared/{47xzb9fc.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/{h8ne16fq.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/{fb00y8fp.js → zm0mafk4.js} +12 -6
- package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
- package/dist/tooling/cli.js +97 -24
- 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/commands.d.ts +1 -2
- package/dist/tooling/migrate/config.d.ts +3 -2
- package/dist/tooling/migrate/drift.d.ts +1 -1
- package/dist/tooling/migrate/estimate.d.ts +14 -1
- package/dist/tooling/migrate/files.d.ts +11 -1
- package/dist/tooling/migrate/index.js +17 -16
- package/dist/tooling/migrate/lint.d.ts +4 -4
- 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 +10 -3
- package/dist/tooling/migrate/policy.d.ts +11 -0
- 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 +39 -0
- package/dist/tooling/migrate/text.d.ts +42 -0
- package/dist/tooling/node-version.d.ts +21 -0
- package/dist/tooling/testing/factories.d.ts +10 -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 +15 -8
- package/package.json +10 -6
- package/dist/shared/0v1g424p.js +0 -26
- package/dist/shared/r73pt594.js +0 -426
- package/dist/shared/rasvvzgz.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)) {
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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,
|
|
714
|
+
function primaryWhere(facts, row, tenantKeys) {
|
|
594
715
|
const where = {};
|
|
595
716
|
for (const field of facts.primary) {
|
|
596
|
-
if (field
|
|
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
|
|
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(
|
|
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." }
|