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
|
@@ -9,51 +9,71 @@ import {
|
|
|
9
9
|
serializeCatalog2,
|
|
10
10
|
catalogHash2,
|
|
11
11
|
startupCatalog2,
|
|
12
|
+
catalogFileText2,
|
|
12
13
|
parseCatalog2
|
|
13
|
-
} from "./
|
|
14
|
+
} from "./qq3899ht.js";
|
|
14
15
|
import {
|
|
15
16
|
open2
|
|
16
|
-
} from "./
|
|
17
|
+
} from "./wkme6tep.js";
|
|
17
18
|
import {
|
|
19
|
+
sqlStatements,
|
|
20
|
+
transactionControl,
|
|
18
21
|
canonicalTypeName,
|
|
22
|
+
prepareMigrationText,
|
|
23
|
+
assertMigrationSteps,
|
|
24
|
+
columnPrivilege,
|
|
19
25
|
batchSizeField,
|
|
20
26
|
aboutBatchCount,
|
|
21
|
-
sqlStatements,
|
|
22
27
|
parseReplace,
|
|
23
28
|
planMigration,
|
|
24
29
|
formatPlan,
|
|
25
30
|
parsePlan,
|
|
26
31
|
staleRenames,
|
|
32
|
+
sessionFailure,
|
|
33
|
+
finishQuiet,
|
|
27
34
|
assertTargetPolicy,
|
|
28
35
|
assertTargetAlias,
|
|
29
36
|
listTargets,
|
|
30
37
|
selectTarget
|
|
31
|
-
} from "./
|
|
38
|
+
} from "./ttmg09vd.js";
|
|
32
39
|
import {
|
|
33
40
|
contributedFields
|
|
34
41
|
} from "./n0dgs0pb.js";
|
|
35
42
|
import {
|
|
36
43
|
assertValidation,
|
|
37
44
|
writeWouldValidate
|
|
38
|
-
} from "./
|
|
45
|
+
} from "./ze4hxsmy.js";
|
|
46
|
+
import {
|
|
47
|
+
tableOwner,
|
|
48
|
+
stampOwnership
|
|
49
|
+
} from "./xqve9598.js";
|
|
39
50
|
import {
|
|
40
51
|
catalog,
|
|
41
52
|
isDomain
|
|
42
|
-
} from "./
|
|
53
|
+
} from "./ywspezzx.js";
|
|
43
54
|
import {
|
|
44
55
|
snakeCase,
|
|
45
56
|
emittedTypeName
|
|
46
|
-
} from "./
|
|
57
|
+
} from "./rpycarcr.js";
|
|
47
58
|
import {
|
|
48
59
|
ColumnBuilder2
|
|
49
|
-
} from "./
|
|
60
|
+
} from "./ywr8hmey.js";
|
|
50
61
|
import {
|
|
51
62
|
staticNamespace2
|
|
52
|
-
} from "./
|
|
63
|
+
} from "./358zsv4r.js";
|
|
64
|
+
import {
|
|
65
|
+
sha2562
|
|
66
|
+
} from "./nfzc5z0w.js";
|
|
53
67
|
import {
|
|
54
68
|
OkmError2
|
|
55
69
|
} from "./52szzakm.js";
|
|
56
70
|
|
|
71
|
+
// src/tooling/migrate/paths.ts
|
|
72
|
+
import { resolve } from "node:path";
|
|
73
|
+
function projectPath(cwd, ...parts) {
|
|
74
|
+
return resolve(cwd, ...parts);
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
// src/tooling/errors/registry.ts
|
|
58
78
|
var ERROR_DOCS = [
|
|
59
79
|
{
|
|
@@ -494,6 +514,18 @@ var ERROR_DOCS = [
|
|
|
494
514
|
summary: "The last migration's catalog does not match the schema.",
|
|
495
515
|
fix: "Run okm generate."
|
|
496
516
|
},
|
|
517
|
+
{
|
|
518
|
+
code: "OKM1550",
|
|
519
|
+
title: "Row-level security weakened",
|
|
520
|
+
summary: "A statement disables row-level security, drops or widens a policy, grants BYPASSRLS, or changes the owner of a tenant table.",
|
|
521
|
+
fix: "Remove the statement, or allow OKM1550 with a reason when the change is reviewed."
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
code: "OKM1551",
|
|
525
|
+
title: "Transaction control in a step",
|
|
526
|
+
summary: "A step contains COMMIT, ROLLBACK, or other transaction control, including inside a DO body. A function or procedure body is not this.",
|
|
527
|
+
fix: "Remove the transaction control. okm migrate check replays one transaction and rolls it back. Apply accepts the step only with -- okm-allow OKM1551: reason."
|
|
528
|
+
},
|
|
497
529
|
{
|
|
498
530
|
code: "OKM1601",
|
|
499
531
|
title: "Stale typed SQL",
|
|
@@ -533,8 +565,14 @@ var ERROR_DOCS = [
|
|
|
533
565
|
{
|
|
534
566
|
code: "OKM1707",
|
|
535
567
|
title: "RLS role",
|
|
536
|
-
summary: "
|
|
537
|
-
fix: "Connect as a role that is
|
|
568
|
+
summary: "A scoped call under rls ran as a role that owns the tenant tables, is a superuser, or has BYPASSRLS. The reason field says which. The message names the endpoint.",
|
|
569
|
+
fix: "Connect as a role that is not the table owner, a superuser, or BYPASSRLS. okm seed under rlsTenancy runs as the application role. The owner gets this error."
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
code: "OKM1708",
|
|
573
|
+
title: "RLS policy",
|
|
574
|
+
summary: "A scoped call under rls found a tenant table whose row-level security is missing, not forced, or whose policies are not the ones okm generate emits. The message names the table and the endpoint.",
|
|
575
|
+
fix: "Restore the policies okm generate emits for this rls schema."
|
|
538
576
|
},
|
|
539
577
|
{
|
|
540
578
|
code: "OKM1801",
|
|
@@ -617,7 +655,7 @@ var ERROR_DOCS = [
|
|
|
617
655
|
{
|
|
618
656
|
code: "OKM1825",
|
|
619
657
|
title: "Missing privilege",
|
|
620
|
-
summary: "
|
|
658
|
+
summary: "A role lacks a privilege on a managed object.",
|
|
621
659
|
fix: "Grant the privilege, or change the role. okm doctor reports the object and the privilege."
|
|
622
660
|
},
|
|
623
661
|
{
|
|
@@ -712,9 +750,53 @@ function indexDocs() {
|
|
|
712
750
|
return index;
|
|
713
751
|
}
|
|
714
752
|
|
|
715
|
-
// src/tooling/migrate/
|
|
753
|
+
// src/tooling/migrate/files.ts
|
|
716
754
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
717
755
|
import { join } from "node:path";
|
|
756
|
+
function loadMigrations(directory) {
|
|
757
|
+
if (!existsSync(directory))
|
|
758
|
+
return [];
|
|
759
|
+
const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
760
|
+
const migrations = [];
|
|
761
|
+
for (const file of names) {
|
|
762
|
+
const id = file.slice(0, -".sql".length);
|
|
763
|
+
const catalogPath = join(directory, `${id}.catalog.json`);
|
|
764
|
+
if (!existsSync(catalogPath)) {
|
|
765
|
+
throw new OkmError2("invalid", `Migration ${id} is missing its catalog snapshot.`, {
|
|
766
|
+
fix: {
|
|
767
|
+
summary: "Generate the migration again so the SQL and the catalog are written together."
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const plan = readMigrationPlan(join(directory, file));
|
|
772
|
+
const stored = parseCatalog2(readFileSync(catalogPath, "utf8"));
|
|
773
|
+
const startup = startupCatalog2(stored);
|
|
774
|
+
const hash = catalogHash2(startup);
|
|
775
|
+
migrations.push(startup === stored ? { id, catalogHash: hash, steps: plan.steps } : { id, catalogHash: hash, legacyHash: catalogHash2(stored), steps: plan.steps });
|
|
776
|
+
}
|
|
777
|
+
return migrations;
|
|
778
|
+
}
|
|
779
|
+
function readMigrationPlan(path) {
|
|
780
|
+
const text = prepareMigrationText(readFileSync(path, "utf8"), path);
|
|
781
|
+
const plan = parsePlan(text);
|
|
782
|
+
assertMigrationSteps(text, plan.steps.length, path);
|
|
783
|
+
return plan;
|
|
784
|
+
}
|
|
785
|
+
function loadHeadSnapshot(directory) {
|
|
786
|
+
const migrations = loadMigrations(directory);
|
|
787
|
+
const last = migrations.at(-1);
|
|
788
|
+
if (last === undefined)
|
|
789
|
+
return;
|
|
790
|
+
return {
|
|
791
|
+
id: last.id,
|
|
792
|
+
catalogHash: last.catalogHash,
|
|
793
|
+
catalog: parseCatalog2(readFileSync(join(directory, `${last.id}.catalog.json`), "utf8"))
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/tooling/migrate/lint.ts
|
|
798
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
799
|
+
import { join as join2 } from "node:path";
|
|
718
800
|
|
|
719
801
|
// src/tooling/migrate/lint-rules.ts
|
|
720
802
|
var INTEGER_WIDTH = {
|
|
@@ -751,7 +833,12 @@ var STEP_RULES = [
|
|
|
751
833
|
rule("OKM1536", "locking", "error", "Adding a foreign key without NOT VALID locks the table while it scans.", (input) => hit(foreignKeyWithoutNotValid(input), "adds a foreign key without NOT VALID")),
|
|
752
834
|
rule("OKM1537", "locking", "error", "SET NOT NULL locks the table while it scans for nulls.", (input) => hit(unsafeSetNotNull(input), "sets NOT NULL")),
|
|
753
835
|
rule("OKM1538", "locking", "warning", "A type change that rewrites the table locks it for the rewrite.", (input) => typeChange(input, "rewrite")),
|
|
754
|
-
rule("OKM1542", "data-dependent", "error", "A data statement outside backfill(), or DO or CALL, is not a migration step.", (input) => dataStatement(input.step))
|
|
836
|
+
rule("OKM1542", "data-dependent", "error", "A data statement outside backfill(), or DO or CALL, is not a migration step.", (input) => dataStatement(input.step)),
|
|
837
|
+
rule("OKM1550", "destructive", "error", "Disabling row-level security, dropping a policy, creating or altering one that is not the emitted tenant predicate, granting BYPASSRLS, or changing the owner of a tenant table removes the rlsTenancy backstop.", (input) => weakensRls(input)),
|
|
838
|
+
rule("OKM1551", "destructive", "error", "Transaction control inside a step commits or leaves the migration transaction.", (input) => {
|
|
839
|
+
const word = transactionControl(input.step.sql);
|
|
840
|
+
return word === undefined ? [] : [`contains ${word}`];
|
|
841
|
+
})
|
|
755
842
|
];
|
|
756
843
|
var COLUMN_RULES = [
|
|
757
844
|
{
|
|
@@ -810,6 +897,146 @@ function normalized(sql) {
|
|
|
810
897
|
}
|
|
811
898
|
return out;
|
|
812
899
|
}
|
|
900
|
+
function weakensRls(input) {
|
|
901
|
+
const text = normalized(input.step.sql);
|
|
902
|
+
if (/\bdisable row level security\b/.test(text))
|
|
903
|
+
return ["disables row level security"];
|
|
904
|
+
if (/\bno force row level security\b/.test(text))
|
|
905
|
+
return ["stops forcing row level security"];
|
|
906
|
+
if (/^drop policy\b/.test(text))
|
|
907
|
+
return ["drops a policy"];
|
|
908
|
+
const drifted = policyDrift(input, text);
|
|
909
|
+
if (drifted !== undefined)
|
|
910
|
+
return [drifted];
|
|
911
|
+
if (/^(?:alter|create) (?:role|user)\b/.test(text) && /(?<!no)bypassrls\b/.test(text)) {
|
|
912
|
+
return ["grants BYPASSRLS"];
|
|
913
|
+
}
|
|
914
|
+
if (changesTenantOwner(input, text))
|
|
915
|
+
return ["changes the owner of a tenant table"];
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
function policyDrift(input, text) {
|
|
919
|
+
const header = /^(create|alter) policy\s+(\S+)\s+on\s+(\S+)/.exec(text);
|
|
920
|
+
const kind = header?.[1];
|
|
921
|
+
const table = lastName(header?.[3]);
|
|
922
|
+
if (kind === undefined || table === undefined)
|
|
923
|
+
return;
|
|
924
|
+
if (!hasPolicy(input.before, table) && !hasPolicy(input.after, table))
|
|
925
|
+
return;
|
|
926
|
+
const name = lastName(header?.[2]) ?? "";
|
|
927
|
+
const using = clause(text, "using");
|
|
928
|
+
const check = clause(text, "with check");
|
|
929
|
+
if (isTrue(using) || isTrue(check)) {
|
|
930
|
+
return kind === "alter" ? "widens a policy to match every row" : "creates a policy that matches every row";
|
|
931
|
+
}
|
|
932
|
+
if (kind === "alter" && using === undefined && check === undefined)
|
|
933
|
+
return;
|
|
934
|
+
const tenant = policyExpression(input, table, `${table}_tenant`);
|
|
935
|
+
const unscoped = policyExpression(input, table, `${table}_unscoped_select`);
|
|
936
|
+
const emitted = !/\bas restrictive\b/.test(text) && (matchesEmitted(name, `${table}_tenant`, using, check, tenant) || matchesEmitted(name, `${table}_unscoped_select`, using, check, unscoped));
|
|
937
|
+
if (emitted)
|
|
938
|
+
return;
|
|
939
|
+
return kind === "alter" ? "widens a policy away from the tenant predicate" : "creates a policy that is not the tenant predicate";
|
|
940
|
+
}
|
|
941
|
+
function matchesEmitted(name, expectedName, using, check, expression) {
|
|
942
|
+
if (name !== expectedName || expression === undefined || using === undefined)
|
|
943
|
+
return false;
|
|
944
|
+
if (!sameExpression(using, expression))
|
|
945
|
+
return false;
|
|
946
|
+
return check === undefined || sameExpression(check, expression);
|
|
947
|
+
}
|
|
948
|
+
function sameExpression(found, expected) {
|
|
949
|
+
return found === expected || unwrap(found) === unwrap(expected);
|
|
950
|
+
}
|
|
951
|
+
function unwrap(expression) {
|
|
952
|
+
const body = expression.trim();
|
|
953
|
+
if (!body.startsWith("(") || !body.endsWith(")"))
|
|
954
|
+
return body;
|
|
955
|
+
let depth = 0;
|
|
956
|
+
for (let index = 0;index < body.length; index += 1) {
|
|
957
|
+
const char = body[index];
|
|
958
|
+
if (char === "(")
|
|
959
|
+
depth += 1;
|
|
960
|
+
else if (char === ")") {
|
|
961
|
+
depth -= 1;
|
|
962
|
+
if (depth === 0)
|
|
963
|
+
return index === body.length - 1 ? body.slice(1, -1).trim() : body;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return body;
|
|
967
|
+
}
|
|
968
|
+
function isTrue(expression) {
|
|
969
|
+
return expression !== undefined && expression.replaceAll(" ", "") === "(true)";
|
|
970
|
+
}
|
|
971
|
+
function clause(text, keyword) {
|
|
972
|
+
const needle = keyword === "using" ? /\busing\s*\(/g : /\bwith check\s*\(/g;
|
|
973
|
+
const found = needle.exec(text);
|
|
974
|
+
if (found === null)
|
|
975
|
+
return;
|
|
976
|
+
return parenthesized(text, found.index + found[0].length - 1);
|
|
977
|
+
}
|
|
978
|
+
function parenthesized(text, open) {
|
|
979
|
+
let depth = 0;
|
|
980
|
+
let quoted = false;
|
|
981
|
+
let single = false;
|
|
982
|
+
for (let index = open;index < text.length; index += 1) {
|
|
983
|
+
const char = text[index];
|
|
984
|
+
if (single) {
|
|
985
|
+
if (char === "'" && text[index + 1] === "'") {
|
|
986
|
+
index += 1;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (char === "'")
|
|
990
|
+
single = false;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (quoted) {
|
|
994
|
+
if (char === '"')
|
|
995
|
+
quoted = false;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (char === "'") {
|
|
999
|
+
single = true;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (char === '"') {
|
|
1003
|
+
quoted = true;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (char === "(")
|
|
1007
|
+
depth += 1;
|
|
1008
|
+
else if (char === ")") {
|
|
1009
|
+
depth -= 1;
|
|
1010
|
+
if (depth === 0)
|
|
1011
|
+
return text.slice(open, index + 1);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
function policyExpression(input, table, name) {
|
|
1017
|
+
const expression = expressionOf(input.after, table, name) ?? expressionOf(input.before, table, name);
|
|
1018
|
+
return expression === undefined ? undefined : normalized(expression);
|
|
1019
|
+
}
|
|
1020
|
+
function expressionOf(source, table, name) {
|
|
1021
|
+
for (const object of source.objects) {
|
|
1022
|
+
if (object.kind !== "policy")
|
|
1023
|
+
continue;
|
|
1024
|
+
if (object.identity.parent.name !== table || object.identity.name !== name)
|
|
1025
|
+
continue;
|
|
1026
|
+
return object.definition.expression;
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
function changesTenantOwner(input, text) {
|
|
1031
|
+
const match = /^alter table\s+(\S+)\s+owner to\b/.exec(text);
|
|
1032
|
+
const table = lastName(match?.[1]);
|
|
1033
|
+
if (table === undefined)
|
|
1034
|
+
return false;
|
|
1035
|
+
return hasPolicy(input.before, table) || hasPolicy(input.after, table);
|
|
1036
|
+
}
|
|
1037
|
+
function hasPolicy(source, table) {
|
|
1038
|
+
return source.objects.some((object) => object.kind === "policy" && object.identity.parent.name === table);
|
|
1039
|
+
}
|
|
813
1040
|
function dataStatement(step) {
|
|
814
1041
|
const text = normalized(step.sql);
|
|
815
1042
|
if (/^(?:do|call)\b/.test(text))
|
|
@@ -1161,15 +1388,15 @@ function lintCatalog(source) {
|
|
|
1161
1388
|
return findings;
|
|
1162
1389
|
}
|
|
1163
1390
|
function lintMigrationDirectory(directory, pending) {
|
|
1164
|
-
if (!
|
|
1391
|
+
if (!existsSync2(directory))
|
|
1165
1392
|
return [];
|
|
1166
|
-
const names =
|
|
1393
|
+
const names = readdirSync2(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
1167
1394
|
let before = catalog([]);
|
|
1168
1395
|
const findings = [];
|
|
1169
1396
|
for (const file of names) {
|
|
1170
1397
|
const id = file.slice(0, -".sql".length);
|
|
1171
|
-
const plan =
|
|
1172
|
-
const after = parseCatalog2(
|
|
1398
|
+
const plan = readMigrationPlan(join2(directory, file));
|
|
1399
|
+
const after = parseCatalog2(readFileSync2(join2(directory, `${id}.catalog.json`), "utf8"));
|
|
1173
1400
|
if (pending === undefined || pending.has(id)) {
|
|
1174
1401
|
for (const item of lintPlan(plan, before, after)) {
|
|
1175
1402
|
findings.push({ ...item, place: `${id} ${item.place}` });
|
|
@@ -1288,8 +1515,8 @@ function columnPlace(column) {
|
|
|
1288
1515
|
}
|
|
1289
1516
|
|
|
1290
1517
|
// src/tooling/migrate/project.ts
|
|
1291
|
-
import { existsSync as
|
|
1292
|
-
import { basename, join as
|
|
1518
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync, writeFileSync } from "node:fs";
|
|
1519
|
+
import { basename, dirname, join as join3 } from "node:path";
|
|
1293
1520
|
import { pathToFileURL } from "node:url";
|
|
1294
1521
|
|
|
1295
1522
|
// src/dialects/pg/declarations.ts
|
|
@@ -1331,7 +1558,7 @@ function sqlIdentifier(name, casing) {
|
|
|
1331
1558
|
}
|
|
1332
1559
|
|
|
1333
1560
|
// src/dialects/pg/emit.ts
|
|
1334
|
-
function emitRowTypes(source) {
|
|
1561
|
+
function emitRowTypes(source, jsonTypes) {
|
|
1335
1562
|
const lines = [
|
|
1336
1563
|
"/**",
|
|
1337
1564
|
" * Row types emitted from the schema.",
|
|
@@ -1349,13 +1576,13 @@ function emitRowTypes(source) {
|
|
|
1349
1576
|
inserts.push(` readonly ${key}: ${name}Insert;`);
|
|
1350
1577
|
updates.push(` readonly ${key}: ${name}Update;`);
|
|
1351
1578
|
lines.push(`export interface ${name} {`);
|
|
1352
|
-
lines.push(fields(item, "row", source));
|
|
1579
|
+
lines.push(fields(item, "row", source, jsonTypes));
|
|
1353
1580
|
lines.push("}", "");
|
|
1354
1581
|
lines.push(`export interface ${name}Insert {`);
|
|
1355
|
-
lines.push(fields(item, "insert", source));
|
|
1582
|
+
lines.push(fields(item, "insert", source, jsonTypes));
|
|
1356
1583
|
lines.push("}", "");
|
|
1357
1584
|
lines.push(`export interface ${name}Update {`);
|
|
1358
|
-
lines.push(fields(item, "update", source));
|
|
1585
|
+
lines.push(fields(item, "update", source, jsonTypes));
|
|
1359
1586
|
lines.push("}", "");
|
|
1360
1587
|
}
|
|
1361
1588
|
lines.push("export interface Rows {");
|
|
@@ -1388,7 +1615,74 @@ ${body}`;
|
|
|
1388
1615
|
function propertyName(name) {
|
|
1389
1616
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : JSON.stringify(name);
|
|
1390
1617
|
}
|
|
1391
|
-
function
|
|
1618
|
+
function jsonColumnTypes(source) {
|
|
1619
|
+
const text = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
1620
|
+
const found = new Map;
|
|
1621
|
+
const pattern = /(?:\bt\s*\.\s*)?(jsonb|json)\s*</g;
|
|
1622
|
+
for (const match of text.matchAll(pattern)) {
|
|
1623
|
+
const open = (match.index ?? 0) + match[0].length - 1;
|
|
1624
|
+
const argument = readGeneric(text, open);
|
|
1625
|
+
const field = propertyBefore(text, match.index ?? 0);
|
|
1626
|
+
const table = tableBefore(text, match.index ?? 0);
|
|
1627
|
+
if (argument === undefined || field === undefined || table === undefined)
|
|
1628
|
+
continue;
|
|
1629
|
+
found.set(`${table}.${field}`, argument);
|
|
1630
|
+
}
|
|
1631
|
+
return found;
|
|
1632
|
+
}
|
|
1633
|
+
function readGeneric(source, open) {
|
|
1634
|
+
if (source[open] !== "<")
|
|
1635
|
+
return;
|
|
1636
|
+
let depth = 0;
|
|
1637
|
+
let quote;
|
|
1638
|
+
for (let index = open;index < source.length; index += 1) {
|
|
1639
|
+
const char = source[index];
|
|
1640
|
+
if (char === undefined)
|
|
1641
|
+
return;
|
|
1642
|
+
if (quote !== undefined) {
|
|
1643
|
+
if (char === "\\") {
|
|
1644
|
+
index += 1;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (char === quote)
|
|
1648
|
+
quote = undefined;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
1652
|
+
quote = char;
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (char === "<")
|
|
1656
|
+
depth += 1;
|
|
1657
|
+
else if (char === ">") {
|
|
1658
|
+
depth -= 1;
|
|
1659
|
+
if (depth === 0)
|
|
1660
|
+
return source.slice(open + 1, index).trim().replace(/\s+/g, " ");
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
function propertyBefore(source, index) {
|
|
1666
|
+
let cursor = index;
|
|
1667
|
+
while (cursor > 0 && /\s/.test(source[cursor - 1] ?? ""))
|
|
1668
|
+
cursor -= 1;
|
|
1669
|
+
if (source[cursor - 1] !== ":")
|
|
1670
|
+
return;
|
|
1671
|
+
cursor -= 1;
|
|
1672
|
+
while (cursor > 0 && /\s/.test(source[cursor - 1] ?? ""))
|
|
1673
|
+
cursor -= 1;
|
|
1674
|
+
const end = cursor;
|
|
1675
|
+
while (cursor > 0 && /[\w$]/.test(source[cursor - 1] ?? ""))
|
|
1676
|
+
cursor -= 1;
|
|
1677
|
+
const name = source.slice(cursor, end);
|
|
1678
|
+
return /^[$A-Za-z_][\w$]*$/.test(name) ? name : undefined;
|
|
1679
|
+
}
|
|
1680
|
+
function tableBefore(source, index) {
|
|
1681
|
+
const head = source.slice(0, index);
|
|
1682
|
+
const tables = [...head.matchAll(/table\(\s*["']([^"']+)["']/g)];
|
|
1683
|
+
return tables.at(-1)?.[1];
|
|
1684
|
+
}
|
|
1685
|
+
function fields(item, kind, source, jsonTypes) {
|
|
1392
1686
|
const printed = [];
|
|
1393
1687
|
const options = item.options;
|
|
1394
1688
|
const entries = Object.entries(item.columns);
|
|
@@ -1406,7 +1700,7 @@ function fields(item, kind, source) {
|
|
|
1406
1700
|
const state = builder.state;
|
|
1407
1701
|
if (kind === "row") {
|
|
1408
1702
|
if (!state.hidden) {
|
|
1409
|
-
printed.push(` readonly ${field}: ${valueType(state)};`);
|
|
1703
|
+
printed.push(` readonly ${field}: ${valueType(state, jsonTypes?.get(`${item.name}.${field}`))};`);
|
|
1410
1704
|
}
|
|
1411
1705
|
continue;
|
|
1412
1706
|
}
|
|
@@ -1415,7 +1709,7 @@ function fields(item, kind, source) {
|
|
|
1415
1709
|
continue;
|
|
1416
1710
|
}
|
|
1417
1711
|
const mode = kind === "update" ? "optional" : written;
|
|
1418
|
-
const label = valueType(state);
|
|
1712
|
+
const label = valueType(state, jsonTypes?.get(`${item.name}.${field}`));
|
|
1419
1713
|
printed.push(mode === "optional" ? ` readonly ${field}?: ${label};` : ` readonly ${field}: ${label};`);
|
|
1420
1714
|
}
|
|
1421
1715
|
return printed.join(`
|
|
@@ -1435,8 +1729,8 @@ function updateMode(state) {
|
|
|
1435
1729
|
return "omit";
|
|
1436
1730
|
return "optional";
|
|
1437
1731
|
}
|
|
1438
|
-
function valueType(state) {
|
|
1439
|
-
let label = scalarLabel(state);
|
|
1732
|
+
function valueType(state, jsonArg) {
|
|
1733
|
+
let label = scalarLabel(state, jsonArg);
|
|
1440
1734
|
if (state.dims > 0) {
|
|
1441
1735
|
for (let rank = 0;rank < state.dims; rank += 1) {
|
|
1442
1736
|
label = `readonly (${label})[]`;
|
|
@@ -1447,7 +1741,7 @@ function valueType(state) {
|
|
|
1447
1741
|
}
|
|
1448
1742
|
return label;
|
|
1449
1743
|
}
|
|
1450
|
-
function scalarLabel(state) {
|
|
1744
|
+
function scalarLabel(state, jsonArg) {
|
|
1451
1745
|
if (state.picklist !== undefined) {
|
|
1452
1746
|
return state.picklist.values.map((value) => JSON.stringify(value)).join(" | ");
|
|
1453
1747
|
}
|
|
@@ -1480,9 +1774,9 @@ function scalarLabel(state) {
|
|
|
1480
1774
|
case "bytea":
|
|
1481
1775
|
return "Uint8Array";
|
|
1482
1776
|
case "json":
|
|
1483
|
-
return "
|
|
1777
|
+
return `json<${jsonArg ?? "unknown"}>`;
|
|
1484
1778
|
case "jsonb":
|
|
1485
|
-
return "
|
|
1779
|
+
return `jsonb<${jsonArg ?? "unknown"}>`;
|
|
1486
1780
|
case "timestamptz":
|
|
1487
1781
|
return "Temporal.Instant";
|
|
1488
1782
|
case "timestamp":
|
|
@@ -1526,17 +1820,34 @@ function baseName(dataType) {
|
|
|
1526
1820
|
}
|
|
1527
1821
|
|
|
1528
1822
|
// src/dialects/pg/role/index.ts
|
|
1823
|
+
class RoleDeclarationError extends OkmError2 {
|
|
1824
|
+
reason;
|
|
1825
|
+
constructor(reason, message) {
|
|
1826
|
+
super("invalid", message, {
|
|
1827
|
+
kind: "invalid",
|
|
1828
|
+
fix: {
|
|
1829
|
+
summary: reason === "reporting_write" ? "Grant select, or use the application role for writes." : "Give the reporting role its own name."
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
this.name = "OkmError";
|
|
1833
|
+
this.reason = reason;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1529
1836
|
var PROVENANCE = { origin: "file", name: "roles" };
|
|
1530
1837
|
var RELATION = ["SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
1531
1838
|
var READ = ["SELECT"];
|
|
1532
1839
|
var ROUTINE = ["EXECUTE"];
|
|
1533
1840
|
var SEQUENCE = ["USAGE", "SELECT"];
|
|
1534
|
-
function attachRoles(source, input) {
|
|
1535
|
-
return catalog([...source.objects, ...contributeRoles(input, source.objects)]);
|
|
1841
|
+
function attachRoles(source, input, exposure) {
|
|
1842
|
+
return catalog([...source.objects, ...contributeRoles(input, source.objects, exposure)]);
|
|
1536
1843
|
}
|
|
1537
|
-
function contributeRoles(input, built) {
|
|
1844
|
+
function contributeRoles(input, built, exposure) {
|
|
1538
1845
|
const migration = requireName(input.migration, "migration");
|
|
1539
1846
|
const app = requireName(input.app, "app");
|
|
1847
|
+
const reporting = input.reporting === undefined ? undefined : requireName(input.reporting, "reporting");
|
|
1848
|
+
if (reporting !== undefined && (reporting === app || reporting === migration)) {
|
|
1849
|
+
throw new RoleDeclarationError("reporting_distinct", `Role ${reporting} cannot be the reporting role and also migration or app.`);
|
|
1850
|
+
}
|
|
1540
1851
|
const managed = new Map;
|
|
1541
1852
|
for (const role of input.managed ?? []) {
|
|
1542
1853
|
const name = requireName(role.name, "managed role");
|
|
@@ -1544,6 +1855,10 @@ function contributeRoles(input, built) {
|
|
|
1544
1855
|
}
|
|
1545
1856
|
const objects = [];
|
|
1546
1857
|
const names = new Set([migration, app, ...managed.keys()]);
|
|
1858
|
+
if (reporting !== undefined)
|
|
1859
|
+
names.add(reporting);
|
|
1860
|
+
for (const grant of input.columns ?? [])
|
|
1861
|
+
names.add(requireName(grant.role, "column grant role"));
|
|
1547
1862
|
for (const name of [...names].sort()) {
|
|
1548
1863
|
const declaration = managed.get(name);
|
|
1549
1864
|
objects.push(roleObject({
|
|
@@ -1558,8 +1873,10 @@ function contributeRoles(input, built) {
|
|
|
1558
1873
|
}
|
|
1559
1874
|
const roleIdentity = (name) => ({ kind: "role", name });
|
|
1560
1875
|
const namespaces = new Set;
|
|
1876
|
+
const exposed = indexExposure(exposure);
|
|
1877
|
+
const columnGrants = new Map;
|
|
1561
1878
|
for (const object of built) {
|
|
1562
|
-
if (object.owner === "ignored")
|
|
1879
|
+
if (object.owner === "ignored" || object.owner === "external")
|
|
1563
1880
|
continue;
|
|
1564
1881
|
const ref = grantRef(object);
|
|
1565
1882
|
if (ref === undefined)
|
|
@@ -1577,6 +1894,7 @@ function contributeRoles(input, built) {
|
|
|
1577
1894
|
}));
|
|
1578
1895
|
}
|
|
1579
1896
|
}
|
|
1897
|
+
const relations = built.filter((object) => object.owner !== "ignored" && object.owner !== "external" && (object.kind === "table" || object.kind === "view" || object.kind === "materializedView"));
|
|
1580
1898
|
const schemas = namespaces.size === 0 ? ["public"] : [...namespaces].sort();
|
|
1581
1899
|
for (const name of schemas) {
|
|
1582
1900
|
const namespace = staticNamespace2(name);
|
|
@@ -1597,6 +1915,45 @@ function contributeRoles(input, built) {
|
|
|
1597
1915
|
}));
|
|
1598
1916
|
}
|
|
1599
1917
|
}
|
|
1918
|
+
if (reporting !== undefined) {
|
|
1919
|
+
objects.push(grantObject({
|
|
1920
|
+
role: reporting,
|
|
1921
|
+
object: { kind: "namespace", namespace, name },
|
|
1922
|
+
privilege: "USAGE",
|
|
1923
|
+
provenance: PROVENANCE,
|
|
1924
|
+
dependencies: [roleIdentity(reporting)]
|
|
1925
|
+
}));
|
|
1926
|
+
objects.push(defaultPrivilegeObject({
|
|
1927
|
+
forRole: migration,
|
|
1928
|
+
namespace,
|
|
1929
|
+
objectKind: "table",
|
|
1930
|
+
grantee: reporting,
|
|
1931
|
+
privilege: "SELECT",
|
|
1932
|
+
provenance: PROVENANCE,
|
|
1933
|
+
dependencies: [roleIdentity(migration), roleIdentity(reporting)]
|
|
1934
|
+
}));
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
for (const grant of input.columns ?? []) {
|
|
1938
|
+
noteColumnGrant(columnGrants, grant, built, exposed, reporting);
|
|
1939
|
+
}
|
|
1940
|
+
if (reporting !== undefined) {
|
|
1941
|
+
for (const object of relations) {
|
|
1942
|
+
const ref = grantRef(object);
|
|
1943
|
+
if (ref === undefined)
|
|
1944
|
+
continue;
|
|
1945
|
+
noteReportingSelect(columnGrants, objects, reporting, object, ref, exposed, built);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
for (const pending of [...columnGrants.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0)) {
|
|
1949
|
+
const target = pending.target;
|
|
1950
|
+
objects.push(grantObject({
|
|
1951
|
+
role: pending.role,
|
|
1952
|
+
object: pending.object,
|
|
1953
|
+
privilege: columnPrivilege(pending.privilege, [...pending.columns].sort()),
|
|
1954
|
+
provenance: PROVENANCE,
|
|
1955
|
+
dependencies: [roleIdentity(pending.role), target.identity]
|
|
1956
|
+
}));
|
|
1600
1957
|
}
|
|
1601
1958
|
return objects;
|
|
1602
1959
|
}
|
|
@@ -1638,6 +1995,137 @@ function privilegesFor(kind) {
|
|
|
1638
1995
|
function objectIdentity(object) {
|
|
1639
1996
|
return object.identity;
|
|
1640
1997
|
}
|
|
1998
|
+
function indexExposure(exposure) {
|
|
1999
|
+
const indexed = new Map;
|
|
2000
|
+
for (const table of exposure ?? [])
|
|
2001
|
+
indexed.set(table.table, table);
|
|
2002
|
+
return indexed;
|
|
2003
|
+
}
|
|
2004
|
+
function noteReportingSelect(pending, objects, role, target, ref, exposed, built) {
|
|
2005
|
+
const key = columnKey(role, ref, "SELECT");
|
|
2006
|
+
const declared = pending.get(key);
|
|
2007
|
+
if (ref.kind === "view" || ref.kind === "materializedView") {
|
|
2008
|
+
if (declared !== undefined)
|
|
2009
|
+
return;
|
|
2010
|
+
objects.push(selectGrant(role, ref, target));
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const visible = visibleColumns(ref.name, exposed, built);
|
|
2014
|
+
const hidden = hiddenColumns(ref.name, exposed);
|
|
2015
|
+
if (hidden.size === 0) {
|
|
2016
|
+
pending.delete(key);
|
|
2017
|
+
objects.push(selectGrant(role, ref, target));
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const columns = new Set(visible);
|
|
2021
|
+
for (const name of declared?.columns ?? [])
|
|
2022
|
+
columns.add(name);
|
|
2023
|
+
if (columns.size === 0) {
|
|
2024
|
+
pending.delete(key);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const next = declared ?? openColumnGrant(role, ref, "SELECT", target);
|
|
2028
|
+
next.columns.clear();
|
|
2029
|
+
for (const name of columns)
|
|
2030
|
+
next.columns.add(name);
|
|
2031
|
+
pending.set(key, next);
|
|
2032
|
+
}
|
|
2033
|
+
function selectGrant(role, ref, target) {
|
|
2034
|
+
return grantObject({
|
|
2035
|
+
role,
|
|
2036
|
+
object: ref,
|
|
2037
|
+
privilege: "SELECT",
|
|
2038
|
+
provenance: PROVENANCE,
|
|
2039
|
+
dependencies: [{ kind: "role", name: role }, target.identity]
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
function noteColumnGrant(pending, grant, built, exposed, reporting) {
|
|
2043
|
+
const role = requireName(grant.role, "column grant role");
|
|
2044
|
+
const target = findRelation(built, grant.on);
|
|
2045
|
+
if (target.owner === "ignored") {
|
|
2046
|
+
throw new OkmError2("invalid", `Column grant on ${grant.on} targets an ignored table.`, {
|
|
2047
|
+
fix: { summary: "Remove the grant, or mark the table external." }
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
const ref = grantRef(target);
|
|
2051
|
+
if (ref === undefined) {
|
|
2052
|
+
throw new OkmError2("OKM1020", `Column grant on ${grant.on} is not a table or a view.`, {
|
|
2053
|
+
fix: { summary: "Name a table, view, or materialized view in the schema." }
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
for (const privilege of ["select", "update"]) {
|
|
2057
|
+
const names = grant[privilege];
|
|
2058
|
+
if (names === undefined || names.length === 0)
|
|
2059
|
+
continue;
|
|
2060
|
+
if (role === reporting && privilege === "update") {
|
|
2061
|
+
throw new RoleDeclarationError("reporting_write", `Role ${role} is the reporting role and cannot be granted update.`);
|
|
2062
|
+
}
|
|
2063
|
+
const upper = privilege === "select" ? "SELECT" : "UPDATE";
|
|
2064
|
+
const resolved = names.map((name) => resolveColumn(target, built, ref.name, name, exposed));
|
|
2065
|
+
const current = pending.get(columnKey(role, ref, upper)) ?? openColumnGrant(role, ref, upper, target);
|
|
2066
|
+
for (const name of resolved)
|
|
2067
|
+
current.columns.add(name);
|
|
2068
|
+
pending.set(current.key, current);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function openColumnGrant(role, object, privilege, target) {
|
|
2072
|
+
return {
|
|
2073
|
+
key: columnKey(role, object, privilege),
|
|
2074
|
+
role,
|
|
2075
|
+
privilege,
|
|
2076
|
+
object,
|
|
2077
|
+
target,
|
|
2078
|
+
columns: new Set
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function columnKey(role, object, privilege) {
|
|
2082
|
+
const namespace = object.namespace.form === "static" ? object.namespace.name : object.namespace.pattern;
|
|
2083
|
+
return `${role}\x00${object.kind}\x00${namespace}\x00${object.name}\x00${privilege}`;
|
|
2084
|
+
}
|
|
2085
|
+
function findRelation(built, name) {
|
|
2086
|
+
const found = built.filter((object) => (object.kind === "table" || object.kind === "view" || object.kind === "materializedView") && object.identity.name === name);
|
|
2087
|
+
const target = found[0];
|
|
2088
|
+
if (target === undefined) {
|
|
2089
|
+
throw new OkmError2("OKM1020", `Column grant names ${name}, which is not in the schema.`, {
|
|
2090
|
+
fix: { summary: "Use the SQL name of a table, view, or materialized view." }
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
if (found.length > 1) {
|
|
2094
|
+
throw new OkmError2("OKM1020", `Column grant on ${name} matches more than one object.`, {
|
|
2095
|
+
fix: { summary: "Rename one of the objects so the grant names one." }
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
return target;
|
|
2099
|
+
}
|
|
2100
|
+
function resolveColumn(target, built, table, name, exposed) {
|
|
2101
|
+
const known = exposed.get(table);
|
|
2102
|
+
const fromField = known?.columns.find((column) => column.field === name || column.sql === name);
|
|
2103
|
+
if (fromField !== undefined)
|
|
2104
|
+
return fromField.sql;
|
|
2105
|
+
const sqlNames = columnNames(target, built, table);
|
|
2106
|
+
if (sqlNames.includes(name))
|
|
2107
|
+
return name;
|
|
2108
|
+
throw new OkmError2("OKM1020", `Column ${table}.${name} is not in the schema.`, {
|
|
2109
|
+
fix: { summary: "Name a column of that table or view." }
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
function visibleColumns(table, exposed, built) {
|
|
2113
|
+
const known = exposed.get(table);
|
|
2114
|
+
if (known !== undefined) {
|
|
2115
|
+
return known.columns.filter((column) => !column.hidden).map((column) => column.sql);
|
|
2116
|
+
}
|
|
2117
|
+
return built.flatMap((object) => object.kind === "column" && object.identity.parent.name === table ? [object.identity.name] : []);
|
|
2118
|
+
}
|
|
2119
|
+
function hiddenColumns(table, exposed) {
|
|
2120
|
+
const known = exposed.get(table);
|
|
2121
|
+
return new Set((known?.columns ?? []).filter((column) => column.hidden).map((column) => column.sql));
|
|
2122
|
+
}
|
|
2123
|
+
function columnNames(target, built, table) {
|
|
2124
|
+
if (target.kind === "view" || target.kind === "materializedView") {
|
|
2125
|
+
return target.definition.columns.map((column) => column.name);
|
|
2126
|
+
}
|
|
2127
|
+
return built.flatMap((object) => object.kind === "column" && object.identity.parent.name === table ? [object.identity.name] : []);
|
|
2128
|
+
}
|
|
1641
2129
|
|
|
1642
2130
|
// src/tooling/migrate/estimate.ts
|
|
1643
2131
|
var LARGE_TABLE_ROWS = 1e6;
|
|
@@ -1677,15 +2165,13 @@ async function readRowEstimates(url, schema, tables) {
|
|
|
1677
2165
|
return map;
|
|
1678
2166
|
} catch (error) {
|
|
1679
2167
|
if (began)
|
|
1680
|
-
await connection.execute("rollback")
|
|
1681
|
-
|
|
1682
|
-
});
|
|
1683
|
-
throw error;
|
|
2168
|
+
await finishQuiet(() => connection.execute("rollback"));
|
|
2169
|
+
throw sessionFailure(error) ?? error;
|
|
1684
2170
|
} finally {
|
|
1685
|
-
await connection.release();
|
|
2171
|
+
await finishQuiet(() => connection.release());
|
|
1686
2172
|
}
|
|
1687
2173
|
} finally {
|
|
1688
|
-
await pool.close();
|
|
2174
|
+
await finishQuiet(() => pool.close());
|
|
1689
2175
|
}
|
|
1690
2176
|
}
|
|
1691
2177
|
function annotateLock(step, estimates, aliases = new Map) {
|
|
@@ -1842,15 +2328,17 @@ async function buildProject(cwd) {
|
|
|
1842
2328
|
}
|
|
1843
2329
|
async function generateProject(cwd, name, flags) {
|
|
1844
2330
|
const prepared = await prepare(cwd, name, flags);
|
|
2331
|
+
if (name === "init")
|
|
2332
|
+
writeGitAttributes(cwd);
|
|
1845
2333
|
writeArtifact(cwd, prepared.config, prepared.built);
|
|
1846
2334
|
if (prepared.plan.steps.length === 0)
|
|
1847
2335
|
return;
|
|
1848
|
-
const directory =
|
|
2336
|
+
const directory = projectPath(cwd, prepared.config.migrations ?? "migrations");
|
|
1849
2337
|
mkdirSync(directory, { recursive: true });
|
|
1850
2338
|
const base = `${nextNumber(directory)}_${safeName(name)}`;
|
|
1851
|
-
const sqlPath =
|
|
2339
|
+
const sqlPath = join3(directory, `${base}.sql`);
|
|
1852
2340
|
writeFileSync(sqlPath, formatPlan(prepared.plan));
|
|
1853
|
-
writeFileSync(
|
|
2341
|
+
writeFileSync(join3(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
|
|
1854
2342
|
return { path: sqlPath, findings: lintPrepared(prepared) };
|
|
1855
2343
|
}
|
|
1856
2344
|
async function planProject(cwd, name, flags, invoke) {
|
|
@@ -1873,9 +2361,9 @@ async function checkProject(cwd, flags) {
|
|
|
1873
2361
|
const targets = listTargets(opened.config);
|
|
1874
2362
|
assertTargetAlias(targets);
|
|
1875
2363
|
if (opened.config.tables !== undefined) {
|
|
1876
|
-
const directory =
|
|
1877
|
-
const files =
|
|
1878
|
-
const source =
|
|
2364
|
+
const directory = projectPath(cwd, opened.config.tables);
|
|
2365
|
+
const files = existsSync3(directory) ? readdirSync3(directory) : [];
|
|
2366
|
+
const source = readFileSync3(projectPath(cwd, opened.config.schema), "utf8");
|
|
1879
2367
|
const missing = unlistedTableFiles(source, files);
|
|
1880
2368
|
if (missing.length > 0) {
|
|
1881
2369
|
throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
|
|
@@ -1894,7 +2382,7 @@ async function checkProject(cwd, flags) {
|
|
|
1894
2382
|
const target = selectTarget(opened.config, named);
|
|
1895
2383
|
assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
|
|
1896
2384
|
assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
|
|
1897
|
-
const { assertAuthorDrift } = await import("./
|
|
2385
|
+
const { assertAuthorDrift } = await import("./1f0xa3cb.js");
|
|
1898
2386
|
const allowPooler = flags?.allowPooler === true || opened.config.allowPooler === true;
|
|
1899
2387
|
await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles, allowPooler);
|
|
1900
2388
|
return findings;
|
|
@@ -1975,7 +2463,7 @@ async function openProject(cwd) {
|
|
|
1975
2463
|
const config = await loadConfig(cwd);
|
|
1976
2464
|
const built = withRoles(await loadSchema(cwd, config.schema), config);
|
|
1977
2465
|
const declarations = schemaDeclarations(built);
|
|
1978
|
-
const previous = readPrevious(
|
|
2466
|
+
const previous = readPrevious(projectPath(cwd, config.migrations ?? "migrations"));
|
|
1979
2467
|
const stale = staleRenames(previous?.catalog, declarations.renames);
|
|
1980
2468
|
if (stale.length > 0) {
|
|
1981
2469
|
throw new OkmError2("OKM1020", stale.join(" "), {
|
|
@@ -1990,25 +2478,108 @@ async function openProject(cwd) {
|
|
|
1990
2478
|
};
|
|
1991
2479
|
}
|
|
1992
2480
|
function withRoles(built, config) {
|
|
1993
|
-
|
|
2481
|
+
const marks = ownershipMarks(built);
|
|
2482
|
+
const stamped = marks.size === 0 ? built.catalog : stampOwnership(built.catalog, marks);
|
|
2483
|
+
const roles = config.roles;
|
|
2484
|
+
const catalog = roles === undefined ? stamped : attachRoles(stamped, roles, exposureFor(built, roles));
|
|
2485
|
+
if (catalog === built.catalog)
|
|
1994
2486
|
return built;
|
|
1995
|
-
return { ...built, catalog
|
|
2487
|
+
return { ...built, catalog };
|
|
2488
|
+
}
|
|
2489
|
+
function ownershipMarks(built) {
|
|
2490
|
+
const marks = new Map;
|
|
2491
|
+
const models = built.model;
|
|
2492
|
+
for (const table of built.tables) {
|
|
2493
|
+
const owner = tableOwner(table);
|
|
2494
|
+
if (owner === undefined)
|
|
2495
|
+
continue;
|
|
2496
|
+
const model = models[table.name];
|
|
2497
|
+
if (model === undefined)
|
|
2498
|
+
continue;
|
|
2499
|
+
marks.set(model.sql, owner);
|
|
2500
|
+
}
|
|
2501
|
+
return marks;
|
|
2502
|
+
}
|
|
2503
|
+
function exposureFor(built, roles) {
|
|
2504
|
+
if (roles.reporting === undefined && (roles.columns === undefined || roles.columns.length === 0)) {
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
return Object.values(built.model).map((model) => ({
|
|
2508
|
+
table: model.sql,
|
|
2509
|
+
columns: model.columns.map((column) => ({
|
|
2510
|
+
field: column.field,
|
|
2511
|
+
sql: column.sql,
|
|
2512
|
+
hidden: column.hidden === true || column.hidden === "filter"
|
|
2513
|
+
}))
|
|
2514
|
+
}));
|
|
2515
|
+
}
|
|
2516
|
+
var GIT_ATTRIBUTES = [
|
|
2517
|
+
"migrations/*.sql text eol=lf",
|
|
2518
|
+
"*.catalog.json text eol=lf",
|
|
2519
|
+
".okm/** text eol=lf",
|
|
2520
|
+
""
|
|
2521
|
+
].join(`
|
|
2522
|
+
`);
|
|
2523
|
+
function writeGitAttributes(cwd) {
|
|
2524
|
+
const path = projectPath(cwd, ".gitattributes");
|
|
2525
|
+
if (existsSync3(path))
|
|
2526
|
+
return;
|
|
2527
|
+
writeFileSync(path, GIT_ATTRIBUTES);
|
|
2528
|
+
}
|
|
2529
|
+
function typesText(cwd, spec, built) {
|
|
2530
|
+
return `${emitRowTypes(built, jsonColumnTypes(readSchemaSource(cwd, spec)))}${referenceAugmentation(built)}`;
|
|
2531
|
+
}
|
|
2532
|
+
function readSchemaSource(cwd, spec) {
|
|
2533
|
+
const parts = [];
|
|
2534
|
+
const seen = new Set;
|
|
2535
|
+
const visit = (file) => {
|
|
2536
|
+
if (seen.has(file) || !existsSync3(file))
|
|
2537
|
+
return;
|
|
2538
|
+
let stat;
|
|
2539
|
+
try {
|
|
2540
|
+
stat = statSync(file);
|
|
2541
|
+
} catch {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (!stat.isFile())
|
|
2545
|
+
return;
|
|
2546
|
+
seen.add(file);
|
|
2547
|
+
const text = readFileSync3(file, "utf8");
|
|
2548
|
+
parts.push(text);
|
|
2549
|
+
for (const match of text.matchAll(/from\s+["'](\.[^"']+)["']/g)) {
|
|
2550
|
+
const rel = match[1];
|
|
2551
|
+
if (rel === undefined)
|
|
2552
|
+
continue;
|
|
2553
|
+
const base = join3(dirname(file), rel);
|
|
2554
|
+
const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, join3(base, "index.ts")];
|
|
2555
|
+
for (const candidate of candidates) {
|
|
2556
|
+
if (existsSync3(candidate)) {
|
|
2557
|
+
visit(candidate);
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
visit(projectPath(cwd, spec));
|
|
2564
|
+
return parts.join(`
|
|
2565
|
+
`);
|
|
1996
2566
|
}
|
|
1997
2567
|
function writeArtifact(cwd, config, built) {
|
|
1998
|
-
const directory =
|
|
2568
|
+
const directory = projectPath(cwd, config.out ?? ".okm");
|
|
1999
2569
|
mkdirSync(directory, { recursive: true });
|
|
2000
2570
|
const startup = startupCatalog2(built.catalog);
|
|
2001
|
-
|
|
2002
|
-
writeFileSync(
|
|
2571
|
+
const text = catalogFileText2(serializeCatalog2(startup));
|
|
2572
|
+
writeFileSync(join3(directory, "catalog.json"), text);
|
|
2573
|
+
writeFileSync(join3(directory, "catalog.hash"), `${sha2562(text)}
|
|
2003
2574
|
`);
|
|
2004
|
-
writeFileSync(
|
|
2575
|
+
writeFileSync(join3(directory, "types.d.ts"), typesText(cwd, config.schema, built));
|
|
2005
2576
|
return directory;
|
|
2006
2577
|
}
|
|
2007
2578
|
async function loadBuiltSchema(cwd, config) {
|
|
2008
2579
|
return withRoles(await loadSchema(cwd, config.schema), config);
|
|
2009
2580
|
}
|
|
2010
2581
|
async function loadConfig(cwd) {
|
|
2011
|
-
const path =
|
|
2582
|
+
const path = projectPath(cwd, "okmodel.config.ts");
|
|
2012
2583
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2013
2584
|
const record = isRecord(imported) ? imported : {};
|
|
2014
2585
|
const config = record.default ?? record.config;
|
|
@@ -2018,7 +2589,7 @@ async function loadConfig(cwd) {
|
|
|
2018
2589
|
return config;
|
|
2019
2590
|
}
|
|
2020
2591
|
async function loadSchema(cwd, spec) {
|
|
2021
|
-
const path =
|
|
2592
|
+
const path = projectPath(cwd, spec);
|
|
2022
2593
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2023
2594
|
const record = isRecord(imported) ? imported : {};
|
|
2024
2595
|
for (const value of Object.values(record)) {
|
|
@@ -2030,13 +2601,13 @@ async function loadSchema(cwd, spec) {
|
|
|
2030
2601
|
throw new OkmError2("invalid", `${spec} must export a schema().`);
|
|
2031
2602
|
}
|
|
2032
2603
|
function readPrevious(directory) {
|
|
2033
|
-
if (!
|
|
2604
|
+
if (!existsSync3(directory))
|
|
2034
2605
|
return;
|
|
2035
|
-
const catalogs =
|
|
2606
|
+
const catalogs = readdirSync3(directory).filter((file) => file.endsWith(".catalog.json")).sort();
|
|
2036
2607
|
const latest = catalogs.at(-1);
|
|
2037
2608
|
if (latest === undefined)
|
|
2038
2609
|
return;
|
|
2039
|
-
const text =
|
|
2610
|
+
const text = readFileSync3(join3(directory, latest), "utf8");
|
|
2040
2611
|
return { catalog: parseCatalog2(text) };
|
|
2041
2612
|
}
|
|
2042
2613
|
function referenceAugmentation(source) {
|
|
@@ -2061,8 +2632,8 @@ function referenceAugmentation(source) {
|
|
|
2061
2632
|
}
|
|
2062
2633
|
function nextNumber(directory) {
|
|
2063
2634
|
let max = 0;
|
|
2064
|
-
if (
|
|
2065
|
-
for (const file of
|
|
2635
|
+
if (existsSync3(directory)) {
|
|
2636
|
+
for (const file of readdirSync3(directory)) {
|
|
2066
2637
|
const match = /^(\d+)_/.exec(file);
|
|
2067
2638
|
const value = match?.[1];
|
|
2068
2639
|
if (value !== undefined)
|
|
@@ -2103,14 +2674,14 @@ function projectImportsValidate(cwd) {
|
|
|
2103
2674
|
function directoryImportsValidate(dir) {
|
|
2104
2675
|
let entries;
|
|
2105
2676
|
try {
|
|
2106
|
-
entries =
|
|
2677
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
2107
2678
|
} catch {
|
|
2108
2679
|
return false;
|
|
2109
2680
|
}
|
|
2110
2681
|
for (const entry of entries) {
|
|
2111
2682
|
if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
|
|
2112
2683
|
continue;
|
|
2113
|
-
const path =
|
|
2684
|
+
const path = join3(dir, entry.name);
|
|
2114
2685
|
if (entry.isDirectory()) {
|
|
2115
2686
|
if (directoryImportsValidate(path))
|
|
2116
2687
|
return true;
|
|
@@ -2118,7 +2689,7 @@ function directoryImportsValidate(dir) {
|
|
|
2118
2689
|
}
|
|
2119
2690
|
if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
|
|
2120
2691
|
continue;
|
|
2121
|
-
if (runtimeImportsValidate(
|
|
2692
|
+
if (runtimeImportsValidate(readFileSync3(path, "utf8")))
|
|
2122
2693
|
return true;
|
|
2123
2694
|
}
|
|
2124
2695
|
return false;
|
|
@@ -2140,4 +2711,4 @@ function runtimeImportsValidate(source) {
|
|
|
2140
2711
|
return false;
|
|
2141
2712
|
}
|
|
2142
2713
|
|
|
2143
|
-
export { errorDoc, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
|
|
2714
|
+
export { projectPath, errorDoc, loadMigrations, readMigrationPlan, loadHeadSnapshot, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
|