@drzl/cli 4.19.0 → 4.23.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/config.cjs CHANGED
@@ -33,17 +33,31 @@ var config_exports = {};
33
33
  __export(config_exports, {
34
34
  AffixSchema: () => AffixSchema,
35
35
  AnalyzerSchema: () => AnalyzerSchema,
36
+ CONFIG_FILE_NAMES: () => CONFIG_FILE_NAMES,
37
+ CONFIG_SCHEMA_ID: () => CONFIG_SCHEMA_ID,
38
+ ColumnRulesSchema: () => ColumnRulesSchema,
36
39
  ConfigSchema: () => ConfigSchema,
40
+ GENERATOR_KINDS: () => GENERATOR_KINDS,
41
+ GeneratorKindSchema: () => GeneratorKindSchema,
37
42
  GeneratorSchema: () => GeneratorSchema,
38
43
  ImportExtensionSchema: () => ImportExtensionSchema,
39
44
  NamingSchema: () => NamingSchema,
45
+ buildConfigJsonSchema: () => buildConfigJsonSchema,
40
46
  computeGeneratorOutputDirs: () => computeGeneratorOutputDirs,
41
47
  computeWatchTargets: () => computeWatchTargets,
48
+ configFromKinds: () => configFromKinds,
42
49
  defineConfig: () => defineConfig,
50
+ expressOutDir: () => expressOutDir,
51
+ fastifyOutDir: () => fastifyOutDir,
43
52
  filterTables: () => filterTables,
53
+ graphqlOutDir: () => graphqlOutDir,
54
+ honoOutDir: () => honoOutDir,
55
+ importFreshConfigModule: () => importFreshConfigModule,
44
56
  loadConfig: () => loadConfig,
57
+ nestjsOutDir: () => nestjsOutDir,
45
58
  resolveConfig: () => resolveConfig,
46
59
  resolveTemplateDirsSync: () => resolveTemplateDirsSync,
60
+ tableFilterWarnings: () => tableFilterWarnings,
47
61
  trpcOutDir: () => trpcOutDir
48
62
  });
49
63
  module.exports = __toCommonJS(config_exports);
@@ -52,17 +66,253 @@ var fs = __toESM(require("fs"), 1);
52
66
  var import_node_module = require("module");
53
67
  var path = __toESM(require("path"), 1);
54
68
  var import_zod = require("zod");
69
+
70
+ // src/config-errors.ts
71
+ function hasOwn(object, key) {
72
+ return Object.prototype.hasOwnProperty.call(object, key);
73
+ }
74
+ var CONFIG_INVALID_CODE = "DRZL_CFG_002";
75
+ var ConfigValidationError = class extends Error {
76
+ constructor(message) {
77
+ super(message);
78
+ this.code = CONFIG_INVALID_CODE;
79
+ this.name = "ConfigValidationError";
80
+ }
81
+ };
82
+ var MAX_LISTED_PROBLEMS = 8;
83
+ var MAX_VALUE_CHARS = 60;
84
+ function renderConfigPath(segments) {
85
+ if (!segments.length) return "(root)";
86
+ let out = "";
87
+ for (const segment of segments) {
88
+ if (typeof segment === "number") {
89
+ out += `[${segment}]`;
90
+ continue;
91
+ }
92
+ const key = String(segment);
93
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) out += out ? `.${key}` : key;
94
+ else out += `[${JSON.stringify(key)}]`;
95
+ }
96
+ return out;
97
+ }
98
+ function describeValue(value) {
99
+ if (value === void 0) return "undefined";
100
+ if (value === null) return "null";
101
+ switch (typeof value) {
102
+ case "boolean":
103
+ return String(value);
104
+ case "number":
105
+ return String(value);
106
+ case "bigint":
107
+ return `${value}n`;
108
+ case "function":
109
+ return "[function]";
110
+ case "symbol":
111
+ return String(value);
112
+ case "string":
113
+ return value.length <= MAX_VALUE_CHARS ? JSON.stringify(value) : `${JSON.stringify(value.slice(0, MAX_VALUE_CHARS))} ... (${value.length} characters)`;
114
+ }
115
+ let json;
116
+ try {
117
+ json = JSON.stringify(value);
118
+ } catch {
119
+ return null;
120
+ }
121
+ if (typeof json !== "string") return null;
122
+ return json.length <= MAX_VALUE_CHARS ? json : null;
123
+ }
124
+ function valueAt(raw, segments) {
125
+ let current = raw;
126
+ for (const segment of segments) {
127
+ if (current === null || typeof current !== "object") return { found: false, value: void 0 };
128
+ if (!hasOwn(current, String(segment))) return { found: false, value: void 0 };
129
+ current = current[segment];
130
+ }
131
+ return { found: true, value: current };
132
+ }
133
+ function editDistance(a, b) {
134
+ if (a === b) return 0;
135
+ if (!a.length) return b.length;
136
+ if (!b.length) return a.length;
137
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
138
+ for (let i = 1; i <= a.length; i++) {
139
+ const current = new Array(b.length + 1);
140
+ current[0] = i;
141
+ for (let j = 1; j <= b.length; j++) {
142
+ const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
143
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, substitution);
144
+ }
145
+ previous = current;
146
+ }
147
+ return previous[b.length];
148
+ }
149
+ function nearestKey(key, known) {
150
+ const budget = key.length >= 5 ? 2 : 1;
151
+ let best;
152
+ let bestDistance = Number.POSITIVE_INFINITY;
153
+ for (const candidate of known) {
154
+ if (candidate === key) return void 0;
155
+ const distance = editDistance(key, candidate);
156
+ if (distance <= budget && distance < bestDistance) {
157
+ best = candidate;
158
+ bestDistance = distance;
159
+ }
160
+ }
161
+ return best;
162
+ }
163
+ function objectNodeFor(node) {
164
+ if (!node || typeof node !== "object") return void 0;
165
+ const record = node;
166
+ const anyOf = record.anyOf;
167
+ if (Array.isArray(anyOf)) {
168
+ const branches = anyOf.filter(
169
+ (branch) => branch && typeof branch === "object" && (branch.properties !== void 0 || typeof branch.additionalProperties === "object")
170
+ );
171
+ return branches.length === 1 ? branches[0] : void 0;
172
+ }
173
+ if (record.properties !== void 0 || record.additionalProperties !== void 0) return record;
174
+ return void 0;
175
+ }
176
+ function unknownConfigKeys(raw, schema) {
177
+ const found = [];
178
+ walkForUnknownKeys(raw, schema, [], found);
179
+ return found;
180
+ }
181
+ function walkForUnknownKeys(value, node, segments, found) {
182
+ if (value === null || typeof value !== "object") return;
183
+ if (Array.isArray(value)) {
184
+ const items = node?.items;
185
+ if (!items) return;
186
+ value.forEach((entry, index) => walkForUnknownKeys(entry, items, [...segments, index], found));
187
+ return;
188
+ }
189
+ const object = objectNodeFor(node);
190
+ if (!object) return;
191
+ const properties = object.properties;
192
+ const additional = object.additionalProperties;
193
+ if (!properties) {
194
+ if (additional && typeof additional === "object") {
195
+ for (const [key, entry] of Object.entries(value)) {
196
+ walkForUnknownKeys(entry, additional, [...segments, key], found);
197
+ }
198
+ }
199
+ return;
200
+ }
201
+ const known = Object.keys(properties);
202
+ for (const [key, entry] of Object.entries(value)) {
203
+ if (hasOwn(properties, key)) {
204
+ walkForUnknownKeys(entry, properties[key], [...segments, key], found);
205
+ continue;
206
+ }
207
+ if (additional === false) continue;
208
+ found.push({ path: [...segments], key, suggestion: nearestKey(key, known) });
209
+ }
210
+ }
211
+ function unknownKeyWarnings(raw, schema) {
212
+ return unknownConfigKeys(raw, schema).map((unknown) => {
213
+ const where = unknown.path.length ? `in ${renderConfigPath(unknown.path)}` : "at the top level";
214
+ const suggestion = unknown.suggestion ? ` Did you mean "${unknown.suggestion}"?` : "";
215
+ return `drzl config: unknown key "${unknown.key}" ${where}; it is ignored.${suggestion}`;
216
+ });
217
+ }
218
+ function nodeAt(schema, segments) {
219
+ let node = schema;
220
+ for (const segment of segments) {
221
+ if (typeof segment === "number") {
222
+ node = node?.items;
223
+ continue;
224
+ }
225
+ const object = objectNodeFor(node);
226
+ if (!object) return void 0;
227
+ const properties = object.properties;
228
+ if (properties && hasOwn(properties, String(segment))) {
229
+ node = properties[String(segment)];
230
+ continue;
231
+ }
232
+ const additional = object.additionalProperties;
233
+ if (additional && typeof additional === "object") {
234
+ node = additional;
235
+ continue;
236
+ }
237
+ return void 0;
238
+ }
239
+ return objectNodeFor(node);
240
+ }
241
+ function knownKeysAt(schema, segments) {
242
+ const node = nodeAt(schema, segments);
243
+ const properties = node?.properties;
244
+ return properties ? Object.keys(properties) : [];
245
+ }
246
+ function renderIssue(issue, raw, schema) {
247
+ const segments = issue.path ?? [];
248
+ const where = renderConfigPath(segments);
249
+ if (issue.code === "unrecognized_keys" && issue.keys?.length) {
250
+ const known = knownKeysAt(schema, segments);
251
+ return issue.keys.map((key) => {
252
+ const suggestion = nearestKey(key, known);
253
+ return `${where}: unrecognized key "${key}".${suggestion ? ` Did you mean "${suggestion}"?` : ""}`;
254
+ });
255
+ }
256
+ const what = String(issue.message ?? "is not valid").replace(/^Invalid input:\s*/, "");
257
+ const { found, value } = valueAt(raw, segments);
258
+ const shown = found ? describeValue(value) : null;
259
+ return [`${where}: ${what}${shown === null ? "" : ` (found ${shown})`}`];
260
+ }
261
+ function formatConfigProblems(file, issues, raw, schema) {
262
+ const lines = issues.flatMap((issue) => renderIssue(issue, raw, schema));
263
+ const shown = lines.slice(0, MAX_LISTED_PROBLEMS);
264
+ const rest = lines.length - shown.length;
265
+ const header = `${file} is not valid (${CONFIG_INVALID_CODE}). ${lines.length} problem${lines.length === 1 ? "" : "s"}:`;
266
+ const body = shown.map((line) => ` - ${line}`);
267
+ if (rest > 0) body.push(` ... and ${rest} more`);
268
+ return [header, ...body].join("\n");
269
+ }
270
+
271
+ // src/patterns.ts
272
+ function patternToRegExp(pattern) {
273
+ return new RegExp(
274
+ "^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
275
+ );
276
+ }
277
+ function matchesAny(patterns, name) {
278
+ return patterns.some((p) => patternToRegExp(p).test(name));
279
+ }
280
+ var DEFAULT_SCHEMA_ALIAS = "public";
281
+ function tableAliases(table) {
282
+ return [table.name, `${table.schema ?? DEFAULT_SCHEMA_ALIAS}.${table.name}`];
283
+ }
284
+ function matchesTable(patterns, table) {
285
+ return tableAliases(table).some((alias) => matchesAny(patterns, alias));
286
+ }
287
+ function addressableName(table) {
288
+ return `${table.schema ?? DEFAULT_SCHEMA_ALIAS}.${table.name}`;
289
+ }
290
+ function ambiguousPatternWarnings(patterns, tables, option) {
291
+ const out = [];
292
+ for (const pattern of patterns) {
293
+ if (pattern.includes(".")) continue;
294
+ const matched = tables.filter((t) => matchesTable([pattern], t));
295
+ const schemas = new Set(matched.map((t) => t.schema ?? DEFAULT_SCHEMA_ALIAS));
296
+ if (schemas.size < 2) continue;
297
+ out.push(
298
+ `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]))}.`
299
+ );
300
+ }
301
+ return out;
302
+ }
303
+
304
+ // src/config.ts
55
305
  var NamingSchema = import_zod.z.object({
56
306
  routerSuffix: import_zod.z.string().default("Router"),
57
307
  procedureCase: import_zod.z.enum(["camel", "kebab", "snake"]).default("camel")
58
308
  }).partial();
59
- var AffixValueSchema = import_zod.z.union(
309
+ var affixValueSchema = (pattern) => import_zod.z.union(
60
310
  [
61
- import_zod.z.string(),
311
+ import_zod.z.string().meta({ pattern }),
62
312
  import_zod.z.object({
63
- insert: import_zod.z.string().optional(),
64
- update: import_zod.z.string().optional(),
65
- select: import_zod.z.string().optional()
313
+ insert: import_zod.z.string().meta({ pattern }).optional(),
314
+ update: import_zod.z.string().meta({ pattern }).optional(),
315
+ select: import_zod.z.string().meta({ pattern }).optional()
66
316
  }).strict()
67
317
  ],
68
318
  {
@@ -70,8 +320,8 @@ var AffixValueSchema = import_zod.z.union(
70
320
  }
71
321
  );
72
322
  var AffixPartSchema = import_zod.z.object({
73
- prefix: AffixValueSchema.optional(),
74
- suffix: AffixValueSchema.optional()
323
+ prefix: affixValueSchema(import_validation_core.AFFIX_PREFIX_PATTERN).optional(),
324
+ suffix: affixValueSchema(import_validation_core.AFFIX_SUFFIX_PATTERN).optional()
75
325
  }).strict();
76
326
  var AffixSchema = import_zod.z.object({
77
327
  /**
@@ -84,18 +334,34 @@ var AffixSchema = import_zod.z.object({
84
334
  type: AffixPartSchema.optional()
85
335
  }).strict();
86
336
  var ImportExtensionSchema = import_zod.z.enum(import_validation_core.IMPORT_EXTENSIONS);
337
+ var GeneratorKindSchema = import_zod.z.enum([
338
+ "orpc",
339
+ "trpc",
340
+ "hono",
341
+ "express",
342
+ "fastify",
343
+ "nestjs",
344
+ "graphql",
345
+ "service",
346
+ "zod",
347
+ "valibot",
348
+ "arktype",
349
+ "typebox",
350
+ "effect",
351
+ "json-schema"
352
+ ]);
353
+ var GENERATOR_KINDS = GeneratorKindSchema.options;
87
354
  var GeneratorSchema = import_zod.z.object({
88
- kind: import_zod.z.enum([
89
- "orpc",
90
- "trpc",
91
- "service",
92
- "zod",
93
- "valibot",
94
- "arktype",
95
- "typebox",
96
- "effect",
97
- "json-schema"
98
- ]),
355
+ kind: GeneratorKindSchema,
356
+ /**
357
+ * Which of Hono's two official validator middlewares the emitted routes carry, and therefore
358
+ * which package they import. `hono` only.
359
+ *
360
+ * `standard` is `sValidator` from `@hono/standard-validator`, which takes any Standard Schema
361
+ * and so works with every library `validation.library` can name. `zod` is `zValidator` from
362
+ * `@hono/zod-validator`, which is zod-specific.
363
+ */
364
+ validator: import_zod.z.enum(["standard", "zod"]).optional(),
99
365
  /**
100
366
  * Overrides the top-level `importExtension` for this generator alone, for a project whose
101
367
  * generated directories are compiled by different tsconfigs.
@@ -103,6 +369,17 @@ var GeneratorSchema = import_zod.z.object({
103
369
  importExtension: ImportExtensionSchema.optional(),
104
370
  template: import_zod.z.string().optional(),
105
371
  includeRelations: import_zod.z.boolean().optional(),
372
+ /**
373
+ * Write an enum used by two or more columns once under `$defs` in the `json-schema` per-table
374
+ * modules, and `$ref` it at each use.
375
+ *
376
+ * Off by default, and the reason is a consumer pattern rather than a doubt about the keyword. A
377
+ * per-table schema is used whole and one property at a time, and a `$ref` cannot survive being
378
+ * pulled out with its property: `properties[col]` compiled on its own is a dangling reference
379
+ * that ajv refuses outright. The OpenAPI document shares regardless, because a document is only
380
+ * ever read whole.
381
+ */
382
+ sharedEnums: import_zod.z.boolean().optional(),
106
383
  /**
107
384
  * Type `json` and `jsonb` columns from the schema rather than leaving them wide.
108
385
  *
@@ -131,6 +408,57 @@ var GeneratorSchema = import_zod.z.object({
131
408
  * fact about the table rather than the row. This checks the half that needs no database.
132
409
  */
133
410
  duplicateFinder: import_zod.z.boolean().optional(),
411
+ /**
412
+ * zod and valibot. Also emit `constraints.ts`: every CHECK, unique constraint, primary and
413
+ * foreign key on each table as plain data, plus `constraintForIssue`, which maps a validation
414
+ * issue back to the constraint that caused it.
415
+ *
416
+ * For building forms. A schema states what a value must look like and never says which
417
+ * constraint said so, so a failed parse hands a form a message and no way to attribute it; and
418
+ * uniqueness and foreign keys, the two constraints no per-row schema can check, are absent from
419
+ * the emitted schemas in every form.
420
+ *
421
+ * Not `meta` written to a second file. `meta` describes a *field* and travels with the schema
422
+ * into `z.toJSONSchema`; this describes the table's *constraints*, carries their names, states
423
+ * each operand as data rather than inside a sentence, and is read without holding a schema.
424
+ *
425
+ * `true` is the shorthand for `{ enabled: true }`. `{ errorMap: false }` emits the data alone,
426
+ * without the matcher.
427
+ */
428
+ constraints: import_zod.z.union([
429
+ import_zod.z.boolean(),
430
+ import_zod.z.object({ enabled: import_zod.z.boolean().optional(), errorMap: import_zod.z.boolean().optional() }).strict()
431
+ ]).optional(),
432
+ /**
433
+ * zod only. Attach the facts the analyzer knows and a zod schema cannot state, as `.meta()` on
434
+ * every field and every table schema: the declared SQL type, the primary key, the unique
435
+ * constraints, whether the database generates or defaults the value, and the CHECK constraints,
436
+ * including the ones DRZL declined to enforce.
437
+ *
438
+ * `z.toJSONSchema` copies these through, so they are also how an OpenAPI document built from the
439
+ * emitted schemas gets the declared width back: DRZL enforces one as a `.refine()`, and
440
+ * `toJSONSchema` drops every refinement in silence.
441
+ *
442
+ * `true` is the shorthand for `{ enabled: true }`. `{ description: true }` additionally writes a
443
+ * `description`, which is what an OpenAPI viewer renders to a human.
444
+ */
445
+ meta: import_zod.z.union([
446
+ import_zod.z.boolean(),
447
+ import_zod.z.object({ enabled: import_zod.z.boolean().optional(), description: import_zod.z.boolean().optional() }).strict()
448
+ ]).optional(),
449
+ /**
450
+ * TypeBox only. Give every emitted schema a `~standard` key, so it can be handed to a tRPC or
451
+ * oRPC route.
452
+ *
453
+ * TypeBox is the one validator DRZL emits that carries none of its own: measured on 0.34.52, a
454
+ * bare `Type.Object()` has no `~standard` and the package exports nothing matching
455
+ * `/standard/i`. zod, valibot and arktype all put one on every schema they build, so the option
456
+ * does nothing for them and is not passed through.
457
+ *
458
+ * The property is non-enumerable, so the schema stays a TypeBox schema in every respect that was
459
+ * already observable, including the JSON Schema `JSON.stringify` produces.
460
+ */
461
+ standardSchema: import_zod.z.boolean().optional(),
134
462
  /**
135
463
  * Emit `NestedInsert<Table>` and `NestedSelect<Table>` beside the flat schemas: the table plus
136
464
  * one key per relation, so `{ ...user, posts: [...] }` can be validated whole.
@@ -147,6 +475,29 @@ var GeneratorSchema = import_zod.z.object({
147
475
  * it is also what terminates a cycle: `users -> posts -> users` stops here.
148
476
  */
149
477
  nestedDepth: import_zod.z.number().int().optional(),
478
+ /**
479
+ * Give every primary key, and every foreign key pointing at one, a nominal type, so a
480
+ * `users.id` cannot be passed where a `posts.id` is wanted.
481
+ *
482
+ * Type level only, in all five validators. Measured on zod 4.4.3, `.brand()` returns the same
483
+ * schema object it was called on and the parsed value of `1` is `1`, so nothing about what a
484
+ * schema accepts changes and no bytes are added to the bundle. TypeBox has no brand of its own
485
+ * and gets a `TUnsafe` cast, which leaves the schema object identical.
486
+ *
487
+ * Off by default: it changes the inferred type of every consumer of the select schemas, which
488
+ * is the point, but it is a change to existing call sites rather than an addition.
489
+ *
490
+ * `true` is the shorthand for `{ enabled: true }`. `{ foreignKeys: false }` brands only the
491
+ * keys themselves, and `{ aliases: false }` stops the `export type UsersId = ...` lines.
492
+ */
493
+ branded: import_zod.z.union([
494
+ import_zod.z.boolean(),
495
+ import_zod.z.object({
496
+ enabled: import_zod.z.boolean().optional(),
497
+ foreignKeys: import_zod.z.boolean().optional(),
498
+ aliases: import_zod.z.boolean().optional()
499
+ }).strict()
500
+ ]).optional(),
150
501
  naming: NamingSchema.optional(),
151
502
  outputHeader: import_zod.z.object({
152
503
  enabled: import_zod.z.boolean().default(true).optional(),
@@ -241,13 +592,41 @@ var GeneratorSchema = import_zod.z.object({
241
592
  // template options
242
593
  templateOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
243
594
  });
595
+ var ColumnRulesSchema = import_zod.z.object({
596
+ omit: import_zod.z.array(import_zod.z.string()).optional(),
597
+ pick: import_zod.z.array(import_zod.z.string()).optional()
598
+ }).strict();
244
599
  var AnalyzerSchema = import_zod.z.object({
245
600
  includeRelations: import_zod.z.boolean().default(true),
246
601
  validateConstraints: import_zod.z.boolean().default(true),
247
602
  includeHeuristicRelations: import_zod.z.boolean().default(false)
248
603
  });
249
604
  var ConfigSchema = import_zod.z.object({
250
- schema: import_zod.z.string(),
605
+ /**
606
+ * Path to the Drizzle schema module. Optional since drizzle-kit interop: a project that
607
+ * already names its schema in `drizzle.config.ts` should not have to say it twice, so an
608
+ * omitted `schema` falls back to reading the drizzle-kit config (see `drizzleKit`). The
609
+ * "neither file names a schema" error is raised at resolution time, where it can name both
610
+ * files, rather than here, where "Required" could name only this one.
611
+ */
612
+ schema: import_zod.z.string().optional(),
613
+ /**
614
+ * Read the schema path from drizzle-kit's own config instead of `schema`.
615
+ *
616
+ * `true` reads `drizzle.config.ts`, then `.js`, then `.json`, the same candidates in the
617
+ * same order drizzle-kit's CLI uses (measured on drizzle-kit 0.31.10). A string reads that
618
+ * file, wherever it is, like kit's own `--config` flag. `false` disables the fallback, so
619
+ * an omitted `schema` is an error even beside a drizzle.config. Unset behaves like `true`
620
+ * whenever `schema` is omitted, and does nothing when `schema` is set: `schema` always
621
+ * wins, with a warning when both are stated.
622
+ *
623
+ * Only kit's `schema` (string or array, entries may be globs) and `dialect` (cross-checked
624
+ * against what the analyzer detects) are read. Everything else in that file describes
625
+ * migrations and database credentials, which DRZL has no use for.
626
+ */
627
+ drizzleKit: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()], {
628
+ error: "Expected true (read drizzle.config.ts/.js/.json, the same candidates drizzle-kit uses), false (never read one), or a path to the drizzle-kit config file."
629
+ }).optional(),
251
630
  outDir: import_zod.z.string().default("src/api"),
252
631
  /**
253
632
  * Which tables to generate for, matched against the database table name.
@@ -267,6 +646,37 @@ var ConfigSchema = import_zod.z.object({
267
646
  */
268
647
  include: import_zod.z.array(import_zod.z.string()).optional(),
269
648
  exclude: import_zod.z.array(import_zod.z.string()).optional(),
649
+ /**
650
+ * Which columns of those tables to generate for, keyed by table.
651
+ *
652
+ * `include`/`exclude` is all or nothing per table, and the column that should not be in a
653
+ * generated schema is usually sitting in a table you do want: `passwordHash` on `users`, an
654
+ * internal note beside the public fields, a `tenantId` the server sets from the session and a
655
+ * request body must not carry. Editing the emitted file is not an answer, because the next
656
+ * `drzl generate` overwrites it.
657
+ *
658
+ * ```ts
659
+ * columns: {
660
+ * users: { omit: ['passwordHash'] },
661
+ * 'app_*': { omit: ['deleted_at'] },
662
+ * }
663
+ * ```
664
+ *
665
+ * The key is a table pattern in the same language `include`/`exclude` uses: the database table
666
+ * name, anchored, with `*` as the only metacharacter. Column patterns are the same language
667
+ * again. Every matching entry applies, in the order written; within one entry `pick` narrows
668
+ * first and `omit` then removes, so `omit` wins, exactly as `exclude` wins over `include`.
669
+ *
670
+ * Applies to every mode and every generator at once, because it narrows the analysis rather
671
+ * than any one generator's output. A column cannot be kept in `select` and dropped from
672
+ * `insert`: see the docs for why that form was not taken on.
673
+ *
674
+ * A pattern that matches nothing is an error rather than a no-op, because a typo in `omit`
675
+ * that silently does nothing leaves the column exactly where it was while reading like a fix.
676
+ * Dropping a primary key column is an error too; dropping a NOT NULL column with no default is
677
+ * a warning.
678
+ */
679
+ columns: import_zod.z.record(import_zod.z.string(), ColumnRulesSchema).optional(),
270
680
  /**
271
681
  * How every relative specifier drzl invents spells its extension, for every generator.
272
682
  * A generator may override it. Defaults to `js`, which is the only form that resolves
@@ -301,10 +711,58 @@ var ConfigSchema = import_zod.z.object({
301
711
  function defineConfig(cfg) {
302
712
  return cfg;
303
713
  }
304
- var ROUTER_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc"]);
714
+ var CONFIG_FILE_NAMES = [
715
+ "drzl.config.ts",
716
+ "drzl.config.mjs",
717
+ "drzl.config.js",
718
+ "drzl.config.cjs",
719
+ "drzl.config.json"
720
+ ];
721
+ var CONFIG_SCHEMA_ID = "https://use-drzl.github.io/drzl/drzl.config.schema.json";
722
+ function buildConfigJsonSchema() {
723
+ const generated = import_zod.z.toJSONSchema(ConfigSchema, {
724
+ io: "input",
725
+ target: "draft-7"
726
+ });
727
+ const properties = {
728
+ // Declared so an editor suggests it and does not report the pointer a reader was told to
729
+ // add as an unknown key. `ConfigSchema` is not strict, so the CLI strips it and never sees it.
730
+ $schema: {
731
+ type: "string",
732
+ description: "Path or URL of this schema, for editor completion. Ignored by drzl."
733
+ },
734
+ ...generated.properties
735
+ };
736
+ const { $schema, properties: _dropped, ...rest } = generated;
737
+ return {
738
+ $schema,
739
+ $id: CONFIG_SCHEMA_ID,
740
+ title: "DRZL configuration",
741
+ description: "Configuration for the drzl CLI. Also describes drzl.config.ts, which gets the same shape from the defineConfig export of @drzl/cli/config.",
742
+ ...rest,
743
+ properties
744
+ };
745
+ }
746
+ var ROUTER_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc", "hono", "express"]);
747
+ var INJECTION_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc"]);
305
748
  function trpcOutDir(g, cfg) {
306
749
  return g.path ?? cfg.outDir;
307
750
  }
751
+ function honoOutDir(g, cfg) {
752
+ return g.path ?? cfg.outDir;
753
+ }
754
+ function expressOutDir(g, cfg) {
755
+ return g.path ?? cfg.outDir;
756
+ }
757
+ function fastifyOutDir(g, cfg) {
758
+ return g.path ?? cfg.outDir;
759
+ }
760
+ function nestjsOutDir(g, cfg) {
761
+ return g.path ?? cfg.outDir;
762
+ }
763
+ function graphqlOutDir(g, cfg) {
764
+ return g.path ?? cfg.outDir;
765
+ }
308
766
  function sharedSchemaNames(opts) {
309
767
  const resolved = (0, import_validation_core.resolveAffix)(opts);
310
768
  return import_validation_core.NAME_MODES.map((mode) => (0, import_validation_core.schemaName)(mode, import_validation_core.AFFIX_PROBE_TABLE, resolved));
@@ -316,8 +774,66 @@ function resolveConfig(cfg) {
316
774
  importExtension: g.importExtension ?? cfg.importExtension
317
775
  }));
318
776
  for (const g of generators) {
777
+ if (g.kind === "fastify") {
778
+ if (g.databaseInjection?.enabled) {
779
+ warnings.push(
780
+ `drzl config: the "fastify" generator sets databaseInjection.enabled, which it does not support. Its handlers are stubs and never call a service, so nothing reads the injected handle. Reach your database from inside the handler bodies you fill in, or use the "trpc" or "orpc" generator, which do delegate to @drzl/generator-service.`
781
+ );
782
+ }
783
+ if (g.validation) {
784
+ warnings.push(
785
+ `drzl config: the "fastify" generator sets "validation", which it does not read. Its route schemas are JSON Schema produced by the same builder as the "json-schema" generator and inlined into the routes, so there is no library to choose and no shared schema module to import. Remove the block.`
786
+ );
787
+ }
788
+ continue;
789
+ }
790
+ if (g.kind === "nestjs") {
791
+ if (g.databaseInjection?.enabled) {
792
+ warnings.push(
793
+ `drzl config: the "nestjs" generator sets databaseInjection.enabled, which it does not support. It emits DTO classes with no handlers at all, so nothing reads the injected handle. Reach your database from the controllers you write around these DTOs, or use the "trpc" or "orpc" generator, which do delegate to @drzl/generator-service.`
794
+ );
795
+ }
796
+ if (g.includeRelations) {
797
+ warnings.push(
798
+ `drzl config: the "nestjs" generator sets includeRelations, which it does not read. Relation lookups are routes, and this generator emits DTO classes for your own controllers rather than routes. Remove the flag.`
799
+ );
800
+ }
801
+ if (g.validation?.useShared || g.validation?.importPath) {
802
+ warnings.push(
803
+ `drzl config: the "nestjs" generator sets validation.useShared or validation.importPath, which it does not read. Its DTO modules are self-contained: the class fields and the schema are generated from the same columns, and wrapping a schema another generator wrote would let the two drift. Only validation.library is read on this kind.`
804
+ );
805
+ }
806
+ if (g.validation?.schemaSuffix || g.validation?.affix) {
807
+ warnings.push(
808
+ `drzl config: the "nestjs" generator sets validation.schemaSuffix or validation.affix, which it does not read. Those options spell the names of shared schema modules, and this generator imports none. Only validation.library is read on this kind.`
809
+ );
810
+ }
811
+ continue;
812
+ }
813
+ if (g.kind === "graphql") {
814
+ if (g.databaseInjection?.enabled) {
815
+ warnings.push(
816
+ `drzl config: the "graphql" generator sets databaseInjection.enabled, which it does not support. It emits SDL and resolver stubs with no handlers at all, so nothing reads the injected handle. Reach your database from the resolvers you write in place of the stubs, or use the "trpc" or "orpc" generator, which do delegate to @drzl/generator-service.`
817
+ );
818
+ }
819
+ if (g.includeRelations) {
820
+ warnings.push(
821
+ `drzl config: the "graphql" generator sets includeRelations, which it does not read. Relation lookups are routes on the router generators, and relation fields on a GraphQL type are resolvers you write against your own data layer. Remove the flag.`
822
+ );
823
+ }
824
+ if (g.validation) {
825
+ warnings.push(
826
+ `drzl config: the "graphql" generator sets "validation", which it does not read. Its schema is GraphQL SDL, GraphQL's own type language, so there is no library to choose and no shared schema module to import. Remove the block.`
827
+ );
828
+ }
829
+ continue;
830
+ }
319
831
  if (!ROUTER_KINDS.has(g.kind)) continue;
320
- if (g.databaseInjection?.enabled) {
832
+ if (g.databaseInjection?.enabled && !INJECTION_KINDS.has(g.kind)) {
833
+ warnings.push(
834
+ `drzl config: the "${g.kind}" generator sets databaseInjection.enabled, which it does not support. Its handlers are stubs and never call a service, so nothing reads the injected handle. Reach your database from inside the handler bodies you fill in, or use the "trpc" or "orpc" generator, which do delegate to @drzl/generator-service.`
835
+ );
836
+ } else if (g.databaseInjection?.enabled) {
321
837
  for (const s of generators.filter((x) => x.kind === "service")) {
322
838
  if (!s.databaseInjection) {
323
839
  s.databaseInjection = g.databaseInjection;
@@ -374,20 +890,58 @@ function resolveConfig(cfg) {
374
890
  }
375
891
  return { config: { ...cfg, generators }, warnings };
376
892
  }
377
- function finalize(raw) {
378
- const { config, warnings } = resolveConfig(ConfigSchema.parse(raw));
379
- for (const w of warnings) console.warn(w);
893
+ var configShape = null;
894
+ function configShapeForReading() {
895
+ return configShape ??= buildConfigJsonSchema();
896
+ }
897
+ function displayConfigPath(file, cwd = process.cwd()) {
898
+ const relative2 = path.relative(cwd, file);
899
+ return relative2 && !relative2.startsWith("..") ? relative2 : file;
900
+ }
901
+ function finalize(raw, file, onWarn) {
902
+ const parsed = ConfigSchema.safeParse(raw);
903
+ if (!parsed.success) {
904
+ throw new ConfigValidationError(
905
+ formatConfigProblems(
906
+ displayConfigPath(file),
907
+ parsed.error.issues,
908
+ raw,
909
+ configShapeForReading()
910
+ )
911
+ );
912
+ }
913
+ for (const w of unknownKeyWarnings(raw, configShapeForReading())) onWarn(w);
914
+ const { config, warnings } = resolveConfig(parsed.data);
915
+ for (const w of warnings) onWarn(w);
380
916
  return config;
381
917
  }
382
- async function loadConfig(customPath) {
918
+ async function importFreshConfigModule(p) {
383
919
  const fsp = await import("fs/promises");
384
- const candidates = customPath ? [customPath] : [
385
- "drzl.config.ts",
386
- "drzl.config.mjs",
387
- "drzl.config.js",
388
- "drzl.config.cjs",
389
- "drzl.config.json"
390
- ];
920
+ const ext = path.extname(p).toLowerCase();
921
+ if (ext === ".json") {
922
+ return JSON.parse(await fsp.readFile(p, "utf8"));
923
+ }
924
+ const { createJiti } = await import("jiti");
925
+ const stat = await fsp.stat(p);
926
+ const base = typeof __filename !== "undefined" ? __filename : path.join(process.cwd(), "index.js");
927
+ const jiti = createJiti(base, {
928
+ moduleCache: false,
929
+ // re-evaluate each time
930
+ fsCache: true,
931
+ // keep transform cache
932
+ cacheVersion: String(stat.mtimeMs),
933
+ // bump on edit
934
+ interopDefault: true,
935
+ tryNative: false
936
+ // <-- prevent native import of .ts
937
+ // debug: true,
938
+ });
939
+ const mod = await jiti.import(p);
940
+ return mod?.default ?? mod;
941
+ }
942
+ async function loadConfig(customPath, onWarn = (warning) => console.warn(warning)) {
943
+ const fsp = await import("fs/promises");
944
+ const candidates = customPath ? [customPath] : [...CONFIG_FILE_NAMES];
391
945
  for (const c of candidates) {
392
946
  const p = path.resolve(process.cwd(), c);
393
947
  try {
@@ -395,38 +949,31 @@ async function loadConfig(customPath) {
395
949
  } catch {
396
950
  continue;
397
951
  }
398
- const ext = path.extname(p).toLowerCase();
399
- if (ext === ".json") {
400
- const raw2 = JSON.parse(await fsp.readFile(p, "utf8"));
401
- return finalize(raw2);
402
- }
403
- const { createJiti } = await import("jiti");
404
- const stat = await fsp.stat(p);
405
- const base = typeof __filename !== "undefined" ? __filename : path.join(process.cwd(), "index.js");
406
- const jiti = createJiti(base, {
407
- moduleCache: false,
408
- // re-evaluate each time
409
- fsCache: true,
410
- // keep transform cache
411
- cacheVersion: String(stat.mtimeMs),
412
- // bump on edit
413
- interopDefault: true,
414
- tryNative: false
415
- // <-- prevent native import of .ts
416
- // debug: true,
417
- });
418
- const mod = await jiti.import(p);
419
- const raw = mod?.default ?? mod;
420
- return finalize(raw);
952
+ return finalize(await importFreshConfigModule(p), p, onWarn);
421
953
  }
422
954
  return null;
423
955
  }
956
+ function configFromKinds(kinds, schema, onWarn = () => {
957
+ }) {
958
+ const parsed = ConfigSchema.parse({
959
+ ...schema ? { schema } : {},
960
+ generators: kinds.map((kind) => ({ kind }))
961
+ });
962
+ const { config, warnings } = resolveConfig(parsed);
963
+ for (const w of warnings) onWarn(w);
964
+ return config;
965
+ }
424
966
  function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
425
967
  const abs = (p) => path.resolve(cwd, p);
426
968
  const dirs = /* @__PURE__ */ new Set();
427
969
  dirs.add(abs(cfg.outDir));
428
970
  for (const g of cfg.generators) {
429
971
  if (g.kind === "trpc") dirs.add(abs(trpcOutDir(g, cfg)));
972
+ if (g.kind === "hono") dirs.add(abs(honoOutDir(g, cfg)));
973
+ if (g.kind === "express") dirs.add(abs(expressOutDir(g, cfg)));
974
+ if (g.kind === "fastify") dirs.add(abs(fastifyOutDir(g, cfg)));
975
+ if (g.kind === "nestjs") dirs.add(abs(nestjsOutDir(g, cfg)));
976
+ if (g.kind === "graphql") dirs.add(abs(graphqlOutDir(g, cfg)));
430
977
  if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
431
978
  if (g.kind === "zod") dirs.add(abs(g.path ?? "src/validators/zod"));
432
979
  if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
@@ -463,25 +1010,26 @@ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
463
1010
  return Array.from(new Set(results));
464
1011
  }
465
1012
  function filterTables(tables, opts) {
466
- const toRegExp = (pattern) => new RegExp(
467
- "^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
468
- );
469
- const matches = (patterns, name) => patterns.some((p) => toRegExp(p).test(name));
470
1013
  let out = tables;
471
- if (opts.include?.length) out = out.filter((t) => matches(opts.include, t.name));
472
- if (opts.exclude?.length) out = out.filter((t) => !matches(opts.exclude, t.name));
1014
+ if (opts.include?.length) out = out.filter((t) => matchesTable(opts.include, t));
1015
+ if (opts.exclude?.length) out = out.filter((t) => !matchesTable(opts.exclude, t));
473
1016
  return out;
474
1017
  }
475
- function computeWatchTargets(cfg, cwd = process.cwd()) {
1018
+ function tableFilterWarnings(tables, opts) {
1019
+ return [
1020
+ ...ambiguousPatternWarnings(opts.include ?? [], tables, "include"),
1021
+ ...ambiguousPatternWarnings(opts.exclude ?? [], tables, "exclude")
1022
+ ];
1023
+ }
1024
+ function computeWatchTargets(cfg, cwd = process.cwd(), source) {
476
1025
  const abs = (p) => path.resolve(cwd, p);
477
- const schemaAbs = abs(cfg.schema);
478
- const targets = /* @__PURE__ */ new Set([
479
- path.dirname(schemaAbs),
480
- abs("drzl.config.ts"),
481
- abs("drzl.config.js"),
482
- abs("drzl.config.mjs"),
483
- abs("drzl.config.cjs")
484
- ]);
1026
+ const targets = new Set(CONFIG_FILE_NAMES.map(abs));
1027
+ if (source) {
1028
+ for (const d of source.watchDirs) targets.add(abs(d));
1029
+ if (source.drizzleKitConfigPath) targets.add(abs(source.drizzleKitConfigPath));
1030
+ } else if (cfg.schema) {
1031
+ targets.add(path.dirname(abs(cfg.schema)));
1032
+ }
485
1033
  for (const t of resolveTemplateDirsSync(cfg, cwd)) targets.add(t);
486
1034
  return [...targets];
487
1035
  }
@@ -489,17 +1037,31 @@ function computeWatchTargets(cfg, cwd = process.cwd()) {
489
1037
  0 && (module.exports = {
490
1038
  AffixSchema,
491
1039
  AnalyzerSchema,
1040
+ CONFIG_FILE_NAMES,
1041
+ CONFIG_SCHEMA_ID,
1042
+ ColumnRulesSchema,
492
1043
  ConfigSchema,
1044
+ GENERATOR_KINDS,
1045
+ GeneratorKindSchema,
493
1046
  GeneratorSchema,
494
1047
  ImportExtensionSchema,
495
1048
  NamingSchema,
1049
+ buildConfigJsonSchema,
496
1050
  computeGeneratorOutputDirs,
497
1051
  computeWatchTargets,
1052
+ configFromKinds,
498
1053
  defineConfig,
1054
+ expressOutDir,
1055
+ fastifyOutDir,
499
1056
  filterTables,
1057
+ graphqlOutDir,
1058
+ honoOutDir,
1059
+ importFreshConfigModule,
500
1060
  loadConfig,
1061
+ nestjsOutDir,
501
1062
  resolveConfig,
502
1063
  resolveTemplateDirsSync,
1064
+ tableFilterWarnings,
503
1065
  trpcOutDir
504
1066
  });
505
1067
  //# sourceMappingURL=config.cjs.map