@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/cli.cjs CHANGED
@@ -515,8 +515,11 @@ var GeneratorKindSchema = import_zod.z.enum([
515
515
  "next",
516
516
  "tanstack-start",
517
517
  "ts-rest",
518
+ "openapi-fetch",
519
+ "forms",
518
520
  "seed",
519
521
  "fast-check",
522
+ "pothos",
520
523
  "service",
521
524
  "zod",
522
525
  "valibot",
@@ -561,6 +564,13 @@ var GeneratorSchema = import_zod.z.object({
561
564
  contractName: import_zod.z.string().optional(),
562
565
  /** The identifier the assembled Elysia app is exported as. `elysia` only, defaults to `'app'`. */
563
566
  appName: import_zod.z.string().optional(),
567
+ /**
568
+ * The factory the emitted client exports. `openapi-fetch` only, defaults to `'createApiClient'`.
569
+ *
570
+ * A factory rather than a constructed client, because a client carries a `baseUrl` and a `fetch`
571
+ * and neither is a fact about a Drizzle schema.
572
+ */
573
+ clientName: import_zod.z.string().optional(),
564
574
  /** How many rows each generated seed function returns by default. `seed` only, defaults to 10. */
565
575
  count: import_zod.z.number().int().positive().optional(),
566
576
  /**
@@ -670,7 +680,24 @@ var GeneratorSchema = import_zod.z.object({
670
680
  */
671
681
  meta: import_zod.z.union([
672
682
  import_zod.z.boolean(),
673
- import_zod.z.object({ enabled: import_zod.z.boolean().optional(), description: import_zod.z.boolean().optional() }).strict()
683
+ import_zod.z.object({
684
+ enabled: import_zod.z.boolean().optional(),
685
+ description: import_zod.z.boolean().optional(),
686
+ /**
687
+ * Also give each schema an `id`, which registers it in zod's registry under that name.
688
+ *
689
+ * `z.toJSONSchema` then emits `$ref: '#/$defs/<id>'` wherever one schema references
690
+ * another instead of inlining a copy, and `z.toJSONSchema(z.globalRegistry)` returns a
691
+ * `{ schemas: { <id>: ... } }` bundle, which is what makes a generated document
692
+ * self-describing.
693
+ *
694
+ * Off by default because it is the one metadata key with a failure mode. Measured on zod
695
+ * 4.4.3, two schemas sharing an id make a registry dump silently drop one of them, with
696
+ * no warning. The id is built from the qualified table name, so an analysis cannot
697
+ * produce two the same.
698
+ */
699
+ registryIds: import_zod.z.boolean().optional()
700
+ }).strict()
674
701
  ]).optional(),
675
702
  /**
676
703
  * TypeBox only. Give every emitted schema a `~standard` key, so it can be handed to a tRPC or
@@ -743,7 +770,28 @@ var GeneratorSchema = import_zod.z.object({
743
770
  * in JSON Schema, it is ignored, so emitting the wrong dialect produces a document that
744
771
  * validates and then accepts the values the constraints exist to reject.
745
772
  */
746
- target: import_zod.z.enum(["draft-2020-12", "openapi-3.1", "openapi-3.0"]).optional(),
773
+ /**
774
+ * The `forms` generator reads the same key with its own values: which form library to emit for,
775
+ * defaulting to `react-hook-form`. One key, two kinds, as `document` already is for
776
+ * `json-schema` and `openapi-fetch`. A value from the wrong set on the wrong kind is reported
777
+ * by that generator rather than accepted, since neither shares a value with the other.
778
+ */
779
+ target: import_zod.z.enum([
780
+ "draft-2020-12",
781
+ "openapi-3.1",
782
+ "openapi-3.0",
783
+ "react-hook-form",
784
+ "tanstack-form",
785
+ "both"
786
+ ]).optional(),
787
+ /**
788
+ * Which operations get a resolver. `forms` only, defaults to insert and update.
789
+ *
790
+ * `select` is offered because a filter form is a form, but it is off by default: a select
791
+ * schema describes a row that came out of the database, so validating user input against it
792
+ * asks for the generated columns a form never supplies.
793
+ */
794
+ modes: import_zod.z.array(import_zod.z.enum(["insert", "update", "select"])).optional(),
747
795
  /** Also emit `components.ts` for the `json-schema` generator, ready for an OpenAPI document. */
748
796
  components: import_zod.z.boolean().optional(),
749
797
  /**
@@ -753,6 +801,11 @@ var GeneratorSchema = import_zod.z.object({
753
801
  * `true` is the short form. The object form carries the three things a Drizzle schema genuinely
754
802
  * cannot say: what the API is called, where it is served, and which status code that particular
755
803
  * server answers a request that fails its schema with.
804
+ *
805
+ * **The `openapi-fetch` generator reads the same key**, and the two have to be given the same
806
+ * value where both are configured. They are separate generators and nothing checks one against
807
+ * the other, so different options produce a client that describes a different API from the
808
+ * document beside it. The one that bites is `validationStatus`, which lands in both outputs.
756
809
  */
757
810
  document: import_zod.z.union([
758
811
  import_zod.z.boolean(),
@@ -987,7 +1040,9 @@ var ROUTER_KINDS = /* @__PURE__ */ new Set([
987
1040
  "h3",
988
1041
  "effect-http",
989
1042
  "ts-rest",
990
- "elysia"
1043
+ "elysia",
1044
+ "openapi-fetch",
1045
+ "forms"
991
1046
  ]);
992
1047
  var INJECTION_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc"]);
993
1048
  function trpcOutDir(g, cfg) {
@@ -1029,6 +1084,12 @@ function effectHttpOutDir(g, cfg) {
1029
1084
  function tsRestOutDir(g, cfg) {
1030
1085
  return g.path ?? cfg.outDir;
1031
1086
  }
1087
+ function openApiFetchOutDir(g, cfg) {
1088
+ return g.path ?? cfg.outDir;
1089
+ }
1090
+ function formsOutDir(g, cfg) {
1091
+ return g.path ?? cfg.outDir;
1092
+ }
1032
1093
  function elysiaOutDir(g, cfg) {
1033
1094
  return g.path ?? cfg.outDir;
1034
1095
  }
@@ -1038,6 +1099,9 @@ function seedOutDir(g, cfg) {
1038
1099
  function fastCheckOutDir(g, cfg) {
1039
1100
  return g.path ?? cfg.outDir;
1040
1101
  }
1102
+ function pothosOutDir(g, cfg) {
1103
+ return g.path ?? cfg.outDir;
1104
+ }
1041
1105
  function sharedSchemaNames(opts) {
1042
1106
  const resolved = (0, import_validation_core.resolveAffix)(opts);
1043
1107
  return import_validation_core.NAME_MODES.map((mode) => (0, import_validation_core.schemaName)(mode, import_validation_core.AFFIX_PROBE_TABLE, resolved));
@@ -1354,8 +1418,11 @@ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
1354
1418
  if (g.kind === "effect-http") dirs.add(abs(effectHttpOutDir(g, cfg)));
1355
1419
  if (g.kind === "ts-rest") dirs.add(abs(tsRestOutDir(g, cfg)));
1356
1420
  if (g.kind === "elysia") dirs.add(abs(elysiaOutDir(g, cfg)));
1421
+ if (g.kind === "openapi-fetch") dirs.add(abs(openApiFetchOutDir(g, cfg)));
1422
+ if (g.kind === "forms") dirs.add(abs(formsOutDir(g, cfg)));
1357
1423
  if (g.kind === "seed") dirs.add(abs(seedOutDir(g, cfg)));
1358
1424
  if (g.kind === "fast-check") dirs.add(abs(fastCheckOutDir(g, cfg)));
1425
+ if (g.kind === "pothos") dirs.add(abs(pothosOutDir(g, cfg)));
1359
1426
  if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
1360
1427
  if (g.kind === "zod") dirs.add(abs(g.path ?? "src/validators/zod"));
1361
1428
  if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
@@ -1608,20 +1675,81 @@ function tsRestOptions(g, cfg) {
1608
1675
  };
1609
1676
  }
1610
1677
 
1611
- // src/elysia-options.ts
1678
+ // src/openapi-fetch-options.ts
1612
1679
  var VALIDATOR_DEFAULT_DIRS3 = {
1680
+ zod: "src/validators/zod",
1681
+ valibot: "src/validators/valibot",
1682
+ arktype: "src/validators/arktype"
1683
+ };
1684
+ function projectRelative3(p) {
1685
+ return p.startsWith("./") ? p.slice(2) : p;
1686
+ }
1687
+ function openApiFetchOptions(g, cfg) {
1688
+ const configured = g.validation?.library ?? "zod";
1689
+ const library = configured === "typebox" ? "zod" : configured;
1690
+ const siblings = cfg.generators.filter((s) => s.kind === library);
1691
+ const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
1692
+ return {
1693
+ outputDir: openApiFetchOutDir(g, cfg),
1694
+ clientName: g.clientName,
1695
+ document: g.document,
1696
+ outputHeader: g.outputHeader,
1697
+ format: g.format,
1698
+ importExtension: g.importExtension,
1699
+ validation: {
1700
+ ...g.validation,
1701
+ library,
1702
+ useShared: true,
1703
+ importPath: g.validation?.importPath ?? derived
1704
+ }
1705
+ };
1706
+ }
1707
+
1708
+ // src/forms-options.ts
1709
+ var VALIDATOR_DEFAULT_DIRS4 = {
1710
+ zod: "src/validators/zod",
1711
+ valibot: "src/validators/valibot",
1712
+ arktype: "src/validators/arktype",
1713
+ typebox: "src/validators/typebox",
1714
+ effect: "src/validators/effect"
1715
+ };
1716
+ function projectRelative4(p) {
1717
+ return p.startsWith("./") ? p.slice(2) : p;
1718
+ }
1719
+ function formsOptions(g, cfg) {
1720
+ const library = g.validation?.library ?? "zod";
1721
+ const siblings = cfg.generators.filter((s) => s.kind === library);
1722
+ const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
1723
+ return {
1724
+ outputDir: formsOutDir(g, cfg),
1725
+ target: g.target,
1726
+ modes: g.modes,
1727
+ outputHeader: g.outputHeader,
1728
+ format: g.format,
1729
+ importExtension: g.importExtension,
1730
+ validation: {
1731
+ ...g.validation,
1732
+ library,
1733
+ useShared: true,
1734
+ importPath: g.validation?.importPath ?? derived
1735
+ }
1736
+ };
1737
+ }
1738
+
1739
+ // src/elysia-options.ts
1740
+ var VALIDATOR_DEFAULT_DIRS5 = {
1613
1741
  zod: "src/validators/zod",
1614
1742
  valibot: "src/validators/valibot",
1615
1743
  arktype: "src/validators/arktype",
1616
1744
  typebox: "src/validators/typebox"
1617
1745
  };
1618
- function projectRelative3(p) {
1746
+ function projectRelative5(p) {
1619
1747
  return p.startsWith("./") ? p.slice(2) : p;
1620
1748
  }
1621
1749
  function elysiaOptions(g, cfg) {
1622
1750
  const library = g.validation?.library ?? "zod";
1623
1751
  const siblings = cfg.generators.filter((s) => s.kind === library);
1624
- const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
1752
+ const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
1625
1753
  return {
1626
1754
  outputDir: elysiaOutDir(g, cfg),
1627
1755
  appName: g.appName,
@@ -1662,20 +1790,31 @@ function fastCheckOptions(g, cfg) {
1662
1790
  };
1663
1791
  }
1664
1792
 
1793
+ // src/pothos-options.ts
1794
+ function pothosOptions(g, cfg) {
1795
+ return {
1796
+ outputDir: pothosOutDir(g, cfg),
1797
+ naming: g.naming,
1798
+ outputHeader: g.outputHeader,
1799
+ format: g.format,
1800
+ importExtension: g.importExtension
1801
+ };
1802
+ }
1803
+
1665
1804
  // src/h3-options.ts
1666
- var VALIDATOR_DEFAULT_DIRS4 = {
1805
+ var VALIDATOR_DEFAULT_DIRS6 = {
1667
1806
  zod: "src/validators/zod",
1668
1807
  valibot: "src/validators/valibot",
1669
1808
  arktype: "src/validators/arktype"
1670
1809
  };
1671
- function projectRelative4(p) {
1810
+ function projectRelative6(p) {
1672
1811
  return p.startsWith("./") ? p.slice(2) : p;
1673
1812
  }
1674
1813
  function h3Options(g, cfg) {
1675
1814
  const configured = g.validation?.library ?? "zod";
1676
1815
  const library = configured === "typebox" ? "zod" : configured;
1677
1816
  const siblings = cfg.generators.filter((s) => s.kind === library);
1678
- const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
1817
+ const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
1679
1818
  return {
1680
1819
  outputDir: h3OutDir(g, cfg),
1681
1820
  h3: g.h3,
@@ -1709,19 +1848,19 @@ function mcpOptions(g, cfg) {
1709
1848
  }
1710
1849
 
1711
1850
  // src/next-options.ts
1712
- var VALIDATOR_DEFAULT_DIRS5 = {
1851
+ var VALIDATOR_DEFAULT_DIRS7 = {
1713
1852
  zod: "src/validators/zod",
1714
1853
  valibot: "src/validators/valibot",
1715
1854
  arktype: "src/validators/arktype"
1716
1855
  };
1717
- function projectRelative5(p) {
1856
+ function projectRelative7(p) {
1718
1857
  return p.startsWith("./") ? p.slice(2) : p;
1719
1858
  }
1720
1859
  function nextOptions(g, cfg) {
1721
1860
  const configured = g.validation?.library ?? "zod";
1722
1861
  const library = configured === "typebox" ? "zod" : configured;
1723
1862
  const siblings = cfg.generators.filter((s) => s.kind === library);
1724
- const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
1863
+ const derived = siblings.length === 1 ? projectRelative7(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS7[library]) : void 0;
1725
1864
  return {
1726
1865
  outputDir: nextOutDir(g, cfg),
1727
1866
  naming: g.naming,
@@ -1738,19 +1877,19 @@ function nextOptions(g, cfg) {
1738
1877
  }
1739
1878
 
1740
1879
  // src/tanstack-start-options.ts
1741
- var VALIDATOR_DEFAULT_DIRS6 = {
1880
+ var VALIDATOR_DEFAULT_DIRS8 = {
1742
1881
  zod: "src/validators/zod",
1743
1882
  valibot: "src/validators/valibot",
1744
1883
  arktype: "src/validators/arktype"
1745
1884
  };
1746
- function projectRelative6(p) {
1885
+ function projectRelative8(p) {
1747
1886
  return p.startsWith("./") ? p.slice(2) : p;
1748
1887
  }
1749
1888
  function tanstackStartOptions(g, cfg) {
1750
1889
  const configured = g.validation?.library ?? "zod";
1751
1890
  const library = configured === "typebox" ? "zod" : configured;
1752
1891
  const siblings = cfg.generators.filter((s) => s.kind === library);
1753
- const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
1892
+ const derived = siblings.length === 1 ? projectRelative8(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS8[library]) : void 0;
1754
1893
  return {
1755
1894
  outputDir: tanstackStartOutDir(g, cfg),
1756
1895
  naming: g.naming,
@@ -1834,7 +1973,7 @@ function trpcOptions(g, cfg, servicesDir) {
1834
1973
  }
1835
1974
 
1836
1975
  // src/generator-registry.ts
1837
- var VALIDATOR_DEFAULT_DIRS7 = {
1976
+ var VALIDATOR_DEFAULT_DIRS9 = {
1838
1977
  zod: "src/validators/zod",
1839
1978
  valibot: "src/validators/valibot",
1840
1979
  arktype: "src/validators/arktype",
@@ -1867,10 +2006,14 @@ var GENERATORS = [
1867
2006
  //
1868
2007
  // The side effect was invisible and lasted longer than the reason: tsup externalises
1869
2008
  // `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
1870
- // inside `dist` while the other six were resolved from `node_modules`. All fourteen are on the
1871
- // registry now and all fourteen are `dependencies`, which is what makes every one of them a
1872
- // package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
1873
- // for every kind rather than for six of them.
2009
+ // inside `dist` while the rest were resolved from `node_modules`. Every generator package is on
2010
+ // the registry now and every one is a `dependencies` entry, which is what makes each of them a
2011
+ // package that can genuinely be absent, and `loadGenerator` tell absence apart from failure for
2012
+ // every kind rather than for some of them.
2013
+ //
2014
+ // Counted nowhere on purpose. This paragraph twice carried a number that the next batch of
2015
+ // generators falsified, and `packages/cli/test/generator-registry.spec.ts` asserts the property
2016
+ // against the manifest, which is where a quantity belongs.
1874
2017
  specifier: "@drzl/generator-trpc",
1875
2018
  load: () => import("@drzl/generator-trpc"),
1876
2019
  construct: (m, analysis) => new m.TRPCGenerator(analysis),
@@ -1922,9 +2065,10 @@ var GENERATORS = [
1922
2065
  // This one and the three below spent one release each in `optionalDependencies`, because a
1923
2066
  // package that has never existed cannot publish through npm's trusted-publisher OIDC flow and
1924
2067
  // naming it as a hard dependency in the release that introduces it breaks `npm i @drzl/cli`
1925
- // for everyone until the first publish lands. All four are on the registry now, so all four
1926
- // are ordinary dependencies. `scripts/verify/stages/33-registry-deps.sh` gates both halves of
1927
- // that rule and is what reported the promotion was due.
2068
+ // for everyone until the first publish lands. The six generators added after them went the
2069
+ // same way and were promoted the same way once their first versions were published by hand.
2070
+ // Nothing here is optional any more. `scripts/verify/stages/33-registry-deps.sh` gates both
2071
+ // halves of that rule and is what reported each promotion was due.
1928
2072
  specifier: "@drzl/generator-mcp",
1929
2073
  load: () => import("@drzl/generator-mcp"),
1930
2074
  construct: (m, analysis) => new m.MCPGenerator(analysis),
@@ -1979,6 +2123,22 @@ var GENERATORS = [
1979
2123
  outputDir: (g, cfg) => tsRestOutDir(g, cfg),
1980
2124
  options: (g, cfg) => tsRestOptions(g, cfg)
1981
2125
  },
2126
+ {
2127
+ kind: "openapi-fetch",
2128
+ specifier: "@drzl/generator-openapi-fetch",
2129
+ load: () => import("@drzl/generator-openapi-fetch"),
2130
+ construct: (m, analysis) => new m.OpenApiFetchGenerator(analysis),
2131
+ outputDir: (g, cfg) => openApiFetchOutDir(g, cfg),
2132
+ options: (g, cfg) => openApiFetchOptions(g, cfg)
2133
+ },
2134
+ {
2135
+ kind: "forms",
2136
+ specifier: "@drzl/generator-forms",
2137
+ load: () => import("@drzl/generator-forms"),
2138
+ construct: (m, analysis) => new m.FormsGenerator(analysis),
2139
+ outputDir: (g, cfg) => formsOutDir(g, cfg),
2140
+ options: (g, cfg) => formsOptions(g, cfg)
2141
+ },
1982
2142
  {
1983
2143
  kind: "elysia",
1984
2144
  specifier: "@drzl/generator-elysia",
@@ -2003,6 +2163,14 @@ var GENERATORS = [
2003
2163
  outputDir: (g, cfg) => fastCheckOutDir(g, cfg),
2004
2164
  options: (g, cfg) => fastCheckOptions(g, cfg)
2005
2165
  },
2166
+ {
2167
+ kind: "pothos",
2168
+ specifier: "@drzl/generator-pothos",
2169
+ load: () => import("@drzl/generator-pothos"),
2170
+ construct: (m, analysis) => new m.PothosGenerator(analysis),
2171
+ outputDir: (g, cfg) => pothosOutDir(g, cfg),
2172
+ options: (g, cfg) => pothosOptions(g, cfg)
2173
+ },
2006
2174
  {
2007
2175
  kind: "service",
2008
2176
  specifier: "@drzl/generator-service",
@@ -2016,7 +2184,7 @@ var GENERATORS = [
2016
2184
  specifier: "@drzl/generator-zod",
2017
2185
  load: () => import("@drzl/generator-zod"),
2018
2186
  construct: (m, analysis) => new m.ZodGenerator(analysis),
2019
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.zod,
2187
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.zod,
2020
2188
  // `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
2021
2189
  // four rather than being passed and ignored.
2022
2190
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
@@ -2030,7 +2198,7 @@ var GENERATORS = [
2030
2198
  specifier: "@drzl/generator-valibot",
2031
2199
  load: () => import("@drzl/generator-valibot"),
2032
2200
  construct: (m, analysis) => new m.ValibotGenerator(analysis),
2033
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.valibot,
2201
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.valibot,
2034
2202
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
2035
2203
  },
2036
2204
  {
@@ -2038,7 +2206,7 @@ var GENERATORS = [
2038
2206
  specifier: "@drzl/generator-arktype",
2039
2207
  load: () => import("@drzl/generator-arktype"),
2040
2208
  construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
2041
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.arktype,
2209
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.arktype,
2042
2210
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
2043
2211
  },
2044
2212
  {
@@ -2046,7 +2214,7 @@ var GENERATORS = [
2046
2214
  specifier: "@drzl/generator-typebox",
2047
2215
  load: () => import("@drzl/generator-typebox"),
2048
2216
  construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
2049
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.typebox,
2217
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.typebox,
2050
2218
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
2051
2219
  },
2052
2220
  {
@@ -2054,7 +2222,7 @@ var GENERATORS = [
2054
2222
  specifier: "@drzl/generator-effect",
2055
2223
  load: () => import("@drzl/generator-effect"),
2056
2224
  construct: (m, analysis) => new m.EffectGenerator(analysis),
2057
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7.effect,
2225
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.effect,
2058
2226
  options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
2059
2227
  },
2060
2228
  {
@@ -2062,7 +2230,7 @@ var GENERATORS = [
2062
2230
  specifier: "@drzl/generator-json-schema",
2063
2231
  load: () => import("@drzl/generator-json-schema"),
2064
2232
  construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
2065
- outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS7["json-schema"],
2233
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9["json-schema"],
2066
2234
  options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
2067
2235
  }
2068
2236
  ];
@@ -2246,6 +2414,101 @@ var import_validation_core3 = require("@drzl/validation-core");
2246
2414
 
2247
2415
  // src/doctor.ts
2248
2416
  var import_validation_core2 = require("@drzl/validation-core");
2417
+
2418
+ // src/auth-tables.ts
2419
+ var SIGNATURES = [
2420
+ {
2421
+ model: "account",
2422
+ // `providerId` beside `accountId` is the distinctive pair: it is a link to an external identity
2423
+ // provider, which an application table has no reason to model this way.
2424
+ required: ["accountId", "providerId", "userId"],
2425
+ supporting: ["accessToken", "refreshToken", "idToken", "password", "scope"],
2426
+ conventionalName: "account",
2427
+ secrets: ["accessToken", "refreshToken", "idToken", "password"]
2428
+ },
2429
+ {
2430
+ model: "session",
2431
+ required: ["token", "expiresAt", "userId"],
2432
+ supporting: ["ipAddress", "userAgent"],
2433
+ conventionalName: "session",
2434
+ // A session token is a bearer credential: whoever reads it is that user until it expires.
2435
+ secrets: ["token"]
2436
+ },
2437
+ {
2438
+ model: "verification",
2439
+ // `identifier` and `value` are deliberately generic names, which is why `expiresAt` is required
2440
+ // too: the three together are a short-lived token store and little else.
2441
+ required: ["identifier", "value", "expiresAt"],
2442
+ supporting: [],
2443
+ conventionalName: "verification",
2444
+ secrets: ["value"]
2445
+ },
2446
+ {
2447
+ model: "user",
2448
+ required: ["email", "emailVerified"],
2449
+ supporting: ["name", "image"],
2450
+ conventionalName: "user",
2451
+ secrets: []
2452
+ }
2453
+ ];
2454
+ function columnKeys(table) {
2455
+ const out = /* @__PURE__ */ new Map();
2456
+ for (const c of table.columns) out.set(normalise(c.name), c);
2457
+ return out;
2458
+ }
2459
+ function normalise(name) {
2460
+ return name.replace(/[_-]/g, "").toLowerCase();
2461
+ }
2462
+ function has(keys, name) {
2463
+ return keys.has(normalise(name));
2464
+ }
2465
+ function actualName(table, wanted) {
2466
+ return table.columns.find((c) => normalise(c.name) === normalise(wanted))?.name;
2467
+ }
2468
+ function matchOne(table, sig) {
2469
+ const keys = columnKeys(table);
2470
+ const missing = sig.required.filter((r) => !has(keys, r));
2471
+ if (missing.length) return void 0;
2472
+ const supporting = sig.supporting.filter((s) => has(keys, s));
2473
+ const nameMatches = normalise(table.name) === normalise(sig.conventionalName) || normalise(table.name) === `${normalise(sig.conventionalName)}s`;
2474
+ const confidence = supporting.length > 0 || nameMatches ? "strong" : "likely";
2475
+ const matched = [...sig.required, ...supporting].map((c) => actualName(table, c)).filter((c) => Boolean(c));
2476
+ const secrets = sig.secrets.map((c) => actualName(table, c)).filter((c) => Boolean(c));
2477
+ return { table: table.name, model: sig.model, confidence, matched, secrets };
2478
+ }
2479
+ function detectAuthTables(analysis) {
2480
+ const found = [];
2481
+ for (const table of analysis.tables) {
2482
+ for (const sig of SIGNATURES) {
2483
+ const m = matchOne(table, sig);
2484
+ if (m) {
2485
+ found.push(m);
2486
+ break;
2487
+ }
2488
+ }
2489
+ }
2490
+ const hasNonUser = found.some((f) => f.model !== "user");
2491
+ return hasNonUser ? found : found.filter((f) => f.model !== "user");
2492
+ }
2493
+ function authTablesWithSecrets(matches) {
2494
+ return matches.filter((m) => m.secrets.length > 0);
2495
+ }
2496
+ function excludeSuggestion(matches) {
2497
+ const names = [...new Set(matches.map((m) => m.table))].sort();
2498
+ return `exclude: [${names.map((n) => `'${n}'`).join(", ")}]`;
2499
+ }
2500
+ function authTableWarnings(analysis) {
2501
+ const risky = authTablesWithSecrets(detectAuthTables(analysis));
2502
+ if (!risky.length) return [];
2503
+ const lines = risky.map(
2504
+ (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"}.`
2505
+ );
2506
+ return [
2507
+ `drzl generate: ${lines.join(" ")} Keep ${risky.length === 1 ? "it" : "them"} out with ${excludeSuggestion(risky)}, or leave it if the route is deliberate.`
2508
+ ];
2509
+ }
2510
+
2511
+ // src/doctor.ts
2249
2512
  var import_chalk2 = require("chalk");
2250
2513
  var PLAIN = new import_chalk2.Chalk({ level: 0 });
2251
2514
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
@@ -2418,6 +2681,18 @@ function buildDoctorReport(analysis, schemaPath) {
2418
2681
  hint: i.hint
2419
2682
  });
2420
2683
  }
2684
+ for (const match of detectAuthTables(analysis)) {
2685
+ const secrets = match.secrets.length ? ` It holds ${match.secrets.map((c) => `\`${c}\``).join(", ")}, which a generated read route would return to whoever calls it.` : "";
2686
+ findings.push({
2687
+ kind: "auth-table",
2688
+ // A warning rather than an error: this is a real leak and it is also a guess about someone
2689
+ // else's schema, and a doctor that failed the build on a guess would be switched off.
2690
+ level: "warn",
2691
+ table: match.table,
2692
+ message: `"${match.table}" matches the shape of an authentication library's ${match.model} table (${match.matched.join(", ")}).${secrets}`,
2693
+ hint: `Keep it out of every generator with ${excludeSuggestion([match])}.`
2694
+ });
2695
+ }
2421
2696
  for (const i of analysis.issues) {
2422
2697
  if (i.code !== "DRZL_ANL_UNKNOWN_COLUMN") continue;
2423
2698
  findings.push({
@@ -2456,6 +2731,11 @@ var SECTIONS = [
2456
2731
  title: "Columns DRZL cannot type",
2457
2732
  why: "These get a validator that accepts any value."
2458
2733
  },
2734
+ {
2735
+ kinds: ["auth-table"],
2736
+ title: "Tables that look like an authentication library's",
2737
+ why: "Every generator loops over every table it finds, so these get routes too."
2738
+ },
2459
2739
  {
2460
2740
  kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
2461
2741
  title: "CHECK constraints DRZL does not enforce",
@@ -3578,6 +3858,234 @@ function renderConstraintDriftSql(report) {
3578
3858
  return out.join("\n");
3579
3859
  }
3580
3860
 
3861
+ // src/policy-report.ts
3862
+ var import_chalk5 = require("chalk");
3863
+ var PLAIN4 = new import_chalk5.Chalk({ level: 0 });
3864
+ var COMMANDS = ["select", "insert", "update", "delete"];
3865
+ var DENIAL = {
3866
+ select: "every read returns zero rows",
3867
+ insert: "every insert is refused",
3868
+ update: "every update is refused",
3869
+ delete: "every delete is refused"
3870
+ };
3871
+ function appliesTo(policy, command) {
3872
+ const forCmd = (policy.for ?? "all").toLowerCase();
3873
+ return forCmd === "all" || forCmd === command;
3874
+ }
3875
+ function grants(policy, command) {
3876
+ if (!appliesTo(policy, command)) return false;
3877
+ if ((policy.as ?? "permissive").toLowerCase() === "restrictive") return false;
3878
+ const hasUsing = !!policy.using;
3879
+ const hasCheck = !!policy.withCheck;
3880
+ const forCmd = (policy.for ?? "all").toLowerCase();
3881
+ if (forCmd === "all") return hasUsing || hasCheck;
3882
+ switch (command) {
3883
+ case "select":
3884
+ case "delete":
3885
+ return hasUsing;
3886
+ case "insert":
3887
+ return hasCheck;
3888
+ case "update":
3889
+ return hasCheck || hasUsing;
3890
+ }
3891
+ }
3892
+ function hasAnyExpression(policy) {
3893
+ return !!policy.using || !!policy.withCheck;
3894
+ }
3895
+ function readTable(table) {
3896
+ if (typeof table.rlsEnabled !== "boolean") return void 0;
3897
+ const policies = table.policies ?? [];
3898
+ const declaredRls = table.rlsEnabled;
3899
+ const effective = declaredRls || policies.length > 0;
3900
+ if (!effective) return void 0;
3901
+ const grantMap = {};
3902
+ for (const c of COMMANDS) grantMap[c] = policies.some((p) => grants(p, c));
3903
+ return { table: table.name, declaredRls, effective, policies, grants: grantMap };
3904
+ }
3905
+ function buildPolicyReport(analysis, schemaPath) {
3906
+ const tables = [];
3907
+ const findings = [];
3908
+ for (const table of analysis.tables) {
3909
+ const read = readTable(table);
3910
+ if (!read) continue;
3911
+ tables.push(read);
3912
+ if (COMMANDS.every((c) => !read.grants[c])) {
3913
+ findings.push({
3914
+ kind: "denied",
3915
+ table: read.table,
3916
+ 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",
3917
+ // Deliberately not "give it a USING": a policy written `FOR INSERT` consults only its
3918
+ // WITH CHECK, so that advice would be wrong for exactly the declaration this report exists
3919
+ // to catch. The per-policy findings below name the right expression for each one.
3920
+ 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"
3921
+ });
3922
+ } else {
3923
+ for (const command of COMMANDS) {
3924
+ if (read.grants[command]) continue;
3925
+ const named = read.policies.filter((p) => appliesTo(p, command));
3926
+ findings.push({
3927
+ kind: "denied",
3928
+ table: read.table,
3929
+ command,
3930
+ 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`,
3931
+ 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`
3932
+ });
3933
+ }
3934
+ }
3935
+ for (const policy of read.policies) {
3936
+ if (hasAnyExpression(policy)) continue;
3937
+ findings.push({
3938
+ kind: "grants-nothing",
3939
+ table: read.table,
3940
+ policy: policy.name,
3941
+ 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",
3942
+ 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"
3943
+ });
3944
+ }
3945
+ }
3946
+ const policies = tables.reduce((n, t) => n + t.policies.length, 0);
3947
+ return {
3948
+ schema: schemaPath,
3949
+ dialect: analysis.dialect,
3950
+ ok: findings.length === 0,
3951
+ counts: {
3952
+ tables: analysis.tables.length,
3953
+ withRls: tables.length,
3954
+ policies,
3955
+ findings: findings.length
3956
+ },
3957
+ tables,
3958
+ findings,
3959
+ ignoredByGeneratedCode: tables.map((t) => t.table)
3960
+ };
3961
+ }
3962
+ function wrap4(text, indent, first = indent, width = 96) {
3963
+ const words = text.split(/\s+/).filter(Boolean);
3964
+ const lines = [];
3965
+ let line = first;
3966
+ let started = false;
3967
+ for (const w of words) {
3968
+ if (started && line.length + 1 + w.length > width) {
3969
+ lines.push(line);
3970
+ line = indent + w;
3971
+ } else {
3972
+ line = started ? `${line} ${w}` : line + w;
3973
+ started = true;
3974
+ }
3975
+ }
3976
+ if (started) lines.push(line);
3977
+ return lines.join("\n");
3978
+ }
3979
+ function policyLine(policy) {
3980
+ const bits = [(policy.for ?? "all").toUpperCase()];
3981
+ if ((policy.as ?? "").toLowerCase() === "restrictive") bits.push("restrictive");
3982
+ bits.push(policy.to?.length ? `to ${policy.to.join(", ")}` : "to public");
3983
+ const carries = [policy.using ? "USING" : void 0, policy.withCheck ? "WITH CHECK" : void 0].filter(Boolean).join(" + ");
3984
+ bits.push(carries || "no expression");
3985
+ if (policy.linked) bits.push("linked");
3986
+ return bits.join(", ");
3987
+ }
3988
+ function renderPolicyReport(report, style = PLAIN4) {
3989
+ const chalk = style;
3990
+ const out = [];
3991
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
3992
+ out.push(chalk.bold(`DRZL row-level security ${report.schema}`));
3993
+ out.push(
3994
+ chalk.dim(
3995
+ `${report.dialect}, ${plural(report.counts.withRls, "table")} under RLS, ${plural(report.counts.policies, "policy").replace("policys", "policies")}`
3996
+ )
3997
+ );
3998
+ out.push("");
3999
+ if (!report.counts.withRls) {
4000
+ out.push(chalk.green("No table in this schema uses row-level security."));
4001
+ if (report.dialect !== "postgres" && report.dialect !== "cockroach") {
4002
+ out.push(chalk.dim(` ${report.dialect} has no row-level security to declare.`));
4003
+ } else {
4004
+ out.push(chalk.dim(" No table calls .enableRLS() and none declares a pgPolicy."));
4005
+ }
4006
+ return out.join("\n");
4007
+ }
4008
+ const denied = report.findings.filter((f) => f.kind === "denied");
4009
+ if (denied.length) {
4010
+ out.push(chalk.red("These tables refuse the operation your generated code performs"));
4011
+ out.push(
4012
+ chalk.dim(
4013
+ wrap4(
4014
+ "A table under row-level security permits only what a policy grants. The generated service still compiles and its return type still promises rows.",
4015
+ " "
4016
+ )
4017
+ )
4018
+ );
4019
+ out.push("");
4020
+ for (const f of denied) {
4021
+ out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)} ${chalk.dim(f.command ?? "everything")}`);
4022
+ out.push(wrap4(f.detail, " "));
4023
+ if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
4024
+ out.push("");
4025
+ }
4026
+ }
4027
+ const dead = report.findings.filter((f) => f.kind === "grants-nothing");
4028
+ if (dead.length) {
4029
+ out.push(chalk.yellow("These policies grant nothing"));
4030
+ out.push(
4031
+ chalk.dim(
4032
+ wrap4(
4033
+ "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.",
4034
+ " "
4035
+ )
4036
+ )
4037
+ );
4038
+ out.push("");
4039
+ for (const f of dead) {
4040
+ out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)}.${f.policy}`);
4041
+ out.push(wrap4(f.detail, " "));
4042
+ if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
4043
+ out.push("");
4044
+ }
4045
+ }
4046
+ out.push(chalk.cyan("The policies each table carries"));
4047
+ out.push(
4048
+ chalk.dim(
4049
+ wrap4(
4050
+ "Whether these apply to the role your application connects as is the one question this report cannot answer for you.",
4051
+ " "
4052
+ )
4053
+ )
4054
+ );
4055
+ out.push("");
4056
+ for (const t of report.tables) {
4057
+ const flag = t.declaredRls ? "enableRLS()" : "RLS on, implied by its policies";
4058
+ out.push(` ${chalk.dim("-")} ${chalk.bold(t.table)} ${chalk.dim(`(${flag})`)}`);
4059
+ if (!t.policies.length) {
4060
+ out.push(chalk.dim(" no policies"));
4061
+ }
4062
+ for (const p of t.policies) {
4063
+ out.push(` ${p.name}: ${chalk.dim(policyLine(p))}`);
4064
+ }
4065
+ const permitted = COMMANDS.filter((c) => t.grants[c]);
4066
+ out.push(
4067
+ chalk.dim(` permits: ${permitted.length ? permitted.join(", ") : "nothing"}`)
4068
+ );
4069
+ out.push("");
4070
+ }
4071
+ out.push(chalk.yellow("What DRZL generates does not know about any of this"));
4072
+ out.push(
4073
+ chalk.dim(
4074
+ wrap4(
4075
+ `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.`,
4076
+ " "
4077
+ )
4078
+ )
4079
+ );
4080
+ out.push("");
4081
+ out.push(
4082
+ chalk.dim(
4083
+ `${plural(report.counts.findings, "finding")} across ${plural(report.counts.withRls, "table")} under row-level security.`
4084
+ )
4085
+ );
4086
+ return out.join("\n");
4087
+ }
4088
+
3581
4089
  // src/drift.ts
3582
4090
  var import_node_fs = require("fs");
3583
4091
  var import_node_path = __toESM(require("path"), 1);
@@ -4503,7 +5011,10 @@ withOutputFlags(
4503
5011
  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(
4504
5012
  "--constraints",
4505
5013
  "report what each side enforces that the other does not, instead of the usual findings"
4506
- ).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration")
5014
+ ).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration").option(
5015
+ "--policies",
5016
+ "report the row-level security policies each table carries, and what they refuse"
5017
+ )
4507
5018
  ).action(async (schema, opts) => {
4508
5019
  const out = outputFor(opts);
4509
5020
  {
@@ -4533,7 +5044,14 @@ withOutputFlags(
4533
5044
  process.exit(EXIT_FAILED);
4534
5045
  return;
4535
5046
  }
4536
- if (opts.constraints) {
5047
+ if (opts.constraints && opts.policies) {
5048
+ const msg = "--constraints and --policies are separate reports. Run one, then the other.";
5049
+ if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
5050
+ else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
5051
+ process.exit(EXIT_FAILED);
5052
+ return;
5053
+ }
5054
+ if (opts.constraints || opts.policies) {
4537
5055
  const preflight = buildDoctorReport(analysis, schemaLabel);
4538
5056
  const fatal = preflight.findings.filter((f) => f.level === "error");
4539
5057
  if (fatal.length) {
@@ -4549,6 +5067,8 @@ withOutputFlags(
4549
5067
  process.exit(EXIT_FAILED);
4550
5068
  return;
4551
5069
  }
5070
+ }
5071
+ if (opts.constraints) {
4552
5072
  const drift = buildConstraintDriftReport(analysis, schemaLabel);
4553
5073
  const driftCode = opts.strict && drift.counts.schemaOnly ? EXIT_FINDINGS : EXIT_OK;
4554
5074
  if (opts.sql) {
@@ -4564,6 +5084,17 @@ withOutputFlags(
4564
5084
  process.exit(driftCode);
4565
5085
  return;
4566
5086
  }
5087
+ if (opts.policies) {
5088
+ const policies = buildPolicyReport(analysis, schemaLabel);
5089
+ const policyCode = opts.strict && policies.counts.findings ? EXIT_FINDINGS : EXIT_OK;
5090
+ if (opts.json)
5091
+ out.data(
5092
+ JSON.stringify({ command: "doctor", exitCode: policyCode, ...policies }, null, 2)
5093
+ );
5094
+ else out.data(renderPolicyReport(policies, out.outStyle));
5095
+ process.exit(policyCode);
5096
+ return;
5097
+ }
4567
5098
  const report = buildDoctorReport(analysis, schemaLabel);
4568
5099
  const unreadable = report.findings.some((f) => f.level === "error");
4569
5100
  const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
@@ -4784,6 +5315,7 @@ withOutputFlags(
4784
5315
  analysis.tables = filterTables(narrowed.tables, cfg);
4785
5316
  for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
4786
5317
  for (const w of wideColumnWarning(analysis.issues)) warn(w);
5318
+ for (const w of authTableWarnings(analysis)) warn(w);
4787
5319
  const empty = nothingToGenerate({
4788
5320
  schema: source.schema,
4789
5321
  analyzed: narrowed.tables,