@drzl/cli 4.36.0 → 4.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-S5JWTWWU.js → chunk-VG6SD2GP.js} +74 -4
- package/dist/chunk-VG6SD2GP.js.map +1 -0
- package/dist/cli.cjs +563 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +497 -29
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +76 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +64 -5
- package/dist/config.d.ts +64 -5
- package/dist/config.js +7 -1
- package/dist/drzl.config.schema.json +24 -1
- package/package.json +14 -11
- package/dist/chunk-S5JWTWWU.js.map +0 -1
package/dist/cli.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({
|
|
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
|
-
|
|
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/
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
1871
|
-
// registry now and
|
|
1872
|
-
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
|
|
1873
|
-
//
|
|
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.
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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,
|