@drzl/cli 4.14.4 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -31,12 +31,525 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  mod
32
32
  ));
33
33
 
34
- // ../generator-json-schema/dist/index.js
34
+ // ../generator-trpc/dist/index.js
35
35
  var dist_exports = {};
36
36
  __export(dist_exports, {
37
+ BASE_MODULE: () => BASE_MODULE,
38
+ TRPCGenerator: () => TRPCGenerator,
39
+ TRPC_MAJOR: () => TRPC_MAJOR,
40
+ default: () => index_default
41
+ });
42
+ function isWide(column) {
43
+ if (column.enumValues && column.enumValues.length) return false;
44
+ if (column.shape?.kind === "tuple" || column.shape?.kind === "numberObject") return false;
45
+ return !["number", "string", "boolean", "Date"].includes(column.tsType);
46
+ }
47
+ function mapExpr(column, lib, mode) {
48
+ const d = LIBS[lib];
49
+ let base = (() => {
50
+ if (column.enumValues && column.enumValues.length) return d.enum(column.enumValues);
51
+ if (column.shape?.kind === "tuple" && d.tuple) return d.tuple(column.shape.length);
52
+ if (column.shape?.kind === "numberObject" && d.numberObject) {
53
+ return d.numberObject(column.shape.fields);
54
+ }
55
+ switch (column.tsType) {
56
+ case "number":
57
+ return d.number;
58
+ case "string":
59
+ return d.string;
60
+ case "boolean":
61
+ return d.boolean;
62
+ case "Date":
63
+ return d.date;
64
+ default:
65
+ return d.unknown;
66
+ }
67
+ })();
68
+ if (column.nullable) base = d.nullable(base);
69
+ if (mode !== "select") {
70
+ const optional = mode === "update" || column.nullable || column.hasDefault;
71
+ if (optional) base = d.optional(base);
72
+ }
73
+ return base;
74
+ }
75
+ function field(column, lib, mode) {
76
+ const d = LIBS[lib];
77
+ const expr = mapExpr(column, lib, mode);
78
+ return `${objectKey(column.name)}: ${d.fieldIsString ? JSON.stringify(expr) : expr}`;
79
+ }
80
+ function objectKey(name) {
81
+ return isIdent(name) ? name : JSON.stringify(name);
82
+ }
83
+ function renderSchema(table, lib, mode) {
84
+ const d = LIBS[lib];
85
+ const cols = table.columns.filter((c) => mode === "select" ? true : !c.isGenerated);
86
+ const body = cols.map((c) => ` ${field(c, lib, mode)},`).join("\n");
87
+ const schema = d.object(body);
88
+ return mode === "update" && d.partialUpdate ? d.partialUpdate(schema) : schema;
89
+ }
90
+ function toCase(s, c) {
91
+ if (!c) return s;
92
+ const parts = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/);
93
+ if (c === "camel") {
94
+ return parts.map(
95
+ (p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
96
+ ).join("");
97
+ }
98
+ if (c === "kebab") return parts.map((p) => p.toLowerCase()).join("-");
99
+ if (c === "snake") return parts.map((p) => p.toLowerCase()).join("_");
100
+ return s;
101
+ }
102
+ function keyColumns(table) {
103
+ const names = table.primaryKey?.columns ?? [];
104
+ if (!names.length) return null;
105
+ const cols = names.map((n) => table.columns.find((c) => c.name === n));
106
+ if (cols.some((c) => !c)) return null;
107
+ return cols;
108
+ }
109
+ function renderBase(opts) {
110
+ const injection = opts.databaseInjection?.enabled === true;
111
+ const dbType = opts.databaseInjection?.databaseType ?? "unknown";
112
+ const typeImport = opts.databaseInjection?.databaseTypeImport ? `import type { ${opts.databaseInjection.databaseTypeImport.name} } from '${opts.databaseInjection.databaseTypeImport.from}';
113
+ ` : "";
114
+ const trpcImport = injection ? `import { initTRPC, TRPCError } from '@trpc/server';` : `import { initTRPC } from '@trpc/server';`;
115
+ const context = injection ? `/**
116
+ * What your \`createContext\` hands every procedure.
117
+ *
118
+ * \`db\` is optional here and required by \`dbProcedure\` below. That split is what lets an adapter
119
+ * build a context without a handle, for a health check or a public route, while every generated
120
+ * procedure still sees one that is present.
121
+ */
122
+ export interface Context {
123
+ db?: ${dbType};
124
+ }` : `/**
125
+ * What your \`createContext\` hands every procedure. Nothing generated reads it, so it is left
126
+ * open; narrow it to the shape your own context really has.
127
+ */
128
+ export type Context = Record<string, unknown>;`;
129
+ const middleware = injection ? `
130
+ /**
131
+ * The builder every generated procedure is built from: it refuses to run without a database
132
+ * handle, and narrows \`ctx.db\` from optional to present for everything downstream.
133
+ */
134
+ export const dbProcedure = t.procedure.use(async ({ ctx, next }) => {
135
+ if (!ctx.db) {
136
+ throw new TRPCError({
137
+ code: 'INTERNAL_SERVER_ERROR',
138
+ message: 'No database handle on the tRPC context. Provide one from createContext.',
139
+ });
140
+ }
141
+ return next({ ctx: { db: ctx.db } });
142
+ });
143
+ ` : "";
144
+ return `// Generated by @drzl/generator-trpc
145
+ // The shared tRPC base. Every generated router imports from here.
146
+ ${trpcImport}
147
+ ${typeImport}
148
+ ${context}
149
+
150
+ const t = initTRPC.context<Context>().create();
151
+
152
+ export const router = t.router;
153
+ export const mergeRouters = t.mergeRouters;
154
+ export const middleware = t.middleware;
155
+ /** Needed to call this router in-process, from a test or from SSR. */
156
+ export const createCallerFactory = t.createCallerFactory;
157
+ export const publicProcedure = t.procedure;
158
+ ${middleware}`;
159
+ }
160
+ function renderRouter(table, opts, ctx) {
161
+ const lib = opts.validation?.library ?? "zod";
162
+ const d = LIBS[lib];
163
+ const service = opts.template === "service";
164
+ const injection = opts.databaseInjection?.enabled === true;
165
+ const builder = injection ? "dbProcedure" : "publicProcedure";
166
+ const insertName = `Insert${table.tsName}Schema`;
167
+ const updateName = `Update${table.tsName}Schema`;
168
+ const selectName = `Select${table.tsName}Schema`;
169
+ const writable = !table.readOnly;
170
+ const key = keyColumns(table);
171
+ const Service = `${cap(singularize(table.tsName))}Service`;
172
+ const serviceKeyable = !!key && key.length === 1 && key[0].tsType === "number";
173
+ const keyArg = key && key.length === 1 ? `input.${key[0].name}` : "";
174
+ const dbArg = injection ? "ctx.db, " : "";
175
+ const wiredParams = injection ? "{ ctx, input }" : "{ input }";
176
+ const procedures = [];
177
+ const notImplemented = (what) => `throw new Error('Not implemented: ${what} ${table.tsName}.');`;
178
+ procedures.push({
179
+ name: "list",
180
+ kind: "query",
181
+ output: d.arrayOf(selectName),
182
+ params: service && injection ? "{ ctx }" : "",
183
+ body: service ? [`return await ${Service}.getAll(${injection ? "ctx.db" : ""});`] : ["return [];"]
184
+ });
185
+ const keyInput = key ? d.objectInline(key.map((c) => field(c, lib, "select")).join(", ")) : void 0;
186
+ if (key && keyInput) {
187
+ const wired = service && serviceKeyable;
188
+ procedures.push({
189
+ name: "byId",
190
+ kind: "query",
191
+ input: keyInput,
192
+ output: d.nullableOf(selectName),
193
+ params: wired ? wiredParams : "{ input: _input }",
194
+ body: wired ? [`return await ${Service}.getById(${dbArg}${keyArg});`] : service ? [serviceKeyNote(table), notImplemented("byId")] : ["return null;"]
195
+ });
196
+ if (writable) {
197
+ const updateInput = d.objectInline(
198
+ [...key.map((c) => field(c, lib, "select")), `data: ${updateName}`].join(", ")
199
+ );
200
+ procedures.push({
201
+ name: "update",
202
+ kind: "mutation",
203
+ input: updateInput,
204
+ output: selectName,
205
+ params: wired ? wiredParams : "{ input: _input }",
206
+ body: wired ? [`return await ${Service}.update(${dbArg}${keyArg}, input.data);`] : service ? [serviceKeyNote(table), notImplemented("update")] : [notImplemented("update")]
207
+ });
208
+ procedures.push({
209
+ name: "delete",
210
+ kind: "mutation",
211
+ input: keyInput,
212
+ output: d.booleanSchema,
213
+ params: wired ? wiredParams : "{ input: _input }",
214
+ body: wired ? [`return await ${Service}.delete(${dbArg}${keyArg});`] : service ? [serviceKeyNote(table), notImplemented("delete")] : ["return true;"]
215
+ });
216
+ }
217
+ }
218
+ if (writable) {
219
+ procedures.push({
220
+ name: "create",
221
+ kind: "mutation",
222
+ input: insertName,
223
+ output: selectName,
224
+ params: service ? wiredParams : "{ input: _input }",
225
+ body: service ? [`return await ${Service}.create(${dbArg}input);`] : [notImplemented("create")]
226
+ });
227
+ }
228
+ if (opts.includeRelations) {
229
+ const taken = new Set(procedures.map((p) => p.name));
230
+ procedures.push(...relationProcedures(table, lib, selectName, taken, service));
231
+ }
232
+ const order = ["list", "byId", "create", "update", "delete"];
233
+ const rank = (n) => order.indexOf(n) === -1 ? order.length : order.indexOf(n);
234
+ procedures.sort((a, b) => rank(a.name) - rank(b.name));
235
+ const routerName = routerExportName(table, opts.naming);
236
+ const entries = procedures.map((p) => {
237
+ const rawKey = toCase(p.name, opts.naming?.procedureCase);
238
+ const propKey = isIdent(rawKey) ? rawKey : JSON.stringify(rawKey);
239
+ return [
240
+ ` ${propKey}: ${builder}`,
241
+ ...p.input ? [` .input(${p.input})`] : [],
242
+ ` .output(${p.output})`,
243
+ ` .${p.kind}(async (${p.params}) => {`,
244
+ ...p.body.map((line) => ` ${line}`),
245
+ ` }),`
246
+ ].join("\n");
247
+ }).join("\n");
248
+ const body = `export const ${routerName} = router({
249
+ ${entries}
250
+ });
251
+ `;
252
+ const useShared = !!opts.validation?.useShared && !!opts.validation?.importPath;
253
+ const declared = [];
254
+ if (!useShared) {
255
+ if (writable) {
256
+ declared.push(`export const ${insertName} = ${renderSchema(table, lib, "insert")};`);
257
+ declared.push(`export const ${updateName} = ${renderSchema(table, lib, "update")};`);
258
+ }
259
+ declared.push(`export const ${selectName} = ${renderSchema(table, lib, "select")};`);
260
+ }
261
+ const decided = [...declared, body].join("\n\n");
262
+ const imports = [];
263
+ if (useShared) {
264
+ const sharedAffix = (0, import_validation_core2.resolveAffix)({
265
+ affix: opts.validation?.affix,
266
+ schemaSuffix: opts.validation?.schemaSuffix
267
+ });
268
+ const wanted = [
269
+ ["insert", insertName],
270
+ ["update", updateName],
271
+ ["select", selectName]
272
+ ].filter(([, local]) => decided.includes(local));
273
+ if (wanted.length) {
274
+ const spec = (0, import_validation_core2.resolveConfiguredImport)(
275
+ opts.validation.importPath,
276
+ ctx.out,
277
+ process.cwd(),
278
+ opts.importExtension
279
+ );
280
+ const names = wanted.map(([mode, local]) => {
281
+ const exported = (0, import_validation_core2.schemaName)(mode, table.tsName, sharedAffix);
282
+ return exported === local ? local : `${exported} as ${local}`;
283
+ }).join(", ");
284
+ imports.push(`import { ${names} } from '${spec}';`);
285
+ }
286
+ }
287
+ imports.push(
288
+ `import { ${[builder, "router"].sort().join(", ")} } from '${(0, import_validation_core2.importSpecifier)(
289
+ `./${BASE_MODULE}.ts`,
290
+ opts.importExtension
291
+ )}';`
292
+ );
293
+ if (service) {
294
+ imports.push(`import { ${Service} } from '${serviceImportSpecifier(table, ctx, opts)}';`);
295
+ }
296
+ if (LIB_USAGE[lib].test(decided)) imports.unshift(LIB_IMPORTS[lib]);
297
+ const wide = table.columns.filter(isWide).map((c) => c.name);
298
+ const wideNote = wide.length ? `// No validated type for ${wide.length === 1 ? "this column" : "these columns"}: ${wide.join(", ")}.
299
+ // DRZL could not derive one from the schema, so the router accepts any value there.
300
+ ` : "";
301
+ return `// Generated by @drzl/generator-trpc
302
+ // Router for table: ${table.name}
303
+ ${wideNote}${imports.join("\n")}
304
+
305
+ ${decided}`;
306
+ }
307
+ function relationProcedures(table, lib, selectSchemaName, taken, service) {
308
+ const d = LIBS[lib];
309
+ const out = [];
310
+ for (const fk of table.foreignKeys ?? []) {
311
+ if (fk.columns.length !== 1) continue;
312
+ const colName = fk.columns[0];
313
+ const column = table.columns.find((c) => c.name === colName);
314
+ if (!column) continue;
315
+ const name = `listBy${cap(colName)}`;
316
+ if (taken.has(name)) continue;
317
+ taken.add(name);
318
+ out.push({
319
+ name,
320
+ kind: "query",
321
+ input: d.objectInline(field(column, lib, "select")),
322
+ output: d.arrayOf(selectSchemaName),
323
+ params: "{ input: _input }",
324
+ body: [
325
+ `// Rows of ${table.name} whose ${JSON.stringify(colName)} matches _input.${colName}.`,
326
+ // In `service` mode every other procedure really does reach the database, so a lookup
327
+ // quietly answering with an empty array would read as "no matching rows". There is no
328
+ // generated service method for it, so it says so instead. In `standard` mode everything
329
+ // is a stub and `[]` is consistent with `list`.
330
+ service ? `throw new Error('Not implemented: ${name} ${table.tsName}.');` : "return [];"
331
+ ]
332
+ });
333
+ }
334
+ return out;
335
+ }
336
+ function renderBarrel(routers, ctx, path6, opts) {
337
+ const baseSpec = (0, import_validation_core2.importSpecifier)(`./${BASE_MODULE}.ts`, opts.importExtension);
338
+ const reExports = `export { createCallerFactory, publicProcedure, router } from '${baseSpec}';
339
+ ` + (opts.databaseInjection?.enabled === true ? `export { dbProcedure } from '${baseSpec}';
340
+ ` : "") + `export type { Context } from '${baseSpec}';
341
+ `;
342
+ if (!routers.length) {
343
+ return `// Generated by @drzl/generator-trpc
344
+ // No tables detected in analysis. Add tables to your schema and regenerate.
345
+ import { router } from '${baseSpec}';
346
+
347
+ export const appRouter = router({});
348
+
349
+ /** The type a tRPC client is parameterised by: \`createTRPCClient<AppRouter>()\`. */
350
+ export type AppRouter = typeof appRouter;
351
+
352
+ ${reExports}`;
353
+ }
354
+ const entries = routers.map(({ filePath, exportName, table }) => ({
355
+ rel: (0, import_validation_core2.importSpecifier)(
356
+ "./" + path6.relative(ctx.out, filePath).replace(/\\/g, "/"),
357
+ opts.importExtension
358
+ ),
359
+ exportName,
360
+ // The namespace a client reaches this table's procedures through: `trpc.userProfiles.list`.
361
+ // `tsName` verbatim, because it is already a valid identifier and it is the name the user
362
+ // wrote in their schema. The oRPC barrel lowercases this key, turning `userProfiles` into
363
+ // `userprofiles`: harmless in an object literal nobody reads, and not harmless when the key
364
+ // is the public API of a typed client.
365
+ key: table.tsName
366
+ }));
367
+ const importLines = entries.map(({ rel, exportName }) => `import { ${exportName} } from '${rel}';`).join("\n");
368
+ const bodyLines = entries.map(({ key, exportName }) => ` ${isIdent(key) ? key : JSON.stringify(key)}: ${exportName},`).join("\n");
369
+ return `// Generated by @drzl/generator-trpc
370
+ import { router } from '${baseSpec}';
371
+ ${importLines}
372
+
373
+ export const appRouter = router({
374
+ ${bodyLines}
375
+ });
376
+
377
+ /** The type a tRPC client is parameterised by: \`createTRPCClient<AppRouter>()\`. */
378
+ export type AppRouter = typeof appRouter;
379
+
380
+ ${reExports}`;
381
+ }
382
+ function serviceKeyNote(table) {
383
+ const cols = table.primaryKey?.columns ?? [];
384
+ const shape = cols.length > 1 ? `has a composite primary key (${cols.join(", ")})` : `has a non-numeric primary key (${cols[0]})`;
385
+ return `// ${table.name} ${shape}, and @drzl/generator-service types its key parameter as one number.
386
+ // Wire this to your own lookup.`;
387
+ }
388
+ function routerExportName(table, naming) {
389
+ const base = `${table.tsName}${naming?.routerSuffix ?? "Router"}`;
390
+ const c = naming?.procedureCase;
391
+ return toCase(base, c === "kebab" ? "camel" : c);
392
+ }
393
+ function serviceImportSpecifier(table, ctx, opts) {
394
+ const rel = relativePosix(ctx.out, ctx.services);
395
+ const dir = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
396
+ return (0, import_validation_core2.importSpecifier)(`${dir}/${singularize(table.tsName)}Service.ts`, opts.importExtension);
397
+ }
398
+ function relativePosix(from, to) {
399
+ const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
400
+ const a = norm(from).split("/");
401
+ const b = norm(to).split("/");
402
+ let i = 0;
403
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
404
+ return [...Array.from({ length: a.length - i }, () => ".."), ...b.slice(i)].join("/");
405
+ }
406
+ function buildHeader(h) {
407
+ if (h && h.enabled === false) return "";
408
+ const text = h?.text?.trim();
409
+ const lines = text ? text.split(/\r?\n/).map((l) => `// ${l}`) : [
410
+ "// Generated by DRZL (@drzl/*)",
411
+ "// Generated output is granted to you under your project's license.",
412
+ "// You may use, copy, modify, and distribute without attribution."
413
+ ];
414
+ return lines.join("\n") + "\n\n";
415
+ }
416
+ var import_validation_core2, TRPC_MAJOR, q, LIB_IMPORTS, LIB_USAGE, LIBS, cap, singularize, isIdent, BASE_MODULE, TRPCGenerator, index_default;
417
+ var init_dist = __esm({
418
+ "../generator-trpc/dist/index.js"() {
419
+ "use strict";
420
+ import_validation_core2 = require("@drzl/validation-core");
421
+ TRPC_MAJOR = 11;
422
+ q = (v) => JSON.stringify(v);
423
+ LIB_IMPORTS = {
424
+ zod: "import { z } from 'zod';",
425
+ valibot: "import * as v from 'valibot';",
426
+ arktype: "import { type } from 'arktype';"
427
+ };
428
+ LIB_USAGE = {
429
+ zod: /\bz\./,
430
+ valibot: /\bv\./,
431
+ arktype: /\btype\(/
432
+ };
433
+ LIBS = {
434
+ zod: {
435
+ number: "z.number()",
436
+ string: "z.string()",
437
+ boolean: "z.boolean()",
438
+ date: "z.date()",
439
+ unknown: "z.unknown()",
440
+ tuple: (n) => `z.tuple([${Array.from({ length: n }, () => "z.number()").join(", ")}])`,
441
+ numberObject: (fields) => `z.object({ ${fields.map((f) => `${f}: z.number()`).join(", ")} })`,
442
+ enum: (vals) => `z.enum([${vals.map(q).join(", ")}] as const)`,
443
+ nullable: (b) => `${b}.nullable()`,
444
+ optional: (b) => `${b}.optional()`,
445
+ object: (body) => `z.object({
446
+ ${body}
447
+ })`,
448
+ objectInline: (body) => `z.object({ ${body} })`,
449
+ partialUpdate: (s) => `${s}.partial()`,
450
+ arrayOf: (s) => `z.array(${s})`,
451
+ nullableOf: (s) => `${s}.nullable()`,
452
+ booleanSchema: "z.boolean()"
453
+ },
454
+ valibot: {
455
+ number: "v.number()",
456
+ string: "v.string()",
457
+ boolean: "v.boolean()",
458
+ date: "v.date()",
459
+ unknown: "v.unknown()",
460
+ tuple: (n) => `v.tuple([${Array.from({ length: n }, () => "v.number()").join(", ")}])`,
461
+ numberObject: (fields) => `v.object({ ${fields.map((f) => `${f}: v.number()`).join(", ")} })`,
462
+ enum: (vals) => `v.picklist([${vals.map(q).join(", ")}] as const)`,
463
+ nullable: (b) => `v.nullable(${b})`,
464
+ optional: (b) => `v.optional(${b})`,
465
+ object: (body) => `v.object({
466
+ ${body}
467
+ })`,
468
+ objectInline: (body) => `v.object({ ${body} })`,
469
+ arrayOf: (s) => `v.array(${s})`,
470
+ nullableOf: (s) => `v.nullable(${s})`,
471
+ booleanSchema: "v.boolean()"
472
+ },
473
+ arktype: {
474
+ number: "number",
475
+ string: "string",
476
+ boolean: "boolean",
477
+ date: "Date",
478
+ unknown: "unknown",
479
+ // The surrounding encode adds the quotes, so the union is built with the inner quoting
480
+ // ArkType expects. Emitting `'${...}'` here produces `''admin' | 'user''`, which does not parse.
481
+ enum: (vals) => vals.map((x) => `'${x.replace(/'/g, "\\'")}'`).join(" | "),
482
+ nullable: (b) => `(${b} | null)`,
483
+ optional: (b) => `${b}?`,
484
+ object: (body) => `type({
485
+ ${body}
486
+ })`,
487
+ objectInline: (body) => `type({ ${body} })`,
488
+ fieldIsString: true,
489
+ arrayOf: (s) => `${s}.array()`,
490
+ nullableOf: (s) => `${s}.or('null')`,
491
+ booleanSchema: `type('boolean')`
492
+ }
493
+ };
494
+ cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
495
+ singularize = (s) => s.endsWith("ies") ? s.slice(0, -3) + "y" : s.endsWith("s") ? s.slice(0, -1) : s;
496
+ isIdent = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
497
+ BASE_MODULE = "trpc";
498
+ TRPCGenerator = class {
499
+ constructor(analysis) {
500
+ this.analysis = analysis;
501
+ }
502
+ async generate(opts) {
503
+ const fs3 = await import("fs/promises");
504
+ const path6 = await import("path");
505
+ const out = path6.resolve(process.cwd(), opts.outputDir);
506
+ const ctx = {
507
+ out,
508
+ services: path6.resolve(process.cwd(), opts.servicesDir ?? "src/services")
509
+ };
510
+ await fs3.mkdir(out, { recursive: true });
511
+ const files = [];
512
+ const write = async (filePath, content) => {
513
+ const formatted = await (0, import_validation_core2.formatCode)(
514
+ buildHeader(opts.outputHeader) + content,
515
+ filePath,
516
+ opts.format
517
+ );
518
+ await fs3.writeFile(filePath, formatted, "utf8");
519
+ files.push(filePath);
520
+ };
521
+ const basePath = path6.join(out, `${BASE_MODULE}.ts`);
522
+ await write(basePath, renderBase(opts));
523
+ const routers = [];
524
+ const total = this.analysis.tables.length;
525
+ let index = 0;
526
+ for (const table of this.analysis.tables) {
527
+ const base = `${table.tsName}${opts.naming?.routerSuffix ?? ""}`;
528
+ const filePath = path6.join(out, `${toCase(base, opts.naming?.procedureCase)}.ts`);
529
+ if (filePath === basePath) {
530
+ throw new Error(
531
+ `@drzl/generator-trpc: the router for table "${table.name}" would be written to ${filePath}, which is the shared tRPC base module this generator also writes. Set naming.routerSuffix to move it out of the way.`
532
+ );
533
+ }
534
+ await write(filePath, renderRouter(table, opts, ctx));
535
+ routers.push({ table, filePath, exportName: routerExportName(table, opts.naming) });
536
+ index++;
537
+ opts.onProgress?.({ index, total, table: table.name, filePath });
538
+ }
539
+ await write(path6.join(out, "index.ts"), renderBarrel(routers, ctx, path6, opts));
540
+ return { files };
541
+ }
542
+ };
543
+ index_default = TRPCGenerator;
544
+ }
545
+ });
546
+
547
+ // ../generator-json-schema/dist/index.js
548
+ var dist_exports2 = {};
549
+ __export(dist_exports2, {
37
550
  JsonSchemaGenerator: () => JsonSchemaGenerator,
38
551
  componentsDocument: () => componentsDocument,
39
- default: () => index_default,
552
+ default: () => index_default2,
40
553
  tableSchemas: () => tableSchemas
41
554
  });
42
555
  function baseSchema(c, mode, target, checks, sets, lengths) {
@@ -88,14 +601,14 @@ function baseSchema(c, mode, target, checks, sets, lengths) {
88
601
  case "string": {
89
602
  const out = { type: "string" };
90
603
  if (c.format === "uuid") out.format = UUID_FORMAT;
91
- else if (c.format && import_validation_core2.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core2.COLUMN_FORMATS[c.format];
604
+ else if (c.format && import_validation_core3.COLUMN_FORMATS[c.format]) out.pattern = import_validation_core3.COLUMN_FORMATS[c.format];
92
605
  if (c.maxLength !== void 0) out.maxLength = c.maxLength;
93
606
  applyByteCap(out, c);
94
607
  applyLengths(out, c, lengths);
95
608
  return out;
96
609
  }
97
610
  case "number": {
98
- const out = { type: (0, import_validation_core2.isIntegerColumn)(c) ? "integer" : "number" };
611
+ const out = { type: (0, import_validation_core3.isIntegerColumn)(c) ? "integer" : "number" };
99
612
  if (!c.arrayDimensions) applyNumericBounds(out, c, checks, target);
100
613
  return out;
101
614
  }
@@ -232,7 +745,7 @@ function tableSchema(table, cols, mode, target, applyDefaults, parsed) {
232
745
  };
233
746
  }
234
747
  function collect(table) {
235
- const parsed = (table.checks ?? []).map((k) => (0, import_validation_core2.parseCheck)(k.expression, k.name));
748
+ const parsed = (table.checks ?? []).map((k) => (0, import_validation_core3.parseCheck)(k.expression, k.name));
236
749
  return {
237
750
  checks: parsed.flatMap((p) => p.ok ? p.checks : []),
238
751
  sets: parsed.flatMap((p) => p.ok ? p.sets ?? [] : []),
@@ -246,9 +759,9 @@ function tableSchemas(table, opts = {}) {
246
759
  const parsed = collect(table);
247
760
  const build = (cols, mode) => tableSchema(table, cols, mode, target, !!opts.applyDefaults, parsed);
248
761
  return {
249
- insert: build((0, import_validation_core2.insertColumns)(table), "insert"),
250
- update: build((0, import_validation_core2.updateColumns)(table), "update"),
251
- select: build((0, import_validation_core2.selectColumns)(table), "select")
762
+ insert: build((0, import_validation_core3.insertColumns)(table), "insert"),
763
+ update: build((0, import_validation_core3.updateColumns)(table), "update"),
764
+ select: build((0, import_validation_core3.selectColumns)(table), "select")
252
765
  };
253
766
  }
254
767
  function componentsDocument(tables, opts = {}) {
@@ -266,23 +779,23 @@ function componentsDocument(tables, opts = {}) {
266
779
  function renderTableModule(table, affix, target, applyDefaults) {
267
780
  const T = table.tsName;
268
781
  const schemas = tableSchemas(table, { target, applyDefaults });
269
- const decl = (mode) => `export const ${(0, import_validation_core2.schemaName)(mode, T, affix)} = ${JSON.stringify(schemas[mode], null, 2)} as const;
782
+ const decl = (mode) => `export const ${(0, import_validation_core3.schemaName)(mode, T, affix)} = ${JSON.stringify(schemas[mode], null, 2)} as const;
270
783
 
271
- export type ${(0, import_validation_core2.typeName)(mode, T, affix)} = typeof ${(0, import_validation_core2.schemaName)(mode, T, affix)};`;
784
+ export type ${(0, import_validation_core3.typeName)(mode, T, affix)} = typeof ${(0, import_validation_core3.schemaName)(mode, T, affix)};`;
272
785
  return [decl("insert"), decl("update"), decl("select")].join("\n\n") + "\n";
273
786
  }
274
- function buildHeader(h) {
787
+ function buildHeader2(h) {
275
788
  if (h?.enabled === false) return "";
276
789
  const text = h?.text ?? "// Generated by DRZL. Do not edit by hand.";
277
790
  return `${text}
278
791
 
279
792
  `;
280
793
  }
281
- var import_validation_core2, DEFAULT_FILE_SUFFIX, DRAFT, UUID_FORMAT, JsonSchemaGenerator, index_default;
282
- var init_dist = __esm({
794
+ var import_validation_core3, DEFAULT_FILE_SUFFIX, DRAFT, UUID_FORMAT, JsonSchemaGenerator, index_default2;
795
+ var init_dist2 = __esm({
283
796
  "../generator-json-schema/dist/index.js"() {
284
797
  "use strict";
285
- import_validation_core2 = require("@drzl/validation-core");
798
+ import_validation_core3 = require("@drzl/validation-core");
286
799
  DEFAULT_FILE_SUFFIX = ".schema.ts";
287
800
  DRAFT = "https://json-schema.org/draft/2020-12/schema";
288
801
  UUID_FORMAT = "uuid";
@@ -297,14 +810,14 @@ var init_dist = __esm({
297
810
  const out = path6.resolve(process.cwd(), opts.outDir);
298
811
  const files = [];
299
812
  await fs3.mkdir(out, { recursive: true });
300
- const affix = (0, import_validation_core2.resolveAffix)(opts);
813
+ const affix = (0, import_validation_core3.resolveAffix)(opts);
301
814
  const fileSuffix = opts.fileSuffix ?? DEFAULT_FILE_SUFFIX;
302
815
  const target = opts.target ?? "draft-2020-12";
303
816
  for (const table of this.analysis.tables) {
304
- const filePath = path6.join(out, (0, import_validation_core2.moduleFileName)(table.tsName, fileSuffix));
817
+ const filePath = path6.join(out, (0, import_validation_core3.moduleFileName)(table.tsName, fileSuffix));
305
818
  const code = renderTableModule(table, affix, target, !!opts.applyDefaults);
306
- const formatted = await (0, import_validation_core2.formatCode)(
307
- buildHeader(opts.outputHeader) + code,
819
+ const formatted = await (0, import_validation_core3.formatCode)(
820
+ buildHeader2(opts.outputHeader) + code,
308
821
  filePath,
309
822
  opts.format
310
823
  );
@@ -321,17 +834,17 @@ var init_dist = __esm({
321
834
  `;
322
835
  await fs3.writeFile(
323
836
  componentsPath,
324
- await (0, import_validation_core2.formatCode)(buildHeader(opts.outputHeader) + code, componentsPath, opts.format),
837
+ await (0, import_validation_core3.formatCode)(buildHeader2(opts.outputHeader) + code, componentsPath, opts.format),
325
838
  "utf8"
326
839
  );
327
840
  files.push(componentsPath);
328
841
  }
329
842
  const indexPath = path6.join(out, "index.ts");
330
- const index = this.analysis.tables.map((t) => `export * from '${(0, import_validation_core2.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`).concat(
843
+ const index = this.analysis.tables.map((t) => `export * from '${(0, import_validation_core3.moduleSpecifier)(t.tsName, fileSuffix, opts.importExtension)}';`).concat(
331
844
  opts.components ? [`export * from './components${opts.importExtension === "none" ? "" : ".js"}';`] : []
332
845
  ).join("\n") + "\n";
333
- const indexFormatted = await (0, import_validation_core2.formatCode)(
334
- buildHeader(opts.outputHeader) + index,
846
+ const indexFormatted = await (0, import_validation_core3.formatCode)(
847
+ buildHeader2(opts.outputHeader) + index,
335
848
  indexPath,
336
849
  opts.format
337
850
  );
@@ -342,13 +855,13 @@ var init_dist = __esm({
342
855
  renderTable(table, opts) {
343
856
  return renderTableModule(
344
857
  table,
345
- (0, import_validation_core2.resolveAffix)(opts),
858
+ (0, import_validation_core3.resolveAffix)(opts),
346
859
  opts?.target ?? "draft-2020-12",
347
860
  !!opts?.applyDefaults
348
861
  );
349
862
  }
350
863
  };
351
- index_default = JsonSchemaGenerator;
864
+ index_default2 = JsonSchemaGenerator;
352
865
  }
353
866
  });
354
867
 
@@ -362,30 +875,6 @@ var import_commander = require("commander");
362
875
  var path5 = __toESM(require("path"), 1);
363
876
  var import_ora = __toESM(require("ora"), 1);
364
877
 
365
- // src/validation-options.ts
366
- function validationOptions(g, cfg, outDir, caps = {}) {
367
- return {
368
- outDir,
369
- outputHeader: g.outputHeader,
370
- format: g.format,
371
- schemaSuffix: g.schemaSuffix,
372
- fileSuffix: g.fileSuffix,
373
- importExtension: g.importExtension,
374
- affix: g.affix,
375
- coerceDates: g.coerceDates,
376
- applyDefaults: g.applyDefaults,
377
- duplicateFinder: g.duplicateFinder,
378
- // Only where the generator can act on them, so an unsupported option is absent rather than
379
- // present and ignored.
380
- ...caps.schemaTypes ? {
381
- // Needed by both: the reference is resolved relative to the emitted file.
382
- schemaPath: cfg.schema,
383
- typedJson: g.typedJson,
384
- typedColumns: g.typedColumns
385
- } : {}
386
- };
387
- }
388
-
389
878
  // src/config.ts
390
879
  var import_validation_core = require("@drzl/validation-core");
391
880
  var fs = __toESM(require("fs"), 1);
@@ -425,7 +914,7 @@ var AffixSchema = import_zod.z.object({
425
914
  }).strict();
426
915
  var ImportExtensionSchema = import_zod.z.enum(import_validation_core.IMPORT_EXTENSIONS);
427
916
  var GeneratorSchema = import_zod.z.object({
428
- kind: import_zod.z.enum(["orpc", "service", "zod", "valibot", "arktype", "typebox", "json-schema"]),
917
+ kind: import_zod.z.enum(["orpc", "trpc", "service", "zod", "valibot", "arktype", "typebox", "json-schema"]),
429
918
  /**
430
919
  * Overrides the top-level `importExtension` for this generator alone, for a project whose
431
920
  * generated directories are compiled by different tsconfigs.
@@ -461,6 +950,22 @@ var GeneratorSchema = import_zod.z.object({
461
950
  * fact about the table rather than the row. This checks the half that needs no database.
462
951
  */
463
952
  duplicateFinder: import_zod.z.boolean().optional(),
953
+ /**
954
+ * Emit `NestedInsert<Table>` and `NestedSelect<Table>` beside the flat schemas: the table plus
955
+ * one key per relation, so `{ ...user, posts: [...] }` can be validated whole.
956
+ *
957
+ * Nothing in the Drizzle validator ecosystem describes that payload, and `db.insert` drops the
958
+ * relation key silently rather than refusing it, so the children are never written and nothing
959
+ * says so.
960
+ */
961
+ nestedSchemas: import_zod.z.boolean().optional(),
962
+ /**
963
+ * How many levels of children a nested schema describes. Defaults to 1, capped at 3.
964
+ *
965
+ * Nesting is expanded inline rather than by reference, so this multiplies the emitted size, and
966
+ * it is also what terminates a cycle: `users -> posts -> users` stops here.
967
+ */
968
+ nestedDepth: import_zod.z.number().int().optional(),
464
969
  naming: NamingSchema.optional(),
465
970
  outputHeader: import_zod.z.object({
466
971
  enabled: import_zod.z.boolean().default(true).optional(),
@@ -496,7 +1001,22 @@ var GeneratorSchema = import_zod.z.object({
496
1001
  * Omitting it reproduces the output of every previous release exactly.
497
1002
  */
498
1003
  affix: AffixSchema.optional(),
499
- // orpc validation sharing
1004
+ /**
1005
+ * How the router generators reach a database handle: through the request context, rather than
1006
+ * through a module-level import in the service layer.
1007
+ *
1008
+ * Documented on the oRPC generator since it was added and, until now, absent from this schema
1009
+ * entirely. `GeneratorSchema` is not strict, so zod stripped the key without a word and the
1010
+ * option did nothing at all when set from a config file. It was only ever reachable by calling
1011
+ * the generator's API directly.
1012
+ */
1013
+ databaseInjection: import_zod.z.object({
1014
+ enabled: import_zod.z.boolean().optional(),
1015
+ /** The type annotation for the injected handle, e.g. `DrizzleD1Database`. */
1016
+ databaseType: import_zod.z.string().optional(),
1017
+ databaseTypeImport: import_zod.z.object({ name: import_zod.z.string(), from: import_zod.z.string() }).optional()
1018
+ }).optional(),
1019
+ // router validation sharing (orpc, trpc)
500
1020
  validation: import_zod.z.object({
501
1021
  useShared: import_zod.z.boolean().default(false).optional(),
502
1022
  library: import_zod.z.enum(["zod", "valibot", "arktype"]).default("zod").optional(),
@@ -568,6 +1088,10 @@ var ConfigSchema = import_zod.z.object({
568
1088
  );
569
1089
  });
570
1090
  });
1091
+ var ROUTER_KINDS = /* @__PURE__ */ new Set(["orpc", "trpc"]);
1092
+ function trpcOutDir(g, cfg) {
1093
+ return g.path ?? cfg.outDir;
1094
+ }
571
1095
  function sharedSchemaNames(opts) {
572
1096
  const resolved = (0, import_validation_core.resolveAffix)(opts);
573
1097
  return import_validation_core.NAME_MODES.map((mode) => (0, import_validation_core.schemaName)(mode, import_validation_core.AFFIX_PROBE_TABLE, resolved));
@@ -579,7 +1103,23 @@ function resolveConfig(cfg) {
579
1103
  importExtension: g.importExtension ?? cfg.importExtension
580
1104
  }));
581
1105
  for (const g of generators) {
582
- if (g.kind !== "orpc") continue;
1106
+ if (!ROUTER_KINDS.has(g.kind)) continue;
1107
+ if (g.databaseInjection?.enabled) {
1108
+ for (const s of generators.filter((x) => x.kind === "service")) {
1109
+ if (!s.databaseInjection) {
1110
+ s.databaseInjection = g.databaseInjection;
1111
+ } else if (!s.databaseInjection.enabled) {
1112
+ warnings.push(
1113
+ `drzl config: the "${g.kind}" generator sets databaseInjection.enabled while the "service" generator sets it to false. The router will call Service.method(ctx.db, ...) against services that take no database parameter, so the generated project will not compile. Set both, or neither.`
1114
+ );
1115
+ }
1116
+ if ((s.dataAccess ?? "stub") === "stub") {
1117
+ warnings.push(
1118
+ `drzl config: the "${g.kind}" generator sets databaseInjection.enabled, so its handlers call Service.method(ctx.db, ...). The "service" generator emits stub bodies, which take no database parameter whatever this option says, so those calls will not compile. Set dataAccess: 'drizzle' on the "service" generator, or drop databaseInjection.`
1119
+ );
1120
+ }
1121
+ }
1122
+ }
583
1123
  const v = g.validation;
584
1124
  if (!v?.useShared) continue;
585
1125
  const library = v.library ?? "zod";
@@ -604,7 +1144,7 @@ function resolveConfig(cfg) {
604
1144
  const mine2 = sharedSchemaNames({ schemaSuffix: v.schemaSuffix });
605
1145
  if (mine2.join(",") !== theirs.join(",")) {
606
1146
  warnings.push(
607
- `drzl config: the "orpc" generator's validation.schemaSuffix (${JSON.stringify(v.schemaSuffix ?? "Schema")}) does not match the "${library}" generator's schemaSuffix (${JSON.stringify(sibling.schemaSuffix ?? "Schema")}). The router will import ${mine2.join(", ")} but the "${library}" generator exports ${theirs.join(", ")}, so the generated router will not compile. Set both to the same value, or move to "affix", which is inherited automatically.`
1147
+ `drzl config: the "${g.kind}" generator's validation.schemaSuffix (${JSON.stringify(v.schemaSuffix ?? "Schema")}) does not match the "${library}" generator's schemaSuffix (${JSON.stringify(sibling.schemaSuffix ?? "Schema")}). The router will import ${mine2.join(", ")} but the "${library}" generator exports ${theirs.join(", ")}, so the generated router will not compile. Set both to the same value, or move to "affix", which is inherited automatically.`
608
1148
  );
609
1149
  }
610
1150
  continue;
@@ -615,7 +1155,7 @@ function resolveConfig(cfg) {
615
1155
  });
616
1156
  if (mine.join(",") !== theirs.join(",")) {
617
1157
  throw new Error(
618
- `drzl config: the "orpc" generator imports shared ${library} schemas, but its validation.affix disagrees with the "${library}" generator's own naming. The router would import ${mine.join(", ")} while the "${library}" generator exports ${theirs.join(", ")}. Make them match, or drop validation.affix and let it be inherited from the "${library}" generator.`
1158
+ `drzl config: the "${g.kind}" generator imports shared ${library} schemas, but its validation.affix disagrees with the "${library}" generator's own naming. The router would import ${mine.join(", ")} while the "${library}" generator exports ${theirs.join(", ")}. Make them match, or drop validation.affix and let it be inherited from the "${library}" generator.`
619
1159
  );
620
1160
  }
621
1161
  }
@@ -673,6 +1213,7 @@ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
673
1213
  const dirs = /* @__PURE__ */ new Set();
674
1214
  dirs.add(abs(cfg.outDir));
675
1215
  for (const g of cfg.generators) {
1216
+ if (g.kind === "trpc") dirs.add(abs(trpcOutDir(g, cfg)));
676
1217
  if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
677
1218
  if (g.kind === "zod") dirs.add(abs(g.path ?? "src/validators/zod"));
678
1219
  if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
@@ -689,7 +1230,7 @@ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
689
1230
  );
690
1231
  for (const g of cfg.generators) {
691
1232
  const t = g.template;
692
- if (!t || t === "standard" || t === "minimal") continue;
1233
+ if (!t || t === "standard" || t === "minimal" || t === "service") continue;
693
1234
  let pkgDir = null;
694
1235
  try {
695
1236
  const pkg = req.resolve(`${t}/package.json`, { paths: [cwd] });
@@ -731,6 +1272,51 @@ function computeWatchTargets(cfg, cwd = process.cwd()) {
731
1272
  return [...targets];
732
1273
  }
733
1274
 
1275
+ // src/trpc-options.ts
1276
+ function trpcOptions(g, cfg, servicesDir) {
1277
+ return {
1278
+ outputDir: trpcOutDir(g, cfg),
1279
+ template: g.template,
1280
+ includeRelations: g.includeRelations,
1281
+ naming: g.naming,
1282
+ outputHeader: g.outputHeader,
1283
+ format: g.format,
1284
+ importExtension: g.importExtension,
1285
+ validation: g.validation,
1286
+ databaseInjection: g.databaseInjection,
1287
+ // Where the service generator is actually writing, so `template: 'service'` emits an import
1288
+ // of a module that exists. The generator defaults this to `src/services`, which is right only
1289
+ // by coincidence for a config that puts them elsewhere.
1290
+ servicesDir
1291
+ };
1292
+ }
1293
+
1294
+ // src/validation-options.ts
1295
+ function validationOptions(g, cfg, outDir, caps = {}) {
1296
+ return {
1297
+ outDir,
1298
+ outputHeader: g.outputHeader,
1299
+ format: g.format,
1300
+ schemaSuffix: g.schemaSuffix,
1301
+ fileSuffix: g.fileSuffix,
1302
+ importExtension: g.importExtension,
1303
+ affix: g.affix,
1304
+ coerceDates: g.coerceDates,
1305
+ applyDefaults: g.applyDefaults,
1306
+ duplicateFinder: g.duplicateFinder,
1307
+ nestedSchemas: g.nestedSchemas,
1308
+ nestedDepth: g.nestedDepth,
1309
+ // Only where the generator can act on them, so an unsupported option is absent rather than
1310
+ // present and ignored.
1311
+ ...caps.schemaTypes ? {
1312
+ // Needed by both: the reference is resolved relative to the emitted file.
1313
+ schemaPath: cfg.schema,
1314
+ typedJson: g.typedJson,
1315
+ typedColumns: g.typedColumns
1316
+ } : {}
1317
+ };
1318
+ }
1319
+
734
1320
  // src/drift.ts
735
1321
  var import_node_fs = require("fs");
736
1322
  var import_node_path = __toESM(require("path"), 1);
@@ -1010,12 +1596,34 @@ program.command("generate").description("Run configured generators (drzl.config.
1010
1596
  templateOptions: g.templateOptions,
1011
1597
  importExtension: g.importExtension,
1012
1598
  validation: g.validation,
1599
+ // Documented on this generator since it was added and never reachable from a config
1600
+ // file, because the config schema had no such key and zod stripped it in silence.
1601
+ databaseInjection: g.databaseInjection,
1013
1602
  servicesDir,
1014
1603
  onProgress: ({ index }) => progress.update(index)
1015
1604
  });
1016
1605
  progress.stop();
1017
1606
  (0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (${g.kind}): ${files.length} files`));
1018
1607
  files.forEach((f) => console.log(" -", import_chalk2.default.cyan(f)));
1608
+ } else if (g.kind === "trpc") {
1609
+ try {
1610
+ const { TRPCGenerator: TRPCGenerator2 } = await loadGenerator(
1611
+ "@drzl/generator-trpc",
1612
+ () => Promise.resolve().then(() => (init_dist(), dist_exports))
1613
+ );
1614
+ const gen = new TRPCGenerator2(analysis);
1615
+ const { files } = await gen.generate({
1616
+ ...trpcOptions(g, cfg, servicesDir),
1617
+ onProgress: ({ index }) => progress.update(index)
1618
+ });
1619
+ progress.stop();
1620
+ (0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (trpc): ${files.length} files`));
1621
+ files.forEach((f) => console.log(" -", import_chalk2.default.cyan(f)));
1622
+ } catch (e) {
1623
+ progress.stop();
1624
+ reportGeneratorFailure(g.kind, e);
1625
+ process.exit(1);
1626
+ }
1019
1627
  } else if (g.kind === "service") {
1020
1628
  try {
1021
1629
  const { ServiceGenerator } = await loadGenerator(
@@ -1031,7 +1639,12 @@ program.command("generate").description("Run configured generators (drzl.config.
1031
1639
  dataAccess: g.dataAccess,
1032
1640
  dbImportPath: g.dbImportPath,
1033
1641
  schemaImportPath: g.schemaImportPath,
1034
- importExtension: g.importExtension
1642
+ importExtension: g.importExtension,
1643
+ // The other half of `databaseInjection`. A router generator in injection mode
1644
+ // emits `Service.getById(ctx.db, id)`, and only a service generated in the same
1645
+ // mode has a `db` parameter to receive it. This branch never passed the option, so
1646
+ // the two halves of one generated project disagreed about the signature.
1647
+ databaseInjection: g.databaseInjection
1035
1648
  });
1036
1649
  progress.stop();
1037
1650
  (0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (service): ${files.length} files`));
@@ -1102,7 +1715,7 @@ program.command("generate").description("Run configured generators (drzl.config.
1102
1715
  try {
1103
1716
  const { JsonSchemaGenerator: JsonSchemaGenerator2 } = await loadGenerator(
1104
1717
  "@drzl/generator-json-schema",
1105
- () => Promise.resolve().then(() => (init_dist(), dist_exports))
1718
+ () => Promise.resolve().then(() => (init_dist2(), dist_exports2))
1106
1719
  );
1107
1720
  const gen = new JsonSchemaGenerator2(analysis);
1108
1721
  const target = g.path ?? "src/validators/json-schema";
@@ -1196,7 +1809,34 @@ program.command("generate:orpc").argument("<schema>", "path to drizzle schema (T
1196
1809
  process.exit(1);
1197
1810
  }
1198
1811
  });
1199
- program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option("--pipeline <name>", "all | analyze | generate-orpc", "all").option("--debounce <ms>", "debounce ms", "200").option("--json", "emit JSON logs", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
1812
+ 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) => {
1813
+ try {
1814
+ const analyzer = new import_analyzer.SchemaAnalyzer(schema);
1815
+ const analysis = await analyzer.analyze({
1816
+ includeRelations: !!opts.includeRelations,
1817
+ validateConstraints: true
1818
+ });
1819
+ const { TRPCGenerator: TRPCGenerator2 } = await loadGenerator(
1820
+ "@drzl/generator-trpc",
1821
+ () => Promise.resolve().then(() => (init_dist(), dist_exports))
1822
+ );
1823
+ const gen = new TRPCGenerator2(analysis);
1824
+ const { files } = await gen.generate({
1825
+ outputDir: opts.outDir,
1826
+ template: opts.template,
1827
+ includeRelations: !!opts.includeRelations,
1828
+ // Only consulted by `--template service`, and passed unconditionally so this command
1829
+ // cannot become the branch that forgets it.
1830
+ servicesDir: opts.servicesDir
1831
+ });
1832
+ console.log(import_chalk2.default.green(`Generated:`), files.map((f) => import_chalk2.default.cyan(f)).join(", "));
1833
+ maybeShowSponsorMessage({ reason: "generate:trpc" });
1834
+ } catch (e) {
1835
+ reportGeneratorFailure("trpc", e);
1836
+ process.exit(1);
1837
+ }
1838
+ });
1839
+ program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option("--pipeline <name>", "all | analyze | generate-orpc | generate-trpc", "all").option("--debounce <ms>", "debounce ms", "200").option("--json", "emit JSON logs", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
1200
1840
  let cfg = await loadConfig(opts.config);
1201
1841
  if (!cfg) {
1202
1842
  console.error(import_chalk2.default.red("No config found. Create drzl.config.ts or pass --config."));
@@ -1296,8 +1936,13 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1296
1936
  return;
1297
1937
  }
1298
1938
  const newFiles = [];
1939
+ const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
1940
+ const PIPELINE_KINDS = {
1941
+ "generate-orpc": "orpc",
1942
+ "generate-trpc": "trpc"
1943
+ };
1299
1944
  for (const g of cfg.generators) {
1300
- if (opts.pipeline !== "all" && !(opts.pipeline === "generate-orpc" && g.kind === "orpc")) {
1945
+ if (opts.pipeline !== "all" && PIPELINE_KINDS[opts.pipeline] !== g.kind) {
1301
1946
  continue;
1302
1947
  }
1303
1948
  if (g.kind === "orpc") {
@@ -1311,13 +1956,32 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1311
1956
  format: g.format,
1312
1957
  templateOptions: g.templateOptions,
1313
1958
  importExtension: g.importExtension,
1314
- validation: g.validation
1959
+ validation: g.validation,
1960
+ databaseInjection: g.databaseInjection,
1961
+ servicesDir
1315
1962
  });
1316
1963
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1317
1964
  import_chalk2.default.green(`Generated (${g.kind}):`),
1318
1965
  files.map((f) => import_chalk2.default.cyan(f)).join(", ")
1319
1966
  );
1320
1967
  newFiles.push(...files);
1968
+ } else if (g.kind === "trpc") {
1969
+ try {
1970
+ const { TRPCGenerator: TRPCGenerator2 } = await loadGenerator(
1971
+ "@drzl/generator-trpc",
1972
+ () => Promise.resolve().then(() => (init_dist(), dist_exports))
1973
+ );
1974
+ const gen = new TRPCGenerator2(analysis);
1975
+ const { files } = await gen.generate(trpcOptions(g, cfg, servicesDir));
1976
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1977
+ import_chalk2.default.green(`Generated (trpc): ${files.length} files`),
1978
+ files.map((f) => import_chalk2.default.cyan(f)).join(", ")
1979
+ );
1980
+ newFiles.push(...files);
1981
+ } catch (e) {
1982
+ reportGeneratorFailure(g.kind, e);
1983
+ return;
1984
+ }
1321
1985
  } else if (g.kind === "service") {
1322
1986
  try {
1323
1987
  const { ServiceGenerator } = await loadGenerator(
@@ -1333,7 +1997,8 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1333
1997
  dataAccess: g.dataAccess,
1334
1998
  dbImportPath: g.dbImportPath,
1335
1999
  schemaImportPath: g.schemaImportPath,
1336
- importExtension: g.importExtension
2000
+ importExtension: g.importExtension,
2001
+ databaseInjection: g.databaseInjection
1337
2002
  });
1338
2003
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1339
2004
  import_chalk2.default.green(`Generated (service): ${files.length} files`),
@@ -1352,15 +2017,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1352
2017
  );
1353
2018
  const gen = new ZodGenerator(analysis);
1354
2019
  const target = g.path ?? "src/validators/zod";
1355
- const files = await gen.generate({
1356
- outDir: target,
1357
- outputHeader: g.outputHeader,
1358
- format: g.format,
1359
- schemaSuffix: g.schemaSuffix,
1360
- fileSuffix: g.fileSuffix,
1361
- importExtension: g.importExtension,
1362
- affix: g.affix
1363
- });
2020
+ const files = await gen.generate(
2021
+ validationOptions(g, cfg, target, { schemaTypes: true })
2022
+ );
1364
2023
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1365
2024
  import_chalk2.default.green(`Generated (zod): ${files.length} files`),
1366
2025
  files.map((f) => import_chalk2.default.cyan(f)).join(", ")
@@ -1378,15 +2037,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1378
2037
  );
1379
2038
  const gen = new ValibotGenerator(analysis);
1380
2039
  const target = g.path ?? "src/validators/valibot";
1381
- const files = await gen.generate({
1382
- outDir: target,
1383
- outputHeader: g.outputHeader,
1384
- format: g.format,
1385
- schemaSuffix: g.schemaSuffix,
1386
- fileSuffix: g.fileSuffix,
1387
- importExtension: g.importExtension,
1388
- affix: g.affix
1389
- });
2040
+ const files = await gen.generate(
2041
+ validationOptions(g, cfg, target, { schemaTypes: true })
2042
+ );
1390
2043
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1391
2044
  import_chalk2.default.green(`Generated (valibot): ${files.length} files`),
1392
2045
  files.map((f) => import_chalk2.default.cyan(f)).join(", ")
@@ -1404,17 +2057,54 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1404
2057
  );
1405
2058
  const gen = new ArkTypeGenerator(analysis);
1406
2059
  const target = g.path ?? "src/validators/arktype";
2060
+ const files = await gen.generate(
2061
+ validationOptions(g, cfg, target, { schemaTypes: false })
2062
+ );
2063
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
2064
+ import_chalk2.default.green(`Generated (arktype): ${files.length} files`),
2065
+ files.map((f) => import_chalk2.default.cyan(f)).join(", ")
2066
+ );
2067
+ newFiles.push(...files);
2068
+ } catch (e) {
2069
+ reportGeneratorFailure(g.kind, e);
2070
+ return;
2071
+ }
2072
+ } else if (g.kind === "typebox") {
2073
+ try {
2074
+ const { TypeBoxGenerator } = await loadGenerator(
2075
+ "@drzl/generator-typebox",
2076
+ () => import("@drzl/generator-typebox")
2077
+ );
2078
+ const gen = new TypeBoxGenerator(analysis);
2079
+ const target = g.path ?? "src/validators/typebox";
2080
+ const files = await gen.generate(
2081
+ validationOptions(g, cfg, target, { schemaTypes: true })
2082
+ );
2083
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
2084
+ import_chalk2.default.green(`Generated (typebox): ${files.length} files`),
2085
+ files.map((f) => import_chalk2.default.cyan(f)).join(", ")
2086
+ );
2087
+ newFiles.push(...files);
2088
+ } catch (e) {
2089
+ reportGeneratorFailure(g.kind, e);
2090
+ return;
2091
+ }
2092
+ } else if (g.kind === "json-schema") {
2093
+ try {
2094
+ const { JsonSchemaGenerator: JsonSchemaGenerator2 } = await loadGenerator(
2095
+ "@drzl/generator-json-schema",
2096
+ () => Promise.resolve().then(() => (init_dist2(), dist_exports2))
2097
+ );
2098
+ const gen = new JsonSchemaGenerator2(analysis);
2099
+ const target = g.path ?? "src/validators/json-schema";
1407
2100
  const files = await gen.generate({
1408
- outDir: target,
1409
- outputHeader: g.outputHeader,
1410
- format: g.format,
1411
- schemaSuffix: g.schemaSuffix,
1412
- fileSuffix: g.fileSuffix,
1413
- importExtension: g.importExtension,
1414
- affix: g.affix
2101
+ // JSON Schema is data, so nothing here references a type from the schema module.
2102
+ ...validationOptions(g, cfg, target, { schemaTypes: false }),
2103
+ target: g.target,
2104
+ components: g.components
1415
2105
  });
1416
2106
  opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1417
- import_chalk2.default.green(`Generated (arktype): ${files.length} files`),
2107
+ import_chalk2.default.green(`Generated (json-schema): ${files.length} files`),
1418
2108
  files.map((f) => import_chalk2.default.cyan(f)).join(", ")
1419
2109
  );
1420
2110
  newFiles.push(...files);
@@ -1478,6 +2168,8 @@ program.command("init").description("Scaffold a drzl.config.ts").option("-y, --y
1478
2168
  outDir: 'src/api',
1479
2169
  analyzer: { includeRelations: true, validateConstraints: true },
1480
2170
  generators: [
2171
+ // For tRPC instead: { kind: 'trpc', template: 'standard', includeRelations: true }
2172
+ // To run both, give one of them its own \`path\`; they share \`outDir\` otherwise.
1481
2173
  { kind: 'orpc', template: 'standard', includeRelations: true }
1482
2174
  ]
1483
2175
  } as const