@drzl/cli 4.35.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/{chunk-S5JWTWWU.js → chunk-EQ4OSVMJ.js} +68 -4
- package/dist/chunk-EQ4OSVMJ.js.map +1 -0
- package/dist/cli.cjs +498 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +436 -29
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +69 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +49 -5
- package/dist/config.d.ts +49 -5
- package/dist/config.js +5 -1
- package/dist/drzl.config.schema.json +23 -1
- package/package.json +13 -11
- package/dist/chunk-S5JWTWWU.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -515,6 +515,8 @@ 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",
|
|
520
522
|
"service",
|
|
@@ -561,6 +563,13 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
561
563
|
contractName: import_zod.z.string().optional(),
|
|
562
564
|
/** The identifier the assembled Elysia app is exported as. `elysia` only, defaults to `'app'`. */
|
|
563
565
|
appName: import_zod.z.string().optional(),
|
|
566
|
+
/**
|
|
567
|
+
* The factory the emitted client exports. `openapi-fetch` only, defaults to `'createApiClient'`.
|
|
568
|
+
*
|
|
569
|
+
* A factory rather than a constructed client, because a client carries a `baseUrl` and a `fetch`
|
|
570
|
+
* and neither is a fact about a Drizzle schema.
|
|
571
|
+
*/
|
|
572
|
+
clientName: import_zod.z.string().optional(),
|
|
564
573
|
/** How many rows each generated seed function returns by default. `seed` only, defaults to 10. */
|
|
565
574
|
count: import_zod.z.number().int().positive().optional(),
|
|
566
575
|
/**
|
|
@@ -670,7 +679,24 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
670
679
|
*/
|
|
671
680
|
meta: import_zod.z.union([
|
|
672
681
|
import_zod.z.boolean(),
|
|
673
|
-
import_zod.z.object({
|
|
682
|
+
import_zod.z.object({
|
|
683
|
+
enabled: import_zod.z.boolean().optional(),
|
|
684
|
+
description: import_zod.z.boolean().optional(),
|
|
685
|
+
/**
|
|
686
|
+
* Also give each schema an `id`, which registers it in zod's registry under that name.
|
|
687
|
+
*
|
|
688
|
+
* `z.toJSONSchema` then emits `$ref: '#/$defs/<id>'` wherever one schema references
|
|
689
|
+
* another instead of inlining a copy, and `z.toJSONSchema(z.globalRegistry)` returns a
|
|
690
|
+
* `{ schemas: { <id>: ... } }` bundle, which is what makes a generated document
|
|
691
|
+
* self-describing.
|
|
692
|
+
*
|
|
693
|
+
* Off by default because it is the one metadata key with a failure mode. Measured on zod
|
|
694
|
+
* 4.4.3, two schemas sharing an id make a registry dump silently drop one of them, with
|
|
695
|
+
* no warning. The id is built from the qualified table name, so an analysis cannot
|
|
696
|
+
* produce two the same.
|
|
697
|
+
*/
|
|
698
|
+
registryIds: import_zod.z.boolean().optional()
|
|
699
|
+
}).strict()
|
|
674
700
|
]).optional(),
|
|
675
701
|
/**
|
|
676
702
|
* TypeBox only. Give every emitted schema a `~standard` key, so it can be handed to a tRPC or
|
|
@@ -743,7 +769,28 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
743
769
|
* in JSON Schema, it is ignored, so emitting the wrong dialect produces a document that
|
|
744
770
|
* validates and then accepts the values the constraints exist to reject.
|
|
745
771
|
*/
|
|
746
|
-
|
|
772
|
+
/**
|
|
773
|
+
* The `forms` generator reads the same key with its own values: which form library to emit for,
|
|
774
|
+
* defaulting to `react-hook-form`. One key, two kinds, as `document` already is for
|
|
775
|
+
* `json-schema` and `openapi-fetch`. A value from the wrong set on the wrong kind is reported
|
|
776
|
+
* by that generator rather than accepted, since neither shares a value with the other.
|
|
777
|
+
*/
|
|
778
|
+
target: import_zod.z.enum([
|
|
779
|
+
"draft-2020-12",
|
|
780
|
+
"openapi-3.1",
|
|
781
|
+
"openapi-3.0",
|
|
782
|
+
"react-hook-form",
|
|
783
|
+
"tanstack-form",
|
|
784
|
+
"both"
|
|
785
|
+
]).optional(),
|
|
786
|
+
/**
|
|
787
|
+
* Which operations get a resolver. `forms` only, defaults to insert and update.
|
|
788
|
+
*
|
|
789
|
+
* `select` is offered because a filter form is a form, but it is off by default: a select
|
|
790
|
+
* schema describes a row that came out of the database, so validating user input against it
|
|
791
|
+
* asks for the generated columns a form never supplies.
|
|
792
|
+
*/
|
|
793
|
+
modes: import_zod.z.array(import_zod.z.enum(["insert", "update", "select"])).optional(),
|
|
747
794
|
/** Also emit `components.ts` for the `json-schema` generator, ready for an OpenAPI document. */
|
|
748
795
|
components: import_zod.z.boolean().optional(),
|
|
749
796
|
/**
|
|
@@ -753,6 +800,11 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
753
800
|
* `true` is the short form. The object form carries the three things a Drizzle schema genuinely
|
|
754
801
|
* cannot say: what the API is called, where it is served, and which status code that particular
|
|
755
802
|
* server answers a request that fails its schema with.
|
|
803
|
+
*
|
|
804
|
+
* **The `openapi-fetch` generator reads the same key**, and the two have to be given the same
|
|
805
|
+
* value where both are configured. They are separate generators and nothing checks one against
|
|
806
|
+
* the other, so different options produce a client that describes a different API from the
|
|
807
|
+
* document beside it. The one that bites is `validationStatus`, which lands in both outputs.
|
|
756
808
|
*/
|
|
757
809
|
document: import_zod.z.union([
|
|
758
810
|
import_zod.z.boolean(),
|
|
@@ -987,7 +1039,9 @@ var ROUTER_KINDS = /* @__PURE__ */ new Set([
|
|
|
987
1039
|
"h3",
|
|
988
1040
|
"effect-http",
|
|
989
1041
|
"ts-rest",
|
|
990
|
-
"elysia"
|
|
1042
|
+
"elysia",
|
|
1043
|
+
"openapi-fetch",
|
|
1044
|
+
"forms"
|
|
991
1045
|
]);
|
|
992
1046
|
var INJECTION_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc"]);
|
|
993
1047
|
function trpcOutDir(g, cfg) {
|
|
@@ -1029,6 +1083,12 @@ function effectHttpOutDir(g, cfg) {
|
|
|
1029
1083
|
function tsRestOutDir(g, cfg) {
|
|
1030
1084
|
return g.path ?? cfg.outDir;
|
|
1031
1085
|
}
|
|
1086
|
+
function openApiFetchOutDir(g, cfg) {
|
|
1087
|
+
return g.path ?? cfg.outDir;
|
|
1088
|
+
}
|
|
1089
|
+
function formsOutDir(g, cfg) {
|
|
1090
|
+
return g.path ?? cfg.outDir;
|
|
1091
|
+
}
|
|
1032
1092
|
function elysiaOutDir(g, cfg) {
|
|
1033
1093
|
return g.path ?? cfg.outDir;
|
|
1034
1094
|
}
|
|
@@ -1354,6 +1414,8 @@ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
|
|
|
1354
1414
|
if (g.kind === "effect-http") dirs.add(abs(effectHttpOutDir(g, cfg)));
|
|
1355
1415
|
if (g.kind === "ts-rest") dirs.add(abs(tsRestOutDir(g, cfg)));
|
|
1356
1416
|
if (g.kind === "elysia") dirs.add(abs(elysiaOutDir(g, cfg)));
|
|
1417
|
+
if (g.kind === "openapi-fetch") dirs.add(abs(openApiFetchOutDir(g, cfg)));
|
|
1418
|
+
if (g.kind === "forms") dirs.add(abs(formsOutDir(g, cfg)));
|
|
1357
1419
|
if (g.kind === "seed") dirs.add(abs(seedOutDir(g, cfg)));
|
|
1358
1420
|
if (g.kind === "fast-check") dirs.add(abs(fastCheckOutDir(g, cfg)));
|
|
1359
1421
|
if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
|
|
@@ -1608,20 +1670,81 @@ function tsRestOptions(g, cfg) {
|
|
|
1608
1670
|
};
|
|
1609
1671
|
}
|
|
1610
1672
|
|
|
1611
|
-
// src/
|
|
1673
|
+
// src/openapi-fetch-options.ts
|
|
1612
1674
|
var VALIDATOR_DEFAULT_DIRS3 = {
|
|
1675
|
+
zod: "src/validators/zod",
|
|
1676
|
+
valibot: "src/validators/valibot",
|
|
1677
|
+
arktype: "src/validators/arktype"
|
|
1678
|
+
};
|
|
1679
|
+
function projectRelative3(p) {
|
|
1680
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
1681
|
+
}
|
|
1682
|
+
function openApiFetchOptions(g, cfg) {
|
|
1683
|
+
const configured = g.validation?.library ?? "zod";
|
|
1684
|
+
const library = configured === "typebox" ? "zod" : configured;
|
|
1685
|
+
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1686
|
+
const derived = siblings.length === 1 ? projectRelative3(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS3[library]) : void 0;
|
|
1687
|
+
return {
|
|
1688
|
+
outputDir: openApiFetchOutDir(g, cfg),
|
|
1689
|
+
clientName: g.clientName,
|
|
1690
|
+
document: g.document,
|
|
1691
|
+
outputHeader: g.outputHeader,
|
|
1692
|
+
format: g.format,
|
|
1693
|
+
importExtension: g.importExtension,
|
|
1694
|
+
validation: {
|
|
1695
|
+
...g.validation,
|
|
1696
|
+
library,
|
|
1697
|
+
useShared: true,
|
|
1698
|
+
importPath: g.validation?.importPath ?? derived
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// src/forms-options.ts
|
|
1704
|
+
var VALIDATOR_DEFAULT_DIRS4 = {
|
|
1705
|
+
zod: "src/validators/zod",
|
|
1706
|
+
valibot: "src/validators/valibot",
|
|
1707
|
+
arktype: "src/validators/arktype",
|
|
1708
|
+
typebox: "src/validators/typebox",
|
|
1709
|
+
effect: "src/validators/effect"
|
|
1710
|
+
};
|
|
1711
|
+
function projectRelative4(p) {
|
|
1712
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
1713
|
+
}
|
|
1714
|
+
function formsOptions(g, cfg) {
|
|
1715
|
+
const library = g.validation?.library ?? "zod";
|
|
1716
|
+
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1717
|
+
const derived = siblings.length === 1 ? projectRelative4(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS4[library]) : void 0;
|
|
1718
|
+
return {
|
|
1719
|
+
outputDir: formsOutDir(g, cfg),
|
|
1720
|
+
target: g.target,
|
|
1721
|
+
modes: g.modes,
|
|
1722
|
+
outputHeader: g.outputHeader,
|
|
1723
|
+
format: g.format,
|
|
1724
|
+
importExtension: g.importExtension,
|
|
1725
|
+
validation: {
|
|
1726
|
+
...g.validation,
|
|
1727
|
+
library,
|
|
1728
|
+
useShared: true,
|
|
1729
|
+
importPath: g.validation?.importPath ?? derived
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/elysia-options.ts
|
|
1735
|
+
var VALIDATOR_DEFAULT_DIRS5 = {
|
|
1613
1736
|
zod: "src/validators/zod",
|
|
1614
1737
|
valibot: "src/validators/valibot",
|
|
1615
1738
|
arktype: "src/validators/arktype",
|
|
1616
1739
|
typebox: "src/validators/typebox"
|
|
1617
1740
|
};
|
|
1618
|
-
function
|
|
1741
|
+
function projectRelative5(p) {
|
|
1619
1742
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
1620
1743
|
}
|
|
1621
1744
|
function elysiaOptions(g, cfg) {
|
|
1622
1745
|
const library = g.validation?.library ?? "zod";
|
|
1623
1746
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1624
|
-
const derived = siblings.length === 1 ?
|
|
1747
|
+
const derived = siblings.length === 1 ? projectRelative5(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS5[library]) : void 0;
|
|
1625
1748
|
return {
|
|
1626
1749
|
outputDir: elysiaOutDir(g, cfg),
|
|
1627
1750
|
appName: g.appName,
|
|
@@ -1663,19 +1786,19 @@ function fastCheckOptions(g, cfg) {
|
|
|
1663
1786
|
}
|
|
1664
1787
|
|
|
1665
1788
|
// src/h3-options.ts
|
|
1666
|
-
var
|
|
1789
|
+
var VALIDATOR_DEFAULT_DIRS6 = {
|
|
1667
1790
|
zod: "src/validators/zod",
|
|
1668
1791
|
valibot: "src/validators/valibot",
|
|
1669
1792
|
arktype: "src/validators/arktype"
|
|
1670
1793
|
};
|
|
1671
|
-
function
|
|
1794
|
+
function projectRelative6(p) {
|
|
1672
1795
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
1673
1796
|
}
|
|
1674
1797
|
function h3Options(g, cfg) {
|
|
1675
1798
|
const configured = g.validation?.library ?? "zod";
|
|
1676
1799
|
const library = configured === "typebox" ? "zod" : configured;
|
|
1677
1800
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1678
|
-
const derived = siblings.length === 1 ?
|
|
1801
|
+
const derived = siblings.length === 1 ? projectRelative6(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS6[library]) : void 0;
|
|
1679
1802
|
return {
|
|
1680
1803
|
outputDir: h3OutDir(g, cfg),
|
|
1681
1804
|
h3: g.h3,
|
|
@@ -1709,19 +1832,19 @@ function mcpOptions(g, cfg) {
|
|
|
1709
1832
|
}
|
|
1710
1833
|
|
|
1711
1834
|
// src/next-options.ts
|
|
1712
|
-
var
|
|
1835
|
+
var VALIDATOR_DEFAULT_DIRS7 = {
|
|
1713
1836
|
zod: "src/validators/zod",
|
|
1714
1837
|
valibot: "src/validators/valibot",
|
|
1715
1838
|
arktype: "src/validators/arktype"
|
|
1716
1839
|
};
|
|
1717
|
-
function
|
|
1840
|
+
function projectRelative7(p) {
|
|
1718
1841
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
1719
1842
|
}
|
|
1720
1843
|
function nextOptions(g, cfg) {
|
|
1721
1844
|
const configured = g.validation?.library ?? "zod";
|
|
1722
1845
|
const library = configured === "typebox" ? "zod" : configured;
|
|
1723
1846
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1724
|
-
const derived = siblings.length === 1 ?
|
|
1847
|
+
const derived = siblings.length === 1 ? projectRelative7(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS7[library]) : void 0;
|
|
1725
1848
|
return {
|
|
1726
1849
|
outputDir: nextOutDir(g, cfg),
|
|
1727
1850
|
naming: g.naming,
|
|
@@ -1738,19 +1861,19 @@ function nextOptions(g, cfg) {
|
|
|
1738
1861
|
}
|
|
1739
1862
|
|
|
1740
1863
|
// src/tanstack-start-options.ts
|
|
1741
|
-
var
|
|
1864
|
+
var VALIDATOR_DEFAULT_DIRS8 = {
|
|
1742
1865
|
zod: "src/validators/zod",
|
|
1743
1866
|
valibot: "src/validators/valibot",
|
|
1744
1867
|
arktype: "src/validators/arktype"
|
|
1745
1868
|
};
|
|
1746
|
-
function
|
|
1869
|
+
function projectRelative8(p) {
|
|
1747
1870
|
return p.startsWith("./") ? p.slice(2) : p;
|
|
1748
1871
|
}
|
|
1749
1872
|
function tanstackStartOptions(g, cfg) {
|
|
1750
1873
|
const configured = g.validation?.library ?? "zod";
|
|
1751
1874
|
const library = configured === "typebox" ? "zod" : configured;
|
|
1752
1875
|
const siblings = cfg.generators.filter((s) => s.kind === library);
|
|
1753
|
-
const derived = siblings.length === 1 ?
|
|
1876
|
+
const derived = siblings.length === 1 ? projectRelative8(siblings[0].path ?? VALIDATOR_DEFAULT_DIRS8[library]) : void 0;
|
|
1754
1877
|
return {
|
|
1755
1878
|
outputDir: tanstackStartOutDir(g, cfg),
|
|
1756
1879
|
naming: g.naming,
|
|
@@ -1834,7 +1957,7 @@ function trpcOptions(g, cfg, servicesDir) {
|
|
|
1834
1957
|
}
|
|
1835
1958
|
|
|
1836
1959
|
// src/generator-registry.ts
|
|
1837
|
-
var
|
|
1960
|
+
var VALIDATOR_DEFAULT_DIRS9 = {
|
|
1838
1961
|
zod: "src/validators/zod",
|
|
1839
1962
|
valibot: "src/validators/valibot",
|
|
1840
1963
|
arktype: "src/validators/arktype",
|
|
@@ -1867,10 +1990,14 @@ var GENERATORS = [
|
|
|
1867
1990
|
//
|
|
1868
1991
|
// The side effect was invisible and lasted longer than the reason: tsup externalises
|
|
1869
1992
|
// `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
|
|
1870
|
-
// inside `dist` while the
|
|
1871
|
-
// registry now and
|
|
1872
|
-
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
|
|
1873
|
-
//
|
|
1993
|
+
// inside `dist` while the rest were resolved from `node_modules`. Every generator package is on
|
|
1994
|
+
// the registry now and every one is a `dependencies` entry, which is what makes each of them a
|
|
1995
|
+
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure for
|
|
1996
|
+
// every kind rather than for some of them.
|
|
1997
|
+
//
|
|
1998
|
+
// Counted nowhere on purpose. This paragraph twice carried a number that the next batch of
|
|
1999
|
+
// generators falsified, and `packages/cli/test/generator-registry.spec.ts` asserts the property
|
|
2000
|
+
// against the manifest, which is where a quantity belongs.
|
|
1874
2001
|
specifier: "@drzl/generator-trpc",
|
|
1875
2002
|
load: () => import("@drzl/generator-trpc"),
|
|
1876
2003
|
construct: (m, analysis) => new m.TRPCGenerator(analysis),
|
|
@@ -1922,9 +2049,10 @@ var GENERATORS = [
|
|
|
1922
2049
|
// This one and the three below spent one release each in `optionalDependencies`, because a
|
|
1923
2050
|
// package that has never existed cannot publish through npm's trusted-publisher OIDC flow and
|
|
1924
2051
|
// 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.
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
2052
|
+
// for everyone until the first publish lands. The six generators added after them went the
|
|
2053
|
+
// same way and were promoted the same way once their first versions were published by hand.
|
|
2054
|
+
// Nothing here is optional any more. `scripts/verify/stages/33-registry-deps.sh` gates both
|
|
2055
|
+
// halves of that rule and is what reported each promotion was due.
|
|
1928
2056
|
specifier: "@drzl/generator-mcp",
|
|
1929
2057
|
load: () => import("@drzl/generator-mcp"),
|
|
1930
2058
|
construct: (m, analysis) => new m.MCPGenerator(analysis),
|
|
@@ -1979,6 +2107,22 @@ var GENERATORS = [
|
|
|
1979
2107
|
outputDir: (g, cfg) => tsRestOutDir(g, cfg),
|
|
1980
2108
|
options: (g, cfg) => tsRestOptions(g, cfg)
|
|
1981
2109
|
},
|
|
2110
|
+
{
|
|
2111
|
+
kind: "openapi-fetch",
|
|
2112
|
+
specifier: "@drzl/generator-openapi-fetch",
|
|
2113
|
+
load: () => import("@drzl/generator-openapi-fetch"),
|
|
2114
|
+
construct: (m, analysis) => new m.OpenApiFetchGenerator(analysis),
|
|
2115
|
+
outputDir: (g, cfg) => openApiFetchOutDir(g, cfg),
|
|
2116
|
+
options: (g, cfg) => openApiFetchOptions(g, cfg)
|
|
2117
|
+
},
|
|
2118
|
+
{
|
|
2119
|
+
kind: "forms",
|
|
2120
|
+
specifier: "@drzl/generator-forms",
|
|
2121
|
+
load: () => import("@drzl/generator-forms"),
|
|
2122
|
+
construct: (m, analysis) => new m.FormsGenerator(analysis),
|
|
2123
|
+
outputDir: (g, cfg) => formsOutDir(g, cfg),
|
|
2124
|
+
options: (g, cfg) => formsOptions(g, cfg)
|
|
2125
|
+
},
|
|
1982
2126
|
{
|
|
1983
2127
|
kind: "elysia",
|
|
1984
2128
|
specifier: "@drzl/generator-elysia",
|
|
@@ -2016,7 +2160,7 @@ var GENERATORS = [
|
|
|
2016
2160
|
specifier: "@drzl/generator-zod",
|
|
2017
2161
|
load: () => import("@drzl/generator-zod"),
|
|
2018
2162
|
construct: (m, analysis) => new m.ZodGenerator(analysis),
|
|
2019
|
-
outputDir: (g) => g.path ??
|
|
2163
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.zod,
|
|
2020
2164
|
// `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
|
|
2021
2165
|
// four rather than being passed and ignored.
|
|
2022
2166
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
|
|
@@ -2030,7 +2174,7 @@ var GENERATORS = [
|
|
|
2030
2174
|
specifier: "@drzl/generator-valibot",
|
|
2031
2175
|
load: () => import("@drzl/generator-valibot"),
|
|
2032
2176
|
construct: (m, analysis) => new m.ValibotGenerator(analysis),
|
|
2033
|
-
outputDir: (g) => g.path ??
|
|
2177
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.valibot,
|
|
2034
2178
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
|
|
2035
2179
|
},
|
|
2036
2180
|
{
|
|
@@ -2038,7 +2182,7 @@ var GENERATORS = [
|
|
|
2038
2182
|
specifier: "@drzl/generator-arktype",
|
|
2039
2183
|
load: () => import("@drzl/generator-arktype"),
|
|
2040
2184
|
construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
|
|
2041
|
-
outputDir: (g) => g.path ??
|
|
2185
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.arktype,
|
|
2042
2186
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
|
|
2043
2187
|
},
|
|
2044
2188
|
{
|
|
@@ -2046,7 +2190,7 @@ var GENERATORS = [
|
|
|
2046
2190
|
specifier: "@drzl/generator-typebox",
|
|
2047
2191
|
load: () => import("@drzl/generator-typebox"),
|
|
2048
2192
|
construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
|
|
2049
|
-
outputDir: (g) => g.path ??
|
|
2193
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.typebox,
|
|
2050
2194
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
|
|
2051
2195
|
},
|
|
2052
2196
|
{
|
|
@@ -2054,7 +2198,7 @@ var GENERATORS = [
|
|
|
2054
2198
|
specifier: "@drzl/generator-effect",
|
|
2055
2199
|
load: () => import("@drzl/generator-effect"),
|
|
2056
2200
|
construct: (m, analysis) => new m.EffectGenerator(analysis),
|
|
2057
|
-
outputDir: (g) => g.path ??
|
|
2201
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9.effect,
|
|
2058
2202
|
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
|
|
2059
2203
|
},
|
|
2060
2204
|
{
|
|
@@ -2062,7 +2206,7 @@ var GENERATORS = [
|
|
|
2062
2206
|
specifier: "@drzl/generator-json-schema",
|
|
2063
2207
|
load: () => import("@drzl/generator-json-schema"),
|
|
2064
2208
|
construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
|
|
2065
|
-
outputDir: (g) => g.path ??
|
|
2209
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS9["json-schema"],
|
|
2066
2210
|
options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
|
|
2067
2211
|
}
|
|
2068
2212
|
];
|
|
@@ -3376,6 +3520,24 @@ function sqlLiterals(values) {
|
|
|
3376
3520
|
function derivedName(table, column) {
|
|
3377
3521
|
return `${table}_${column}_check`;
|
|
3378
3522
|
}
|
|
3523
|
+
var ADD_CONSTRAINT_DIALECTS = /* @__PURE__ */ new Set([
|
|
3524
|
+
"postgres",
|
|
3525
|
+
"cockroach",
|
|
3526
|
+
"gel",
|
|
3527
|
+
"mysql",
|
|
3528
|
+
"singlestore",
|
|
3529
|
+
"mssql"
|
|
3530
|
+
]);
|
|
3531
|
+
function fixFor(dialect, table, column, values) {
|
|
3532
|
+
if (!ADD_CONSTRAINT_DIALECTS.has(dialect)) return void 0;
|
|
3533
|
+
return `ALTER TABLE ${table} ADD CONSTRAINT ${derivedName(table, column)} CHECK (${column} IN (${sqlLiterals(values)}));`;
|
|
3534
|
+
}
|
|
3535
|
+
function noFixReason(dialect) {
|
|
3536
|
+
if (dialect === "sqlite") {
|
|
3537
|
+
return "SQLite cannot add a CHECK to an existing column: ALTER TABLE ... ADD CONSTRAINT is a syntax error there. Closing this means rebuilding the table, which is the twelve-step procedure SQLite documents: create the new table with the constraint, copy the rows, drop the old one, rename.";
|
|
3538
|
+
}
|
|
3539
|
+
return `no statement is emitted for the "${dialect}" dialect, which this report cannot name a fix for`;
|
|
3540
|
+
}
|
|
3379
3541
|
function buildConstraintDriftReport(analysis, schemaPath) {
|
|
3380
3542
|
const entries = [];
|
|
3381
3543
|
for (const table of analysis.tables) {
|
|
@@ -3411,7 +3573,10 @@ function buildConstraintDriftReport(analysis, schemaPath) {
|
|
|
3411
3573
|
columns: [column.name],
|
|
3412
3574
|
rule: `${column.name} IN (${sqlLiterals(values)})`,
|
|
3413
3575
|
reason: `the column is declared ${column.sqlType}, which holds any text; the set exists only in the generated schemas`,
|
|
3414
|
-
|
|
3576
|
+
...(() => {
|
|
3577
|
+
const fix = fixFor(analysis.dialect, table.name, column.name, values);
|
|
3578
|
+
return fix ? { fix } : { noFix: noFixReason(analysis.dialect) };
|
|
3579
|
+
})()
|
|
3415
3580
|
});
|
|
3416
3581
|
}
|
|
3417
3582
|
}
|
|
@@ -3486,6 +3651,8 @@ function renderConstraintDriftReport(report, style = PLAIN3) {
|
|
|
3486
3651
|
if (e.fix) {
|
|
3487
3652
|
out.push(chalk.dim(" Close it with:"));
|
|
3488
3653
|
out.push(` ${e.fix}`);
|
|
3654
|
+
} else if (e.noFix) {
|
|
3655
|
+
out.push(chalk.dim(wrap3(e.noFix, " ", " Not one statement here: ")));
|
|
3489
3656
|
}
|
|
3490
3657
|
out.push("");
|
|
3491
3658
|
}
|
|
@@ -3517,6 +3684,271 @@ function renderConstraintDriftReport(report, style = PLAIN3) {
|
|
|
3517
3684
|
);
|
|
3518
3685
|
return out.join("\n");
|
|
3519
3686
|
}
|
|
3687
|
+
function renderConstraintDriftSql(report) {
|
|
3688
|
+
const gaps = report.entries.filter((e) => e.side === "schema-only");
|
|
3689
|
+
if (!gaps.length) return "";
|
|
3690
|
+
const out = [];
|
|
3691
|
+
const runnable = gaps.filter((g) => g.fix);
|
|
3692
|
+
const unrunnable = gaps.filter((g) => !g.fix);
|
|
3693
|
+
out.push(`-- Generated by drzl doctor --constraints --sql`);
|
|
3694
|
+
out.push(`-- ${report.schema}, ${report.dialect}`);
|
|
3695
|
+
out.push(
|
|
3696
|
+
`-- ${runnable.length} constraint(s) your schemas enforce and the database does not.`
|
|
3697
|
+
);
|
|
3698
|
+
if (unrunnable.length) {
|
|
3699
|
+
out.push(
|
|
3700
|
+
`-- ${unrunnable.length} more cannot be closed by one statement on this dialect; see below.`
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3703
|
+
out.push("");
|
|
3704
|
+
const byPlace = (a, b) => a.table.localeCompare(b.table) || a.columns.join().localeCompare(b.columns.join());
|
|
3705
|
+
for (const g of [...runnable].sort(byPlace)) {
|
|
3706
|
+
out.push(`-- ${g.table}.${g.columns.join(", ")}: ${g.reason}`);
|
|
3707
|
+
out.push(g.fix);
|
|
3708
|
+
out.push("");
|
|
3709
|
+
}
|
|
3710
|
+
if (unrunnable.length) {
|
|
3711
|
+
out.push("-- The rest, which this dialect cannot do in one statement:");
|
|
3712
|
+
for (const g of [...unrunnable].sort(byPlace)) {
|
|
3713
|
+
out.push(`--`);
|
|
3714
|
+
out.push(`-- ${g.table}.${g.columns.join(", ")}`);
|
|
3715
|
+
out.push(`-- ${g.rule}`);
|
|
3716
|
+
for (const line of (g.noFix ?? "").match(/.{1,88}(\s|$)/g) ?? []) {
|
|
3717
|
+
out.push(`-- ${line.trim()}`);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
out.push("");
|
|
3721
|
+
}
|
|
3722
|
+
return out.join("\n");
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
// src/policy-report.ts
|
|
3726
|
+
var import_chalk5 = require("chalk");
|
|
3727
|
+
var PLAIN4 = new import_chalk5.Chalk({ level: 0 });
|
|
3728
|
+
var COMMANDS = ["select", "insert", "update", "delete"];
|
|
3729
|
+
var DENIAL = {
|
|
3730
|
+
select: "every read returns zero rows",
|
|
3731
|
+
insert: "every insert is refused",
|
|
3732
|
+
update: "every update is refused",
|
|
3733
|
+
delete: "every delete is refused"
|
|
3734
|
+
};
|
|
3735
|
+
function appliesTo(policy, command) {
|
|
3736
|
+
const forCmd = (policy.for ?? "all").toLowerCase();
|
|
3737
|
+
return forCmd === "all" || forCmd === command;
|
|
3738
|
+
}
|
|
3739
|
+
function grants(policy, command) {
|
|
3740
|
+
if (!appliesTo(policy, command)) return false;
|
|
3741
|
+
if ((policy.as ?? "permissive").toLowerCase() === "restrictive") return false;
|
|
3742
|
+
const hasUsing = !!policy.using;
|
|
3743
|
+
const hasCheck = !!policy.withCheck;
|
|
3744
|
+
const forCmd = (policy.for ?? "all").toLowerCase();
|
|
3745
|
+
if (forCmd === "all") return hasUsing || hasCheck;
|
|
3746
|
+
switch (command) {
|
|
3747
|
+
case "select":
|
|
3748
|
+
case "delete":
|
|
3749
|
+
return hasUsing;
|
|
3750
|
+
case "insert":
|
|
3751
|
+
return hasCheck;
|
|
3752
|
+
case "update":
|
|
3753
|
+
return hasCheck || hasUsing;
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
function hasAnyExpression(policy) {
|
|
3757
|
+
return !!policy.using || !!policy.withCheck;
|
|
3758
|
+
}
|
|
3759
|
+
function readTable(table) {
|
|
3760
|
+
if (typeof table.rlsEnabled !== "boolean") return void 0;
|
|
3761
|
+
const policies = table.policies ?? [];
|
|
3762
|
+
const declaredRls = table.rlsEnabled;
|
|
3763
|
+
const effective = declaredRls || policies.length > 0;
|
|
3764
|
+
if (!effective) return void 0;
|
|
3765
|
+
const grantMap = {};
|
|
3766
|
+
for (const c of COMMANDS) grantMap[c] = policies.some((p) => grants(p, c));
|
|
3767
|
+
return { table: table.name, declaredRls, effective, policies, grants: grantMap };
|
|
3768
|
+
}
|
|
3769
|
+
function buildPolicyReport(analysis, schemaPath) {
|
|
3770
|
+
const tables = [];
|
|
3771
|
+
const findings = [];
|
|
3772
|
+
for (const table of analysis.tables) {
|
|
3773
|
+
const read = readTable(table);
|
|
3774
|
+
if (!read) continue;
|
|
3775
|
+
tables.push(read);
|
|
3776
|
+
if (COMMANDS.every((c) => !read.grants[c])) {
|
|
3777
|
+
findings.push({
|
|
3778
|
+
kind: "denied",
|
|
3779
|
+
table: read.table,
|
|
3780
|
+
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",
|
|
3781
|
+
// Deliberately not "give it a USING": a policy written `FOR INSERT` consults only its
|
|
3782
|
+
// WITH CHECK, so that advice would be wrong for exactly the declaration this report exists
|
|
3783
|
+
// to catch. The per-policy findings below name the right expression for each one.
|
|
3784
|
+
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"
|
|
3785
|
+
});
|
|
3786
|
+
} else {
|
|
3787
|
+
for (const command of COMMANDS) {
|
|
3788
|
+
if (read.grants[command]) continue;
|
|
3789
|
+
const named = read.policies.filter((p) => appliesTo(p, command));
|
|
3790
|
+
findings.push({
|
|
3791
|
+
kind: "denied",
|
|
3792
|
+
table: read.table,
|
|
3793
|
+
command,
|
|
3794
|
+
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`,
|
|
3795
|
+
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`
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
for (const policy of read.policies) {
|
|
3800
|
+
if (hasAnyExpression(policy)) continue;
|
|
3801
|
+
findings.push({
|
|
3802
|
+
kind: "grants-nothing",
|
|
3803
|
+
table: read.table,
|
|
3804
|
+
policy: policy.name,
|
|
3805
|
+
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",
|
|
3806
|
+
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"
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
const policies = tables.reduce((n, t) => n + t.policies.length, 0);
|
|
3811
|
+
return {
|
|
3812
|
+
schema: schemaPath,
|
|
3813
|
+
dialect: analysis.dialect,
|
|
3814
|
+
ok: findings.length === 0,
|
|
3815
|
+
counts: {
|
|
3816
|
+
tables: analysis.tables.length,
|
|
3817
|
+
withRls: tables.length,
|
|
3818
|
+
policies,
|
|
3819
|
+
findings: findings.length
|
|
3820
|
+
},
|
|
3821
|
+
tables,
|
|
3822
|
+
findings,
|
|
3823
|
+
ignoredByGeneratedCode: tables.map((t) => t.table)
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
function wrap4(text, indent, first = indent, width = 96) {
|
|
3827
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
3828
|
+
const lines = [];
|
|
3829
|
+
let line = first;
|
|
3830
|
+
let started = false;
|
|
3831
|
+
for (const w of words) {
|
|
3832
|
+
if (started && line.length + 1 + w.length > width) {
|
|
3833
|
+
lines.push(line);
|
|
3834
|
+
line = indent + w;
|
|
3835
|
+
} else {
|
|
3836
|
+
line = started ? `${line} ${w}` : line + w;
|
|
3837
|
+
started = true;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
if (started) lines.push(line);
|
|
3841
|
+
return lines.join("\n");
|
|
3842
|
+
}
|
|
3843
|
+
function policyLine(policy) {
|
|
3844
|
+
const bits = [(policy.for ?? "all").toUpperCase()];
|
|
3845
|
+
if ((policy.as ?? "").toLowerCase() === "restrictive") bits.push("restrictive");
|
|
3846
|
+
bits.push(policy.to?.length ? `to ${policy.to.join(", ")}` : "to public");
|
|
3847
|
+
const carries = [policy.using ? "USING" : void 0, policy.withCheck ? "WITH CHECK" : void 0].filter(Boolean).join(" + ");
|
|
3848
|
+
bits.push(carries || "no expression");
|
|
3849
|
+
if (policy.linked) bits.push("linked");
|
|
3850
|
+
return bits.join(", ");
|
|
3851
|
+
}
|
|
3852
|
+
function renderPolicyReport(report, style = PLAIN4) {
|
|
3853
|
+
const chalk = style;
|
|
3854
|
+
const out = [];
|
|
3855
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
3856
|
+
out.push(chalk.bold(`DRZL row-level security ${report.schema}`));
|
|
3857
|
+
out.push(
|
|
3858
|
+
chalk.dim(
|
|
3859
|
+
`${report.dialect}, ${plural(report.counts.withRls, "table")} under RLS, ${plural(report.counts.policies, "policy").replace("policys", "policies")}`
|
|
3860
|
+
)
|
|
3861
|
+
);
|
|
3862
|
+
out.push("");
|
|
3863
|
+
if (!report.counts.withRls) {
|
|
3864
|
+
out.push(chalk.green("No table in this schema uses row-level security."));
|
|
3865
|
+
if (report.dialect !== "postgres" && report.dialect !== "cockroach") {
|
|
3866
|
+
out.push(chalk.dim(` ${report.dialect} has no row-level security to declare.`));
|
|
3867
|
+
} else {
|
|
3868
|
+
out.push(chalk.dim(" No table calls .enableRLS() and none declares a pgPolicy."));
|
|
3869
|
+
}
|
|
3870
|
+
return out.join("\n");
|
|
3871
|
+
}
|
|
3872
|
+
const denied = report.findings.filter((f) => f.kind === "denied");
|
|
3873
|
+
if (denied.length) {
|
|
3874
|
+
out.push(chalk.red("These tables refuse the operation your generated code performs"));
|
|
3875
|
+
out.push(
|
|
3876
|
+
chalk.dim(
|
|
3877
|
+
wrap4(
|
|
3878
|
+
"A table under row-level security permits only what a policy grants. The generated service still compiles and its return type still promises rows.",
|
|
3879
|
+
" "
|
|
3880
|
+
)
|
|
3881
|
+
)
|
|
3882
|
+
);
|
|
3883
|
+
out.push("");
|
|
3884
|
+
for (const f of denied) {
|
|
3885
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)} ${chalk.dim(f.command ?? "everything")}`);
|
|
3886
|
+
out.push(wrap4(f.detail, " "));
|
|
3887
|
+
if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
|
|
3888
|
+
out.push("");
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
const dead = report.findings.filter((f) => f.kind === "grants-nothing");
|
|
3892
|
+
if (dead.length) {
|
|
3893
|
+
out.push(chalk.yellow("These policies grant nothing"));
|
|
3894
|
+
out.push(
|
|
3895
|
+
chalk.dim(
|
|
3896
|
+
wrap4(
|
|
3897
|
+
"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.",
|
|
3898
|
+
" "
|
|
3899
|
+
)
|
|
3900
|
+
)
|
|
3901
|
+
);
|
|
3902
|
+
out.push("");
|
|
3903
|
+
for (const f of dead) {
|
|
3904
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(f.table)}.${f.policy}`);
|
|
3905
|
+
out.push(wrap4(f.detail, " "));
|
|
3906
|
+
if (f.fix) out.push(chalk.dim(wrap4(f.fix, " ", " Close it: ")));
|
|
3907
|
+
out.push("");
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
out.push(chalk.cyan("The policies each table carries"));
|
|
3911
|
+
out.push(
|
|
3912
|
+
chalk.dim(
|
|
3913
|
+
wrap4(
|
|
3914
|
+
"Whether these apply to the role your application connects as is the one question this report cannot answer for you.",
|
|
3915
|
+
" "
|
|
3916
|
+
)
|
|
3917
|
+
)
|
|
3918
|
+
);
|
|
3919
|
+
out.push("");
|
|
3920
|
+
for (const t of report.tables) {
|
|
3921
|
+
const flag = t.declaredRls ? "enableRLS()" : "RLS on, implied by its policies";
|
|
3922
|
+
out.push(` ${chalk.dim("-")} ${chalk.bold(t.table)} ${chalk.dim(`(${flag})`)}`);
|
|
3923
|
+
if (!t.policies.length) {
|
|
3924
|
+
out.push(chalk.dim(" no policies"));
|
|
3925
|
+
}
|
|
3926
|
+
for (const p of t.policies) {
|
|
3927
|
+
out.push(` ${p.name}: ${chalk.dim(policyLine(p))}`);
|
|
3928
|
+
}
|
|
3929
|
+
const permitted = COMMANDS.filter((c) => t.grants[c]);
|
|
3930
|
+
out.push(
|
|
3931
|
+
chalk.dim(` permits: ${permitted.length ? permitted.join(", ") : "nothing"}`)
|
|
3932
|
+
);
|
|
3933
|
+
out.push("");
|
|
3934
|
+
}
|
|
3935
|
+
out.push(chalk.yellow("What DRZL generates does not know about any of this"));
|
|
3936
|
+
out.push(
|
|
3937
|
+
chalk.dim(
|
|
3938
|
+
wrap4(
|
|
3939
|
+
`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.`,
|
|
3940
|
+
" "
|
|
3941
|
+
)
|
|
3942
|
+
)
|
|
3943
|
+
);
|
|
3944
|
+
out.push("");
|
|
3945
|
+
out.push(
|
|
3946
|
+
chalk.dim(
|
|
3947
|
+
`${plural(report.counts.findings, "finding")} across ${plural(report.counts.withRls, "table")} under row-level security.`
|
|
3948
|
+
)
|
|
3949
|
+
);
|
|
3950
|
+
return out.join("\n");
|
|
3951
|
+
}
|
|
3520
3952
|
|
|
3521
3953
|
// src/drift.ts
|
|
3522
3954
|
var import_node_fs = require("fs");
|
|
@@ -4443,6 +4875,9 @@ withOutputFlags(
|
|
|
4443
4875
|
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(
|
|
4444
4876
|
"--constraints",
|
|
4445
4877
|
"report what each side enforces that the other does not, instead of the usual findings"
|
|
4878
|
+
).option("--sql", "with --constraints, emit the statements alone, for redirecting to a migration").option(
|
|
4879
|
+
"--policies",
|
|
4880
|
+
"report the row-level security policies each table carries, and what they refuse"
|
|
4446
4881
|
)
|
|
4447
4882
|
).action(async (schema, opts) => {
|
|
4448
4883
|
const out = outputFor(opts);
|
|
@@ -4466,7 +4901,21 @@ withOutputFlags(
|
|
|
4466
4901
|
validateConstraints: true
|
|
4467
4902
|
});
|
|
4468
4903
|
const schemaLabel = Array.isArray(target) ? target.join(", ") : target;
|
|
4469
|
-
if (opts.constraints) {
|
|
4904
|
+
if (opts.sql && !opts.constraints) {
|
|
4905
|
+
const msg = "--sql reports the constraint drift as statements, so it needs --constraints.";
|
|
4906
|
+
if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
|
|
4907
|
+
else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
|
|
4908
|
+
process.exit(EXIT_FAILED);
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
if (opts.constraints && opts.policies) {
|
|
4912
|
+
const msg = "--constraints and --policies are separate reports. Run one, then the other.";
|
|
4913
|
+
if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
|
|
4914
|
+
else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
|
|
4915
|
+
process.exit(EXIT_FAILED);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
if (opts.constraints || opts.policies) {
|
|
4470
4919
|
const preflight = buildDoctorReport(analysis, schemaLabel);
|
|
4471
4920
|
const fatal = preflight.findings.filter((f) => f.level === "error");
|
|
4472
4921
|
if (fatal.length) {
|
|
@@ -4482,8 +4931,15 @@ withOutputFlags(
|
|
|
4482
4931
|
process.exit(EXIT_FAILED);
|
|
4483
4932
|
return;
|
|
4484
4933
|
}
|
|
4934
|
+
}
|
|
4935
|
+
if (opts.constraints) {
|
|
4485
4936
|
const drift = buildConstraintDriftReport(analysis, schemaLabel);
|
|
4486
4937
|
const driftCode = opts.strict && drift.counts.schemaOnly ? EXIT_FINDINGS : EXIT_OK;
|
|
4938
|
+
if (opts.sql) {
|
|
4939
|
+
out.data(renderConstraintDriftSql(drift));
|
|
4940
|
+
process.exit(driftCode);
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4487
4943
|
if (opts.json)
|
|
4488
4944
|
out.data(
|
|
4489
4945
|
JSON.stringify({ command: "doctor", exitCode: driftCode, ...drift }, null, 2)
|
|
@@ -4492,6 +4948,17 @@ withOutputFlags(
|
|
|
4492
4948
|
process.exit(driftCode);
|
|
4493
4949
|
return;
|
|
4494
4950
|
}
|
|
4951
|
+
if (opts.policies) {
|
|
4952
|
+
const policies = buildPolicyReport(analysis, schemaLabel);
|
|
4953
|
+
const policyCode = opts.strict && policies.counts.findings ? EXIT_FINDINGS : EXIT_OK;
|
|
4954
|
+
if (opts.json)
|
|
4955
|
+
out.data(
|
|
4956
|
+
JSON.stringify({ command: "doctor", exitCode: policyCode, ...policies }, null, 2)
|
|
4957
|
+
);
|
|
4958
|
+
else out.data(renderPolicyReport(policies, out.outStyle));
|
|
4959
|
+
process.exit(policyCode);
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4495
4962
|
const report = buildDoctorReport(analysis, schemaLabel);
|
|
4496
4963
|
const unreadable = report.findings.some((f) => f.level === "error");
|
|
4497
4964
|
const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
|