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
|
@@ -9,46 +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
27
|
parseReplace,
|
|
22
28
|
planMigration,
|
|
23
29
|
formatPlan,
|
|
24
30
|
parsePlan,
|
|
25
|
-
staleRenames
|
|
26
|
-
|
|
31
|
+
staleRenames,
|
|
32
|
+
sessionFailure,
|
|
33
|
+
finishQuiet,
|
|
34
|
+
assertTargetPolicy,
|
|
35
|
+
assertTargetAlias,
|
|
36
|
+
listTargets,
|
|
37
|
+
selectTarget
|
|
38
|
+
} from "./ttmg09vd.js";
|
|
27
39
|
import {
|
|
28
40
|
contributedFields
|
|
29
41
|
} from "./n0dgs0pb.js";
|
|
30
42
|
import {
|
|
31
43
|
assertValidation,
|
|
32
44
|
writeWouldValidate
|
|
33
|
-
} from "./
|
|
45
|
+
} from "./ze4hxsmy.js";
|
|
46
|
+
import {
|
|
47
|
+
tableOwner,
|
|
48
|
+
stampOwnership
|
|
49
|
+
} from "./xqve9598.js";
|
|
34
50
|
import {
|
|
35
51
|
catalog,
|
|
36
52
|
isDomain
|
|
37
|
-
} from "./
|
|
53
|
+
} from "./ywspezzx.js";
|
|
38
54
|
import {
|
|
39
55
|
snakeCase,
|
|
40
56
|
emittedTypeName
|
|
41
|
-
} from "./
|
|
57
|
+
} from "./rpycarcr.js";
|
|
42
58
|
import {
|
|
43
59
|
ColumnBuilder2
|
|
44
|
-
} from "./
|
|
60
|
+
} from "./ywr8hmey.js";
|
|
45
61
|
import {
|
|
46
62
|
staticNamespace2
|
|
47
|
-
} from "./
|
|
63
|
+
} from "./358zsv4r.js";
|
|
64
|
+
import {
|
|
65
|
+
sha2562
|
|
66
|
+
} from "./nfzc5z0w.js";
|
|
48
67
|
import {
|
|
49
68
|
OkmError2
|
|
50
69
|
} from "./52szzakm.js";
|
|
51
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
|
+
|
|
52
77
|
// src/tooling/errors/registry.ts
|
|
53
78
|
var ERROR_DOCS = [
|
|
54
79
|
{
|
|
@@ -150,8 +175,8 @@ var ERROR_DOCS = [
|
|
|
150
175
|
{
|
|
151
176
|
code: "OKM1102",
|
|
152
177
|
title: "Unfiltered write",
|
|
153
|
-
summary: "
|
|
154
|
-
fix: "Pass a
|
|
178
|
+
summary: "A write with no effective predicate, including undefined values, is OKM1102.",
|
|
179
|
+
fix: "Pass a predicate, or .all(reason). An undefined-only where is the same as {}."
|
|
155
180
|
},
|
|
156
181
|
{
|
|
157
182
|
code: "OKM1104",
|
|
@@ -180,14 +205,14 @@ var ERROR_DOCS = [
|
|
|
180
205
|
{
|
|
181
206
|
code: "OKM1120",
|
|
182
207
|
title: "Unknown field",
|
|
183
|
-
summary: "
|
|
184
|
-
fix: "Use a field from the catalog. The error includes a nearest-name hint when one is close."
|
|
208
|
+
summary: "The name is not a field or an option. timeout and signal go in the second argument of update. Options go first: tx(options, fn).",
|
|
209
|
+
fix: "Use a field from the catalog, or move the option. The error includes a nearest-name hint when one is close."
|
|
185
210
|
},
|
|
186
211
|
{
|
|
187
212
|
code: "OKM1121",
|
|
188
213
|
title: "Object as value",
|
|
189
|
-
summary: "A plain object was passed where a value is required.
|
|
190
|
-
fix: "
|
|
214
|
+
summary: "An empty or() branch is OKM1121. or() takes one array: or([a, b]). A plain object was passed where a value is required.",
|
|
215
|
+
fix: "Give every or() branch a predicate. Pass an array: or([a, b]). Wrap a json value with eq."
|
|
191
216
|
},
|
|
192
217
|
{
|
|
193
218
|
code: "OKM1122",
|
|
@@ -252,8 +277,8 @@ var ERROR_DOCS = [
|
|
|
252
277
|
{
|
|
253
278
|
code: "OKM1510",
|
|
254
279
|
title: "Unsafe migration",
|
|
255
|
-
summary: "A migration has an unresolved lint error, or an override with an empty reason or a code
|
|
256
|
-
fix: "Put `-- okm-allow OKM15xx: reason` on the
|
|
280
|
+
summary: "A migration has an unresolved lint error, or an override with an empty reason or a code no statement in the step triggered.",
|
|
281
|
+
fix: "Put `-- okm-allow OKM15xx: reason` on the step. The code must be one a statement in that step triggered, and the reason must not be empty. The line silences that code on every statement of the step."
|
|
257
282
|
},
|
|
258
283
|
{
|
|
259
284
|
code: "OKM1511",
|
|
@@ -444,8 +469,8 @@ var ERROR_DOCS = [
|
|
|
444
469
|
{
|
|
445
470
|
code: "OKM1542",
|
|
446
471
|
title: "Data statement",
|
|
447
|
-
summary: "A migration contains a data statement outside backfill().",
|
|
448
|
-
fix: "Move
|
|
472
|
+
summary: "A migration contains a data statement outside backfill(), or a DO or CALL whose body is not analysed.",
|
|
473
|
+
fix: "Move insert, update, delete, merge, and truncate into backfill(). DO and CALL are refused because the body is not analysed. Allow OKM1542 with a reason to keep one."
|
|
449
474
|
},
|
|
450
475
|
{
|
|
451
476
|
code: "OKM1543",
|
|
@@ -489,6 +514,18 @@ var ERROR_DOCS = [
|
|
|
489
514
|
summary: "The last migration's catalog does not match the schema.",
|
|
490
515
|
fix: "Run okm generate."
|
|
491
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
|
+
},
|
|
492
529
|
{
|
|
493
530
|
code: "OKM1601",
|
|
494
531
|
title: "Stale typed SQL",
|
|
@@ -528,8 +565,14 @@ var ERROR_DOCS = [
|
|
|
528
565
|
{
|
|
529
566
|
code: "OKM1707",
|
|
530
567
|
title: "RLS role",
|
|
531
|
-
summary: "
|
|
532
|
-
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."
|
|
533
576
|
},
|
|
534
577
|
{
|
|
535
578
|
code: "OKM1801",
|
|
@@ -612,7 +655,7 @@ var ERROR_DOCS = [
|
|
|
612
655
|
{
|
|
613
656
|
code: "OKM1825",
|
|
614
657
|
title: "Missing privilege",
|
|
615
|
-
summary: "
|
|
658
|
+
summary: "A role lacks a privilege on a managed object.",
|
|
616
659
|
fix: "Grant the privilege, or change the role. okm doctor reports the object and the privilege."
|
|
617
660
|
},
|
|
618
661
|
{
|
|
@@ -686,6 +729,12 @@ var ERROR_DOCS = [
|
|
|
686
729
|
title: "Target not named",
|
|
687
730
|
summary: "Several targets are configured and the command was not given --target.",
|
|
688
731
|
fix: "Pass --target <name>. The command does not guess."
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
code: "OKM1854",
|
|
735
|
+
title: "Pooler session",
|
|
736
|
+
summary: "Two statements on the same connection saw different backend pids. A transaction-mode pooler is in the way.",
|
|
737
|
+
fix: "Use a direct connection, or pass --allow-pooler."
|
|
689
738
|
}
|
|
690
739
|
];
|
|
691
740
|
var byCode;
|
|
@@ -701,9 +750,53 @@ function indexDocs() {
|
|
|
701
750
|
return index;
|
|
702
751
|
}
|
|
703
752
|
|
|
704
|
-
// src/tooling/migrate/
|
|
753
|
+
// src/tooling/migrate/files.ts
|
|
705
754
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
706
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";
|
|
707
800
|
|
|
708
801
|
// src/tooling/migrate/lint-rules.ts
|
|
709
802
|
var INTEGER_WIDTH = {
|
|
@@ -740,7 +833,12 @@ var STEP_RULES = [
|
|
|
740
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")),
|
|
741
834
|
rule("OKM1537", "locking", "error", "SET NOT NULL locks the table while it scans for nulls.", (input) => hit(unsafeSetNotNull(input), "sets NOT NULL")),
|
|
742
835
|
rule("OKM1538", "locking", "warning", "A type change that rewrites the table locks it for the rewrite.", (input) => typeChange(input, "rewrite")),
|
|
743
|
-
rule("OKM1542", "data-dependent", "error", "A data statement outside backfill() is not a migration step.", (input) =>
|
|
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
|
+
})
|
|
744
842
|
];
|
|
745
843
|
var COLUMN_RULES = [
|
|
746
844
|
{
|
|
@@ -799,13 +897,159 @@ function normalized(sql) {
|
|
|
799
897
|
}
|
|
800
898
|
return out;
|
|
801
899
|
}
|
|
802
|
-
function
|
|
803
|
-
|
|
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)
|
|
804
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
|
+
}
|
|
1040
|
+
function dataStatement(step) {
|
|
805
1041
|
const text = normalized(step.sql);
|
|
806
|
-
if (/^(?:
|
|
807
|
-
return
|
|
808
|
-
|
|
1042
|
+
if (/^(?:do|call)\b/.test(text))
|
|
1043
|
+
return ["the body is not analysed"];
|
|
1044
|
+
if (step.backfill !== undefined)
|
|
1045
|
+
return [];
|
|
1046
|
+
if (/^(?:insert|update|delete|merge|truncate)\b/.test(text)) {
|
|
1047
|
+
return ["changes rows outside backfill()"];
|
|
1048
|
+
}
|
|
1049
|
+
if (/^with\b/.test(text) && /\b(?:insert|update|delete|merge)\b/.test(text)) {
|
|
1050
|
+
return ["changes rows outside backfill()"];
|
|
1051
|
+
}
|
|
1052
|
+
return [];
|
|
809
1053
|
}
|
|
810
1054
|
function matches(step, kind, pattern) {
|
|
811
1055
|
return pattern.test(normalized(step.sql)) || step.kind === kind;
|
|
@@ -980,9 +1224,10 @@ function validates(input, kind) {
|
|
|
980
1224
|
return true;
|
|
981
1225
|
return false;
|
|
982
1226
|
}
|
|
1227
|
+
var TYPE_CHANGE = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+(?:set data type|type)\b/;
|
|
983
1228
|
function typeChange(input, mode) {
|
|
984
1229
|
const text = normalized(input.step.sql);
|
|
985
|
-
const changing =
|
|
1230
|
+
const changing = TYPE_CHANGE.test(text) || input.step.kind === "set-column-type";
|
|
986
1231
|
if (!changing)
|
|
987
1232
|
return [];
|
|
988
1233
|
const compared = columnTypes(input);
|
|
@@ -1009,7 +1254,7 @@ function typeChange(input, mode) {
|
|
|
1009
1254
|
}
|
|
1010
1255
|
function columnTypes(input) {
|
|
1011
1256
|
const text = normalized(input.step.sql);
|
|
1012
|
-
const match =
|
|
1257
|
+
const match = TYPE_CHANGE.exec(text);
|
|
1013
1258
|
const table = lastName(match?.[1]);
|
|
1014
1259
|
const column = lastName(match?.[2]);
|
|
1015
1260
|
if (table === undefined || column === undefined)
|
|
@@ -1143,15 +1388,15 @@ function lintCatalog(source) {
|
|
|
1143
1388
|
return findings;
|
|
1144
1389
|
}
|
|
1145
1390
|
function lintMigrationDirectory(directory, pending) {
|
|
1146
|
-
if (!
|
|
1391
|
+
if (!existsSync2(directory))
|
|
1147
1392
|
return [];
|
|
1148
|
-
const names =
|
|
1393
|
+
const names = readdirSync2(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
1149
1394
|
let before = catalog([]);
|
|
1150
1395
|
const findings = [];
|
|
1151
1396
|
for (const file of names) {
|
|
1152
1397
|
const id = file.slice(0, -".sql".length);
|
|
1153
|
-
const plan =
|
|
1154
|
-
const after = parseCatalog2(
|
|
1398
|
+
const plan = readMigrationPlan(join2(directory, file));
|
|
1399
|
+
const after = parseCatalog2(readFileSync2(join2(directory, `${id}.catalog.json`), "utf8"));
|
|
1155
1400
|
if (pending === undefined || pending.has(id)) {
|
|
1156
1401
|
for (const item of lintPlan(plan, before, after)) {
|
|
1157
1402
|
findings.push({ ...item, place: `${id} ${item.place}` });
|
|
@@ -1178,34 +1423,76 @@ function lintRefusal(findings) {
|
|
|
1178
1423
|
});
|
|
1179
1424
|
}
|
|
1180
1425
|
function lintStep(step, number, before, after, existingTables, earlier, later) {
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1426
|
+
const texts = statementTexts(step);
|
|
1427
|
+
const multi = texts.length > 1;
|
|
1428
|
+
const earlierStatements = earlier.flatMap(contextSteps);
|
|
1429
|
+
const laterStatements = later.flatMap(contextSteps);
|
|
1430
|
+
const perStatement = [];
|
|
1431
|
+
const triggered = new Set;
|
|
1432
|
+
texts.forEach((sql, index) => {
|
|
1433
|
+
const statement = multi ? withoutKind(step, sql) : withSql(step, sql);
|
|
1434
|
+
const hits = [];
|
|
1435
|
+
for (const rule of STEP_RULES) {
|
|
1436
|
+
for (const reason of rule.check({
|
|
1437
|
+
step: statement,
|
|
1438
|
+
before,
|
|
1439
|
+
after,
|
|
1440
|
+
existingTables,
|
|
1441
|
+
earlier: earlierStatements,
|
|
1442
|
+
later: laterStatements
|
|
1443
|
+
})) {
|
|
1444
|
+
const hit = { code: rule.code, severity: rule.severity, reason };
|
|
1445
|
+
hits.push(hit);
|
|
1446
|
+
triggered.add(hit.code);
|
|
1447
|
+
}
|
|
1185
1448
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1449
|
+
const place = multi ? `step ${String(number)} statement ${String(index + 1)}` : `step ${String(number)}`;
|
|
1450
|
+
perStatement.push({ place, hits });
|
|
1451
|
+
});
|
|
1188
1452
|
const silenced = new Set;
|
|
1189
1453
|
const findings = [];
|
|
1190
|
-
const
|
|
1454
|
+
const stepPlace = `step ${String(number)}`;
|
|
1191
1455
|
for (const allow of step.allows ?? []) {
|
|
1192
1456
|
if (allow.reason.trim().length === 0 || allow.code.length === 0) {
|
|
1193
|
-
findings.push(finding("error", "OKM1510",
|
|
1457
|
+
findings.push(finding("error", "OKM1510", stepPlace, "override needs a reason"));
|
|
1194
1458
|
continue;
|
|
1195
1459
|
}
|
|
1196
1460
|
if (!triggered.has(allow.code)) {
|
|
1197
|
-
findings.push(finding("error", "OKM1510",
|
|
1461
|
+
findings.push(finding("error", "OKM1510", stepPlace, `override ${allow.code} does not match this statement`));
|
|
1198
1462
|
continue;
|
|
1199
1463
|
}
|
|
1200
1464
|
silenced.add(allow.code);
|
|
1201
1465
|
}
|
|
1202
|
-
for (const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1466
|
+
for (const item of perStatement) {
|
|
1467
|
+
for (const hit of item.hits) {
|
|
1468
|
+
if (silenced.has(hit.code))
|
|
1469
|
+
continue;
|
|
1470
|
+
findings.push(finding(hit.severity, hit.code, item.place, hit.reason));
|
|
1471
|
+
}
|
|
1206
1472
|
}
|
|
1207
1473
|
return findings;
|
|
1208
1474
|
}
|
|
1475
|
+
function statementTexts(step) {
|
|
1476
|
+
const statements = sqlStatements(step.sql);
|
|
1477
|
+
return statements.length === 0 ? [step.sql] : statements;
|
|
1478
|
+
}
|
|
1479
|
+
function contextSteps(step) {
|
|
1480
|
+
const statements = sqlStatements(step.sql);
|
|
1481
|
+
if (statements.length === 0)
|
|
1482
|
+
return [step];
|
|
1483
|
+
if (statements.length === 1 && statements[0] === step.sql)
|
|
1484
|
+
return [step];
|
|
1485
|
+
return statements.map((sql) => withSql(step, sql));
|
|
1486
|
+
}
|
|
1487
|
+
function withSql(step, sql) {
|
|
1488
|
+
if (sql === step.sql)
|
|
1489
|
+
return step;
|
|
1490
|
+
return { ...step, sql };
|
|
1491
|
+
}
|
|
1492
|
+
function withoutKind(step, sql) {
|
|
1493
|
+
const { kind: _kind, ...rest } = step;
|
|
1494
|
+
return { ...rest, sql };
|
|
1495
|
+
}
|
|
1209
1496
|
function finding(severity, code, place, reason) {
|
|
1210
1497
|
const doc = errorDoc(code);
|
|
1211
1498
|
if (doc === undefined)
|
|
@@ -1227,161 +1514,9 @@ function columnPlace(column) {
|
|
|
1227
1514
|
return `${column.identity.parent.name}.${column.identity.name}`;
|
|
1228
1515
|
}
|
|
1229
1516
|
|
|
1230
|
-
// src/tooling/migrate/policy.ts
|
|
1231
|
-
var READ = new Set([
|
|
1232
|
-
"plan",
|
|
1233
|
-
"status",
|
|
1234
|
-
"check",
|
|
1235
|
-
"drift",
|
|
1236
|
-
"verify",
|
|
1237
|
-
"pull",
|
|
1238
|
-
"catalog-export",
|
|
1239
|
-
"inspect"
|
|
1240
|
-
]);
|
|
1241
|
-
var ALLOWED = new Set(["expand", "reference", "provision"]);
|
|
1242
|
-
var FORWARD_ONLY = new Set(["drop", "rollback"]);
|
|
1243
|
-
function assertTargetPolicy(target, operation, allowProtected = false) {
|
|
1244
|
-
if (FORWARD_ONLY.has(operation)) {
|
|
1245
|
-
throw new OkmError2("invalid", "There is no down migration.", {
|
|
1246
|
-
fix: { summary: "Recovery is a new forward migration." }
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
if (!target.protected || allowProtected)
|
|
1250
|
-
return;
|
|
1251
|
-
if (READ.has(operation) || ALLOWED.has(operation))
|
|
1252
|
-
return;
|
|
1253
|
-
throw new OkmError2("OKM1850", `${operation} is blocked on protected target ${target.name}.`, {
|
|
1254
|
-
kind: "forbidden",
|
|
1255
|
-
fix: {
|
|
1256
|
-
summary: "Run a read-only command or an expand migration, or pass --allow-protected for this invocation."
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
function assertTargetAlias(targets) {
|
|
1261
|
-
const groups = new Map;
|
|
1262
|
-
for (const target of targets) {
|
|
1263
|
-
const key = `${target.host}
|
|
1264
|
-
${target.port}
|
|
1265
|
-
${target.database}`;
|
|
1266
|
-
const group = groups.get(key);
|
|
1267
|
-
if (group === undefined)
|
|
1268
|
-
groups.set(key, [target]);
|
|
1269
|
-
else
|
|
1270
|
-
group.push(target);
|
|
1271
|
-
}
|
|
1272
|
-
for (const group of groups.values()) {
|
|
1273
|
-
const protectedTarget = group.find((target) => target.protected);
|
|
1274
|
-
const open = group.find((target) => !target.protected);
|
|
1275
|
-
if (protectedTarget !== undefined && open !== undefined) {
|
|
1276
|
-
throw new OkmError2("OKM1852", `${open.name} and ${protectedTarget.name} resolve to the same database and disagree about protection.`, {
|
|
1277
|
-
fix: {
|
|
1278
|
-
summary: "Give those targets the same protection flag. okm check and okm doctor both report this."
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
function assertDirectConnection(url, allowPooler) {
|
|
1285
|
-
if (allowPooler)
|
|
1286
|
-
return;
|
|
1287
|
-
const endpoint = endpointOf(url);
|
|
1288
|
-
const parsed = new URL(url);
|
|
1289
|
-
const host = endpoint.host;
|
|
1290
|
-
if (host.includes("pooler") || host.includes("pgbouncer") || host.includes("-pooler") || endpoint.port === "6543" || parsed.searchParams.get("pgbouncer") === "true") {
|
|
1291
|
-
throw new OkmError2("invalid", `Refusing pooler host ${host}. Apply needs a direct connection.`, { fix: { summary: "Use a direct connection, or pass --allow-pooler." } });
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
function listTargets(config) {
|
|
1295
|
-
if (config.database !== undefined && config.targets !== undefined) {
|
|
1296
|
-
throw new OkmError2("invalid", "Set database or targets, not both.", {
|
|
1297
|
-
fix: { summary: "database is the one target named default. Use targets for more than one." }
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
const entries = [];
|
|
1301
|
-
if (config.database !== undefined)
|
|
1302
|
-
entries.push(["default", config.database]);
|
|
1303
|
-
if (config.targets !== undefined) {
|
|
1304
|
-
for (const name of Object.keys(config.targets).sort()) {
|
|
1305
|
-
const input = config.targets[name];
|
|
1306
|
-
if (input !== undefined)
|
|
1307
|
-
entries.push([name, input]);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
return entries.map(([name, input]) => resolveTarget(name, input));
|
|
1311
|
-
}
|
|
1312
|
-
function selectTarget(config, name) {
|
|
1313
|
-
const targets = listTargets(config);
|
|
1314
|
-
if (targets.length === 0) {
|
|
1315
|
-
throw new OkmError2("OKM1845", "No target is configured.", {
|
|
1316
|
-
fix: { summary: "Set database or targets in okmodel.config.ts." }
|
|
1317
|
-
});
|
|
1318
|
-
}
|
|
1319
|
-
if (name === undefined) {
|
|
1320
|
-
if (targets.length === 1) {
|
|
1321
|
-
const only = targets[0];
|
|
1322
|
-
if (only !== undefined)
|
|
1323
|
-
return only;
|
|
1324
|
-
}
|
|
1325
|
-
throw new OkmError2("OKM1853", "Several targets are configured and --target was not passed.", {
|
|
1326
|
-
fix: { summary: "Pass --target <name>. The command does not guess." }
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
const found = targets.find((target) => target.name === name);
|
|
1330
|
-
if (found === undefined) {
|
|
1331
|
-
throw new OkmError2("OKM1845", `Target ${name} is not configured.`, {
|
|
1332
|
-
fix: { summary: "Pass a name from targets, or database for the target named default." }
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
return found;
|
|
1336
|
-
}
|
|
1337
|
-
function resolveTarget(name, input) {
|
|
1338
|
-
if (typeof input !== "string" && isReplicaTarget(input)) {
|
|
1339
|
-
throw new OkmError2("OKM1845", `Target ${name} is a replica. Migrations run on the primary only.`, {
|
|
1340
|
-
fix: {
|
|
1341
|
-
summary: "Set database or targets to the primary URL. A replica is not a migration target."
|
|
1342
|
-
}
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
const url = typeof input === "string" ? input : input.url;
|
|
1346
|
-
const protectedTarget = typeof input === "string" ? false : input.protected === true;
|
|
1347
|
-
if (url === undefined || url.length === 0) {
|
|
1348
|
-
throw new OkmError2("OKM1845", `Target ${name} has no url.`, {
|
|
1349
|
-
fix: { summary: "Set the target url. A target is not inferred from the environment." }
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
const endpoint = endpointOf(url);
|
|
1353
|
-
return {
|
|
1354
|
-
name,
|
|
1355
|
-
url,
|
|
1356
|
-
protected: protectedTarget,
|
|
1357
|
-
host: endpoint.host,
|
|
1358
|
-
port: endpoint.port,
|
|
1359
|
-
database: endpoint.database
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
function isReplicaTarget(input) {
|
|
1363
|
-
return "primary" in input || "replicas" in input || "weight" in input || "pool" in input;
|
|
1364
|
-
}
|
|
1365
|
-
function endpointOf(url) {
|
|
1366
|
-
let parsed;
|
|
1367
|
-
try {
|
|
1368
|
-
parsed = new URL(url);
|
|
1369
|
-
} catch {
|
|
1370
|
-
throw new OkmError2("OKM1845", "The target URL is not a Postgres URL.", {
|
|
1371
|
-
fix: { summary: "Pass a postgres:// URL for the target." }
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
|
|
1375
|
-
return {
|
|
1376
|
-
host: parsed.hostname.toLowerCase(),
|
|
1377
|
-
port: parsed.port.length > 0 ? parsed.port : "5432",
|
|
1378
|
-
database
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
1517
|
// src/tooling/migrate/project.ts
|
|
1383
|
-
import { existsSync as
|
|
1384
|
-
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";
|
|
1385
1520
|
import { pathToFileURL } from "node:url";
|
|
1386
1521
|
|
|
1387
1522
|
// src/dialects/pg/declarations.ts
|
|
@@ -1423,7 +1558,7 @@ function sqlIdentifier(name, casing) {
|
|
|
1423
1558
|
}
|
|
1424
1559
|
|
|
1425
1560
|
// src/dialects/pg/emit.ts
|
|
1426
|
-
function emitRowTypes(source) {
|
|
1561
|
+
function emitRowTypes(source, jsonTypes) {
|
|
1427
1562
|
const lines = [
|
|
1428
1563
|
"/**",
|
|
1429
1564
|
" * Row types emitted from the schema.",
|
|
@@ -1441,13 +1576,13 @@ function emitRowTypes(source) {
|
|
|
1441
1576
|
inserts.push(` readonly ${key}: ${name}Insert;`);
|
|
1442
1577
|
updates.push(` readonly ${key}: ${name}Update;`);
|
|
1443
1578
|
lines.push(`export interface ${name} {`);
|
|
1444
|
-
lines.push(fields(item, "row", source));
|
|
1579
|
+
lines.push(fields(item, "row", source, jsonTypes));
|
|
1445
1580
|
lines.push("}", "");
|
|
1446
1581
|
lines.push(`export interface ${name}Insert {`);
|
|
1447
|
-
lines.push(fields(item, "insert", source));
|
|
1582
|
+
lines.push(fields(item, "insert", source, jsonTypes));
|
|
1448
1583
|
lines.push("}", "");
|
|
1449
1584
|
lines.push(`export interface ${name}Update {`);
|
|
1450
|
-
lines.push(fields(item, "update", source));
|
|
1585
|
+
lines.push(fields(item, "update", source, jsonTypes));
|
|
1451
1586
|
lines.push("}", "");
|
|
1452
1587
|
}
|
|
1453
1588
|
lines.push("export interface Rows {");
|
|
@@ -1462,14 +1597,92 @@ function emitRowTypes(source) {
|
|
|
1462
1597
|
lines.push(updates.join(`
|
|
1463
1598
|
`));
|
|
1464
1599
|
lines.push("}");
|
|
1465
|
-
|
|
1600
|
+
const body = `${lines.join(`
|
|
1466
1601
|
`)}
|
|
1467
1602
|
`;
|
|
1603
|
+
const aliases = [];
|
|
1604
|
+
if (body.includes("jsonb<"))
|
|
1605
|
+
aliases.push("export type jsonb<T> = T;");
|
|
1606
|
+
if (body.includes("json<"))
|
|
1607
|
+
aliases.push("export type json<T> = T;");
|
|
1608
|
+
if (aliases.length === 0)
|
|
1609
|
+
return body;
|
|
1610
|
+
return `${aliases.join(`
|
|
1611
|
+
`)}
|
|
1612
|
+
|
|
1613
|
+
${body}`;
|
|
1468
1614
|
}
|
|
1469
1615
|
function propertyName(name) {
|
|
1470
1616
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : JSON.stringify(name);
|
|
1471
1617
|
}
|
|
1472
|
-
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) {
|
|
1473
1686
|
const printed = [];
|
|
1474
1687
|
const options = item.options;
|
|
1475
1688
|
const entries = Object.entries(item.columns);
|
|
@@ -1487,7 +1700,7 @@ function fields(item, kind, source) {
|
|
|
1487
1700
|
const state = builder.state;
|
|
1488
1701
|
if (kind === "row") {
|
|
1489
1702
|
if (!state.hidden) {
|
|
1490
|
-
printed.push(` readonly ${field}: ${valueType(state,
|
|
1703
|
+
printed.push(` readonly ${field}: ${valueType(state, jsonTypes?.get(`${item.name}.${field}`))};`);
|
|
1491
1704
|
}
|
|
1492
1705
|
continue;
|
|
1493
1706
|
}
|
|
@@ -1496,7 +1709,8 @@ function fields(item, kind, source) {
|
|
|
1496
1709
|
continue;
|
|
1497
1710
|
}
|
|
1498
1711
|
const mode = kind === "update" ? "optional" : written;
|
|
1499
|
-
|
|
1712
|
+
const label = valueType(state, jsonTypes?.get(`${item.name}.${field}`));
|
|
1713
|
+
printed.push(mode === "optional" ? ` readonly ${field}?: ${label};` : ` readonly ${field}: ${label};`);
|
|
1500
1714
|
}
|
|
1501
1715
|
return printed.join(`
|
|
1502
1716
|
`);
|
|
@@ -1515,8 +1729,8 @@ function updateMode(state) {
|
|
|
1515
1729
|
return "omit";
|
|
1516
1730
|
return "optional";
|
|
1517
1731
|
}
|
|
1518
|
-
function valueType(state,
|
|
1519
|
-
let label = scalarLabel(state);
|
|
1732
|
+
function valueType(state, jsonArg) {
|
|
1733
|
+
let label = scalarLabel(state, jsonArg);
|
|
1520
1734
|
if (state.dims > 0) {
|
|
1521
1735
|
for (let rank = 0;rank < state.dims; rank += 1) {
|
|
1522
1736
|
label = `readonly (${label})[]`;
|
|
@@ -1525,12 +1739,9 @@ function valueType(state, mode) {
|
|
|
1525
1739
|
if (state.nullable) {
|
|
1526
1740
|
label += " | null";
|
|
1527
1741
|
}
|
|
1528
|
-
if (mode === "optional") {
|
|
1529
|
-
label += " | undefined";
|
|
1530
|
-
}
|
|
1531
1742
|
return label;
|
|
1532
1743
|
}
|
|
1533
|
-
function scalarLabel(state) {
|
|
1744
|
+
function scalarLabel(state, jsonArg) {
|
|
1534
1745
|
if (state.picklist !== undefined) {
|
|
1535
1746
|
return state.picklist.values.map((value) => JSON.stringify(value)).join(" | ");
|
|
1536
1747
|
}
|
|
@@ -1563,8 +1774,9 @@ function scalarLabel(state) {
|
|
|
1563
1774
|
case "bytea":
|
|
1564
1775
|
return "Uint8Array";
|
|
1565
1776
|
case "json":
|
|
1777
|
+
return `json<${jsonArg ?? "unknown"}>`;
|
|
1566
1778
|
case "jsonb":
|
|
1567
|
-
return "unknown"
|
|
1779
|
+
return `jsonb<${jsonArg ?? "unknown"}>`;
|
|
1568
1780
|
case "timestamptz":
|
|
1569
1781
|
return "Temporal.Instant";
|
|
1570
1782
|
case "timestamp":
|
|
@@ -1608,17 +1820,34 @@ function baseName(dataType) {
|
|
|
1608
1820
|
}
|
|
1609
1821
|
|
|
1610
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
|
+
}
|
|
1611
1836
|
var PROVENANCE = { origin: "file", name: "roles" };
|
|
1612
1837
|
var RELATION = ["SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
1613
|
-
var
|
|
1838
|
+
var READ = ["SELECT"];
|
|
1614
1839
|
var ROUTINE = ["EXECUTE"];
|
|
1615
1840
|
var SEQUENCE = ["USAGE", "SELECT"];
|
|
1616
|
-
function attachRoles(source, input) {
|
|
1617
|
-
return catalog([...source.objects, ...contributeRoles(input, source.objects)]);
|
|
1841
|
+
function attachRoles(source, input, exposure) {
|
|
1842
|
+
return catalog([...source.objects, ...contributeRoles(input, source.objects, exposure)]);
|
|
1618
1843
|
}
|
|
1619
|
-
function contributeRoles(input, built) {
|
|
1844
|
+
function contributeRoles(input, built, exposure) {
|
|
1620
1845
|
const migration = requireName(input.migration, "migration");
|
|
1621
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
|
+
}
|
|
1622
1851
|
const managed = new Map;
|
|
1623
1852
|
for (const role of input.managed ?? []) {
|
|
1624
1853
|
const name = requireName(role.name, "managed role");
|
|
@@ -1626,6 +1855,10 @@ function contributeRoles(input, built) {
|
|
|
1626
1855
|
}
|
|
1627
1856
|
const objects = [];
|
|
1628
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"));
|
|
1629
1862
|
for (const name of [...names].sort()) {
|
|
1630
1863
|
const declaration = managed.get(name);
|
|
1631
1864
|
objects.push(roleObject({
|
|
@@ -1640,8 +1873,10 @@ function contributeRoles(input, built) {
|
|
|
1640
1873
|
}
|
|
1641
1874
|
const roleIdentity = (name) => ({ kind: "role", name });
|
|
1642
1875
|
const namespaces = new Set;
|
|
1876
|
+
const exposed = indexExposure(exposure);
|
|
1877
|
+
const columnGrants = new Map;
|
|
1643
1878
|
for (const object of built) {
|
|
1644
|
-
if (object.owner === "ignored")
|
|
1879
|
+
if (object.owner === "ignored" || object.owner === "external")
|
|
1645
1880
|
continue;
|
|
1646
1881
|
const ref = grantRef(object);
|
|
1647
1882
|
if (ref === undefined)
|
|
@@ -1659,6 +1894,7 @@ function contributeRoles(input, built) {
|
|
|
1659
1894
|
}));
|
|
1660
1895
|
}
|
|
1661
1896
|
}
|
|
1897
|
+
const relations = built.filter((object) => object.owner !== "ignored" && object.owner !== "external" && (object.kind === "table" || object.kind === "view" || object.kind === "materializedView"));
|
|
1662
1898
|
const schemas = namespaces.size === 0 ? ["public"] : [...namespaces].sort();
|
|
1663
1899
|
for (const name of schemas) {
|
|
1664
1900
|
const namespace = staticNamespace2(name);
|
|
@@ -1679,6 +1915,45 @@ function contributeRoles(input, built) {
|
|
|
1679
1915
|
}));
|
|
1680
1916
|
}
|
|
1681
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
|
+
}));
|
|
1682
1957
|
}
|
|
1683
1958
|
return objects;
|
|
1684
1959
|
}
|
|
@@ -1712,7 +1987,7 @@ function privilegesFor(kind) {
|
|
|
1712
1987
|
if (kind === "sequence")
|
|
1713
1988
|
return SEQUENCE;
|
|
1714
1989
|
if (kind === "materializedView")
|
|
1715
|
-
return
|
|
1990
|
+
return READ;
|
|
1716
1991
|
if (kind === "namespace")
|
|
1717
1992
|
return [];
|
|
1718
1993
|
return RELATION;
|
|
@@ -1720,6 +1995,137 @@ function privilegesFor(kind) {
|
|
|
1720
1995
|
function objectIdentity(object) {
|
|
1721
1996
|
return object.identity;
|
|
1722
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
|
+
}
|
|
1723
2129
|
|
|
1724
2130
|
// src/tooling/migrate/estimate.ts
|
|
1725
2131
|
var LARGE_TABLE_ROWS = 1e6;
|
|
@@ -1759,31 +2165,49 @@ async function readRowEstimates(url, schema, tables) {
|
|
|
1759
2165
|
return map;
|
|
1760
2166
|
} catch (error) {
|
|
1761
2167
|
if (began)
|
|
1762
|
-
await connection.execute("rollback")
|
|
1763
|
-
|
|
1764
|
-
});
|
|
1765
|
-
throw error;
|
|
2168
|
+
await finishQuiet(() => connection.execute("rollback"));
|
|
2169
|
+
throw sessionFailure(error) ?? error;
|
|
1766
2170
|
} finally {
|
|
1767
|
-
await connection.release();
|
|
2171
|
+
await finishQuiet(() => connection.release());
|
|
1768
2172
|
}
|
|
1769
2173
|
} finally {
|
|
1770
|
-
await pool.close();
|
|
2174
|
+
await finishQuiet(() => pool.close());
|
|
1771
2175
|
}
|
|
1772
2176
|
}
|
|
1773
|
-
function annotateLock(step, estimates) {
|
|
2177
|
+
function annotateLock(step, estimates, aliases = new Map) {
|
|
1774
2178
|
const pieces = lockPieces(step);
|
|
1775
2179
|
const text = withBatches(step, pieces.map((piece) => {
|
|
1776
2180
|
if (piece.table === undefined)
|
|
1777
2181
|
return piece.mode;
|
|
1778
|
-
return `${piece.mode} on ${piece.table}, ${estimatePhrase(
|
|
1779
|
-
}).join("; "), estimates);
|
|
2182
|
+
return `${piece.mode} on ${piece.table}, ${estimatePhrase(lookupEstimate(piece.table, estimates, aliases))}`;
|
|
2183
|
+
}).join("; "), estimates, aliases);
|
|
1780
2184
|
if (step.safeRewrite === true)
|
|
1781
2185
|
return `${text}; safe rewrite applied`;
|
|
1782
|
-
if (exclusiveOverLarge(pieces, estimates)) {
|
|
2186
|
+
if (exclusiveOverLarge(pieces, estimates, aliases)) {
|
|
1783
2187
|
return `${text}; note: more than ${String(LARGE_TABLE_ROWS)} estimated rows`;
|
|
1784
2188
|
}
|
|
1785
2189
|
return text;
|
|
1786
2190
|
}
|
|
2191
|
+
function renameAliases(steps) {
|
|
2192
|
+
const map = new Map;
|
|
2193
|
+
for (const step of steps) {
|
|
2194
|
+
if (step.kind !== "rename-table")
|
|
2195
|
+
continue;
|
|
2196
|
+
const match = /^alter table "(?:(?:[^"]|"")*)"\."((?:[^"]|"")*)" rename to "((?:[^"]|"")*)"/.exec(step.sql);
|
|
2197
|
+
const from = match?.[1]?.replaceAll('""', '"');
|
|
2198
|
+
const to = match?.[2]?.replaceAll('""', '"');
|
|
2199
|
+
if (from !== undefined && to !== undefined)
|
|
2200
|
+
map.set(to, from);
|
|
2201
|
+
}
|
|
2202
|
+
return map;
|
|
2203
|
+
}
|
|
2204
|
+
function lookupEstimate(table, estimates, aliases) {
|
|
2205
|
+
const direct = estimates.get(table);
|
|
2206
|
+
if (direct !== undefined)
|
|
2207
|
+
return direct;
|
|
2208
|
+
const previous = aliases.get(table);
|
|
2209
|
+
return previous === undefined ? undefined : estimates.get(previous);
|
|
2210
|
+
}
|
|
1787
2211
|
function aboutRows(count) {
|
|
1788
2212
|
const rounded = Math.max(0, Math.round(count));
|
|
1789
2213
|
if (rounded < 1000)
|
|
@@ -1803,12 +2227,12 @@ function aboutRows(count) {
|
|
|
1803
2227
|
}
|
|
1804
2228
|
return `about ${String(rounded)} rows`;
|
|
1805
2229
|
}
|
|
1806
|
-
function withBatches(step, text, estimates) {
|
|
2230
|
+
function withBatches(step, text, estimates, aliases = new Map) {
|
|
1807
2231
|
const batch = step.backfill?.batch;
|
|
1808
2232
|
if (batch === undefined)
|
|
1809
2233
|
return text;
|
|
1810
2234
|
const name = step.tables?.[0];
|
|
1811
|
-
const estimate = name === undefined ? undefined :
|
|
2235
|
+
const estimate = name === undefined ? undefined : lookupEstimate(name, estimates, aliases);
|
|
1812
2236
|
if (estimate === undefined || estimate.kind !== "rows")
|
|
1813
2237
|
return text;
|
|
1814
2238
|
return `${text}, about ${String(aboutBatchCount(estimate.reltuples, batch))} batches`;
|
|
@@ -1870,11 +2294,11 @@ function lockPieces(step) {
|
|
|
1870
2294
|
}
|
|
1871
2295
|
return pieces;
|
|
1872
2296
|
}
|
|
1873
|
-
function exclusiveOverLarge(pieces, estimates) {
|
|
2297
|
+
function exclusiveOverLarge(pieces, estimates, aliases = new Map) {
|
|
1874
2298
|
for (const piece of pieces) {
|
|
1875
2299
|
if (piece.mode !== "ACCESS EXCLUSIVE" || piece.table === undefined)
|
|
1876
2300
|
continue;
|
|
1877
|
-
const estimate =
|
|
2301
|
+
const estimate = lookupEstimate(piece.table, estimates, aliases);
|
|
1878
2302
|
if (estimate === undefined || estimate.kind !== "rows")
|
|
1879
2303
|
continue;
|
|
1880
2304
|
if (estimate.reltuples > LARGE_TABLE_ROWS)
|
|
@@ -1904,15 +2328,17 @@ async function buildProject(cwd) {
|
|
|
1904
2328
|
}
|
|
1905
2329
|
async function generateProject(cwd, name, flags) {
|
|
1906
2330
|
const prepared = await prepare(cwd, name, flags);
|
|
2331
|
+
if (name === "init")
|
|
2332
|
+
writeGitAttributes(cwd);
|
|
1907
2333
|
writeArtifact(cwd, prepared.config, prepared.built);
|
|
1908
2334
|
if (prepared.plan.steps.length === 0)
|
|
1909
2335
|
return;
|
|
1910
|
-
const directory =
|
|
2336
|
+
const directory = projectPath(cwd, prepared.config.migrations ?? "migrations");
|
|
1911
2337
|
mkdirSync(directory, { recursive: true });
|
|
1912
2338
|
const base = `${nextNumber(directory)}_${safeName(name)}`;
|
|
1913
|
-
const sqlPath =
|
|
2339
|
+
const sqlPath = join3(directory, `${base}.sql`);
|
|
1914
2340
|
writeFileSync(sqlPath, formatPlan(prepared.plan));
|
|
1915
|
-
writeFileSync(
|
|
2341
|
+
writeFileSync(join3(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
|
|
1916
2342
|
return { path: sqlPath, findings: lintPrepared(prepared) };
|
|
1917
2343
|
}
|
|
1918
2344
|
async function planProject(cwd, name, flags, invoke) {
|
|
@@ -1935,9 +2361,9 @@ async function checkProject(cwd, flags) {
|
|
|
1935
2361
|
const targets = listTargets(opened.config);
|
|
1936
2362
|
assertTargetAlias(targets);
|
|
1937
2363
|
if (opened.config.tables !== undefined) {
|
|
1938
|
-
const directory =
|
|
1939
|
-
const files =
|
|
1940
|
-
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");
|
|
1941
2367
|
const missing = unlistedTableFiles(source, files);
|
|
1942
2368
|
if (missing.length > 0) {
|
|
1943
2369
|
throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
|
|
@@ -1956,8 +2382,9 @@ async function checkProject(cwd, flags) {
|
|
|
1956
2382
|
const target = selectTarget(opened.config, named);
|
|
1957
2383
|
assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
|
|
1958
2384
|
assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
|
|
1959
|
-
const { assertAuthorDrift } = await import("./
|
|
1960
|
-
|
|
2385
|
+
const { assertAuthorDrift } = await import("./1f0xa3cb.js");
|
|
2386
|
+
const allowPooler = flags?.allowPooler === true || opened.config.allowPooler === true;
|
|
2387
|
+
await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles, allowPooler);
|
|
1961
2388
|
return findings;
|
|
1962
2389
|
}
|
|
1963
2390
|
function unlistedTableFiles(schemaSource, fileNames) {
|
|
@@ -2016,7 +2443,8 @@ async function displayPlan(config, plan, invoke) {
|
|
|
2016
2443
|
} catch {
|
|
2017
2444
|
return offline;
|
|
2018
2445
|
}
|
|
2019
|
-
|
|
2446
|
+
const aliases = renameAliases(plan.steps);
|
|
2447
|
+
return formatPlan(plan, (step) => annotateLock(step, estimates, aliases));
|
|
2020
2448
|
}
|
|
2021
2449
|
function lintOpened(opened) {
|
|
2022
2450
|
const plan = planMigration({
|
|
@@ -2035,7 +2463,7 @@ async function openProject(cwd) {
|
|
|
2035
2463
|
const config = await loadConfig(cwd);
|
|
2036
2464
|
const built = withRoles(await loadSchema(cwd, config.schema), config);
|
|
2037
2465
|
const declarations = schemaDeclarations(built);
|
|
2038
|
-
const previous = readPrevious(
|
|
2466
|
+
const previous = readPrevious(projectPath(cwd, config.migrations ?? "migrations"));
|
|
2039
2467
|
const stale = staleRenames(previous?.catalog, declarations.renames);
|
|
2040
2468
|
if (stale.length > 0) {
|
|
2041
2469
|
throw new OkmError2("OKM1020", stale.join(" "), {
|
|
@@ -2050,25 +2478,108 @@ async function openProject(cwd) {
|
|
|
2050
2478
|
};
|
|
2051
2479
|
}
|
|
2052
2480
|
function withRoles(built, config) {
|
|
2053
|
-
|
|
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)
|
|
2054
2486
|
return built;
|
|
2055
|
-
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
|
+
`);
|
|
2056
2566
|
}
|
|
2057
2567
|
function writeArtifact(cwd, config, built) {
|
|
2058
|
-
const directory =
|
|
2568
|
+
const directory = projectPath(cwd, config.out ?? ".okm");
|
|
2059
2569
|
mkdirSync(directory, { recursive: true });
|
|
2060
2570
|
const startup = startupCatalog2(built.catalog);
|
|
2061
|
-
|
|
2062
|
-
writeFileSync(
|
|
2571
|
+
const text = catalogFileText2(serializeCatalog2(startup));
|
|
2572
|
+
writeFileSync(join3(directory, "catalog.json"), text);
|
|
2573
|
+
writeFileSync(join3(directory, "catalog.hash"), `${sha2562(text)}
|
|
2063
2574
|
`);
|
|
2064
|
-
writeFileSync(
|
|
2575
|
+
writeFileSync(join3(directory, "types.d.ts"), typesText(cwd, config.schema, built));
|
|
2065
2576
|
return directory;
|
|
2066
2577
|
}
|
|
2067
2578
|
async function loadBuiltSchema(cwd, config) {
|
|
2068
2579
|
return withRoles(await loadSchema(cwd, config.schema), config);
|
|
2069
2580
|
}
|
|
2070
2581
|
async function loadConfig(cwd) {
|
|
2071
|
-
const path =
|
|
2582
|
+
const path = projectPath(cwd, "okmodel.config.ts");
|
|
2072
2583
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2073
2584
|
const record = isRecord(imported) ? imported : {};
|
|
2074
2585
|
const config = record.default ?? record.config;
|
|
@@ -2078,7 +2589,7 @@ async function loadConfig(cwd) {
|
|
|
2078
2589
|
return config;
|
|
2079
2590
|
}
|
|
2080
2591
|
async function loadSchema(cwd, spec) {
|
|
2081
|
-
const path =
|
|
2592
|
+
const path = projectPath(cwd, spec);
|
|
2082
2593
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2083
2594
|
const record = isRecord(imported) ? imported : {};
|
|
2084
2595
|
for (const value of Object.values(record)) {
|
|
@@ -2090,13 +2601,13 @@ async function loadSchema(cwd, spec) {
|
|
|
2090
2601
|
throw new OkmError2("invalid", `${spec} must export a schema().`);
|
|
2091
2602
|
}
|
|
2092
2603
|
function readPrevious(directory) {
|
|
2093
|
-
if (!
|
|
2604
|
+
if (!existsSync3(directory))
|
|
2094
2605
|
return;
|
|
2095
|
-
const catalogs =
|
|
2606
|
+
const catalogs = readdirSync3(directory).filter((file) => file.endsWith(".catalog.json")).sort();
|
|
2096
2607
|
const latest = catalogs.at(-1);
|
|
2097
2608
|
if (latest === undefined)
|
|
2098
2609
|
return;
|
|
2099
|
-
const text =
|
|
2610
|
+
const text = readFileSync3(join3(directory, latest), "utf8");
|
|
2100
2611
|
return { catalog: parseCatalog2(text) };
|
|
2101
2612
|
}
|
|
2102
2613
|
function referenceAugmentation(source) {
|
|
@@ -2121,8 +2632,8 @@ function referenceAugmentation(source) {
|
|
|
2121
2632
|
}
|
|
2122
2633
|
function nextNumber(directory) {
|
|
2123
2634
|
let max = 0;
|
|
2124
|
-
if (
|
|
2125
|
-
for (const file of
|
|
2635
|
+
if (existsSync3(directory)) {
|
|
2636
|
+
for (const file of readdirSync3(directory)) {
|
|
2126
2637
|
const match = /^(\d+)_/.exec(file);
|
|
2127
2638
|
const value = match?.[1];
|
|
2128
2639
|
if (value !== undefined)
|
|
@@ -2163,14 +2674,14 @@ function projectImportsValidate(cwd) {
|
|
|
2163
2674
|
function directoryImportsValidate(dir) {
|
|
2164
2675
|
let entries;
|
|
2165
2676
|
try {
|
|
2166
|
-
entries =
|
|
2677
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
2167
2678
|
} catch {
|
|
2168
2679
|
return false;
|
|
2169
2680
|
}
|
|
2170
2681
|
for (const entry of entries) {
|
|
2171
2682
|
if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
|
|
2172
2683
|
continue;
|
|
2173
|
-
const path =
|
|
2684
|
+
const path = join3(dir, entry.name);
|
|
2174
2685
|
if (entry.isDirectory()) {
|
|
2175
2686
|
if (directoryImportsValidate(path))
|
|
2176
2687
|
return true;
|
|
@@ -2178,7 +2689,7 @@ function directoryImportsValidate(dir) {
|
|
|
2178
2689
|
}
|
|
2179
2690
|
if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
|
|
2180
2691
|
continue;
|
|
2181
|
-
if (runtimeImportsValidate(
|
|
2692
|
+
if (runtimeImportsValidate(readFileSync3(path, "utf8")))
|
|
2182
2693
|
return true;
|
|
2183
2694
|
}
|
|
2184
2695
|
return false;
|
|
@@ -2200,4 +2711,4 @@ function runtimeImportsValidate(source) {
|
|
|
2200
2711
|
return false;
|
|
2201
2712
|
}
|
|
2202
2713
|
|
|
2203
|
-
export {
|
|
2714
|
+
export { projectPath, errorDoc, loadMigrations, readMigrationPlan, loadHeadSnapshot, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
|