@drzl/cli 4.36.0 → 4.37.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/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,14 @@ import {
28
29
  nearestKey,
29
30
  nestjsOutDir,
30
31
  nextOutDir,
32
+ openApiFetchOutDir,
31
33
  seedOutDir,
32
34
  tableAliases,
33
35
  tableFilterWarnings,
34
36
  tanstackStartOutDir,
35
37
  trpcOutDir,
36
38
  tsRestOutDir
37
- } from "./chunk-S5JWTWWU.js";
39
+ } from "./chunk-EQ4OSVMJ.js";
38
40
 
39
41
  // src/cli.ts
40
42
  import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
@@ -414,20 +416,81 @@ function tsRestOptions(g, cfg) {
414
416
  };
415
417
  }
416
418
 
417
- // src/elysia-options.ts
419
+ // src/openapi-fetch-options.ts
418
420
  var VALIDATOR_DEFAULT_DIRS3 = {
421
+ zod: "src/validators/zod",
422
+ valibot: "src/validators/valibot",
423
+ arktype: "src/validators/arktype"
424
+ };
425
+ function projectRelative3(p) {
426
+ return p.startsWith("./") ? p.slice(2) : p;
427
+ }
428
+ function openApiFetchOptions(g, cfg) {
429
+ const configured = g.validation?.library ?? "zod";
430
+ const library = configured === "typebox" ? "zod" : configured;
431
+ const siblings = cfg.generators.filter((s) => s.kind === library);
432
+ const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
433
+ return {
434
+ outputDir: openApiFetchOutDir(g, cfg),
435
+ clientName: g.clientName,
436
+ document: g.document,
437
+ outputHeader: g.outputHeader,
438
+ format: g.format,
439
+ importExtension: g.importExtension,
440
+ validation: {
441
+ ...g.validation,
442
+ library,
443
+ useShared: true,
444
+ importPath: g.validation?.importPath ?? derived
445
+ }
446
+ };
447
+ }
448
+
449
+ // src/forms-options.ts
450
+ var VALIDATOR_DEFAULT_DIRS4 = {
451
+ zod: "src/validators/zod",
452
+ valibot: "src/validators/valibot",
453
+ arktype: "src/validators/arktype",
454
+ typebox: "src/validators/typebox",
455
+ effect: "src/validators/effect"
456
+ };
457
+ function projectRelative4(p) {
458
+ return p.startsWith("./") ? p.slice(2) : p;
459
+ }
460
+ function formsOptions(g, cfg) {
461
+ const library = g.validation?.library ?? "zod";
462
+ const siblings = cfg.generators.filter((s) => s.kind === library);
463
+ const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
464
+ return {
465
+ outputDir: formsOutDir(g, cfg),
466
+ target: g.target,
467
+ modes: g.modes,
468
+ outputHeader: g.outputHeader,
469
+ format: g.format,
470
+ importExtension: g.importExtension,
471
+ validation: {
472
+ ...g.validation,
473
+ library,
474
+ useShared: true,
475
+ importPath: g.validation?.importPath ?? derived
476
+ }
477
+ };
478
+ }
479
+
480
+ // src/elysia-options.ts
481
+ var VALIDATOR_DEFAULT_DIRS5 = {
419
482
  zod: "src/validators/zod",
420
483
  valibot: "src/validators/valibot",
421
484
  arktype: "src/validators/arktype",
422
485
  typebox: "src/validators/typebox"
423
486
  };
424
- function projectRelative3(p) {
487
+ function projectRelative5(p) {
425
488
  return p.startsWith("./") ? p.slice(2) : p;
426
489
  }
427
490
  function elysiaOptions(g, cfg) {
428
491
  const library = g.validation?.library ?? "zod";
429
492
  const siblings = cfg.generators.filter((s) => s.kind === library);
430
- const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
493
+ const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
431
494
  return {
432
495
  outputDir: elysiaOutDir(g, cfg),
433
496
  appName: g.appName,
@@ -469,19 +532,19 @@ function fastCheckOptions(g, cfg) {
469
532
  }
470
533
 
471
534
  // src/h3-options.ts
472
- var VALIDATOR_DEFAULT_DIRS4 = {
535
+ var VALIDATOR_DEFAULT_DIRS6 = {
473
536
  zod: "src/validators/zod",
474
537
  valibot: "src/validators/valibot",
475
538
  arktype: "src/validators/arktype"
476
539
  };
477
- function projectRelative4(p) {
540
+ function projectRelative6(p) {
478
541
  return p.startsWith("./") ? p.slice(2) : p;
479
542
  }
480
543
  function h3Options(g, cfg) {
481
544
  const configured = g.validation?.library ?? "zod";
482
545
  const library = configured === "typebox" ? "zod" : configured;
483
546
  const siblings = cfg.generators.filter((s) => s.kind === library);
484
- const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
547
+ const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
485
548
  return {
486
549
  outputDir: h3OutDir(g, cfg),
487
550
  h3: g.h3,
@@ -515,19 +578,19 @@ function mcpOptions(g, cfg) {
515
578
  }
516
579
 
517
580
  // src/next-options.ts
518
- var VALIDATOR_DEFAULT_DIRS5 = {
581
+ var VALIDATOR_DEFAULT_DIRS7 = {
519
582
  zod: "src/validators/zod",
520
583
  valibot: "src/validators/valibot",
521
584
  arktype: "src/validators/arktype"
522
585
  };
523
- function projectRelative5(p) {
586
+ function projectRelative7(p) {
524
587
  return p.startsWith("./") ? p.slice(2) : p;
525
588
  }
526
589
  function nextOptions(g, cfg) {
527
590
  const configured = g.validation?.library ?? "zod";
528
591
  const library = configured === "typebox" ? "zod" : configured;
529
592
  const siblings = cfg.generators.filter((s) => s.kind === library);
530
- const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
593
+ const derived = siblings.length === 1 ? projectRelative7(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS7[library]) : void 0;
531
594
  return {
532
595
  outputDir: nextOutDir(g, cfg),
533
596
  naming: g.naming,
@@ -544,19 +607,19 @@ function nextOptions(g, cfg) {
544
607
  }
545
608
 
546
609
  // src/tanstack-start-options.ts
547
- var VALIDATOR_DEFAULT_DIRS6 = {
610
+ var VALIDATOR_DEFAULT_DIRS8 = {
548
611
  zod: "src/validators/zod",
549
612
  valibot: "src/validators/valibot",
550
613
  arktype: "src/validators/arktype"
551
614
  };
552
- function projectRelative6(p) {
615
+ function projectRelative8(p) {
553
616
  return p.startsWith("./") ? p.slice(2) : p;
554
617
  }
555
618
  function tanstackStartOptions(g, cfg) {
556
619
  const configured = g.validation?.library ?? "zod";
557
620
  const library = configured === "typebox" ? "zod" : configured;
558
621
  const siblings = cfg.generators.filter((s) => s.kind === library);
559
- const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
622
+ const derived = siblings.length === 1 ? projectRelative8(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS8[library]) : void 0;
560
623
  return {
561
624
  outputDir: tanstackStartOutDir(g, cfg),
562
625
  naming: g.naming,
@@ -640,7 +703,7 @@ function trpcOptions(g, cfg, servicesDir) {
640
703
  }
641
704
 
642
705
  // src/generator-registry.ts
643
- var VALIDATOR_DEFAULT_DIRS7 = {
706
+ var VALIDATOR_DEFAULT_DIRS9 = {
644
707
  zod: "src/validators/zod",
645
708
  valibot: "src/validators/valibot",
646
709
  arktype: "src/validators/arktype",
@@ -673,10 +736,14 @@ var GENERATORS = [
673
736
  //
674
737
  // The side effect was invisible and lasted longer than the reason: tsup externalises
675
738
  // `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
676
- // inside `dist` while the other six were resolved from `node_modules`. All fourteen are on the
677
- // registry now and all fourteen are `dependencies`, which is what makes every one of them a
678
- // package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
679
- // for every kind rather than for six of them.
739
+ // inside `dist` while the rest were resolved from `node_modules`. Every generator package is on
740
+ // the registry now and every one is a `dependencies` entry, which is what makes each of them a
741
+ // package that can genuinely be absent, and `loadGenerator` tell absence apart from failure for
742
+ // every kind rather than for some of them.
743
+ //
744
+ // Counted nowhere on purpose. This paragraph twice carried a number that the next batch of
745
+ // generators falsified, and `packages/cli/test/generator-registry.spec.ts` asserts the property
746
+ // against the manifest, which is where a quantity belongs.
680
747
  specifier: "@drzl/generator-trpc",
681
748
  load: () => import("@drzl/generator-trpc"),
682
749
  construct: (m, analysis) => new m.TRPCGenerator(analysis),
@@ -728,9 +795,10 @@ var GENERATORS = [
728
795
  // This one and the three below spent one release each in `optionalDependencies`, because a
729
796
  // package that has never existed cannot publish through npm's trusted-publisher OIDC flow and
730
797
  // 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. All four are on the registry now, so all four
732
- // are ordinary dependencies. `scripts/verify/stages/33-registry-deps.sh` gates both halves of
733
- // that rule and is what reported the promotion was due.
798
+ // for everyone until the first publish lands. The six generators added after them went the
799
+ // same way and were promoted the same way once their first versions were published by hand.
800
+ // Nothing here is optional any more. `scripts/verify/stages/33-registry-deps.sh` gates both
801
+ // halves of that rule and is what reported each promotion was due.
734
802
  specifier: "@drzl/generator-mcp",
735
803
  load: () => import("@drzl/generator-mcp"),
736
804
  construct: (m, analysis) => new m.MCPGenerator(analysis),
@@ -785,6 +853,22 @@ var GENERATORS = [
785
853
  outputDir: (g, cfg) => tsRestOutDir(g, cfg),
786
854
  options: (g, cfg) => tsRestOptions(g, cfg)
787
855
  },
856
+ {
857
+ kind: "openapi-fetch",
858
+ specifier: "@drzl/generator-openapi-fetch",
859
+ load: () => import("@drzl/generator-openapi-fetch"),
860
+ construct: (m, analysis) => new m.OpenApiFetchGenerator(analysis),
861
+ outputDir: (g, cfg) => openApiFetchOutDir(g, cfg),
862
+ options: (g, cfg) => openApiFetchOptions(g, cfg)
863
+ },
864
+ {
865
+ kind: "forms",
866
+ specifier: "@drzl/generator-forms",
867
+ load: () => import("@drzl/generator-forms"),
868
+ construct: (m, analysis) => new m.FormsGenerator(analysis),
869
+ outputDir: (g, cfg) => formsOutDir(g, cfg),
870
+ options: (g, cfg) => formsOptions(g, cfg)
871
+ },
788
872
  {
789
873
  kind: "elysia",
790
874
  specifier: "@drzl/generator-elysia",
@@ -822,7 +906,7 @@ var GENERATORS = [
822
906
  specifier: "@drzl/generator-zod",
823
907
  load: () => import("@drzl/generator-zod"),
824
908
  construct: (m, analysis) => new m.ZodGenerator(analysis),
825
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.zod,
909
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.zod,
826
910
  // `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
827
911
  // four rather than being passed and ignored.
828
912
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
@@ -836,7 +920,7 @@ var GENERATORS = [
836
920
  specifier: "@drzl/generator-valibot",
837
921
  load: () => import("@drzl/generator-valibot"),
838
922
  construct: (m, analysis) => new m.ValibotGenerator(analysis),
839
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.valibot,
923
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.valibot,
840
924
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
841
925
  },
842
926
  {
@@ -844,7 +928,7 @@ var GENERATORS = [
844
928
  specifier: "@drzl/generator-arktype",
845
929
  load: () => import("@drzl/generator-arktype"),
846
930
  construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
847
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.arktype,
931
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.arktype,
848
932
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
849
933
  },
850
934
  {
@@ -852,7 +936,7 @@ var GENERATORS = [
852
936
  specifier: "@drzl/generator-typebox",
853
937
  load: () => import("@drzl/generator-typebox"),
854
938
  construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
855
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.typebox,
939
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.typebox,
856
940
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
857
941
  },
858
942
  {
@@ -860,7 +944,7 @@ var GENERATORS = [
860
944
  specifier: "@drzl/generator-effect",
861
945
  load: () => import("@drzl/generator-effect"),
862
946
  construct: (m, analysis) => new m.EffectGenerator(analysis),
863
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.effect,
947
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.effect,
864
948
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
865
949
  },
866
950
  {
@@ -868,7 +952,7 @@ var GENERATORS = [
868
952
  specifier: "@drzl/generator-json-schema",
869
953
  load: () => import("@drzl/generator-json-schema"),
870
954
  construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
871
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7["json-schema"],
955
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9["json-schema"],
872
956
  options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
873
957
  }
874
958
  ];
@@ -2384,6 +2468,234 @@ function renderConstraintDriftSql(report) {
2384
2468
  return out.join("\n");
2385
2469
  }
2386
2470
 
2471
+ // src/policy-report.ts
2472
+ import { Chalk as Chalk5 } from "chalk";
2473
+ var PLAIN4 = new Chalk5({ level: 0 });
2474
+ var COMMANDS = ["select", "insert", "update", "delete"];
2475
+ var DENIAL = {
2476
+ select: "every read returns zero rows",
2477
+ insert: "every insert is refused",
2478
+ update: "every update is refused",
2479
+ delete: "every delete is refused"
2480
+ };
2481
+ function appliesTo(policy, command) {
2482
+ const forCmd = (policy.for ?? "all").toLowerCase();
2483
+ return forCmd === "all" || forCmd === command;
2484
+ }
2485
+ function grants(policy, command) {
2486
+ if (!appliesTo(policy, command)) return false;
2487
+ if ((policy.as ?? "permissive").toLowerCase() === "restrictive") return false;
2488
+ const hasUsing = !!policy.using;
2489
+ const hasCheck = !!policy.withCheck;
2490
+ const forCmd = (policy.for ?? "all").toLowerCase();
2491
+ if (forCmd === "all") return hasUsing || hasCheck;
2492
+ switch (command) {
2493
+ case "select":
2494
+ case "delete":
2495
+ return hasUsing;
2496
+ case "insert":
2497
+ return hasCheck;
2498
+ case "update":
2499
+ return hasCheck || hasUsing;
2500
+ }
2501
+ }
2502
+ function hasAnyExpression(policy) {
2503
+ return !!policy.using || !!policy.withCheck;
2504
+ }
2505
+ function readTable(table) {
2506
+ if (typeof table.rlsEnabled !== "boolean") return void 0;
2507
+ const policies = table.policies ?? [];
2508
+ const declaredRls = table.rlsEnabled;
2509
+ const effective = declaredRls || policies.length > 0;
2510
+ if (!effective) return void 0;
2511
+ const grantMap = {};
2512
+ for (const c of COMMANDS) grantMap[c] = policies.some((p) => grants(p, c));
2513
+ return { table: table.name, declaredRls, effective, policies, grants: grantMap };
2514
+ }
2515
+ function buildPolicyReport(analysis, schemaPath) {
2516
+ const tables = [];
2517
+ const findings = [];
2518
+ for (const table of analysis.tables) {
2519
+ const read = readTable(table);
2520
+ if (!read) continue;
2521
+ tables.push(read);
2522
+ if (COMMANDS.every((c) => !read.grants[c])) {
2523
+ findings.push({
2524
+ kind: "denied",
2525
+ table: read.table,
2526
+ 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",
2527
+ // Deliberately not "give it a USING": a policy written `FOR INSERT` consults only its
2528
+ // WITH CHECK, so that advice would be wrong for exactly the declaration this report exists
2529
+ // to catch. The per-policy findings below name the right expression for each one.
2530
+ 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"
2531
+ });
2532
+ } else {
2533
+ for (const command of COMMANDS) {
2534
+ if (read.grants[command]) continue;
2535
+ const named = read.policies.filter((p) => appliesTo(p, command));
2536
+ findings.push({
2537
+ kind: "denied",
2538
+ table: read.table,
2539
+ command,
2540
+ 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`,
2541
+ 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`
2542
+ });
2543
+ }
2544
+ }
2545
+ for (const policy of read.policies) {
2546
+ if (hasAnyExpression(policy)) continue;
2547
+ findings.push({
2548
+ kind: "grants-nothing",
2549
+ table: read.table,
2550
+ policy: policy.name,
2551
+ 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",
2552
+ 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"
2553
+ });
2554
+ }
2555
+ }
2556
+ const policies = tables.reduce((n, t) => n + t.policies.length, 0);
2557
+ return {
2558
+ schema: schemaPath,
2559
+ dialect: analysis.dialect,
2560
+ ok: findings.length === 0,
2561
+ counts: {
2562
+ tables: analysis.tables.length,
2563
+ withRls: tables.length,
2564
+ policies,
2565
+ findings: findings.length
2566
+ },
2567
+ tables,
2568
+ findings,
2569
+ ignoredByGeneratedCode: tables.map((t) => t.table)
2570
+ };
2571
+ }
2572
+ function wrap4(text, indent, first = indent, width = 96) {
2573
+ const words = text.split(/\s+/).filter(Boolean);
2574
+ const lines = [];
2575
+ let line = first;
2576
+ let started = false;
2577
+ for (const w of words) {
2578
+ if (started && line.length + 1 + w.length > width) {
2579
+ lines.push(line);
2580
+ line = indent + w;
2581
+ } else {
2582
+ line = started ? `${line} ${w}` : line + w;
2583
+ started = true;
2584
+ }
2585
+ }
2586
+ if (started) lines.push(line);
2587
+ return lines.join("\n");
2588
+ }
2589
+ function policyLine(policy) {
2590
+ const bits = [(policy.for ?? "all").toUpperCase()];
2591
+ if ((policy.as ?? "").toLowerCase() === "restrictive") bits.push("restrictive");
2592
+ bits.push(policy.to?.length ? `to ${policy.to.join(", ")}` : "to public");
2593
+ const carries = [policy.using ? "USING" : void 0, policy.withCheck ? "WITH CHECK" : void 0].filter(Boolean).join(" + ");
2594
+ bits.push(carries || "no expression");
2595
+ if (policy.linked) bits.push("linked");
2596
+ return bits.join(", ");
2597
+ }
2598
+ function renderPolicyReport(report, style = PLAIN4) {
2599
+ const chalk = style;
2600
+ const out = [];
2601
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
2602
+ out.push(chalk.bold(`DRZL row-level security ${report.schema}`));
2603
+ out.push(
2604
+ chalk.dim(
2605
+ `${report.dialect}, ${plural(report.counts.withRls, "table")} under RLS, ${plural(report.counts.policies, "policy").replace("policys", "policies")}`
2606
+ )
2607
+ );
2608
+ out.push("");
2609
+ if (!report.counts.withRls) {
2610
+ out.push(chalk.green("No table in this schema uses row-level security."));
2611
+ if (report.dialect !== "postgres" && report.dialect !== "cockroach") {
2612
+ out.push(chalk.dim(` ${report.dialect} has no row-level security to declare.`));
2613
+ } else {
2614
+ out.push(chalk.dim(" No table calls .enableRLS() and none declares a pgPolicy."));
2615
+ }
2616
+ return out.join("\n");
2617
+ }
2618
+ const denied = report.findings.filter((f) => f.kind === "denied");
2619
+ if (denied.length) {
2620
+ out.push(chalk.red("These tables refuse the operation your generated code performs"));
2621
+ out.push(
2622
+ chalk.dim(
2623
+ wrap4(
2624
+ "A table under row-level security permits only what a policy grants. The generated service still compiles and its return type still promises rows.",
2625
+ " "
2626
+ )
2627
+ )
2628
+ );
2629
+ out.push("");
2630
+ for (const f of denied) {
2631
+ out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)} ${chalk.dim(f.command ?? "everything")}`);
2632
+ out.push(wrap4(f.detail, " "));
2633
+ if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
2634
+ out.push("");
2635
+ }
2636
+ }
2637
+ const dead = report.findings.filter((f) => f.kind === "grants-nothing");
2638
+ if (dead.length) {
2639
+ out.push(chalk.yellow("These policies grant nothing"));
2640
+ out.push(
2641
+ chalk.dim(
2642
+ wrap4(
2643
+ "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.",
2644
+ " "
2645
+ )
2646
+ )
2647
+ );
2648
+ out.push("");
2649
+ for (const f of dead) {
2650
+ out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)}.${f.policy}`);
2651
+ out.push(wrap4(f.detail, " "));
2652
+ if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
2653
+ out.push("");
2654
+ }
2655
+ }
2656
+ out.push(chalk.cyan("The policies each table carries"));
2657
+ out.push(
2658
+ chalk.dim(
2659
+ wrap4(
2660
+ "Whether these apply to the role your application connects as is the one question this report cannot answer for you.",
2661
+ " "
2662
+ )
2663
+ )
2664
+ );
2665
+ out.push("");
2666
+ for (const t of report.tables) {
2667
+ const flag = t.declaredRls ? "enableRLS()" : "RLS on, implied by its policies";
2668
+ out.push(` ${chalk.dim("-")} ${chalk.bold(t.table)} ${chalk.dim(`(${flag})`)}`);
2669
+ if (!t.policies.length) {
2670
+ out.push(chalk.dim(" no policies"));
2671
+ }
2672
+ for (const p of t.policies) {
2673
+ out.push(` ${p.name}: ${chalk.dim(policyLine(p))}`);
2674
+ }
2675
+ const permitted = COMMANDS.filter((c) => t.grants[c]);
2676
+ out.push(
2677
+ chalk.dim(` permits: ${permitted.length ? permitted.join(", ") : "nothing"}`)
2678
+ );
2679
+ out.push("");
2680
+ }
2681
+ out.push(chalk.yellow("What DRZL generates does not know about any of this"));
2682
+ out.push(
2683
+ chalk.dim(
2684
+ wrap4(
2685
+ `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.`,
2686
+ " "
2687
+ )
2688
+ )
2689
+ );
2690
+ out.push("");
2691
+ out.push(
2692
+ chalk.dim(
2693
+ `${plural(report.counts.findings, "finding")} across ${plural(report.counts.withRls, "table")} under row-level security.`
2694
+ )
2695
+ );
2696
+ return out.join("\n");
2697
+ }
2698
+
2387
2699
  // src/drift.ts
2388
2700
  import { promises as fs2 } from "fs";
2389
2701
  import path2 from "path";
@@ -3309,7 +3621,10 @@ withOutputFlags(
3309
3621
  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
3622
  "--constraints",
3311
3623
  "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")
3624
+ ).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration").option(
3625
+ "--policies",
3626
+ "report the row-level security policies each table carries, and what they refuse"
3627
+ )
3313
3628
  ).action(async (schema, opts) => {
3314
3629
  const out = outputFor(opts);
3315
3630
  {
@@ -3339,7 +3654,14 @@ withOutputFlags(
3339
3654
  process.exit(EXIT_FAILED);
3340
3655
  return;
3341
3656
  }
3342
- if (opts.constraints) {
3657
+ if (opts.constraints && opts.policies) {
3658
+ const msg = "--constraints and --policies are separate reports. Run one, then the other.";
3659
+ if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
3660
+ else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
3661
+ process.exit(EXIT_FAILED);
3662
+ return;
3663
+ }
3664
+ if (opts.constraints || opts.policies) {
3343
3665
  const preflight = buildDoctorReport(analysis, schemaLabel);
3344
3666
  const fatal = preflight.findings.filter((f) => f.level === "error");
3345
3667
  if (fatal.length) {
@@ -3355,6 +3677,8 @@ withOutputFlags(
3355
3677
  process.exit(EXIT_FAILED);
3356
3678
  return;
3357
3679
  }
3680
+ }
3681
+ if (opts.constraints) {
3358
3682
  const drift = buildConstraintDriftReport(analysis, schemaLabel);
3359
3683
  const driftCode = opts.strict && drift.counts.schemaOnly ? EXIT_FINDINGS : EXIT_OK;
3360
3684
  if (opts.sql) {
@@ -3370,6 +3694,17 @@ withOutputFlags(
3370
3694
  process.exit(driftCode);
3371
3695
  return;
3372
3696
  }
3697
+ if (opts.policies) {
3698
+ const policies = buildPolicyReport(analysis, schemaLabel);
3699
+ const policyCode = opts.strict && policies.counts.findings ? EXIT_FINDINGS : EXIT_OK;
3700
+ if (opts.json)
3701
+ out.data(
3702
+ JSON.stringify({ command: "doctor", exitCode: policyCode, ...policies }, null, 2)
3703
+ );
3704
+ else out.data(renderPolicyReport(policies, out.outStyle));
3705
+ process.exit(policyCode);
3706
+ return;
3707
+ }
3373
3708
  const report = buildDoctorReport(analysis, schemaLabel);
3374
3709
  const unreadable = report.findings.some((f) => f.level === "error");
3375
3710
  const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;