@drzl/cli 4.36.0 → 4.38.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/dist/{chunk-S5JWTWWU.js → chunk-VG6SD2GP.js} +74 -4
- package/dist/chunk-VG6SD2GP.js.map +1 -0
- package/dist/cli.cjs +563 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +497 -29
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +76 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +64 -5
- package/dist/config.d.ts +64 -5
- package/dist/config.js +7 -1
- package/dist/drzl.config.schema.json +24 -1
- package/package.json +14 -11
- package/dist/chunk-S5JWTWWU.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fastCheckOutDir,
|
|
17
17
|
fastifyOutDir,
|
|
18
18
|
filterTables,
|
|
19
|
+
formsOutDir,
|
|
19
20
|
graphqlOutDir,
|
|
20
21
|
h3OutDir,
|
|
21
22
|
hasNamedSchemas,
|
|
@@ -28,13 +29,15 @@ import {
|
|
|
28
29
|
nearestKey,
|
|
29
30
|
nestjsOutDir,
|
|
30
31
|
nextOutDir,
|
|
32
|
+
openApiFetchOutDir,
|
|
33
|
+
pothosOutDir,
|
|
31
34
|
seedOutDir,
|
|
32
35
|
tableAliases,
|
|
33
36
|
tableFilterWarnings,
|
|
34
37
|
tanstackStartOutDir,
|
|
35
38
|
trpcOutDir,
|
|
36
39
|
tsRestOutDir
|
|
37
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-VG6SD2GP.js";
|
|
38
41
|
|
|
39
42
|
// src/cli.ts
|
|
40
43
|
import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
|
|
@@ -414,20 +417,81 @@ function tsRestOptions(g, cfg) {
|
|
|
414
417
|
};
|
|
415
418
|
}
|
|
416
419
|
|
|
417
|
-
// src/
|
|
420
|
+
// src/openapi-fetch-options.ts
|
|
418
421
|
var VALIDATOR_DEFAULT_DIRS3 = {
|
|
422
|
+
zod: "src/validators/zod",
|
|
423
|
+
valibot: "src/validators/valibot",
|
|
424
|
+
arktype: "src/validators/arktype"
|
|
425
|
+
};
|
|
426
|
+
function projectRelative3(p) {
|
|
427
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
428
|
+
}
|
|
429
|
+
function openApiFetchOptions(g, cfg) {
|
|
430
|
+
const configured = g.validation?.library ?? "zod";
|
|
431
|
+
const library = configured === "typebox" ? "zod" : configured;
|
|
432
|
+
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
433
|
+
const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
|
|
434
|
+
return {
|
|
435
|
+
outputDir: openApiFetchOutDir(g, cfg),
|
|
436
|
+
clientName: g.clientName,
|
|
437
|
+
document: g.document,
|
|
438
|
+
outputHeader: g.outputHeader,
|
|
439
|
+
format: g.format,
|
|
440
|
+
importExtension: g.importExtension,
|
|
441
|
+
validation: {
|
|
442
|
+
...g.validation,
|
|
443
|
+
library,
|
|
444
|
+
useShared: true,
|
|
445
|
+
importPath: g.validation?.importPath ?? derived
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/forms-options.ts
|
|
451
|
+
var VALIDATOR_DEFAULT_DIRS4 = {
|
|
452
|
+
zod: "src/validators/zod",
|
|
453
|
+
valibot: "src/validators/valibot",
|
|
454
|
+
arktype: "src/validators/arktype",
|
|
455
|
+
typebox: "src/validators/typebox",
|
|
456
|
+
effect: "src/validators/effect"
|
|
457
|
+
};
|
|
458
|
+
function projectRelative4(p) {
|
|
459
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
460
|
+
}
|
|
461
|
+
function formsOptions(g, cfg) {
|
|
462
|
+
const library = g.validation?.library ?? "zod";
|
|
463
|
+
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
464
|
+
const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
|
|
465
|
+
return {
|
|
466
|
+
outputDir: formsOutDir(g, cfg),
|
|
467
|
+
target: g.target,
|
|
468
|
+
modes: g.modes,
|
|
469
|
+
outputHeader: g.outputHeader,
|
|
470
|
+
format: g.format,
|
|
471
|
+
importExtension: g.importExtension,
|
|
472
|
+
validation: {
|
|
473
|
+
...g.validation,
|
|
474
|
+
library,
|
|
475
|
+
useShared: true,
|
|
476
|
+
importPath: g.validation?.importPath ?? derived
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/elysia-options.ts
|
|
482
|
+
var VALIDATOR_DEFAULT_DIRS5 = {
|
|
419
483
|
zod: "src/validators/zod",
|
|
420
484
|
valibot: "src/validators/valibot",
|
|
421
485
|
arktype: "src/validators/arktype",
|
|
422
486
|
typebox: "src/validators/typebox"
|
|
423
487
|
};
|
|
424
|
-
function
|
|
488
|
+
function projectRelative5(p) {
|
|
425
489
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
426
490
|
}
|
|
427
491
|
function elysiaOptions(g, cfg) {
|
|
428
492
|
const library = g.validation?.library ?? "zod";
|
|
429
493
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
430
|
-
const derived = siblings.length === 1 ?
|
|
494
|
+
const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
|
|
431
495
|
return {
|
|
432
496
|
outputDir: elysiaOutDir(g, cfg),
|
|
433
497
|
appName: g.appName,
|
|
@@ -468,20 +532,31 @@ function fastCheckOptions(g, cfg) {
|
|
|
468
532
|
};
|
|
469
533
|
}
|
|
470
534
|
|
|
535
|
+
// src/pothos-options.ts
|
|
536
|
+
function pothosOptions(g, cfg) {
|
|
537
|
+
return {
|
|
538
|
+
outputDir: pothosOutDir(g, cfg),
|
|
539
|
+
naming: g.naming,
|
|
540
|
+
outputHeader: g.outputHeader,
|
|
541
|
+
format: g.format,
|
|
542
|
+
importExtension: g.importExtension
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
471
546
|
// src/h3-options.ts
|
|
472
|
-
var
|
|
547
|
+
var VALIDATOR_DEFAULT_DIRS6 = {
|
|
473
548
|
zod: "src/validators/zod",
|
|
474
549
|
valibot: "src/validators/valibot",
|
|
475
550
|
arktype: "src/validators/arktype"
|
|
476
551
|
};
|
|
477
|
-
function
|
|
552
|
+
function projectRelative6(p) {
|
|
478
553
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
479
554
|
}
|
|
480
555
|
function h3Options(g, cfg) {
|
|
481
556
|
const configured = g.validation?.library ?? "zod";
|
|
482
557
|
const library = configured === "typebox" ? "zod" : configured;
|
|
483
558
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
484
|
-
const derived = siblings.length === 1 ?
|
|
559
|
+
const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
|
|
485
560
|
return {
|
|
486
561
|
outputDir: h3OutDir(g, cfg),
|
|
487
562
|
h3: g.h3,
|
|
@@ -515,19 +590,19 @@ function mcpOptions(g, cfg) {
|
|
|
515
590
|
}
|
|
516
591
|
|
|
517
592
|
// src/next-options.ts
|
|
518
|
-
var
|
|
593
|
+
var VALIDATOR_DEFAULT_DIRS7 = {
|
|
519
594
|
zod: "src/validators/zod",
|
|
520
595
|
valibot: "src/validators/valibot",
|
|
521
596
|
arktype: "src/validators/arktype"
|
|
522
597
|
};
|
|
523
|
-
function
|
|
598
|
+
function projectRelative7(p) {
|
|
524
599
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
525
600
|
}
|
|
526
601
|
function nextOptions(g, cfg) {
|
|
527
602
|
const configured = g.validation?.library ?? "zod";
|
|
528
603
|
const library = configured === "typebox" ? "zod" : configured;
|
|
529
604
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
530
|
-
const derived = siblings.length === 1 ?
|
|
605
|
+
const derived = siblings.length === 1 ? projectRelative7(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS7[library]) : void 0;
|
|
531
606
|
return {
|
|
532
607
|
outputDir: nextOutDir(g, cfg),
|
|
533
608
|
naming: g.naming,
|
|
@@ -544,19 +619,19 @@ function nextOptions(g, cfg) {
|
|
|
544
619
|
}
|
|
545
620
|
|
|
546
621
|
// src/tanstack-start-options.ts
|
|
547
|
-
var
|
|
622
|
+
var VALIDATOR_DEFAULT_DIRS8 = {
|
|
548
623
|
zod: "src/validators/zod",
|
|
549
624
|
valibot: "src/validators/valibot",
|
|
550
625
|
arktype: "src/validators/arktype"
|
|
551
626
|
};
|
|
552
|
-
function
|
|
627
|
+
function projectRelative8(p) {
|
|
553
628
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
554
629
|
}
|
|
555
630
|
function tanstackStartOptions(g, cfg) {
|
|
556
631
|
const configured = g.validation?.library ?? "zod";
|
|
557
632
|
const library = configured === "typebox" ? "zod" : configured;
|
|
558
633
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
559
|
-
const derived = siblings.length === 1 ?
|
|
634
|
+
const derived = siblings.length === 1 ? projectRelative8(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS8[library]) : void 0;
|
|
560
635
|
return {
|
|
561
636
|
outputDir: tanstackStartOutDir(g, cfg),
|
|
562
637
|
naming: g.naming,
|
|
@@ -640,7 +715,7 @@ function trpcOptions(g, cfg, servicesDir) {
|
|
|
640
715
|
}
|
|
641
716
|
|
|
642
717
|
// src/generator-registry.ts
|
|
643
|
-
var
|
|
718
|
+
var VALIDATOR_DEFAULT_DIRS9 = {
|
|
644
719
|
zod: "src/validators/zod",
|
|
645
720
|
valibot: "src/validators/valibot",
|
|
646
721
|
arktype: "src/validators/arktype",
|
|
@@ -673,10 +748,14 @@ var GENERATORS = [
|
|
|
673
748
|
//
|
|
674
749
|
// The side effect was invisible and lasted longer than the reason: tsup externalises
|
|
675
750
|
// `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
|
|
676
|
-
// inside `dist` while the
|
|
677
|
-
// registry now and
|
|
678
|
-
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
|
|
679
|
-
//
|
|
751
|
+
// inside `dist` while the rest were resolved from `node_modules`. Every generator package is on
|
|
752
|
+
// the registry now and every one is a `dependencies` entry, which is what makes each of them a
|
|
753
|
+
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure for
|
|
754
|
+
// every kind rather than for some of them.
|
|
755
|
+
//
|
|
756
|
+
// Counted nowhere on purpose. This paragraph twice carried a number that the next batch of
|
|
757
|
+
// generators falsified, and `packages/cli/test/generator-registry.spec.ts` asserts the property
|
|
758
|
+
// against the manifest, which is where a quantity belongs.
|
|
680
759
|
specifier: "@drzl/generator-trpc",
|
|
681
760
|
load: () => import("@drzl/generator-trpc"),
|
|
682
761
|
construct: (m, analysis) => new m.TRPCGenerator(analysis),
|
|
@@ -728,9 +807,10 @@ var GENERATORS = [
|
|
|
728
807
|
// This one and the three below spent one release each in `optionalDependencies`, because a
|
|
729
808
|
// package that has never existed cannot publish through npm's trusted-publisher OIDC flow and
|
|
730
809
|
// naming it as a hard dependency in the release that introduces it breaks `npm i @drzl/cli`
|
|
731
|
-
// for everyone until the first publish lands.
|
|
732
|
-
//
|
|
733
|
-
//
|
|
810
|
+
// for everyone until the first publish lands. The six generators added after them went the
|
|
811
|
+
// same way and were promoted the same way once their first versions were published by hand.
|
|
812
|
+
// Nothing here is optional any more. `scripts/verify/stages/33-registry-deps.sh` gates both
|
|
813
|
+
// halves of that rule and is what reported each promotion was due.
|
|
734
814
|
specifier: "@drzl/generator-mcp",
|
|
735
815
|
load: () => import("@drzl/generator-mcp"),
|
|
736
816
|
construct: (m, analysis) => new m.MCPGenerator(analysis),
|
|
@@ -785,6 +865,22 @@ var GENERATORS = [
|
|
|
785
865
|
outputDir: (g, cfg) => tsRestOutDir(g, cfg),
|
|
786
866
|
options: (g, cfg) => tsRestOptions(g, cfg)
|
|
787
867
|
},
|
|
868
|
+
{
|
|
869
|
+
kind: "openapi-fetch",
|
|
870
|
+
specifier: "@drzl/generator-openapi-fetch",
|
|
871
|
+
load: () => import("@drzl/generator-openapi-fetch"),
|
|
872
|
+
construct: (m, analysis) => new m.OpenApiFetchGenerator(analysis),
|
|
873
|
+
outputDir: (g, cfg) => openApiFetchOutDir(g, cfg),
|
|
874
|
+
options: (g, cfg) => openApiFetchOptions(g, cfg)
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
kind: "forms",
|
|
878
|
+
specifier: "@drzl/generator-forms",
|
|
879
|
+
load: () => import("@drzl/generator-forms"),
|
|
880
|
+
construct: (m, analysis) => new m.FormsGenerator(analysis),
|
|
881
|
+
outputDir: (g, cfg) => formsOutDir(g, cfg),
|
|
882
|
+
options: (g, cfg) => formsOptions(g, cfg)
|
|
883
|
+
},
|
|
788
884
|
{
|
|
789
885
|
kind: "elysia",
|
|
790
886
|
specifier: "@drzl/generator-elysia",
|
|
@@ -809,6 +905,14 @@ var GENERATORS = [
|
|
|
809
905
|
outputDir: (g, cfg) => fastCheckOutDir(g, cfg),
|
|
810
906
|
options: (g, cfg) => fastCheckOptions(g, cfg)
|
|
811
907
|
},
|
|
908
|
+
{
|
|
909
|
+
kind: "pothos",
|
|
910
|
+
specifier: "@drzl/generator-pothos",
|
|
911
|
+
load: () => import("@drzl/generator-pothos"),
|
|
912
|
+
construct: (m, analysis) => new m.PothosGenerator(analysis),
|
|
913
|
+
outputDir: (g, cfg) => pothosOutDir(g, cfg),
|
|
914
|
+
options: (g, cfg) => pothosOptions(g, cfg)
|
|
915
|
+
},
|
|
812
916
|
{
|
|
813
917
|
kind: "service",
|
|
814
918
|
specifier: "@drzl/generator-service",
|
|
@@ -822,7 +926,7 @@ var GENERATORS = [
|
|
|
822
926
|
specifier: "@drzl/generator-zod",
|
|
823
927
|
load: () => import("@drzl/generator-zod"),
|
|
824
928
|
construct: (m, analysis) => new m.ZodGenerator(analysis),
|
|
825
|
-
outputDir: (g) => g.path ??
|
|
929
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.zod,
|
|
826
930
|
// `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
|
|
827
931
|
// four rather than being passed and ignored.
|
|
828
932
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
|
|
@@ -836,7 +940,7 @@ var GENERATORS = [
|
|
|
836
940
|
specifier: "@drzl/generator-valibot",
|
|
837
941
|
load: () => import("@drzl/generator-valibot"),
|
|
838
942
|
construct: (m, analysis) => new m.ValibotGenerator(analysis),
|
|
839
|
-
outputDir: (g) => g.path ??
|
|
943
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.valibot,
|
|
840
944
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
|
|
841
945
|
},
|
|
842
946
|
{
|
|
@@ -844,7 +948,7 @@ var GENERATORS = [
|
|
|
844
948
|
specifier: "@drzl/generator-arktype",
|
|
845
949
|
load: () => import("@drzl/generator-arktype"),
|
|
846
950
|
construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
|
|
847
|
-
outputDir: (g) => g.path ??
|
|
951
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.arktype,
|
|
848
952
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
|
|
849
953
|
},
|
|
850
954
|
{
|
|
@@ -852,7 +956,7 @@ var GENERATORS = [
|
|
|
852
956
|
specifier: "@drzl/generator-typebox",
|
|
853
957
|
load: () => import("@drzl/generator-typebox"),
|
|
854
958
|
construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
|
|
855
|
-
outputDir: (g) => g.path ??
|
|
959
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.typebox,
|
|
856
960
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
|
|
857
961
|
},
|
|
858
962
|
{
|
|
@@ -860,7 +964,7 @@ var GENERATORS = [
|
|
|
860
964
|
specifier: "@drzl/generator-effect",
|
|
861
965
|
load: () => import("@drzl/generator-effect"),
|
|
862
966
|
construct: (m, analysis) => new m.EffectGenerator(analysis),
|
|
863
|
-
outputDir: (g) => g.path ??
|
|
967
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.effect,
|
|
864
968
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
|
|
865
969
|
},
|
|
866
970
|
{
|
|
@@ -868,7 +972,7 @@ var GENERATORS = [
|
|
|
868
972
|
specifier: "@drzl/generator-json-schema",
|
|
869
973
|
load: () => import("@drzl/generator-json-schema"),
|
|
870
974
|
construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
|
|
871
|
-
outputDir: (g) => g.path ??
|
|
975
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9["json-schema"],
|
|
872
976
|
options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
|
|
873
977
|
}
|
|
874
978
|
];
|
|
@@ -1052,6 +1156,101 @@ import { parseCheck as parseCheck2 } from "@drzl/validation-core";
|
|
|
1052
1156
|
|
|
1053
1157
|
// src/doctor.ts
|
|
1054
1158
|
import { lengthMeasure, parseCheck } from "@drzl/validation-core";
|
|
1159
|
+
|
|
1160
|
+
// src/auth-tables.ts
|
|
1161
|
+
var SIGNATURES = [
|
|
1162
|
+
{
|
|
1163
|
+
model: "account",
|
|
1164
|
+
// `providerId` beside `accountId` is the distinctive pair: it is a link to an external identity
|
|
1165
|
+
// provider, which an application table has no reason to model this way.
|
|
1166
|
+
required: ["accountId", "providerId", "userId"],
|
|
1167
|
+
supporting: ["accessToken", "refreshToken", "idToken", "password", "scope"],
|
|
1168
|
+
conventionalName: "account",
|
|
1169
|
+
secrets: ["accessToken", "refreshToken", "idToken", "password"]
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
model: "session",
|
|
1173
|
+
required: ["token", "expiresAt", "userId"],
|
|
1174
|
+
supporting: ["ipAddress", "userAgent"],
|
|
1175
|
+
conventionalName: "session",
|
|
1176
|
+
// A session token is a bearer credential: whoever reads it is that user until it expires.
|
|
1177
|
+
secrets: ["token"]
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
model: "verification",
|
|
1181
|
+
// `identifier` and `value` are deliberately generic names, which is why `expiresAt` is required
|
|
1182
|
+
// too: the three together are a short-lived token store and little else.
|
|
1183
|
+
required: ["identifier", "value", "expiresAt"],
|
|
1184
|
+
supporting: [],
|
|
1185
|
+
conventionalName: "verification",
|
|
1186
|
+
secrets: ["value"]
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
model: "user",
|
|
1190
|
+
required: ["email", "emailVerified"],
|
|
1191
|
+
supporting: ["name", "image"],
|
|
1192
|
+
conventionalName: "user",
|
|
1193
|
+
secrets: []
|
|
1194
|
+
}
|
|
1195
|
+
];
|
|
1196
|
+
function columnKeys(table) {
|
|
1197
|
+
const out = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const c of table.columns) out.set(normalise(c.name), c);
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function normalise(name) {
|
|
1202
|
+
return name.replace(/[_-]/g, "").toLowerCase();
|
|
1203
|
+
}
|
|
1204
|
+
function has(keys, name) {
|
|
1205
|
+
return keys.has(normalise(name));
|
|
1206
|
+
}
|
|
1207
|
+
function actualName(table, wanted) {
|
|
1208
|
+
return table.columns.find((c) => normalise(c.name) === normalise(wanted))?.name;
|
|
1209
|
+
}
|
|
1210
|
+
function matchOne(table, sig) {
|
|
1211
|
+
const keys = columnKeys(table);
|
|
1212
|
+
const missing = sig.required.filter((r) => !has(keys, r));
|
|
1213
|
+
if (missing.length) return void 0;
|
|
1214
|
+
const supporting = sig.supporting.filter((s) => has(keys, s));
|
|
1215
|
+
const nameMatches = normalise(table.name) === normalise(sig.conventionalName) || normalise(table.name) === `${normalise(sig.conventionalName)}s`;
|
|
1216
|
+
const confidence = supporting.length > 0 || nameMatches ? "strong" : "likely";
|
|
1217
|
+
const matched = [...sig.required, ...supporting].map((c) => actualName(table, c)).filter((c) => Boolean(c));
|
|
1218
|
+
const secrets = sig.secrets.map((c) => actualName(table, c)).filter((c) => Boolean(c));
|
|
1219
|
+
return { table: table.name, model: sig.model, confidence, matched, secrets };
|
|
1220
|
+
}
|
|
1221
|
+
function detectAuthTables(analysis) {
|
|
1222
|
+
const found = [];
|
|
1223
|
+
for (const table of analysis.tables) {
|
|
1224
|
+
for (const sig of SIGNATURES) {
|
|
1225
|
+
const m = matchOne(table, sig);
|
|
1226
|
+
if (m) {
|
|
1227
|
+
found.push(m);
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const hasNonUser = found.some((f) => f.model !== "user");
|
|
1233
|
+
return hasNonUser ? found : found.filter((f) => f.model !== "user");
|
|
1234
|
+
}
|
|
1235
|
+
function authTablesWithSecrets(matches) {
|
|
1236
|
+
return matches.filter((m) => m.secrets.length > 0);
|
|
1237
|
+
}
|
|
1238
|
+
function excludeSuggestion(matches) {
|
|
1239
|
+
const names = [...new Set(matches.map((m) => m.table))].sort();
|
|
1240
|
+
return `exclude: [${names.map((n) => `'${n}'`).join(", ")}]`;
|
|
1241
|
+
}
|
|
1242
|
+
function authTableWarnings(analysis) {
|
|
1243
|
+
const risky = authTablesWithSecrets(detectAuthTables(analysis));
|
|
1244
|
+
if (!risky.length) return [];
|
|
1245
|
+
const lines = risky.map(
|
|
1246
|
+
(m) => `"${m.table}" looks like an authentication library's ${m.model} table and holds ${m.secrets.map((c) => `"${c}"`).join(", ")}. Generating for it publishes ${m.secrets.length === 1 ? "that column" : "those columns"}.`
|
|
1247
|
+
);
|
|
1248
|
+
return [
|
|
1249
|
+
`drzl generate: ${lines.join(" ")} Keep ${risky.length === 1 ? "it" : "them"} out with ${excludeSuggestion(risky)}, or leave it if the route is deliberate.`
|
|
1250
|
+
];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/doctor.ts
|
|
1055
1254
|
import { Chalk as Chalk2 } from "chalk";
|
|
1056
1255
|
var PLAIN = new Chalk2({ level: 0 });
|
|
1057
1256
|
var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
|
|
@@ -1224,6 +1423,18 @@ function buildDoctorReport(analysis, schemaPath) {
|
|
|
1224
1423
|
hint: i.hint
|
|
1225
1424
|
});
|
|
1226
1425
|
}
|
|
1426
|
+
for (const match of detectAuthTables(analysis)) {
|
|
1427
|
+
const secrets = match.secrets.length ? ` It holds ${match.secrets.map((c) => `\`${c}\``).join(", ")}, which a generated read route would return to whoever calls it.` : "";
|
|
1428
|
+
findings.push({
|
|
1429
|
+
kind: "auth-table",
|
|
1430
|
+
// A warning rather than an error: this is a real leak and it is also a guess about someone
|
|
1431
|
+
// else's schema, and a doctor that failed the build on a guess would be switched off.
|
|
1432
|
+
level: "warn",
|
|
1433
|
+
table: match.table,
|
|
1434
|
+
message: `"${match.table}" matches the shape of an authentication library's ${match.model} table (${match.matched.join(", ")}).${secrets}`,
|
|
1435
|
+
hint: `Keep it out of every generator with ${excludeSuggestion([match])}.`
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1227
1438
|
for (const i of analysis.issues) {
|
|
1228
1439
|
if (i.code !== "DRZL_ANL_UNKNOWN_COLUMN") continue;
|
|
1229
1440
|
findings.push({
|
|
@@ -1262,6 +1473,11 @@ var SECTIONS = [
|
|
|
1262
1473
|
title: "Columns DRZL cannot type",
|
|
1263
1474
|
why: "These get a validator that accepts any value."
|
|
1264
1475
|
},
|
|
1476
|
+
{
|
|
1477
|
+
kinds: ["auth-table"],
|
|
1478
|
+
title: "Tables that look like an authentication library's",
|
|
1479
|
+
why: "Every generator loops over every table it finds, so these get routes too."
|
|
1480
|
+
},
|
|
1265
1481
|
{
|
|
1266
1482
|
kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
|
|
1267
1483
|
title: "CHECK constraints DRZL does not enforce",
|
|
@@ -2384,6 +2600,234 @@ function renderConstraintDriftSql(report) {
|
|
|
2384
2600
|
return out.join("\n");
|
|
2385
2601
|
}
|
|
2386
2602
|
|
|
2603
|
+
// src/policy-report.ts
|
|
2604
|
+
import { Chalk as Chalk5 } from "chalk";
|
|
2605
|
+
var PLAIN4 = new Chalk5({ level: 0 });
|
|
2606
|
+
var COMMANDS = ["select", "insert", "update", "delete"];
|
|
2607
|
+
var DENIAL = {
|
|
2608
|
+
select: "every read returns zero rows",
|
|
2609
|
+
insert: "every insert is refused",
|
|
2610
|
+
update: "every update is refused",
|
|
2611
|
+
delete: "every delete is refused"
|
|
2612
|
+
};
|
|
2613
|
+
function appliesTo(policy, command) {
|
|
2614
|
+
const forCmd = (policy.for ?? "all").toLowerCase();
|
|
2615
|
+
return forCmd === "all" || forCmd === command;
|
|
2616
|
+
}
|
|
2617
|
+
function grants(policy, command) {
|
|
2618
|
+
if (!appliesTo(policy, command)) return false;
|
|
2619
|
+
if ((policy.as ?? "permissive").toLowerCase() === "restrictive") return false;
|
|
2620
|
+
const hasUsing = !!policy.using;
|
|
2621
|
+
const hasCheck = !!policy.withCheck;
|
|
2622
|
+
const forCmd = (policy.for ?? "all").toLowerCase();
|
|
2623
|
+
if (forCmd === "all") return hasUsing || hasCheck;
|
|
2624
|
+
switch (command) {
|
|
2625
|
+
case "select":
|
|
2626
|
+
case "delete":
|
|
2627
|
+
return hasUsing;
|
|
2628
|
+
case "insert":
|
|
2629
|
+
return hasCheck;
|
|
2630
|
+
case "update":
|
|
2631
|
+
return hasCheck || hasUsing;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function hasAnyExpression(policy) {
|
|
2635
|
+
return !!policy.using || !!policy.withCheck;
|
|
2636
|
+
}
|
|
2637
|
+
function readTable(table) {
|
|
2638
|
+
if (typeof table.rlsEnabled !== "boolean") return void 0;
|
|
2639
|
+
const policies = table.policies ?? [];
|
|
2640
|
+
const declaredRls = table.rlsEnabled;
|
|
2641
|
+
const effective = declaredRls || policies.length > 0;
|
|
2642
|
+
if (!effective) return void 0;
|
|
2643
|
+
const grantMap = {};
|
|
2644
|
+
for (const c of COMMANDS) grantMap[c] = policies.some((p) => grants(p, c));
|
|
2645
|
+
return { table: table.name, declaredRls, effective, policies, grants: grantMap };
|
|
2646
|
+
}
|
|
2647
|
+
function buildPolicyReport(analysis, schemaPath) {
|
|
2648
|
+
const tables = [];
|
|
2649
|
+
const findings = [];
|
|
2650
|
+
for (const table of analysis.tables) {
|
|
2651
|
+
const read = readTable(table);
|
|
2652
|
+
if (!read) continue;
|
|
2653
|
+
tables.push(read);
|
|
2654
|
+
if (COMMANDS.every((c) => !read.grants[c])) {
|
|
2655
|
+
findings.push({
|
|
2656
|
+
kind: "denied",
|
|
2657
|
+
table: read.table,
|
|
2658
|
+
detail: "row-level security is on and no permissive policy grants anything, so every read returns zero rows and every write is refused, for every role but the table's owner and any role with BYPASSRLS",
|
|
2659
|
+
// Deliberately not "give it a USING": a policy written `FOR INSERT` consults only its
|
|
2660
|
+
// WITH CHECK, so that advice would be wrong for exactly the declaration this report exists
|
|
2661
|
+
// to catch. The per-policy findings below name the right expression for each one.
|
|
2662
|
+
fix: read.policies.length ? `no policy here grants a command: ${read.policies.map((p) => `"${p.name}"`).join(", ")} ${read.policies.length === 1 ? "needs" : "need"} a USING expression, or a WITH CHECK where the command is INSERT` : "declare a policy, or drop the row-level security on this table"
|
|
2663
|
+
});
|
|
2664
|
+
} else {
|
|
2665
|
+
for (const command of COMMANDS) {
|
|
2666
|
+
if (read.grants[command]) continue;
|
|
2667
|
+
const named = read.policies.filter((p) => appliesTo(p, command));
|
|
2668
|
+
findings.push({
|
|
2669
|
+
kind: "denied",
|
|
2670
|
+
table: read.table,
|
|
2671
|
+
command,
|
|
2672
|
+
detail: `row-level security is on and no permissive policy grants ${command.toUpperCase()}, so ${DENIAL[command]} for every role but the table's owner and any role with BYPASSRLS`,
|
|
2673
|
+
fix: named.length ? `${named.length === 1 ? "the policy" : "the policies"} ${named.map((p) => `"${p.name}"`).join(", ")} name${named.length === 1 ? "s" : ""} ${command.toUpperCase()} but grant${named.length === 1 ? "s" : ""} nothing; give ${command === "insert" ? "it a WITH CHECK" : "it a USING"} expression` : `declare a policy for ${command.toUpperCase()}, or drop the row-level security on this table`
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
for (const policy of read.policies) {
|
|
2678
|
+
if (hasAnyExpression(policy)) continue;
|
|
2679
|
+
findings.push({
|
|
2680
|
+
kind: "grants-nothing",
|
|
2681
|
+
table: read.table,
|
|
2682
|
+
policy: policy.name,
|
|
2683
|
+
detail: "the policy carries neither a USING nor a WITH CHECK expression, so it permits nothing and only the policies beside it decide what this table allows",
|
|
2684
|
+
fix: (policy.for ?? "all").toLowerCase() === "insert" ? "give it a WITH CHECK expression, which is the only one INSERT consults" : "give it a USING expression, and a WITH CHECK too where writes need a different rule"
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
const policies = tables.reduce((n, t) => n + t.policies.length, 0);
|
|
2689
|
+
return {
|
|
2690
|
+
schema: schemaPath,
|
|
2691
|
+
dialect: analysis.dialect,
|
|
2692
|
+
ok: findings.length === 0,
|
|
2693
|
+
counts: {
|
|
2694
|
+
tables: analysis.tables.length,
|
|
2695
|
+
withRls: tables.length,
|
|
2696
|
+
policies,
|
|
2697
|
+
findings: findings.length
|
|
2698
|
+
},
|
|
2699
|
+
tables,
|
|
2700
|
+
findings,
|
|
2701
|
+
ignoredByGeneratedCode: tables.map((t) => t.table)
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function wrap4(text, indent, first = indent, width = 96) {
|
|
2705
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
2706
|
+
const lines = [];
|
|
2707
|
+
let line = first;
|
|
2708
|
+
let started = false;
|
|
2709
|
+
for (const w of words) {
|
|
2710
|
+
if (started && line.length + 1 + w.length > width) {
|
|
2711
|
+
lines.push(line);
|
|
2712
|
+
line = indent + w;
|
|
2713
|
+
} else {
|
|
2714
|
+
line = started ? `${line} ${w}` : line + w;
|
|
2715
|
+
started = true;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (started) lines.push(line);
|
|
2719
|
+
return lines.join("\n");
|
|
2720
|
+
}
|
|
2721
|
+
function policyLine(policy) {
|
|
2722
|
+
const bits = [(policy.for ?? "all").toUpperCase()];
|
|
2723
|
+
if ((policy.as ?? "").toLowerCase() === "restrictive") bits.push("restrictive");
|
|
2724
|
+
bits.push(policy.to?.length ? `to ${policy.to.join(", ")}` : "to public");
|
|
2725
|
+
const carries = [policy.using ? "USING" : void 0, policy.withCheck ? "WITH CHECK" : void 0].filter(Boolean).join(" + ");
|
|
2726
|
+
bits.push(carries || "no expression");
|
|
2727
|
+
if (policy.linked) bits.push("linked");
|
|
2728
|
+
return bits.join(", ");
|
|
2729
|
+
}
|
|
2730
|
+
function renderPolicyReport(report, style = PLAIN4) {
|
|
2731
|
+
const chalk = style;
|
|
2732
|
+
const out = [];
|
|
2733
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
2734
|
+
out.push(chalk.bold(`DRZL row-level security ${report.schema}`));
|
|
2735
|
+
out.push(
|
|
2736
|
+
chalk.dim(
|
|
2737
|
+
`${report.dialect}, ${plural(report.counts.withRls, "table")} under RLS, ${plural(report.counts.policies, "policy").replace("policys", "policies")}`
|
|
2738
|
+
)
|
|
2739
|
+
);
|
|
2740
|
+
out.push("");
|
|
2741
|
+
if (!report.counts.withRls) {
|
|
2742
|
+
out.push(chalk.green("No table in this schema uses row-level security."));
|
|
2743
|
+
if (report.dialect !== "postgres" && report.dialect !== "cockroach") {
|
|
2744
|
+
out.push(chalk.dim(` ${report.dialect} has no row-level security to declare.`));
|
|
2745
|
+
} else {
|
|
2746
|
+
out.push(chalk.dim(" No table calls .enableRLS() and none declares a pgPolicy."));
|
|
2747
|
+
}
|
|
2748
|
+
return out.join("\n");
|
|
2749
|
+
}
|
|
2750
|
+
const denied = report.findings.filter((f) => f.kind === "denied");
|
|
2751
|
+
if (denied.length) {
|
|
2752
|
+
out.push(chalk.red("These tables refuse the operation your generated code performs"));
|
|
2753
|
+
out.push(
|
|
2754
|
+
chalk.dim(
|
|
2755
|
+
wrap4(
|
|
2756
|
+
"A table under row-level security permits only what a policy grants. The generated service still compiles and its return type still promises rows.",
|
|
2757
|
+
" "
|
|
2758
|
+
)
|
|
2759
|
+
)
|
|
2760
|
+
);
|
|
2761
|
+
out.push("");
|
|
2762
|
+
for (const f of denied) {
|
|
2763
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)} ${chalk.dim(f.command ?? "everything")}`);
|
|
2764
|
+
out.push(wrap4(f.detail, " "));
|
|
2765
|
+
if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
|
|
2766
|
+
out.push("");
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
const dead = report.findings.filter((f) => f.kind === "grants-nothing");
|
|
2770
|
+
if (dead.length) {
|
|
2771
|
+
out.push(chalk.yellow("These policies grant nothing"));
|
|
2772
|
+
out.push(
|
|
2773
|
+
chalk.dim(
|
|
2774
|
+
wrap4(
|
|
2775
|
+
"A policy with neither expression is not a permissive default. Measured against Postgres 18.3: a lone FOR INSERT policy carrying no WITH CHECK refused every insert.",
|
|
2776
|
+
" "
|
|
2777
|
+
)
|
|
2778
|
+
)
|
|
2779
|
+
);
|
|
2780
|
+
out.push("");
|
|
2781
|
+
for (const f of dead) {
|
|
2782
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)}.${f.policy}`);
|
|
2783
|
+
out.push(wrap4(f.detail, " "));
|
|
2784
|
+
if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
|
|
2785
|
+
out.push("");
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
out.push(chalk.cyan("The policies each table carries"));
|
|
2789
|
+
out.push(
|
|
2790
|
+
chalk.dim(
|
|
2791
|
+
wrap4(
|
|
2792
|
+
"Whether these apply to the role your application connects as is the one question this report cannot answer for you.",
|
|
2793
|
+
" "
|
|
2794
|
+
)
|
|
2795
|
+
)
|
|
2796
|
+
);
|
|
2797
|
+
out.push("");
|
|
2798
|
+
for (const t of report.tables) {
|
|
2799
|
+
const flag = t.declaredRls ? "enableRLS()" : "RLS on, implied by its policies";
|
|
2800
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(t.table)} ${chalk.dim(`(${flag})`)}`);
|
|
2801
|
+
if (!t.policies.length) {
|
|
2802
|
+
out.push(chalk.dim(" no policies"));
|
|
2803
|
+
}
|
|
2804
|
+
for (const p of t.policies) {
|
|
2805
|
+
out.push(` ${p.name}: ${chalk.dim(policyLine(p))}`);
|
|
2806
|
+
}
|
|
2807
|
+
const permitted = COMMANDS.filter((c) => t.grants[c]);
|
|
2808
|
+
out.push(
|
|
2809
|
+
chalk.dim(` permits: ${permitted.length ? permitted.join(", ") : "nothing"}`)
|
|
2810
|
+
);
|
|
2811
|
+
out.push("");
|
|
2812
|
+
}
|
|
2813
|
+
out.push(chalk.yellow("What DRZL generates does not know about any of this"));
|
|
2814
|
+
out.push(
|
|
2815
|
+
chalk.dim(
|
|
2816
|
+
wrap4(
|
|
2817
|
+
`No generator emits policy awareness, so a generated read path describes rows the caller may not be allowed to see, and a reader of the emitted types will believe otherwise. That is true of ${plural(report.ignoredByGeneratedCode.length, "table")} here, and it is a fact about DRZL rather than a defect in your schema.`,
|
|
2818
|
+
" "
|
|
2819
|
+
)
|
|
2820
|
+
)
|
|
2821
|
+
);
|
|
2822
|
+
out.push("");
|
|
2823
|
+
out.push(
|
|
2824
|
+
chalk.dim(
|
|
2825
|
+
`${plural(report.counts.findings, "finding")} across ${plural(report.counts.withRls, "table")} under row-level security.`
|
|
2826
|
+
)
|
|
2827
|
+
);
|
|
2828
|
+
return out.join("\n");
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2387
2831
|
// src/drift.ts
|
|
2388
2832
|
import { promises as fs2 } from "fs";
|
|
2389
2833
|
import path2 from "path";
|
|
@@ -3309,7 +3753,10 @@ withOutputFlags(
|
|
|
3309
3753
|
program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--strict", "exit 2 when anything is reported", false).option(
|
|
3310
3754
|
"--constraints",
|
|
3311
3755
|
"report what each side enforces that the other does not, instead of the usual findings"
|
|
3312
|
-
).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration")
|
|
3756
|
+
).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration").option(
|
|
3757
|
+
"--policies",
|
|
3758
|
+
"report the row-level security policies each table carries, and what they refuse"
|
|
3759
|
+
)
|
|
3313
3760
|
).action(async (schema, opts) => {
|
|
3314
3761
|
const out = outputFor(opts);
|
|
3315
3762
|
{
|
|
@@ -3339,7 +3786,14 @@ withOutputFlags(
|
|
|
3339
3786
|
process.exit(EXIT_FAILED);
|
|
3340
3787
|
return;
|
|
3341
3788
|
}
|
|
3342
|
-
if (opts.constraints) {
|
|
3789
|
+
if (opts.constraints && opts.policies) {
|
|
3790
|
+
const msg = "--constraints and --policies are separate reports. Run one, then the other.";
|
|
3791
|
+
if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
|
|
3792
|
+
else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
|
|
3793
|
+
process.exit(EXIT_FAILED);
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
if (opts.constraints || opts.policies) {
|
|
3343
3797
|
const preflight = buildDoctorReport(analysis, schemaLabel);
|
|
3344
3798
|
const fatal = preflight.findings.filter((f) => f.level === "error");
|
|
3345
3799
|
if (fatal.length) {
|
|
@@ -3355,6 +3809,8 @@ withOutputFlags(
|
|
|
3355
3809
|
process.exit(EXIT_FAILED);
|
|
3356
3810
|
return;
|
|
3357
3811
|
}
|
|
3812
|
+
}
|
|
3813
|
+
if (opts.constraints) {
|
|
3358
3814
|
const drift = buildConstraintDriftReport(analysis, schemaLabel);
|
|
3359
3815
|
const driftCode = opts.strict && drift.counts.schemaOnly ? EXIT_FINDINGS : EXIT_OK;
|
|
3360
3816
|
if (opts.sql) {
|
|
@@ -3370,6 +3826,17 @@ withOutputFlags(
|
|
|
3370
3826
|
process.exit(driftCode);
|
|
3371
3827
|
return;
|
|
3372
3828
|
}
|
|
3829
|
+
if (opts.policies) {
|
|
3830
|
+
const policies = buildPolicyReport(analysis, schemaLabel);
|
|
3831
|
+
const policyCode = opts.strict && policies.counts.findings ? EXIT_FINDINGS : EXIT_OK;
|
|
3832
|
+
if (opts.json)
|
|
3833
|
+
out.data(
|
|
3834
|
+
JSON.stringify({ command: "doctor", exitCode: policyCode, ...policies }, null, 2)
|
|
3835
|
+
);
|
|
3836
|
+
else out.data(renderPolicyReport(policies, out.outStyle));
|
|
3837
|
+
process.exit(policyCode);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3373
3840
|
const report = buildDoctorReport(analysis, schemaLabel);
|
|
3374
3841
|
const unreadable = report.findings.some((f) => f.level === "error");
|
|
3375
3842
|
const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
|
|
@@ -3590,6 +4057,7 @@ withOutputFlags(
|
|
|
3590
4057
|
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
3591
4058
|
for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
|
|
3592
4059
|
for (const w of wideColumnWarning(analysis.issues)) warn(w);
|
|
4060
|
+
for (const w of authTableWarnings(analysis)) warn(w);
|
|
3593
4061
|
const empty = nothingToGenerate({
|
|
3594
4062
|
schema: source.schema,
|
|
3595
4063
|
analyzed: narrowed.tables,
|