@drzl/cli 4.18.0 → 4.21.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-I4KMDCQR.js → chunk-XNNKHBGV.js} +158 -8
- package/dist/chunk-XNNKHBGV.js.map +1 -0
- package/dist/cli.cjs +931 -59
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +173 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +147 -7
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +57 -3
- package/dist/config.d.ts +57 -3
- package/dist/config.js +5 -1
- package/dist/{dist-TRJLPIWT.js → dist-K6N4F3XW.js} +19 -10
- package/dist/dist-K6N4F3XW.js.map +1 -0
- package/dist/dist-WHAW3AMQ.js +585 -0
- package/dist/dist-WHAW3AMQ.js.map +1 -0
- package/package.json +10 -9
- package/dist/chunk-I4KMDCQR.js.map +0 -1
- package/dist/dist-TRJLPIWT.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -261,7 +261,7 @@ ${entries}
|
|
|
261
261
|
const decided = [...declared, body].join("\n\n");
|
|
262
262
|
const imports = [];
|
|
263
263
|
if (useShared) {
|
|
264
|
-
const sharedAffix = (0,
|
|
264
|
+
const sharedAffix = (0, import_validation_core4.resolveAffix)({
|
|
265
265
|
affix: opts.validation?.affix,
|
|
266
266
|
schemaSuffix: opts.validation?.schemaSuffix
|
|
267
267
|
});
|
|
@@ -271,21 +271,21 @@ ${entries}
|
|
|
271
271
|
["select", selectName]
|
|
272
272
|
].filter(([, local]) => decided.includes(local));
|
|
273
273
|
if (wanted.length) {
|
|
274
|
-
const spec = (0,
|
|
274
|
+
const spec = (0, import_validation_core4.resolveConfiguredImport)(
|
|
275
275
|
opts.validation.importPath,
|
|
276
276
|
ctx.out,
|
|
277
277
|
process.cwd(),
|
|
278
278
|
opts.importExtension
|
|
279
279
|
);
|
|
280
280
|
const names = wanted.map(([mode, local]) => {
|
|
281
|
-
const exported = (0,
|
|
281
|
+
const exported = (0, import_validation_core4.schemaName)(mode, table.tsName, sharedAffix);
|
|
282
282
|
return exported === local ? local : `${exported} as ${local}`;
|
|
283
283
|
}).join(", ");
|
|
284
284
|
imports.push(`import { ${names} } from '${spec}';`);
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
imports.push(
|
|
288
|
-
`import { ${[builder, "router"].sort().join(", ")} } from '${(0,
|
|
288
|
+
`import { ${[builder, "router"].sort().join(", ")} } from '${(0, import_validation_core4.importSpecifier)(
|
|
289
289
|
`./${BASE_MODULE}.ts`,
|
|
290
290
|
opts.importExtension
|
|
291
291
|
)}';`
|
|
@@ -334,7 +334,7 @@ function relationProcedures(table, lib, selectSchemaName, taken, service) {
|
|
|
334
334
|
return out;
|
|
335
335
|
}
|
|
336
336
|
function renderBarrel(routers, ctx, path6, opts) {
|
|
337
|
-
const baseSpec = (0,
|
|
337
|
+
const baseSpec = (0, import_validation_core4.importSpecifier)(`./${BASE_MODULE}.ts`, opts.importExtension);
|
|
338
338
|
const reExports = `export { createCallerFactory, publicProcedure, router } from '${baseSpec}';
|
|
339
339
|
` + (opts.databaseInjection?.enabled === true ? `export { dbProcedure } from '${baseSpec}';
|
|
340
340
|
` : "") + `export type { Context } from '${baseSpec}';
|
|
@@ -352,7 +352,7 @@ export type AppRouter = typeof appRouter;
|
|
|
352
352
|
${reExports}`;
|
|
353
353
|
}
|
|
354
354
|
const entries = routers.map(({ filePath, exportName, table }) => ({
|
|
355
|
-
rel: (0,
|
|
355
|
+
rel: (0, import_validation_core4.importSpecifier)(
|
|
356
356
|
"./" + path6.relative(ctx.out, filePath).replace(/\\/g, "/"),
|
|
357
357
|
opts.importExtension
|
|
358
358
|
),
|
|
@@ -393,7 +393,7 @@ function routerExportName(table, naming) {
|
|
|
393
393
|
function serviceImportSpecifier(table, ctx, opts) {
|
|
394
394
|
const rel = relativePosix(ctx.out, ctx.services);
|
|
395
395
|
const dir = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
396
|
-
return (0,
|
|
396
|
+
return (0, import_validation_core4.importSpecifier)(`${dir}/${singularize(table.tsName)}Service.ts`, opts.importExtension);
|
|
397
397
|
}
|
|
398
398
|
function relativePosix(from, to) {
|
|
399
399
|
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -413,11 +413,11 @@ function buildHeader(h) {
|
|
|
413
413
|
];
|
|
414
414
|
return lines.join("\n") + "\n\n";
|
|
415
415
|
}
|
|
416
|
-
var
|
|
416
|
+
var import_validation_core4, TRPC_MAJOR, q, LIB_IMPORTS, LIB_USAGE, LIBS, cap, singularize, isIdent, BASE_MODULE, TRPCGenerator, index_default;
|
|
417
417
|
var init_dist = __esm({
|
|
418
418
|
"../generator-trpc/dist/index.js"() {
|
|
419
419
|
"use strict";
|
|
420
|
-
|
|
420
|
+
import_validation_core4 = require("@drzl/validation-core");
|
|
421
421
|
TRPC_MAJOR = 11;
|
|
422
422
|
q = (v) => JSON.stringify(v);
|
|
423
423
|
LIB_IMPORTS = {
|
|
@@ -510,7 +510,7 @@ ${body}
|
|
|
510
510
|
await fs3.mkdir(out, { recursive: true });
|
|
511
511
|
const files = [];
|
|
512
512
|
const write = async (filePath, content) => {
|
|
513
|
-
const formatted = await (0,
|
|
513
|
+
const formatted = await (0, import_validation_core4.formatCode)(
|
|
514
514
|
buildHeader(opts.outputHeader) + content,
|
|
515
515
|
filePath,
|
|
516
516
|
opts.format
|
|
@@ -606,14 +606,14 @@ function baseSchema(c, mode, target, checks, sets, lengths) {
|
|
|
606
606
|
case "string": {
|
|
607
607
|
const out = { type: "string" };
|
|
608
608
|
if (c.format === "uuid") out.format = UUID_FORMAT;
|
|
609
|
-
else if (c.format &&
|
|
609
|
+
else if (c.format && import_validation_core6.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core6.COLUMN_FORMATS[c.format];
|
|
610
610
|
if (c.maxLength !== void 0) out.maxLength = c.maxLength;
|
|
611
611
|
applyByteCap(out, c);
|
|
612
612
|
applyLengths(out, c, lengths);
|
|
613
613
|
return out;
|
|
614
614
|
}
|
|
615
615
|
case "number": {
|
|
616
|
-
const out = { type: (0,
|
|
616
|
+
const out = { type: (0, import_validation_core6.isIntegerColumn)(c) ? "integer" : "number" };
|
|
617
617
|
if (!c.arrayDimensions) applyNumericBounds(out, c, checks, target);
|
|
618
618
|
return out;
|
|
619
619
|
}
|
|
@@ -750,7 +750,7 @@ function tableSchema(table, cols, mode, target, applyDefaults, parsed) {
|
|
|
750
750
|
};
|
|
751
751
|
}
|
|
752
752
|
function collect(table) {
|
|
753
|
-
const parsed = (table.checks ?? []).map((k) => (0,
|
|
753
|
+
const parsed = (table.checks ?? []).map((k) => (0, import_validation_core6.parseCheck)(k.expression, k.name));
|
|
754
754
|
return {
|
|
755
755
|
checks: parsed.flatMap((p) => p.ok ? p.checks : []),
|
|
756
756
|
sets: parsed.flatMap((p) => p.ok ? p.sets ?? [] : []),
|
|
@@ -764,9 +764,9 @@ function tableSchemas(table, opts = {}) {
|
|
|
764
764
|
const parsed = collect(table);
|
|
765
765
|
const build2 = (cols, mode) => tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed);
|
|
766
766
|
return {
|
|
767
|
-
insert: build2((0,
|
|
768
|
-
update: build2((0,
|
|
769
|
-
select: build2((0,
|
|
767
|
+
insert: build2((0, import_validation_core6.insertColumns)(table), "insert"),
|
|
768
|
+
update: build2((0, import_validation_core6.updateColumns)(table), "update"),
|
|
769
|
+
select: build2((0, import_validation_core6.selectColumns)(table), "select")
|
|
770
770
|
};
|
|
771
771
|
}
|
|
772
772
|
function componentsDocument(tables, opts = {}) {
|
|
@@ -793,6 +793,7 @@ function foreignKeysOf(table) {
|
|
|
793
793
|
return table.columns.filter((c) => c.references).map((c) => ({
|
|
794
794
|
columns: [c.name],
|
|
795
795
|
foreignTable: c.references.table,
|
|
796
|
+
...c.references.schema ? { foreignSchema: c.references.schema } : {},
|
|
796
797
|
foreignColumns: [c.references.column]
|
|
797
798
|
}));
|
|
798
799
|
}
|
|
@@ -818,11 +819,11 @@ function build(tables, opts) {
|
|
|
818
819
|
const clash = operationIds.get(id);
|
|
819
820
|
if (clash !== void 0) {
|
|
820
821
|
throw new Error(
|
|
821
|
-
`@drzl/generator-json-schema: the operationId "${id}" would be emitted for both "${clash}" and "${table
|
|
822
|
+
`@drzl/generator-json-schema: the operationId "${id}" would be emitted for both "${clash}" and "${(0, import_analyzer.qualifiedTableName)(table)}". An operationId is the method name a client generator derives, and the specification requires it to be unique across the document.`
|
|
822
823
|
);
|
|
823
824
|
}
|
|
824
|
-
operationIds.set(id,
|
|
825
|
-
return { operationId: id, tags: [table
|
|
825
|
+
operationIds.set(id, (0, import_analyzer.qualifiedTableName)(table));
|
|
826
|
+
return { operationId: id, tags: [(0, import_analyzer.qualifiedTableName)(table)], ...rest };
|
|
826
827
|
};
|
|
827
828
|
const built = tables.map((table) => ({
|
|
828
829
|
table,
|
|
@@ -840,7 +841,10 @@ function build(tables, opts) {
|
|
|
840
841
|
if (table.readOnly) {
|
|
841
842
|
notes.push("It refuses every write, so only reads are described.");
|
|
842
843
|
}
|
|
843
|
-
tags.push({
|
|
844
|
+
tags.push({
|
|
845
|
+
name: (0, import_analyzer.qualifiedTableName)(table),
|
|
846
|
+
description: [`Table "${(0, import_analyzer.qualifiedTableName)(table)}".`, ...notes].join(" ")
|
|
847
|
+
});
|
|
844
848
|
const T = pascal(table.tsName);
|
|
845
849
|
const select = ref(componentName(table, "select"));
|
|
846
850
|
const validationFailed = {
|
|
@@ -856,7 +860,7 @@ function build(tables, opts) {
|
|
|
856
860
|
...jsonBody(ref(ERROR_SCHEMA))
|
|
857
861
|
});
|
|
858
862
|
const collection = `/${segment}`;
|
|
859
|
-
claim(collection, table.tsName,
|
|
863
|
+
claim(collection, table.tsName, (0, import_analyzer.qualifiedTableName)(table));
|
|
860
864
|
const item = {
|
|
861
865
|
get: operation(`list${T}`, table, {
|
|
862
866
|
summary: `List every ${table.name} row.`,
|
|
@@ -885,7 +889,7 @@ function build(tables, opts) {
|
|
|
885
889
|
paths[collection] = item;
|
|
886
890
|
if (!key) continue;
|
|
887
891
|
const itemPath = `${collection}/${key.map((c) => `{${c.name}}`).join("/")}`;
|
|
888
|
-
claim(itemPath, table.tsName,
|
|
892
|
+
claim(itemPath, table.tsName, (0, import_analyzer.qualifiedTableName)(table));
|
|
889
893
|
const parameters = key.map((c) => ({
|
|
890
894
|
name: c.name,
|
|
891
895
|
in: "path",
|
|
@@ -944,14 +948,18 @@ function build(tables, opts) {
|
|
|
944
948
|
for (const child of built) {
|
|
945
949
|
if (child.table === table) continue;
|
|
946
950
|
const matching = foreignKeysOf(child.table).filter(
|
|
947
|
-
(fk) =>
|
|
951
|
+
(fk) => (
|
|
952
|
+
// Qualified on both sides. On the bare name a key pointing at `reporting.users` also
|
|
953
|
+
// answered for `public.users`, so the child was hung under a parent in another schema.
|
|
954
|
+
(0, import_analyzer.qualifiedForeignTable)(fk) === (0, import_analyzer.qualifiedTableName)(table) && fk.foreignColumns.length === key.length && fk.foreignColumns.every((c, i) => c === key[i].name)
|
|
955
|
+
)
|
|
948
956
|
);
|
|
949
957
|
if (matching.length !== 1) continue;
|
|
950
958
|
const subPath = `${itemPath}/${child.segment}`;
|
|
951
959
|
claim(
|
|
952
960
|
subPath,
|
|
953
961
|
`${table.tsName} -> ${child.table.tsName}`,
|
|
954
|
-
`${table
|
|
962
|
+
`${(0, import_analyzer.qualifiedTableName)(table)} -> ${(0, import_analyzer.qualifiedTableName)(child.table)}`
|
|
955
963
|
);
|
|
956
964
|
paths[subPath] = {
|
|
957
965
|
parameters,
|
|
@@ -995,9 +1003,9 @@ function openApiDocument(tables, opts = {}) {
|
|
|
995
1003
|
function renderTableModule(table, affix, target, applyDefaults) {
|
|
996
1004
|
const T = table.tsName;
|
|
997
1005
|
const schemas = tableSchemas(table, { target, applyDefaults });
|
|
998
|
-
const decl = (mode) => `export const ${(0,
|
|
1006
|
+
const decl = (mode) => `export const ${(0, import_validation_core5.schemaName)(mode, T, affix)} = ${JSON.stringify(schemas[mode], null, 2)} as const;
|
|
999
1007
|
|
|
1000
|
-
export type ${(0,
|
|
1008
|
+
export type ${(0, import_validation_core5.typeName)(mode, T, affix)} = typeof ${(0, import_validation_core5.schemaName)(mode, T, affix)};`;
|
|
1001
1009
|
return [decl("insert"), decl("update"), decl("select")].join("\n\n") + "\n";
|
|
1002
1010
|
}
|
|
1003
1011
|
function resolveDocument(opt) {
|
|
@@ -1013,12 +1021,13 @@ function buildHeader2(h) {
|
|
|
1013
1021
|
|
|
1014
1022
|
`;
|
|
1015
1023
|
}
|
|
1016
|
-
var
|
|
1024
|
+
var import_validation_core5, import_analyzer, import_validation_core6, DRAFT, UUID_FORMAT, base64, ERROR_SCHEMA, componentName, ref, pascal, modesFor, resourceSegment, jsonBody, errorSchema, DEFAULT_FILE_SUFFIX, JsonSchemaGenerator, index_default2;
|
|
1017
1025
|
var init_dist2 = __esm({
|
|
1018
1026
|
"../generator-json-schema/dist/index.js"() {
|
|
1019
1027
|
"use strict";
|
|
1020
|
-
import_validation_core4 = require("@drzl/validation-core");
|
|
1021
1028
|
import_validation_core5 = require("@drzl/validation-core");
|
|
1029
|
+
import_analyzer = require("@drzl/analyzer");
|
|
1030
|
+
import_validation_core6 = require("@drzl/validation-core");
|
|
1022
1031
|
DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
1023
1032
|
UUID_FORMAT = "uuid";
|
|
1024
1033
|
base64 = (target) => target === "openapi-3.0" ? { type: "string", format: "byte" } : { type: "string", contentEncoding: "base64" };
|
|
@@ -1031,7 +1040,7 @@ var init_dist2 = __esm({
|
|
|
1031
1040
|
...table.readOnly || !key ? [] : ["update"],
|
|
1032
1041
|
"select"
|
|
1033
1042
|
];
|
|
1034
|
-
resourceSegment = (table) => encodeURIComponent(table.name);
|
|
1043
|
+
resourceSegment = (table) => table.schema ? `${encodeURIComponent(table.schema)}/${encodeURIComponent(table.name)}` : encodeURIComponent(table.name);
|
|
1035
1044
|
jsonBody = (schema) => ({ content: { "application/json": { schema } } });
|
|
1036
1045
|
errorSchema = () => ({
|
|
1037
1046
|
title: "error",
|
|
@@ -1056,14 +1065,14 @@ var init_dist2 = __esm({
|
|
|
1056
1065
|
const out = path6.resolve(process.cwd(), opts.outDir);
|
|
1057
1066
|
const files = [];
|
|
1058
1067
|
await fs3.mkdir(out, { recursive: true });
|
|
1059
|
-
const affix = (0,
|
|
1068
|
+
const affix = (0, import_validation_core5.resolveAffix)(opts);
|
|
1060
1069
|
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX;
|
|
1061
1070
|
const target = opts.target ?? "draft-2020-12";
|
|
1062
1071
|
const document = resolveDocument(opts.document);
|
|
1063
1072
|
for (const table of this.analysis.tables) {
|
|
1064
|
-
const filePath = path6.join(out, (0,
|
|
1073
|
+
const filePath = path6.join(out, (0, import_validation_core5.moduleFileName)(table.tsName, fileSuffix));
|
|
1065
1074
|
const code = renderTableModule(table, affix, target, !!opts.applyDefaults);
|
|
1066
|
-
const formatted = await (0,
|
|
1075
|
+
const formatted = await (0, import_validation_core5.formatCode)(
|
|
1067
1076
|
buildHeader2(opts.outputHeader) + code,
|
|
1068
1077
|
filePath,
|
|
1069
1078
|
opts.format
|
|
@@ -1081,7 +1090,7 @@ var init_dist2 = __esm({
|
|
|
1081
1090
|
`;
|
|
1082
1091
|
await fs3.writeFile(
|
|
1083
1092
|
componentsPath,
|
|
1084
|
-
await (0,
|
|
1093
|
+
await (0, import_validation_core5.formatCode)(buildHeader2(opts.outputHeader) + code, componentsPath, opts.format),
|
|
1085
1094
|
"utf8"
|
|
1086
1095
|
);
|
|
1087
1096
|
files.push(componentsPath);
|
|
@@ -1102,7 +1111,7 @@ var init_dist2 = __esm({
|
|
|
1102
1111
|
`;
|
|
1103
1112
|
await fs3.writeFile(
|
|
1104
1113
|
tsPath,
|
|
1105
|
-
await (0,
|
|
1114
|
+
await (0, import_validation_core5.formatCode)(buildHeader2(opts.outputHeader) + code, tsPath, opts.format),
|
|
1106
1115
|
"utf8"
|
|
1107
1116
|
);
|
|
1108
1117
|
files.push(tsPath);
|
|
@@ -1116,9 +1125,9 @@ var init_dist2 = __esm({
|
|
|
1116
1125
|
const ext = opts.importExtension === "none" ? "" : ".js";
|
|
1117
1126
|
const indexPath = path6.join(out, "index.ts");
|
|
1118
1127
|
const index = this.analysis.tables.map(
|
|
1119
|
-
(t) => `export * from '${(0,
|
|
1128
|
+
(t) => `export * from '${(0, import_validation_core5.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`
|
|
1120
1129
|
).concat(opts.components ? [`export * from './components${ext}';`] : []).concat(document && document.format !== "json" ? [`export * from './openapi${ext}';`] : []).join("\n") + "\n";
|
|
1121
|
-
const indexFormatted = await (0,
|
|
1130
|
+
const indexFormatted = await (0, import_validation_core5.formatCode)(
|
|
1122
1131
|
buildHeader2(opts.outputHeader) + index,
|
|
1123
1132
|
indexPath,
|
|
1124
1133
|
opts.format
|
|
@@ -1130,7 +1139,7 @@ var init_dist2 = __esm({
|
|
|
1130
1139
|
renderTable(table, opts) {
|
|
1131
1140
|
return renderTableModule(
|
|
1132
1141
|
table,
|
|
1133
|
-
(0,
|
|
1142
|
+
(0, import_validation_core5.resolveAffix)(opts),
|
|
1134
1143
|
opts?.target ?? "draft-2020-12",
|
|
1135
1144
|
!!opts?.applyDefaults
|
|
1136
1145
|
);
|
|
@@ -1140,8 +1149,574 @@ var init_dist2 = __esm({
|
|
|
1140
1149
|
}
|
|
1141
1150
|
});
|
|
1142
1151
|
|
|
1152
|
+
// ../generator-effect/dist/index.js
|
|
1153
|
+
var dist_exports3 = {};
|
|
1154
|
+
__export(dist_exports3, {
|
|
1155
|
+
EffectGenerator: () => EffectGenerator,
|
|
1156
|
+
default: () => index_default3
|
|
1157
|
+
});
|
|
1158
|
+
function filter(expr, description) {
|
|
1159
|
+
return `${NS}.filter((v) => ${expr}, { description: ${JSON.stringify(description)} })`;
|
|
1160
|
+
}
|
|
1161
|
+
function piped(base, steps) {
|
|
1162
|
+
return steps.length ? `${base}.pipe(${steps.join(", ")})` : base;
|
|
1163
|
+
}
|
|
1164
|
+
function isUnknownExpr(expr) {
|
|
1165
|
+
return expr === UNKNOWN_EXPR;
|
|
1166
|
+
}
|
|
1167
|
+
function withNarrowedType(expr, ref2) {
|
|
1168
|
+
return `${expr} as unknown as ${NS}.Schema<${ref2}>`;
|
|
1169
|
+
}
|
|
1170
|
+
function numericBounds(c, checks) {
|
|
1171
|
+
let lo = c.min !== void 0 ? { fn: "greaterThanOrEqualTo", value: c.min } : void 0;
|
|
1172
|
+
let hi = c.max !== void 0 ? { fn: "lessThanOrEqualTo", value: c.max } : void 0;
|
|
1173
|
+
for (const k of checks.filter((x) => x.column === c.name && x.kind === "number")) {
|
|
1174
|
+
if (k.operator === ">=") lo = { fn: "greaterThanOrEqualTo", value: k.value };
|
|
1175
|
+
else if (k.operator === ">") lo = { fn: "greaterThan", value: k.value };
|
|
1176
|
+
else if (k.operator === "<=") hi = { fn: "lessThanOrEqualTo", value: k.value };
|
|
1177
|
+
else if (k.operator === "<") hi = { fn: "lessThan", value: k.value };
|
|
1178
|
+
}
|
|
1179
|
+
return [lo, hi].filter(Boolean).map((x) => `${NS}.${x.fn}(${x.value})`);
|
|
1180
|
+
}
|
|
1181
|
+
function foldedIntoBounds(c, checks) {
|
|
1182
|
+
if (c.arrayDimensions || c.shape) return /* @__PURE__ */ new Set();
|
|
1183
|
+
if (c.tsType !== "number" && c.tsType !== "bigint") return /* @__PURE__ */ new Set();
|
|
1184
|
+
return new Set(
|
|
1185
|
+
checks.filter(
|
|
1186
|
+
(k) => k.column === c.name && k.kind === "number" && k.operator !== "=" && k.operator !== "<>"
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
function nonFiniteBranches(c) {
|
|
1191
|
+
const { nan, infinity } = (0, import_validation_core7.nonFiniteAccepted)(c);
|
|
1192
|
+
return [
|
|
1193
|
+
...nan ? [`${NS}.Number.pipe(${filter("Number.isNaN(v)", "NaN, which this column stores")})`] : [],
|
|
1194
|
+
...infinity ? [`${NS}.Literal(Infinity, -Infinity)`] : []
|
|
1195
|
+
];
|
|
1196
|
+
}
|
|
1197
|
+
function withNonFinite(c, base) {
|
|
1198
|
+
const branches = nonFiniteBranches(c);
|
|
1199
|
+
return branches.length ? `${NS}.Union(${base}, ${branches.join(", ")})` : base;
|
|
1200
|
+
}
|
|
1201
|
+
function dateExpr(mode, coerceDates) {
|
|
1202
|
+
const plain = `${NS}.ValidDateFromSelf`;
|
|
1203
|
+
if (coerceDates === "none") return plain;
|
|
1204
|
+
const fromString = piped(`${NS}.String`, [
|
|
1205
|
+
`${NS}.pattern(new RegExp(${JSON.stringify(import_validation_core7.COERCIBLE_DATE_STRING)}))`,
|
|
1206
|
+
filter((0, import_validation_core7.parsesToADate)("new Date(v)"), "a date the runtime can parse")
|
|
1207
|
+
]);
|
|
1208
|
+
const fromNumber = piped(`${NS}.Number`, [
|
|
1209
|
+
filter((0, import_validation_core7.parsesToADate)("new Date(v)"), "a date the runtime can parse")
|
|
1210
|
+
]);
|
|
1211
|
+
const union = `${NS}.Union(${plain}, ${fromString}, ${fromNumber})`;
|
|
1212
|
+
if (coerceDates === "all") return union;
|
|
1213
|
+
return mode === "select" ? plain : union;
|
|
1214
|
+
}
|
|
1215
|
+
function capSteps(c, mode) {
|
|
1216
|
+
const steps = [];
|
|
1217
|
+
if (c.shape?.kind === "byteString") {
|
|
1218
|
+
const n = c.shape.length;
|
|
1219
|
+
if (!n) return steps;
|
|
1220
|
+
return mode === "select" ? [filter(`${import_validation_core7.CODEPOINT_LENGTH} <= ${n}`, `at most ${n} characters`)] : [filter(`new TextEncoder().encode(v).length <= ${n}`, `at most ${n} bytes`)];
|
|
1221
|
+
}
|
|
1222
|
+
if (c.maxLength) {
|
|
1223
|
+
steps.push(
|
|
1224
|
+
filter(`${import_validation_core7.CODEPOINT_LENGTH} <= ${c.maxLength}`, `at most ${c.maxLength} characters`)
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
if (c.maxBytes) {
|
|
1228
|
+
steps.push(
|
|
1229
|
+
filter(`new TextEncoder().encode(v).length <= ${c.maxBytes}`, `at most ${c.maxBytes} bytes`)
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
return steps;
|
|
1233
|
+
}
|
|
1234
|
+
function lengthSteps(c, lengths) {
|
|
1235
|
+
if (c.arrayDimensions || c.shape) return [];
|
|
1236
|
+
return lengths.filter((k) => k.column === c.name).map(
|
|
1237
|
+
(k) => filter(
|
|
1238
|
+
`${import_validation_core7.CODEPOINT_LENGTH} ${OPS[k.operator]} ${k.value}`,
|
|
1239
|
+
`${k.name ? `${k.name}: ` : ""}length(${c.name}) ${k.operator} ${k.value}`
|
|
1240
|
+
)
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
function cardinalitySteps(c, cardinalities) {
|
|
1244
|
+
if (!c.arrayDimensions) return [];
|
|
1245
|
+
return cardinalities.filter((k) => k.column === c.name).map(
|
|
1246
|
+
(k) => filter(
|
|
1247
|
+
`v.length ${OPS[k.operator]} ${k.value}`,
|
|
1248
|
+
`${k.name ? `${k.name}: ` : ""}cardinality(${c.name}) ${k.operator} ${k.value}`
|
|
1249
|
+
)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
function checkSteps(c, checks) {
|
|
1253
|
+
if (c.arrayDimensions || c.shape) return [];
|
|
1254
|
+
const folded = foldedIntoBounds(c, checks);
|
|
1255
|
+
return checks.filter((k) => k.column === c.name && !folded.has(k)).map((k) => {
|
|
1256
|
+
const literal = k.kind === "string" ? JSON.stringify(k.value) : k.value;
|
|
1257
|
+
return filter(
|
|
1258
|
+
`v ${OPS[k.operator]} ${literal}`,
|
|
1259
|
+
`${k.name ? `${k.name}: ` : ""}${c.name} ${k.operator} ${k.value}`
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function hasNoRuntimeType(c) {
|
|
1264
|
+
return c.tsType === "any" || c.shape?.kind === "custom" || c.shape?.kind === "json";
|
|
1265
|
+
}
|
|
1266
|
+
function shapeExpr(c, mode, replaced = false) {
|
|
1267
|
+
const s = c.shape;
|
|
1268
|
+
if (!s) return void 0;
|
|
1269
|
+
switch (s.kind) {
|
|
1270
|
+
case "json":
|
|
1271
|
+
return replaced ? UNKNOWN_EXPR : JSON_CONST;
|
|
1272
|
+
case "custom":
|
|
1273
|
+
return UNKNOWN_EXPR;
|
|
1274
|
+
case "buffer":
|
|
1275
|
+
return `${NS}.Uint8ArrayFromSelf`;
|
|
1276
|
+
case "tuple":
|
|
1277
|
+
return `${NS}.Tuple(${Array.from({ length: s.length }, () => `${NS}.Number`).join(", ")})`;
|
|
1278
|
+
case "numberObject":
|
|
1279
|
+
return `${NS}.Struct({ ${s.fields.map((f) => `${f}: ${NS}.Number`).join(", ")} })`;
|
|
1280
|
+
case "numberVector":
|
|
1281
|
+
return piped(
|
|
1282
|
+
`${NS}.Array(${NS}.Number)`,
|
|
1283
|
+
s.length ? [filter(`v.length === ${s.length}`, `exactly ${s.length} elements`)] : []
|
|
1284
|
+
);
|
|
1285
|
+
case "bitstring":
|
|
1286
|
+
return piped(`${NS}.String`, [
|
|
1287
|
+
`${NS}.pattern(/^[01]*$/)`,
|
|
1288
|
+
...s.length ? [
|
|
1289
|
+
s.exact ? filter(`v.length === ${s.length}`, `exactly ${s.length} binary digits`) : filter(`v.length <= ${s.length}`, `at most ${s.length} binary digits`)
|
|
1290
|
+
] : []
|
|
1291
|
+
]);
|
|
1292
|
+
case "byteString":
|
|
1293
|
+
return piped(`${NS}.String`, capSteps(c, mode));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function exprForColumn(c, mode, coerceDates, checks, sets, lengths, replaced) {
|
|
1297
|
+
const shaped = shapeExpr(c, mode, replaced);
|
|
1298
|
+
if (shaped) return shaped;
|
|
1299
|
+
const set = sets.find((x) => x.column === c.name);
|
|
1300
|
+
if (set) {
|
|
1301
|
+
const values = set.values.map((v) => set.kind === "string" ? JSON.stringify(v) : v);
|
|
1302
|
+
return `${NS}.Literal(${values.join(", ")})`;
|
|
1303
|
+
}
|
|
1304
|
+
if (c.arrayDimensions) checks = [];
|
|
1305
|
+
if (c.enumValues && c.enumValues.length) {
|
|
1306
|
+
return `${NS}.Literal(${c.enumValues.map((v) => JSON.stringify(v)).join(", ")})`;
|
|
1307
|
+
}
|
|
1308
|
+
const eq = checks.find((k) => k.column === c.name && k.operator === "=");
|
|
1309
|
+
if (eq && !c.shape) {
|
|
1310
|
+
return `${NS}.Literal(${eq.kind === "string" ? JSON.stringify(eq.value) : eq.value})`;
|
|
1311
|
+
}
|
|
1312
|
+
const rest = [...checkSteps(c, checks), ...lengthSteps(c, lengths)];
|
|
1313
|
+
switch (c.tsType) {
|
|
1314
|
+
case "string": {
|
|
1315
|
+
const base = c.format === "uuid" ? `${NS}.UUID` : `${NS}.String`;
|
|
1316
|
+
const pattern = c.format && c.format !== "uuid" && import_validation_core7.COLUMN_FORMATS[c.format] ? [`${NS}.pattern(new RegExp(${JSON.stringify(import_validation_core7.COLUMN_FORMATS[c.format])}))`] : [];
|
|
1317
|
+
return piped(base, [...pattern, ...capSteps(c, mode), ...rest]);
|
|
1318
|
+
}
|
|
1319
|
+
case "number": {
|
|
1320
|
+
const base = (0, import_validation_core7.isIntegerColumn)(c) ? `${NS}.Int` : `${NS}.Finite`;
|
|
1321
|
+
return withNonFinite(c, piped(base, [...numericBounds(c, checks), ...rest]));
|
|
1322
|
+
}
|
|
1323
|
+
case "bigint":
|
|
1324
|
+
return piped(`${NS}.BigIntFromSelf`, [
|
|
1325
|
+
...c.min !== void 0 ? [`${NS}.greaterThanOrEqualToBigInt(${c.min}n)`] : [],
|
|
1326
|
+
...c.max !== void 0 ? [`${NS}.lessThanOrEqualToBigInt(${c.max}n)`] : [],
|
|
1327
|
+
...rest
|
|
1328
|
+
]);
|
|
1329
|
+
case "boolean":
|
|
1330
|
+
return `${NS}.Boolean`;
|
|
1331
|
+
case "Date":
|
|
1332
|
+
return dateExpr(mode, coerceDates);
|
|
1333
|
+
case "Uint8Array":
|
|
1334
|
+
return `${NS}.Uint8ArrayFromSelf`;
|
|
1335
|
+
case "any":
|
|
1336
|
+
return UNKNOWN_EXPR;
|
|
1337
|
+
default:
|
|
1338
|
+
return UNKNOWN_EXPR;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function renderField(c, mode, coerceDates, checks, sets, lengths, cardinalities, applyDefault, narrowRef, brand) {
|
|
1342
|
+
let expr = exprForColumn(
|
|
1343
|
+
c,
|
|
1344
|
+
mode,
|
|
1345
|
+
coerceDates,
|
|
1346
|
+
checks,
|
|
1347
|
+
sets,
|
|
1348
|
+
lengths,
|
|
1349
|
+
!!narrowRef && hasNoRuntimeType(c)
|
|
1350
|
+
);
|
|
1351
|
+
const dims = c.arrayDimensions ?? 0;
|
|
1352
|
+
for (let i = 0; i < dims; i++) {
|
|
1353
|
+
expr = `${NS}.Array(${expr})`;
|
|
1354
|
+
if (i === dims - 1) expr = piped(expr, cardinalitySteps(c, cardinalities));
|
|
1355
|
+
}
|
|
1356
|
+
if (brand) expr = piped(expr, [`${NS}.brand(${JSON.stringify(brand)})`]);
|
|
1357
|
+
if (c.nullable && !isUnknownExpr(expr)) expr = `${NS}.NullOr(${expr})`;
|
|
1358
|
+
if (narrowRef && !brand) expr = withNarrowedType(expr, narrowRef);
|
|
1359
|
+
if (mode === "select") return expr;
|
|
1360
|
+
const wantsDefault = mode === "insert" && applyDefault && c.defaultValue !== void 0;
|
|
1361
|
+
if (wantsDefault) {
|
|
1362
|
+
return `${NS}.optionalWith(${expr}, { default: () => ${JSON.stringify(c.defaultValue)} })`;
|
|
1363
|
+
}
|
|
1364
|
+
if (mode === "update" || c.nullable || c.hasDefault) return `${NS}.optional(${expr})`;
|
|
1365
|
+
return expr;
|
|
1366
|
+
}
|
|
1367
|
+
function wantsRef(c, allColumns) {
|
|
1368
|
+
return allColumns || hasNoRuntimeType(c);
|
|
1369
|
+
}
|
|
1370
|
+
function renderObjectShape(cols, mode, coerceDates, checks, sets, lengths, cardinalities, typedJson, applyDefaults, brands) {
|
|
1371
|
+
return cols.map((c) => {
|
|
1372
|
+
const ref2 = typedJson && wantsRef(c, !!typedJson.allColumns) ? `(typeof ${typedJson.table}.$infer${typedJson.mode === "insert" ? "Insert" : "Select"})[${JSON.stringify(c.name)}]` : void 0;
|
|
1373
|
+
const field2 = renderField(
|
|
1374
|
+
c,
|
|
1375
|
+
mode,
|
|
1376
|
+
coerceDates,
|
|
1377
|
+
checks,
|
|
1378
|
+
sets,
|
|
1379
|
+
lengths,
|
|
1380
|
+
cardinalities,
|
|
1381
|
+
applyDefaults,
|
|
1382
|
+
ref2,
|
|
1383
|
+
brands?.plan.brandOf(brands.tsName, c.name)
|
|
1384
|
+
);
|
|
1385
|
+
return ` ${JSON.stringify(c.name)}: ${field2},`;
|
|
1386
|
+
}).join("\n");
|
|
1387
|
+
}
|
|
1388
|
+
function rowSteps(rows, cols) {
|
|
1389
|
+
const present = new Set(cols.map((c) => c.name));
|
|
1390
|
+
return rows.filter((r) => present.has(r.left) && present.has(r.right)).map((r) => {
|
|
1391
|
+
const l = `o[${JSON.stringify(r.left)}]`;
|
|
1392
|
+
const rt = `o[${JSON.stringify(r.right)}]`;
|
|
1393
|
+
const msg = `${r.name ? `${r.name}: ` : ""}${r.left} ${r.operator} ${r.right}`;
|
|
1394
|
+
return `${NS}.filter((o) => ${l} == null || ${rt} == null || ${l} ${OPS[r.operator]} ${rt}, { description: ${JSON.stringify(msg)} })`;
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
function indentBlock(code, by = " ") {
|
|
1398
|
+
return code.split("\n").map((line) => line ? by + line : line).join("\n");
|
|
1399
|
+
}
|
|
1400
|
+
function parsedChecksFor(table) {
|
|
1401
|
+
const parsed = (table.checks ?? []).map((k) => (0, import_validation_core7.parseCheck)(k.expression, k.name));
|
|
1402
|
+
return {
|
|
1403
|
+
checks: parsed.flatMap((p) => p.ok ? p.checks : []),
|
|
1404
|
+
sets: parsed.flatMap((p) => p.ok ? p.sets ?? [] : []),
|
|
1405
|
+
rows: parsed.flatMap((p) => p.ok ? p.rows ?? [] : []),
|
|
1406
|
+
lengths: parsed.flatMap((p) => p.ok ? p.lengths ?? [] : []),
|
|
1407
|
+
cardinalities: parsed.flatMap((p) => p.ok ? p.cardinalities ?? [] : [])
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function nestedNodeCols(node, mode) {
|
|
1411
|
+
const all = mode === "insert" ? (0, import_validation_core7.insertColumns)(node.table) : (0, import_validation_core7.selectColumns)(node.table);
|
|
1412
|
+
return (0, import_validation_core7.nestedNodeColumns)(all, node);
|
|
1413
|
+
}
|
|
1414
|
+
function nestedNodes(node, into = []) {
|
|
1415
|
+
into.push(node);
|
|
1416
|
+
for (const arm of node.arms) nestedNodes(arm.child, into);
|
|
1417
|
+
return into;
|
|
1418
|
+
}
|
|
1419
|
+
function renderNestedObject(node, mode, coerceDates, typedJson, applyDefaults, brands) {
|
|
1420
|
+
const cols = nestedNodeCols(node, mode);
|
|
1421
|
+
const { checks, sets, rows, lengths, cardinalities } = parsedChecksFor(node.table);
|
|
1422
|
+
const tj = typedJson ? { table: node.table.tsName, mode, allColumns: typedJson.allColumns } : void 0;
|
|
1423
|
+
const fields = renderObjectShape(
|
|
1424
|
+
cols,
|
|
1425
|
+
mode,
|
|
1426
|
+
coerceDates,
|
|
1427
|
+
checks,
|
|
1428
|
+
sets,
|
|
1429
|
+
lengths,
|
|
1430
|
+
cardinalities,
|
|
1431
|
+
tj,
|
|
1432
|
+
applyDefaults,
|
|
1433
|
+
brands ? { plan: brands, tsName: node.table.tsName } : void 0
|
|
1434
|
+
);
|
|
1435
|
+
const arms = node.arms.map((arm) => {
|
|
1436
|
+
const notes = (0, import_validation_core7.nestedArmNotes)(arm).map((n) => ` // ${n}
|
|
1437
|
+
`).join("");
|
|
1438
|
+
const child = renderNestedObject(
|
|
1439
|
+
arm.child,
|
|
1440
|
+
mode,
|
|
1441
|
+
coerceDates,
|
|
1442
|
+
typedJson,
|
|
1443
|
+
applyDefaults,
|
|
1444
|
+
brands
|
|
1445
|
+
);
|
|
1446
|
+
const inner = arm.single ? `${NS}.NullOr(
|
|
1447
|
+
${indentBlock(indentBlock(child))}
|
|
1448
|
+
)` : `${NS}.Array(
|
|
1449
|
+
${indentBlock(indentBlock(child))}
|
|
1450
|
+
)`;
|
|
1451
|
+
return `${notes} ${JSON.stringify(arm.key)}: ${NS}.optional(${inner}),`;
|
|
1452
|
+
});
|
|
1453
|
+
const body = [fields, ...arms].filter(Boolean).join("\n");
|
|
1454
|
+
return piped(`${NS}.Struct({
|
|
1455
|
+
${body}
|
|
1456
|
+
})`, rowSteps(rows, cols));
|
|
1457
|
+
}
|
|
1458
|
+
function renderNestedSchemas(table, affix, coerceDates, typedJson, applyDefaults, plans, brands) {
|
|
1459
|
+
const out = [];
|
|
1460
|
+
for (const mode of ["insert", "select"]) {
|
|
1461
|
+
const plan = plans[mode];
|
|
1462
|
+
if (!plan) continue;
|
|
1463
|
+
const name = (0, import_validation_core7.nestedSchemaName)(mode, table.tsName, affix);
|
|
1464
|
+
const tname = (0, import_validation_core7.nestedTypeName)(mode, table.tsName, affix);
|
|
1465
|
+
const expr = renderNestedObject(plan, mode, coerceDates, typedJson, applyDefaults, brands);
|
|
1466
|
+
out.push(
|
|
1467
|
+
`export const ${name} = ${expr};
|
|
1468
|
+
|
|
1469
|
+
export type ${tname} = ${NS}.Schema.Type<typeof ${name}>;
|
|
1470
|
+
|
|
1471
|
+
export const ${STANDARD_PREFIX}${name} = ${NS}.standardSchemaV1(${name});`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
return out.length ? `
|
|
1475
|
+
${out.join("\n\n")}
|
|
1476
|
+
` : "";
|
|
1477
|
+
}
|
|
1478
|
+
function nestedPlansFor(table, analysis, depth) {
|
|
1479
|
+
const out = {};
|
|
1480
|
+
for (const mode of ["insert", "select"]) {
|
|
1481
|
+
if (mode === "insert" && table.readOnly) continue;
|
|
1482
|
+
const plan = (0, import_validation_core7.buildNestedPlan)(table, analysis.tables, analysis.relations ?? [], mode, depth);
|
|
1483
|
+
if (plan) out[mode] = plan;
|
|
1484
|
+
}
|
|
1485
|
+
return out;
|
|
1486
|
+
}
|
|
1487
|
+
function renderTableSchemas(table, affix, coerceDates, typedJson, applyDefaults = false, wantsDuplicateFinder = false, nested = {}, brands) {
|
|
1488
|
+
const T = table.tsName;
|
|
1489
|
+
const insertCols = (0, import_validation_core7.insertColumns)(table);
|
|
1490
|
+
const updateCols = (0, import_validation_core7.updateColumns)(table);
|
|
1491
|
+
const selectCols = (0, import_validation_core7.selectColumns)(table);
|
|
1492
|
+
const { checks, sets, rows, lengths, cardinalities } = parsedChecksFor(table);
|
|
1493
|
+
const tj = typedJson ? { table: T, allColumns: typedJson.allColumns } : void 0;
|
|
1494
|
+
const modes = [
|
|
1495
|
+
["insert", insertCols],
|
|
1496
|
+
["update", updateCols],
|
|
1497
|
+
["select", selectCols]
|
|
1498
|
+
];
|
|
1499
|
+
const blocks = modes.map(([mode, cols]) => {
|
|
1500
|
+
const name = (0, import_validation_core7.schemaName)(mode, T, affix);
|
|
1501
|
+
const tname = (0, import_validation_core7.typeName)(mode, T, affix);
|
|
1502
|
+
const body = renderObjectShape(
|
|
1503
|
+
cols,
|
|
1504
|
+
mode,
|
|
1505
|
+
coerceDates,
|
|
1506
|
+
checks,
|
|
1507
|
+
sets,
|
|
1508
|
+
lengths,
|
|
1509
|
+
cardinalities,
|
|
1510
|
+
// The update schema references the insert-side inferred types: both describe a value going
|
|
1511
|
+
// in, and `$inferSelect` would name the post-default type for a column a write may omit.
|
|
1512
|
+
tj ? { ...tj, mode: mode === "select" ? "select" : "insert" } : void 0,
|
|
1513
|
+
applyDefaults,
|
|
1514
|
+
brands ? { plan: brands, tsName: T } : void 0
|
|
1515
|
+
);
|
|
1516
|
+
const expr = piped(`${NS}.Struct({
|
|
1517
|
+
${body}
|
|
1518
|
+
})`, rowSteps(rows, cols));
|
|
1519
|
+
return `export const ${name} = ${expr};
|
|
1520
|
+
|
|
1521
|
+
export type ${tname} = ${NS}.Schema.Type<typeof ${name}>;
|
|
1522
|
+
|
|
1523
|
+
export const ${STANDARD_PREFIX}${name} = ${NS}.standardSchemaV1(${name});`;
|
|
1524
|
+
});
|
|
1525
|
+
const nestedByTable = ["insert", "select"].flatMap((m) => {
|
|
1526
|
+
const plan = nested[m];
|
|
1527
|
+
return plan ? nestedNodes(plan).map((n) => [n.table.tsName, nestedNodeCols(n, m)]) : [];
|
|
1528
|
+
});
|
|
1529
|
+
const nestedCols = nestedByTable.flatMap(([, cs]) => cs);
|
|
1530
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1531
|
+
if (typedJson) {
|
|
1532
|
+
const all = !!typedJson.allColumns;
|
|
1533
|
+
if ([...insertCols, ...updateCols, ...selectCols].some((c) => wantsRef(c, all))) {
|
|
1534
|
+
referenced.add(T);
|
|
1535
|
+
}
|
|
1536
|
+
for (const [name, cs] of nestedByTable) {
|
|
1537
|
+
if (cs.some((c) => wantsRef(c, all))) referenced.add(name);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const schemaImport = referenced.size ? `import type { ${[...referenced].join(", ")} } from '${typedJson.schemaSpecifier}';
|
|
1541
|
+
` : "";
|
|
1542
|
+
const needsJson = !typedJson && [...insertCols, ...updateCols, ...selectCols, ...nestedCols].some(
|
|
1543
|
+
(c) => c.shape?.kind === "json"
|
|
1544
|
+
);
|
|
1545
|
+
const finder = wantsDuplicateFinder ? (0, import_validation_core7.renderDuplicateFinder)(table, `findDuplicate${T}`, (0, import_validation_core7.typeName)("insert", T, affix)) : void 0;
|
|
1546
|
+
const duplicates = finder ? `
|
|
1547
|
+
${finder}
|
|
1548
|
+
` : "";
|
|
1549
|
+
const nestedCode = renderNestedSchemas(
|
|
1550
|
+
table,
|
|
1551
|
+
affix,
|
|
1552
|
+
coerceDates,
|
|
1553
|
+
typedJson,
|
|
1554
|
+
applyDefaults,
|
|
1555
|
+
nested,
|
|
1556
|
+
brands
|
|
1557
|
+
);
|
|
1558
|
+
const selectName = (0, import_validation_core7.schemaName)("select", T, affix);
|
|
1559
|
+
const brandAliases = (brands?.aliasesFor(T) ?? []).map(
|
|
1560
|
+
(a) => `/** The nominal type of ${T}.${a.column}. */
|
|
1561
|
+
export type ${a.alias} = ${NS}.Schema.Type<typeof ${selectName}>[${JSON.stringify(a.column)}];`
|
|
1562
|
+
).join("\n\n");
|
|
1563
|
+
const brandCode = brandAliases ? `
|
|
1564
|
+
${brandAliases}
|
|
1565
|
+
` : "";
|
|
1566
|
+
return `import * as ${NS} from 'effect/Schema';
|
|
1567
|
+
${schemaImport}${needsJson ? `
|
|
1568
|
+
${JSON_PREAMBLE}` : ""}
|
|
1569
|
+
${blocks.join("\n\n")}
|
|
1570
|
+
${brandCode}${nestedCode}${duplicates}`;
|
|
1571
|
+
}
|
|
1572
|
+
function buildHeader3(h) {
|
|
1573
|
+
if (h && h.enabled === false) return "";
|
|
1574
|
+
const text = h?.text?.trim();
|
|
1575
|
+
const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
|
|
1576
|
+
"// Generated by DRZL (@drzl/*)",
|
|
1577
|
+
"// Generated output is granted to you under your project's license.",
|
|
1578
|
+
"// You may use, copy, modify, and distribute without attribution."
|
|
1579
|
+
];
|
|
1580
|
+
return lines.join("\n") + "\n\n";
|
|
1581
|
+
}
|
|
1582
|
+
var import_validation_core7, DEFAULT_FILE_SUFFIX2, STANDARD_PREFIX, NS, OPS, JSON_CONST, JSON_PREAMBLE, UNKNOWN_EXPR, EffectGenerator, index_default3;
|
|
1583
|
+
var init_dist3 = __esm({
|
|
1584
|
+
"../generator-effect/dist/index.js"() {
|
|
1585
|
+
"use strict";
|
|
1586
|
+
import_validation_core7 = require("@drzl/validation-core");
|
|
1587
|
+
DEFAULT_FILE_SUFFIX2 = ".effect.ts";
|
|
1588
|
+
STANDARD_PREFIX = "Standard";
|
|
1589
|
+
NS = "Schema";
|
|
1590
|
+
OPS = {
|
|
1591
|
+
">=": ">=",
|
|
1592
|
+
">": ">",
|
|
1593
|
+
"<=": "<=",
|
|
1594
|
+
"<": "<",
|
|
1595
|
+
"=": "===",
|
|
1596
|
+
"<>": "!=="
|
|
1597
|
+
};
|
|
1598
|
+
JSON_CONST = "DrzlJsonValue";
|
|
1599
|
+
JSON_PREAMBLE = `type ${JSON_CONST}Type =
|
|
1600
|
+
| string
|
|
1601
|
+
| number
|
|
1602
|
+
| boolean
|
|
1603
|
+
| null
|
|
1604
|
+
| readonly ${JSON_CONST}Type[]
|
|
1605
|
+
| { readonly [key: string]: ${JSON_CONST}Type };
|
|
1606
|
+
|
|
1607
|
+
const ${JSON_CONST}: ${NS}.Schema<${JSON_CONST}Type, unknown> = ${NS}.suspend(() =>
|
|
1608
|
+
${NS}.Union(
|
|
1609
|
+
${NS}.String,
|
|
1610
|
+
${NS}.Finite,
|
|
1611
|
+
${NS}.Boolean,
|
|
1612
|
+
${NS}.Null,
|
|
1613
|
+
${NS}.Array(${JSON_CONST}),
|
|
1614
|
+
// The plain-object test comes before the record, not after it. \`Schema.Record\` rebuilds its
|
|
1615
|
+
// output, so a check placed after it inspects that new object and reports every input as
|
|
1616
|
+
// plain. A Date sailed through: it has no own enumerable keys, so the record accepted it and
|
|
1617
|
+
// rebuilt it as \`{}\`.
|
|
1618
|
+
${NS}.Unknown.pipe(
|
|
1619
|
+
${NS}.filter(
|
|
1620
|
+
(o) => {
|
|
1621
|
+
if (typeof o !== 'object' || o === null || Array.isArray(o)) return false;
|
|
1622
|
+
const p = Object.getPrototypeOf(o);
|
|
1623
|
+
return p === Object.prototype || p === null;
|
|
1624
|
+
},
|
|
1625
|
+
{ description: 'a plain object' }
|
|
1626
|
+
),
|
|
1627
|
+
${NS}.compose(${NS}.Record({ key: ${NS}.String, value: ${JSON_CONST} }), { strict: false })
|
|
1628
|
+
)
|
|
1629
|
+
)
|
|
1630
|
+
);
|
|
1631
|
+
`;
|
|
1632
|
+
UNKNOWN_EXPR = `${NS}.Unknown`;
|
|
1633
|
+
EffectGenerator = class {
|
|
1634
|
+
constructor(analysis) {
|
|
1635
|
+
this.analysis = analysis;
|
|
1636
|
+
this.library = "effect";
|
|
1637
|
+
}
|
|
1638
|
+
async generate(opts) {
|
|
1639
|
+
const fs3 = await import("fs/promises");
|
|
1640
|
+
const path6 = await import("path");
|
|
1641
|
+
const out = path6.resolve(process.cwd(), opts.outDir);
|
|
1642
|
+
const files = [];
|
|
1643
|
+
await fs3.mkdir(out, { recursive: true });
|
|
1644
|
+
const affix = (0, import_validation_core7.resolveAffix)(opts);
|
|
1645
|
+
const coerceDates = opts.coerceDates ?? "input";
|
|
1646
|
+
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX2;
|
|
1647
|
+
const wantsTypes = opts.typedJson || opts.typedColumns;
|
|
1648
|
+
const typedJson = wantsTypes && opts.schemaPath ? {
|
|
1649
|
+
schemaSpecifier: (0, import_validation_core7.resolveConfiguredImport)(
|
|
1650
|
+
opts.schemaPath,
|
|
1651
|
+
out,
|
|
1652
|
+
process.cwd(),
|
|
1653
|
+
opts.importExtension
|
|
1654
|
+
),
|
|
1655
|
+
allColumns: !!opts.typedColumns
|
|
1656
|
+
} : void 0;
|
|
1657
|
+
if (wantsTypes && !opts.schemaPath) {
|
|
1658
|
+
console.warn(
|
|
1659
|
+
"[drzl] typedJson was requested but the schema path is unknown, so json columns keep their wide type."
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const nestedDepth = opts.nestedSchemas ? (0, import_validation_core7.resolveNestedDepth)(opts.nestedDepth, (m) => console.warn(m)) : 0;
|
|
1663
|
+
const brands = (0, import_validation_core7.buildBrandPlan)(this.analysis.tables, opts.branded);
|
|
1664
|
+
for (const note of brands?.notes ?? []) console.warn(`[drzl] ${note}`);
|
|
1665
|
+
for (const table of this.analysis.tables) {
|
|
1666
|
+
const filePath = path6.join(out, (0, import_validation_core7.moduleFileName)(table.tsName, fileSuffix));
|
|
1667
|
+
const code = renderTableSchemas(
|
|
1668
|
+
table,
|
|
1669
|
+
affix,
|
|
1670
|
+
coerceDates,
|
|
1671
|
+
typedJson,
|
|
1672
|
+
!!opts.applyDefaults,
|
|
1673
|
+
!!opts?.duplicateFinder,
|
|
1674
|
+
opts.nestedSchemas ? nestedPlansFor(table, this.analysis, nestedDepth) : {},
|
|
1675
|
+
brands
|
|
1676
|
+
);
|
|
1677
|
+
const formatted = await (0, import_validation_core7.formatCode)(
|
|
1678
|
+
buildHeader3(opts.outputHeader) + code,
|
|
1679
|
+
filePath,
|
|
1680
|
+
opts.format
|
|
1681
|
+
);
|
|
1682
|
+
await fs3.writeFile(filePath, formatted, "utf8");
|
|
1683
|
+
files.push(filePath);
|
|
1684
|
+
}
|
|
1685
|
+
const indexPath = path6.join(out, "index.ts");
|
|
1686
|
+
const indexFormatted = await (0, import_validation_core7.formatCode)(
|
|
1687
|
+
buildHeader3(opts.outputHeader) + this.defaultIndex(this.analysis, opts),
|
|
1688
|
+
indexPath,
|
|
1689
|
+
opts.format
|
|
1690
|
+
);
|
|
1691
|
+
await fs3.writeFile(indexPath, indexFormatted, "utf8");
|
|
1692
|
+
files.push(indexPath);
|
|
1693
|
+
return files;
|
|
1694
|
+
}
|
|
1695
|
+
renderTable(table, opts) {
|
|
1696
|
+
return renderTableSchemas(
|
|
1697
|
+
table,
|
|
1698
|
+
(0, import_validation_core7.resolveAffix)(opts),
|
|
1699
|
+
opts?.coerceDates ?? "input",
|
|
1700
|
+
void 0,
|
|
1701
|
+
!!opts?.applyDefaults,
|
|
1702
|
+
!!opts?.duplicateFinder,
|
|
1703
|
+
{},
|
|
1704
|
+
(0, import_validation_core7.buildBrandPlan)(this.analysis.tables, opts?.branded)
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
defaultIndex(analysis, opts) {
|
|
1708
|
+
const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX2;
|
|
1709
|
+
return analysis.tables.map(
|
|
1710
|
+
(t) => `export * from '${(0, import_validation_core7.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`
|
|
1711
|
+
).join("\n") + "\n";
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
index_default3 = EffectGenerator;
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1143
1718
|
// src/cli.ts
|
|
1144
|
-
var
|
|
1719
|
+
var import_analyzer2 = require("@drzl/analyzer");
|
|
1145
1720
|
var import_generator_orpc = require("@drzl/generator-orpc");
|
|
1146
1721
|
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1147
1722
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
@@ -1165,6 +1740,11 @@ function validationOptions(g, cfg, outDir, caps = {}) {
|
|
|
1165
1740
|
duplicateFinder: g.duplicateFinder,
|
|
1166
1741
|
nestedSchemas: g.nestedSchemas,
|
|
1167
1742
|
nestedDepth: g.nestedDepth,
|
|
1743
|
+
// Every validation generator can express a brand, including TypeBox, which has no brand
|
|
1744
|
+
// helper and gets one from `TUnsafe` instead. So this needs no capability flag: an option
|
|
1745
|
+
// that reached only four of the five would be the class of defect this file exists to
|
|
1746
|
+
// remove.
|
|
1747
|
+
branded: g.branded,
|
|
1168
1748
|
// Only where the generator can act on them, so an unsupported option is absent rather than
|
|
1169
1749
|
// present and ignored.
|
|
1170
1750
|
...caps.schemaTypes ? {
|
|
@@ -1172,7 +1752,9 @@ function validationOptions(g, cfg, outDir, caps = {}) {
|
|
|
1172
1752
|
schemaPath: cfg.schema,
|
|
1173
1753
|
typedJson: g.typedJson,
|
|
1174
1754
|
typedColumns: g.typedColumns
|
|
1175
|
-
} : {}
|
|
1755
|
+
} : {},
|
|
1756
|
+
...caps.standardSchema ? { standardSchema: g.standardSchema } : {},
|
|
1757
|
+
...caps.meta ? { meta: g.meta } : {}
|
|
1176
1758
|
};
|
|
1177
1759
|
}
|
|
1178
1760
|
|
|
@@ -1196,6 +1778,47 @@ var fs = __toESM(require("fs"), 1);
|
|
|
1196
1778
|
var import_node_module = require("module");
|
|
1197
1779
|
var path = __toESM(require("path"), 1);
|
|
1198
1780
|
var import_zod = require("zod");
|
|
1781
|
+
|
|
1782
|
+
// src/patterns.ts
|
|
1783
|
+
function patternToRegExp(pattern) {
|
|
1784
|
+
return new RegExp(
|
|
1785
|
+
"^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function matchesAny(patterns, name) {
|
|
1789
|
+
return patterns.some((p) => patternToRegExp(p).test(name));
|
|
1790
|
+
}
|
|
1791
|
+
var DEFAULT_SCHEMA_ALIAS = "public";
|
|
1792
|
+
function tableAliases(table) {
|
|
1793
|
+
return [table.name, `${table.schema ?? DEFAULT_SCHEMA_ALIAS}.${table.name}`];
|
|
1794
|
+
}
|
|
1795
|
+
function matchesTable(patterns, table) {
|
|
1796
|
+
return tableAliases(table).some((alias) => matchesAny(patterns, alias));
|
|
1797
|
+
}
|
|
1798
|
+
function displayTableName(table) {
|
|
1799
|
+
return table.schema ? `${table.schema}.${table.name}` : table.name;
|
|
1800
|
+
}
|
|
1801
|
+
function addressableName(table) {
|
|
1802
|
+
return `${table.schema ?? DEFAULT_SCHEMA_ALIAS}.${table.name}`;
|
|
1803
|
+
}
|
|
1804
|
+
function hasNamedSchemas(tables) {
|
|
1805
|
+
return tables.some((t) => t.schema);
|
|
1806
|
+
}
|
|
1807
|
+
function ambiguousPatternWarnings(patterns, tables, option) {
|
|
1808
|
+
const out = [];
|
|
1809
|
+
for (const pattern of patterns) {
|
|
1810
|
+
if (pattern.includes(".")) continue;
|
|
1811
|
+
const matched = tables.filter((t) => matchesTable([pattern], t));
|
|
1812
|
+
const schemas = new Set(matched.map((t) => t.schema ?? DEFAULT_SCHEMA_ALIAS));
|
|
1813
|
+
if (schemas.size < 2) continue;
|
|
1814
|
+
out.push(
|
|
1815
|
+
`drzl config: ${option} pattern ${JSON.stringify(pattern)} matches tables in more than one schema, and every one of them is affected: ${matched.map(addressableName).sort().join(", ")}. Write the schema to mean one of them, for example ${JSON.stringify(addressableName(matched[0]))}.`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
return out;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/config.ts
|
|
1199
1822
|
var NamingSchema = import_zod.z.object({
|
|
1200
1823
|
routerSuffix: import_zod.z.string().default("Router"),
|
|
1201
1824
|
procedureCase: import_zod.z.enum(["camel", "kebab", "snake"]).default("camel")
|
|
@@ -1229,7 +1852,17 @@ var AffixSchema = import_zod.z.object({
|
|
|
1229
1852
|
}).strict();
|
|
1230
1853
|
var ImportExtensionSchema = import_zod.z.enum(import_validation_core.IMPORT_EXTENSIONS);
|
|
1231
1854
|
var GeneratorSchema = import_zod.z.object({
|
|
1232
|
-
kind: import_zod.z.enum([
|
|
1855
|
+
kind: import_zod.z.enum([
|
|
1856
|
+
"orpc",
|
|
1857
|
+
"trpc",
|
|
1858
|
+
"service",
|
|
1859
|
+
"zod",
|
|
1860
|
+
"valibot",
|
|
1861
|
+
"arktype",
|
|
1862
|
+
"typebox",
|
|
1863
|
+
"effect",
|
|
1864
|
+
"json-schema"
|
|
1865
|
+
]),
|
|
1233
1866
|
/**
|
|
1234
1867
|
* Overrides the top-level `importExtension` for this generator alone, for a project whose
|
|
1235
1868
|
* generated directories are compiled by different tsconfigs.
|
|
@@ -1265,6 +1898,36 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
1265
1898
|
* fact about the table rather than the row. This checks the half that needs no database.
|
|
1266
1899
|
*/
|
|
1267
1900
|
duplicateFinder: import_zod.z.boolean().optional(),
|
|
1901
|
+
/**
|
|
1902
|
+
* zod only. Attach the facts the analyzer knows and a zod schema cannot state, as `.meta()` on
|
|
1903
|
+
* every field and every table schema: the declared SQL type, the primary key, the unique
|
|
1904
|
+
* constraints, whether the database generates or defaults the value, and the CHECK constraints,
|
|
1905
|
+
* including the ones DRZL declined to enforce.
|
|
1906
|
+
*
|
|
1907
|
+
* `z.toJSONSchema` copies these through, so they are also how an OpenAPI document built from the
|
|
1908
|
+
* emitted schemas gets the declared width back: DRZL enforces one as a `.refine()`, and
|
|
1909
|
+
* `toJSONSchema` drops every refinement in silence.
|
|
1910
|
+
*
|
|
1911
|
+
* `true` is the shorthand for `{ enabled: true }`. `{ description: true }` additionally writes a
|
|
1912
|
+
* `description`, which is what an OpenAPI viewer renders to a human.
|
|
1913
|
+
*/
|
|
1914
|
+
meta: import_zod.z.union([
|
|
1915
|
+
import_zod.z.boolean(),
|
|
1916
|
+
import_zod.z.object({ enabled: import_zod.z.boolean().optional(), description: import_zod.z.boolean().optional() }).strict()
|
|
1917
|
+
]).optional(),
|
|
1918
|
+
/**
|
|
1919
|
+
* TypeBox only. Give every emitted schema a `~standard` key, so it can be handed to a tRPC or
|
|
1920
|
+
* oRPC route.
|
|
1921
|
+
*
|
|
1922
|
+
* TypeBox is the one validator DRZL emits that carries none of its own: measured on 0.34.52, a
|
|
1923
|
+
* bare `Type.Object()` has no `~standard` and the package exports nothing matching
|
|
1924
|
+
* `/standard/i`. zod, valibot and arktype all put one on every schema they build, so the option
|
|
1925
|
+
* does nothing for them and is not passed through.
|
|
1926
|
+
*
|
|
1927
|
+
* The property is non-enumerable, so the schema stays a TypeBox schema in every respect that was
|
|
1928
|
+
* already observable, including the JSON Schema `JSON.stringify` produces.
|
|
1929
|
+
*/
|
|
1930
|
+
standardSchema: import_zod.z.boolean().optional(),
|
|
1268
1931
|
/**
|
|
1269
1932
|
* Emit `NestedInsert<Table>` and `NestedSelect<Table>` beside the flat schemas: the table plus
|
|
1270
1933
|
* one key per relation, so `{ ...user, posts: [...] }` can be validated whole.
|
|
@@ -1281,6 +1944,29 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
1281
1944
|
* it is also what terminates a cycle: `users -> posts -> users` stops here.
|
|
1282
1945
|
*/
|
|
1283
1946
|
nestedDepth: import_zod.z.number().int().optional(),
|
|
1947
|
+
/**
|
|
1948
|
+
* Give every primary key, and every foreign key pointing at one, a nominal type, so a
|
|
1949
|
+
* `users.id` cannot be passed where a `posts.id` is wanted.
|
|
1950
|
+
*
|
|
1951
|
+
* Type level only, in all five validators. Measured on zod 4.4.3, `.brand()` returns the same
|
|
1952
|
+
* schema object it was called on and the parsed value of `1` is `1`, so nothing about what a
|
|
1953
|
+
* schema accepts changes and no bytes are added to the bundle. TypeBox has no brand of its own
|
|
1954
|
+
* and gets a `TUnsafe` cast, which leaves the schema object identical.
|
|
1955
|
+
*
|
|
1956
|
+
* Off by default: it changes the inferred type of every consumer of the select schemas, which
|
|
1957
|
+
* is the point, but it is a change to existing call sites rather than an addition.
|
|
1958
|
+
*
|
|
1959
|
+
* `true` is the shorthand for `{ enabled: true }`. `{ foreignKeys: false }` brands only the
|
|
1960
|
+
* keys themselves, and `{ aliases: false }` stops the `export type UsersId = ...` lines.
|
|
1961
|
+
*/
|
|
1962
|
+
branded: import_zod.z.union([
|
|
1963
|
+
import_zod.z.boolean(),
|
|
1964
|
+
import_zod.z.object({
|
|
1965
|
+
enabled: import_zod.z.boolean().optional(),
|
|
1966
|
+
foreignKeys: import_zod.z.boolean().optional(),
|
|
1967
|
+
aliases: import_zod.z.boolean().optional()
|
|
1968
|
+
}).strict()
|
|
1969
|
+
]).optional(),
|
|
1284
1970
|
naming: NamingSchema.optional(),
|
|
1285
1971
|
outputHeader: import_zod.z.object({
|
|
1286
1972
|
enabled: import_zod.z.boolean().default(true).optional(),
|
|
@@ -1375,6 +2061,10 @@ var GeneratorSchema = import_zod.z.object({
|
|
|
1375
2061
|
// template options
|
|
1376
2062
|
templateOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
|
|
1377
2063
|
});
|
|
2064
|
+
var ColumnRulesSchema = import_zod.z.object({
|
|
2065
|
+
omit: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2066
|
+
pick: import_zod.z.array(import_zod.z.string()).optional()
|
|
2067
|
+
}).strict();
|
|
1378
2068
|
var AnalyzerSchema = import_zod.z.object({
|
|
1379
2069
|
includeRelations: import_zod.z.boolean().default(true),
|
|
1380
2070
|
validateConstraints: import_zod.z.boolean().default(true),
|
|
@@ -1401,6 +2091,37 @@ var ConfigSchema = import_zod.z.object({
|
|
|
1401
2091
|
*/
|
|
1402
2092
|
include: import_zod.z.array(import_zod.z.string()).optional(),
|
|
1403
2093
|
exclude: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2094
|
+
/**
|
|
2095
|
+
* Which columns of those tables to generate for, keyed by table.
|
|
2096
|
+
*
|
|
2097
|
+
* `include`/`exclude` is all or nothing per table, and the column that should not be in a
|
|
2098
|
+
* generated schema is usually sitting in a table you do want: `passwordHash` on `users`, an
|
|
2099
|
+
* internal note beside the public fields, a `tenantId` the server sets from the session and a
|
|
2100
|
+
* request body must not carry. Editing the emitted file is not an answer, because the next
|
|
2101
|
+
* `drzl generate` overwrites it.
|
|
2102
|
+
*
|
|
2103
|
+
* ```ts
|
|
2104
|
+
* columns: {
|
|
2105
|
+
* users: { omit: ['passwordHash'] },
|
|
2106
|
+
* 'app_*': { omit: ['deleted_at'] },
|
|
2107
|
+
* }
|
|
2108
|
+
* ```
|
|
2109
|
+
*
|
|
2110
|
+
* The key is a table pattern in the same language `include`/`exclude` uses: the database table
|
|
2111
|
+
* name, anchored, with `*` as the only metacharacter. Column patterns are the same language
|
|
2112
|
+
* again. Every matching entry applies, in the order written; within one entry `pick` narrows
|
|
2113
|
+
* first and `omit` then removes, so `omit` wins, exactly as `exclude` wins over `include`.
|
|
2114
|
+
*
|
|
2115
|
+
* Applies to every mode and every generator at once, because it narrows the analysis rather
|
|
2116
|
+
* than any one generator's output. A column cannot be kept in `select` and dropped from
|
|
2117
|
+
* `insert`: see the docs for why that form was not taken on.
|
|
2118
|
+
*
|
|
2119
|
+
* A pattern that matches nothing is an error rather than a no-op, because a typo in `omit`
|
|
2120
|
+
* that silently does nothing leaves the column exactly where it was while reading like a fix.
|
|
2121
|
+
* Dropping a primary key column is an error too; dropping a NOT NULL column with no default is
|
|
2122
|
+
* a warning.
|
|
2123
|
+
*/
|
|
2124
|
+
columns: import_zod.z.record(import_zod.z.string(), ColumnRulesSchema).optional(),
|
|
1404
2125
|
/**
|
|
1405
2126
|
* How every relative specifier drzl invents spells its extension, for every generator.
|
|
1406
2127
|
* A generator may override it. Defaults to `js`, which is the only form that resolves
|
|
@@ -1563,6 +2284,7 @@ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
|
|
|
1563
2284
|
if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
|
|
1564
2285
|
if (g.kind === "arktype") dirs.add(abs(g.path ?? "src/validators/arktype"));
|
|
1565
2286
|
if (g.kind === "typebox") dirs.add(abs(g.path ?? "src/validators/typebox"));
|
|
2287
|
+
if (g.kind === "effect") dirs.add(abs(g.path ?? "src/validators/effect"));
|
|
1566
2288
|
if (g.kind === "json-schema") dirs.add(abs(g.path ?? "src/validators/json-schema"));
|
|
1567
2289
|
}
|
|
1568
2290
|
return [...dirs];
|
|
@@ -1593,15 +2315,17 @@ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
|
|
|
1593
2315
|
return Array.from(new Set(results));
|
|
1594
2316
|
}
|
|
1595
2317
|
function filterTables(tables, opts) {
|
|
1596
|
-
const toRegExp = (pattern) => new RegExp(
|
|
1597
|
-
"^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
1598
|
-
);
|
|
1599
|
-
const matches = (patterns, name) => patterns.some((p) => toRegExp(p).test(name));
|
|
1600
2318
|
let out = tables;
|
|
1601
|
-
if (opts.include?.length) out = out.filter((t) =>
|
|
1602
|
-
if (opts.exclude?.length) out = out.filter((t) => !
|
|
2319
|
+
if (opts.include?.length) out = out.filter((t) => matchesTable(opts.include, t));
|
|
2320
|
+
if (opts.exclude?.length) out = out.filter((t) => !matchesTable(opts.exclude, t));
|
|
1603
2321
|
return out;
|
|
1604
2322
|
}
|
|
2323
|
+
function tableFilterWarnings(tables, opts) {
|
|
2324
|
+
return [
|
|
2325
|
+
...ambiguousPatternWarnings(opts.include ?? [], tables, "include"),
|
|
2326
|
+
...ambiguousPatternWarnings(opts.exclude ?? [], tables, "exclude")
|
|
2327
|
+
];
|
|
2328
|
+
}
|
|
1605
2329
|
function computeWatchTargets(cfg, cwd = process.cwd()) {
|
|
1606
2330
|
const abs = (p) => path.resolve(cwd, p);
|
|
1607
2331
|
const schemaAbs = abs(cfg.schema);
|
|
@@ -1635,6 +2359,9 @@ function trpcOptions(g, cfg, servicesDir) {
|
|
|
1635
2359
|
};
|
|
1636
2360
|
}
|
|
1637
2361
|
|
|
2362
|
+
// src/column-filter.ts
|
|
2363
|
+
var import_validation_core3 = require("@drzl/validation-core");
|
|
2364
|
+
|
|
1638
2365
|
// src/doctor.ts
|
|
1639
2366
|
var import_validation_core2 = require("@drzl/validation-core");
|
|
1640
2367
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
@@ -1893,6 +2620,99 @@ function renderDoctorReport(report) {
|
|
|
1893
2620
|
return out.join("\n");
|
|
1894
2621
|
}
|
|
1895
2622
|
|
|
2623
|
+
// src/column-filter.ts
|
|
2624
|
+
function checkedColumns(expression, name) {
|
|
2625
|
+
const parsed = (0, import_validation_core3.parseCheck)(expression, name);
|
|
2626
|
+
if (!parsed.ok) return [];
|
|
2627
|
+
return [...new Set(namedColumns(parsed).map((n) => n.column))];
|
|
2628
|
+
}
|
|
2629
|
+
function filterColumns(tables, spec) {
|
|
2630
|
+
const entries = Object.entries(spec ?? {});
|
|
2631
|
+
if (!entries.length) return { tables, warnings: [] };
|
|
2632
|
+
const errors = [];
|
|
2633
|
+
const warnings = [];
|
|
2634
|
+
const nameForConfig = hasNamedSchemas(tables) ? addressableName : displayTableName;
|
|
2635
|
+
for (const [tablePattern, rules] of entries) {
|
|
2636
|
+
const matched = tables.filter((t) => matchesTable([tablePattern], t));
|
|
2637
|
+
if (!matched.length) {
|
|
2638
|
+
errors.push(
|
|
2639
|
+
`columns[${JSON.stringify(tablePattern)}] matches no table. The schema declares: ${tables.map(nameForConfig).join(", ") || "(no tables)"}.`
|
|
2640
|
+
);
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
const available = [...new Set(matched.flatMap((t) => t.columns.map((c) => c.name)))];
|
|
2644
|
+
for (const which of ["pick", "omit"]) {
|
|
2645
|
+
for (const pattern of rules[which] ?? []) {
|
|
2646
|
+
if (available.some((name) => matchesAny([pattern], name))) continue;
|
|
2647
|
+
errors.push(
|
|
2648
|
+
`columns[${JSON.stringify(tablePattern)}].${which} names ${JSON.stringify(pattern)}, which matches no column of ${matched.map(nameForConfig).join(", ")}. Available: ${available.join(", ")}.`
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
warnings.push(
|
|
2654
|
+
...ambiguousPatternWarnings(
|
|
2655
|
+
entries.map(([p]) => p),
|
|
2656
|
+
tables,
|
|
2657
|
+
"columns"
|
|
2658
|
+
)
|
|
2659
|
+
);
|
|
2660
|
+
const out = tables.map((table) => {
|
|
2661
|
+
const mine = entries.filter(([pattern]) => matchesTable([pattern], table));
|
|
2662
|
+
if (!mine.length) return table;
|
|
2663
|
+
let keep = table.columns;
|
|
2664
|
+
for (const [, rules] of mine) {
|
|
2665
|
+
if (rules.pick?.length) keep = keep.filter((c) => matchesAny(rules.pick, c.name));
|
|
2666
|
+
if (rules.omit?.length) keep = keep.filter((c) => !matchesAny(rules.omit, c.name));
|
|
2667
|
+
}
|
|
2668
|
+
if (keep.length === table.columns.length) return table;
|
|
2669
|
+
const kept = new Set(keep.map((c) => c.name));
|
|
2670
|
+
const dropped = table.columns.filter((c) => !kept.has(c.name));
|
|
2671
|
+
if (!keep.length) {
|
|
2672
|
+
errors.push(
|
|
2673
|
+
`columns leaves table "${displayTableName(table)}" with no columns at all. An empty schema describes no row, so this is never a narrower API. Exclude the table instead, with the top-level "exclude" option.`
|
|
2674
|
+
);
|
|
2675
|
+
return table;
|
|
2676
|
+
}
|
|
2677
|
+
const lostKey = (table.primaryKey?.columns ?? []).filter((n) => !kept.has(n));
|
|
2678
|
+
if (lostKey.length) {
|
|
2679
|
+
errors.push(
|
|
2680
|
+
`columns drops ${lostKey.map((n) => JSON.stringify(n)).join(", ")} from table "${displayTableName(table)}", which is part of its primary key (${table.primaryKey?.columns.join(", ")}). The generated getById, update and delete address rows by that key, so the emitted schemas would describe a row nothing can address. Keep the key, or leave the whole table out with the top-level "exclude" option.`
|
|
2681
|
+
);
|
|
2682
|
+
return table;
|
|
2683
|
+
}
|
|
2684
|
+
for (const c of dropped) {
|
|
2685
|
+
if (c.nullable || c.hasDefault || c.isGenerated || table.readOnly) continue;
|
|
2686
|
+
warnings.push(
|
|
2687
|
+
`drzl config: the "columns" option drops "${c.name}" from table "${displayTableName(table)}", and the database requires it: NOT NULL with no default. The emitted insert schema therefore describes a payload that is not a complete row, so whatever calls db.insert has to supply "${c.name}" itself.`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
for (const k of table.checks ?? []) {
|
|
2691
|
+
const lost = checkedColumns(k.expression, k.name).filter(
|
|
2692
|
+
(n) => !kept.has(n) && table.columns.some((c) => c.name === n)
|
|
2693
|
+
);
|
|
2694
|
+
if (!lost.length) continue;
|
|
2695
|
+
warnings.push(
|
|
2696
|
+
`drzl config: CHECK ${k.name ? `"${k.name}"` : "(unnamed)"} on table "${displayTableName(table)}" names ${lost.map((n) => JSON.stringify(n)).join(", ")}, which the "columns" option drops, so nothing DRZL emits enforces it. Your database still does.`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
return {
|
|
2700
|
+
...table,
|
|
2701
|
+
columns: keep,
|
|
2702
|
+
unique: (table.unique ?? []).filter((k) => k.columns.every((n) => kept.has(n))),
|
|
2703
|
+
indexes: (table.indexes ?? []).filter((i) => i.columns.every((n) => kept.has(n))),
|
|
2704
|
+
...table.foreignKeys ? { foreignKeys: table.foreignKeys.filter((f) => f.columns.every((n) => kept.has(n))) } : {}
|
|
2705
|
+
};
|
|
2706
|
+
});
|
|
2707
|
+
if (errors.length) {
|
|
2708
|
+
throw new Error(
|
|
2709
|
+
`drzl config: the "columns" option cannot be honoured.
|
|
2710
|
+
` + errors.map((e) => ` - ${e}`).join("\n")
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
return { tables: out, warnings };
|
|
2714
|
+
}
|
|
2715
|
+
|
|
1896
2716
|
// src/drift.ts
|
|
1897
2717
|
var import_node_fs = require("fs");
|
|
1898
2718
|
var import_node_path = __toESM(require("path"), 1);
|
|
@@ -1979,7 +2799,7 @@ var shownThisProcess = false;
|
|
|
1979
2799
|
var tips = [
|
|
1980
2800
|
"Pair DRZL watch mode with drizzle-kit to keep schema & API synced.",
|
|
1981
2801
|
"Templatize your ORPC routers to roll out new endpoints safely.",
|
|
1982
|
-
"Need typed validators? Enable the zod, valibot, arktype, or
|
|
2802
|
+
"Need typed validators? Enable the zod, valibot, arktype, typebox, or effect generators.",
|
|
1983
2803
|
"Need JSON Schema or OpenAPI? The json-schema generator emits both, with no runtime dependency.",
|
|
1984
2804
|
"Use output headers to track generated files and trim noisy diffs."
|
|
1985
2805
|
];
|
|
@@ -2093,7 +2913,7 @@ Need a template, adapter, or generator DRZL doesn't ship yet?
|
|
|
2093
2913
|
);
|
|
2094
2914
|
program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").option("--relations", "include relations", true).option("--validate", "validate constraints", true).option("--out <file>", "write analysis JSON to file").option("--json", "print JSON to stdout (overrides --out)", false).action(async (schema, opts) => {
|
|
2095
2915
|
try {
|
|
2096
|
-
const analyzer = new
|
|
2916
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(schema);
|
|
2097
2917
|
const spinner = !opts.json ? (0, import_ora.default)("Analyzing schema...").start() : null;
|
|
2098
2918
|
const start = Date.now();
|
|
2099
2919
|
const res = await analyzer.analyze({
|
|
@@ -2141,7 +2961,7 @@ program.command("doctor").description("Report what DRZL cannot type or enforce i
|
|
|
2141
2961
|
process.exit(1);
|
|
2142
2962
|
return;
|
|
2143
2963
|
}
|
|
2144
|
-
const analyzer = new
|
|
2964
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(target);
|
|
2145
2965
|
const analysis = await analyzer.analyze({
|
|
2146
2966
|
includeRelations: true,
|
|
2147
2967
|
validateConstraints: true
|
|
@@ -2180,7 +3000,7 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
2180
3000
|
process.exit(2);
|
|
2181
3001
|
return;
|
|
2182
3002
|
}
|
|
2183
|
-
const analyzer = new
|
|
3003
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(cfg.schema);
|
|
2184
3004
|
const spinner = (0, import_ora.default)("Analyzing...").start();
|
|
2185
3005
|
const t0 = Date.now();
|
|
2186
3006
|
const analysis = await analyzer.analyze({
|
|
@@ -2188,8 +3008,11 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
2188
3008
|
validateConstraints: cfg.analyzer.validateConstraints,
|
|
2189
3009
|
includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
|
|
2190
3010
|
});
|
|
2191
|
-
analysis.tables = filterTables(analysis.tables, cfg);
|
|
2192
3011
|
spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
|
|
3012
|
+
const narrowed = filterColumns(analysis.tables, cfg.columns);
|
|
3013
|
+
const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
|
|
3014
|
+
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
3015
|
+
for (const w of [...narrowed.warnings, ...filterWarnings]) console.warn(import_chalk3.default.yellow(w));
|
|
2193
3016
|
reportWideColumns(analysis.issues);
|
|
2194
3017
|
const driftDirs = computeGeneratorOutputDirs(cfg);
|
|
2195
3018
|
const driftBefore = opts.check ? await snapshotAll(driftDirs) : null;
|
|
@@ -2280,7 +3103,7 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
2280
3103
|
const gen = new ZodGenerator(analysis);
|
|
2281
3104
|
const target = g.path ?? "src/validators/zod";
|
|
2282
3105
|
const files = await gen.generate(
|
|
2283
|
-
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
3106
|
+
validationOptions(g, cfg, target, { schemaTypes: true, meta: true })
|
|
2284
3107
|
);
|
|
2285
3108
|
progress.stop();
|
|
2286
3109
|
(0, import_ora.default)().succeed(import_chalk3.default.green(`Generated (zod): ${files.length} files`));
|
|
@@ -2354,7 +3177,10 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
2354
3177
|
const gen = new TypeBoxGenerator(analysis);
|
|
2355
3178
|
const target = g.path ?? "src/validators/typebox";
|
|
2356
3179
|
const files = await gen.generate(
|
|
2357
|
-
validationOptions(g, cfg, target, {
|
|
3180
|
+
validationOptions(g, cfg, target, {
|
|
3181
|
+
schemaTypes: true,
|
|
3182
|
+
standardSchema: true
|
|
3183
|
+
})
|
|
2358
3184
|
);
|
|
2359
3185
|
progress.stop();
|
|
2360
3186
|
(0, import_ora.default)().succeed(import_chalk3.default.green(`Generated (typebox): ${files.length} files`));
|
|
@@ -2364,6 +3190,25 @@ program.command("generate").description("Run configured generators (drzl.config.
|
|
|
2364
3190
|
reportGeneratorFailure(g.kind, e);
|
|
2365
3191
|
process.exit(1);
|
|
2366
3192
|
}
|
|
3193
|
+
} else if (g.kind === "effect") {
|
|
3194
|
+
try {
|
|
3195
|
+
const { EffectGenerator: EffectGenerator2 } = await loadGenerator(
|
|
3196
|
+
"@drzl/generator-effect",
|
|
3197
|
+
() => Promise.resolve().then(() => (init_dist3(), dist_exports3))
|
|
3198
|
+
);
|
|
3199
|
+
const gen = new EffectGenerator2(analysis);
|
|
3200
|
+
const target = g.path ?? "src/validators/effect";
|
|
3201
|
+
const files = await gen.generate(
|
|
3202
|
+
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
3203
|
+
);
|
|
3204
|
+
progress.stop();
|
|
3205
|
+
(0, import_ora.default)().succeed(import_chalk3.default.green(`Generated (effect): ${files.length} files`));
|
|
3206
|
+
files.forEach((f) => console.log(" -", import_chalk3.default.cyan(f)));
|
|
3207
|
+
} catch (e) {
|
|
3208
|
+
progress.stop();
|
|
3209
|
+
reportGeneratorFailure(g.kind, e);
|
|
3210
|
+
process.exit(1);
|
|
3211
|
+
}
|
|
2367
3212
|
}
|
|
2368
3213
|
}
|
|
2369
3214
|
if (driftBefore) {
|
|
@@ -2403,7 +3248,7 @@ Generated output is out of date (${drift.length} file(s)):`));
|
|
|
2403
3248
|
});
|
|
2404
3249
|
program.command("generate:orpc").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "template name", "standard").option("--includeRelations", "include relation endpoints").action(async (schema, opts) => {
|
|
2405
3250
|
try {
|
|
2406
|
-
const analyzer = new
|
|
3251
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(schema);
|
|
2407
3252
|
const analysis = await analyzer.analyze({
|
|
2408
3253
|
includeRelations: !!opts.includeRelations,
|
|
2409
3254
|
validateConstraints: true
|
|
@@ -2423,7 +3268,7 @@ program.command("generate:orpc").argument("<schema>", "path to drizzle schema (T
|
|
|
2423
3268
|
});
|
|
2424
3269
|
program.command("generate:trpc").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "standard | service", "standard").option("--includeRelations", "include relation endpoints").option("--servicesDir <dir>", "where the service generator writes", "src/services").action(async (schema, opts) => {
|
|
2425
3270
|
try {
|
|
2426
|
-
const analyzer = new
|
|
3271
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(schema);
|
|
2427
3272
|
const analysis = await analyzer.analyze({
|
|
2428
3273
|
includeRelations: !!opts.includeRelations,
|
|
2429
3274
|
validateConstraints: true
|
|
@@ -2525,13 +3370,17 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2525
3370
|
})
|
|
2526
3371
|
);
|
|
2527
3372
|
}
|
|
2528
|
-
const analyzer = new
|
|
3373
|
+
const analyzer = new import_analyzer2.SchemaAnalyzer(cfg.schema);
|
|
2529
3374
|
const analysis = await analyzer.analyze({
|
|
2530
3375
|
includeRelations: cfg.analyzer.includeRelations,
|
|
2531
3376
|
validateConstraints: cfg.analyzer.validateConstraints,
|
|
2532
3377
|
includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
|
|
2533
3378
|
});
|
|
2534
|
-
|
|
3379
|
+
const narrowed = filterColumns(analysis.tables, cfg.columns);
|
|
3380
|
+
const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
|
|
3381
|
+
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
3382
|
+
if (!opts.json)
|
|
3383
|
+
for (const w of [...narrowed.warnings, ...filterWarnings]) console.warn(import_chalk3.default.yellow(w));
|
|
2535
3384
|
if (!opts.json) reportWideColumns(analysis.issues);
|
|
2536
3385
|
if (opts.pipeline === "analyze") {
|
|
2537
3386
|
if (opts.json) {
|
|
@@ -2630,7 +3479,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2630
3479
|
const gen = new ZodGenerator(analysis);
|
|
2631
3480
|
const target = g.path ?? "src/validators/zod";
|
|
2632
3481
|
const files = await gen.generate(
|
|
2633
|
-
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
3482
|
+
validationOptions(g, cfg, target, { schemaTypes: true, meta: true })
|
|
2634
3483
|
);
|
|
2635
3484
|
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2636
3485
|
import_chalk3.default.green(`Generated (zod): ${files.length} files`),
|
|
@@ -2690,7 +3539,10 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2690
3539
|
const gen = new TypeBoxGenerator(analysis);
|
|
2691
3540
|
const target = g.path ?? "src/validators/typebox";
|
|
2692
3541
|
const files = await gen.generate(
|
|
2693
|
-
validationOptions(g, cfg, target, {
|
|
3542
|
+
validationOptions(g, cfg, target, {
|
|
3543
|
+
schemaTypes: true,
|
|
3544
|
+
standardSchema: true
|
|
3545
|
+
})
|
|
2694
3546
|
);
|
|
2695
3547
|
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
2696
3548
|
import_chalk3.default.green(`Generated (typebox): ${files.length} files`),
|
|
@@ -2701,6 +3553,26 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
2701
3553
|
reportGeneratorFailure(g.kind, e);
|
|
2702
3554
|
return;
|
|
2703
3555
|
}
|
|
3556
|
+
} else if (g.kind === "effect") {
|
|
3557
|
+
try {
|
|
3558
|
+
const { EffectGenerator: EffectGenerator2 } = await loadGenerator(
|
|
3559
|
+
"@drzl/generator-effect",
|
|
3560
|
+
() => Promise.resolve().then(() => (init_dist3(), dist_exports3))
|
|
3561
|
+
);
|
|
3562
|
+
const gen = new EffectGenerator2(analysis);
|
|
3563
|
+
const target = g.path ?? "src/validators/effect";
|
|
3564
|
+
const files = await gen.generate(
|
|
3565
|
+
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
3566
|
+
);
|
|
3567
|
+
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
3568
|
+
import_chalk3.default.green(`Generated (effect): ${files.length} files`),
|
|
3569
|
+
files.map((f) => import_chalk3.default.cyan(f)).join(", ")
|
|
3570
|
+
);
|
|
3571
|
+
newFiles.push(...files);
|
|
3572
|
+
} catch (e) {
|
|
3573
|
+
reportGeneratorFailure(g.kind, e);
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
2704
3576
|
} else if (g.kind === "json-schema") {
|
|
2705
3577
|
try {
|
|
2706
3578
|
const { JsonSchemaGenerator: JsonSchemaGenerator2 } = await loadGenerator(
|