@veloxts/cli 0.7.2 → 0.7.4

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.
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Sync Orchestrator
3
+ *
4
+ * Wires together the 5-stage `velox sync` pipeline:
5
+ * 1. Analyze Prisma schema -> SyncModelInfo[]
6
+ * 2. Detect existing code -> ExistingCodeMap
7
+ * 3. Prompt user for choices -> ModelChoices[]
8
+ * 4. Build generation plan -> SyncPlan
9
+ * 5. Generate files + register in router
10
+ *
11
+ * Supports --dry-run, --force, and --skip-registration flags.
12
+ */
13
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
14
+ import { dirname, relative } from 'node:path';
15
+ import * as p from '@clack/prompts';
16
+ import pc from 'picocolors';
17
+ import { registerProcedures } from '../generators/utils/router-integration.js';
18
+ import { executeWithRollback, saveOriginal, trackCreated } from '../generators/utils/snapshot.js';
19
+ import { analyzeSchema } from './analyzer.js';
20
+ import { detectExisting } from './detector.js';
21
+ import { buildPlan } from './planner.js';
22
+ import { generateProcedureFile } from './procedure-generator.js';
23
+ import { promptAllModels } from './prompter.js';
24
+ import { generateSchemaFile } from './schema-generator.js';
25
+ // ============================================================================
26
+ // Public API
27
+ // ============================================================================
28
+ /**
29
+ * Execute the full sync pipeline.
30
+ *
31
+ * @param projectRoot - Absolute path to the project root directory
32
+ * @param options - CLI flags (dryRun, force, skipRegistration)
33
+ * @returns Summary of files created, overwritten, skipped, registered, and errors
34
+ */
35
+ export async function executeSync(projectRoot, options) {
36
+ // ── Stage 1: Analyze ────────────────────────────────────────
37
+ const models = analyzeSchema(projectRoot);
38
+ if (models.length === 0) {
39
+ p.log.warn('No models found in Prisma schema. Nothing to sync.');
40
+ return emptyResult();
41
+ }
42
+ // ── Stage 2: Detect ─────────────────────────────────────────
43
+ const existing = detectExisting(projectRoot, models);
44
+ // Show discovery summary
45
+ const existingProcCount = existing.procedures.size;
46
+ const existingSchemaCount = existing.schemas.size;
47
+ p.log.info(`Found ${pc.cyan(String(models.length))} model${models.length === 1 ? '' : 's'}` +
48
+ (existingProcCount > 0 || existingSchemaCount > 0
49
+ ? ` (${existingProcCount} existing procedure${existingProcCount === 1 ? '' : 's'}, ` +
50
+ `${existingSchemaCount} existing schema${existingSchemaCount === 1 ? '' : 's'})`
51
+ : ''));
52
+ // ── Stage 3: Prompt ─────────────────────────────────────────
53
+ let choices;
54
+ if (options.force) {
55
+ choices = models.map((model) => buildDefaultChoices(model));
56
+ }
57
+ else {
58
+ const prompted = await promptAllModels(models, existing);
59
+ if (prompted === null) {
60
+ p.log.warn('Cancelled.');
61
+ return emptyResult();
62
+ }
63
+ choices = prompted;
64
+ }
65
+ // Check if anything was selected
66
+ const activeChoices = choices.filter((c) => c.action !== 'skip');
67
+ if (activeChoices.length === 0) {
68
+ p.log.warn('All models skipped. Nothing to generate.');
69
+ return emptyResult();
70
+ }
71
+ // ── Stage 4: Plan ───────────────────────────────────────────
72
+ const plan = buildPlan(models, choices, projectRoot);
73
+ // Show plan summary
74
+ displayPlanSummary(plan, projectRoot);
75
+ // Dry run: show plan and exit
76
+ if (options.dryRun) {
77
+ p.log.info(pc.dim('Dry run — no files were written.'));
78
+ return buildDryRunResult(plan);
79
+ }
80
+ // Confirm before writing (unless --force)
81
+ if (!options.force) {
82
+ const totalFiles = plan.schemas.length + plan.procedures.length;
83
+ const confirmed = await p.confirm({
84
+ message: `Generate ${totalFiles} file${totalFiles === 1 ? '' : 's'}?`,
85
+ initialValue: true,
86
+ });
87
+ if (p.isCancel(confirmed) || !confirmed) {
88
+ p.log.warn('Cancelled.');
89
+ return emptyResult();
90
+ }
91
+ }
92
+ // ── Stage 5: Generate ───────────────────────────────────────
93
+ return executeWithRollback(async (snapshot) => {
94
+ return generateFiles(snapshot, plan, projectRoot, options);
95
+ });
96
+ }
97
+ // ============================================================================
98
+ // File Generation
99
+ // ============================================================================
100
+ /**
101
+ * Generate all planned files within a snapshot transaction.
102
+ */
103
+ async function generateFiles(snapshot, plan, projectRoot, options) {
104
+ const created = [];
105
+ const overwritten = [];
106
+ const skipped = [];
107
+ const registered = [];
108
+ const errors = [];
109
+ // ── Write schema files ──────────────────────────────────────
110
+ for (const schemaPlan of plan.schemas) {
111
+ try {
112
+ const content = generateSchemaFile(schemaPlan);
113
+ const outputPath = schemaPlan.outputPath;
114
+ mkdirSync(dirname(outputPath), { recursive: true });
115
+ if (schemaPlan.action === 'overwrite' && existsSync(outputPath)) {
116
+ saveOriginal(snapshot, outputPath);
117
+ writeFileSync(outputPath, content, 'utf-8');
118
+ overwritten.push(outputPath);
119
+ }
120
+ else {
121
+ writeFileSync(outputPath, content, 'utf-8');
122
+ trackCreated(snapshot, outputPath);
123
+ created.push(outputPath);
124
+ }
125
+ }
126
+ catch (err) {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ errors.push(`Schema ${schemaPlan.model.name}: ${msg}`);
129
+ }
130
+ }
131
+ // ── Write procedure files ───────────────────────────────────
132
+ for (const procPlan of plan.procedures) {
133
+ try {
134
+ const content = generateProcedureFile(procPlan);
135
+ const outputPath = procPlan.outputPath;
136
+ mkdirSync(dirname(outputPath), { recursive: true });
137
+ if (procPlan.action === 'overwrite' && existsSync(outputPath)) {
138
+ saveOriginal(snapshot, outputPath);
139
+ writeFileSync(outputPath, content, 'utf-8');
140
+ overwritten.push(outputPath);
141
+ }
142
+ else {
143
+ writeFileSync(outputPath, content, 'utf-8');
144
+ trackCreated(snapshot, outputPath);
145
+ created.push(outputPath);
146
+ }
147
+ }
148
+ catch (err) {
149
+ const msg = err instanceof Error ? err.message : String(err);
150
+ errors.push(`Procedure ${procPlan.model.name}: ${msg}`);
151
+ }
152
+ }
153
+ // ── Register procedures in router ───────────────────────────
154
+ if (!options.skipRegistration) {
155
+ for (const reg of plan.registrations) {
156
+ try {
157
+ const result = registerProcedures(projectRoot, reg.entityName, reg.procedureVarName, false);
158
+ if (result.success) {
159
+ registered.push(reg.procedureVarName);
160
+ }
161
+ else if (result.error) {
162
+ errors.push(`Registration ${reg.procedureVarName}: ${result.error}`);
163
+ }
164
+ }
165
+ catch (err) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ errors.push(`Registration ${reg.procedureVarName}: ${msg}`);
168
+ }
169
+ }
170
+ }
171
+ // ── Display results ─────────────────────────────────────────
172
+ displayResults(created, overwritten, registered, errors, projectRoot);
173
+ return { created, overwritten, skipped, registered, errors };
174
+ }
175
+ // ============================================================================
176
+ // Display Helpers
177
+ // ============================================================================
178
+ /**
179
+ * Display a summary of the generation plan before executing.
180
+ */
181
+ function displayPlanSummary(plan, projectRoot) {
182
+ const lines = [];
183
+ if (plan.schemas.length > 0) {
184
+ lines.push(pc.bold('Schemas:'));
185
+ for (const s of plan.schemas) {
186
+ const rel = relative(projectRoot, s.outputPath);
187
+ const tag = s.action === 'overwrite' ? pc.yellow('overwrite') : pc.green('create');
188
+ lines.push(` ${tag} ${rel}`);
189
+ }
190
+ }
191
+ if (plan.procedures.length > 0) {
192
+ lines.push(pc.bold('Procedures:'));
193
+ for (const proc of plan.procedures) {
194
+ const rel = relative(projectRoot, proc.outputPath);
195
+ const tag = proc.action === 'overwrite' ? pc.yellow('overwrite') : pc.green('create');
196
+ lines.push(` ${tag} ${rel}`);
197
+ }
198
+ }
199
+ if (plan.registrations.length > 0) {
200
+ lines.push(pc.bold('Registrations:'));
201
+ for (const reg of plan.registrations) {
202
+ lines.push(` ${pc.cyan('register')} ${reg.procedureVarName} -> ${reg.importPath}`);
203
+ }
204
+ }
205
+ if (lines.length > 0) {
206
+ p.log.info(lines.join('\n'));
207
+ }
208
+ }
209
+ /**
210
+ * Display the final results after generation.
211
+ */
212
+ function displayResults(created, overwritten, registered, errors, projectRoot) {
213
+ if (created.length > 0) {
214
+ for (const filePath of created) {
215
+ p.log.success(`${pc.green('created')} ${relative(projectRoot, filePath)}`);
216
+ }
217
+ }
218
+ if (overwritten.length > 0) {
219
+ for (const filePath of overwritten) {
220
+ p.log.success(`${pc.yellow('overwritten')} ${relative(projectRoot, filePath)}`);
221
+ }
222
+ }
223
+ if (registered.length > 0) {
224
+ for (const name of registered) {
225
+ p.log.success(`${pc.cyan('registered')} ${name}`);
226
+ }
227
+ }
228
+ if (errors.length > 0) {
229
+ for (const msg of errors) {
230
+ p.log.error(msg);
231
+ }
232
+ }
233
+ }
234
+ // ============================================================================
235
+ // Default Choices (--force mode)
236
+ // ============================================================================
237
+ /**
238
+ * Build default ModelChoices for a model (used when --force is set).
239
+ * Generates all CRUD, uses output strategy, includes no relations.
240
+ */
241
+ function buildDefaultChoices(model) {
242
+ return {
243
+ model: model.name,
244
+ action: 'generate',
245
+ outputStrategy: 'output',
246
+ crud: {
247
+ get: true,
248
+ list: true,
249
+ create: true,
250
+ update: true,
251
+ delete: true,
252
+ },
253
+ schemaRelations: [],
254
+ includeRelations: [],
255
+ fieldVisibility: undefined,
256
+ };
257
+ }
258
+ // ============================================================================
259
+ // Result Helpers
260
+ // ============================================================================
261
+ /**
262
+ * Build an empty SyncResult (for cancellation or no-op cases).
263
+ */
264
+ function emptyResult() {
265
+ return {
266
+ created: [],
267
+ overwritten: [],
268
+ skipped: [],
269
+ registered: [],
270
+ errors: [],
271
+ };
272
+ }
273
+ /**
274
+ * Build a SyncResult for dry-run mode (no files written).
275
+ */
276
+ function buildDryRunResult(plan) {
277
+ const created = [];
278
+ const overwritten = [];
279
+ for (const s of plan.schemas) {
280
+ if (s.action === 'overwrite') {
281
+ overwritten.push(s.outputPath);
282
+ }
283
+ else {
284
+ created.push(s.outputPath);
285
+ }
286
+ }
287
+ for (const proc of plan.procedures) {
288
+ if (proc.action === 'overwrite') {
289
+ overwritten.push(proc.outputPath);
290
+ }
291
+ else {
292
+ created.push(proc.outputPath);
293
+ }
294
+ }
295
+ return {
296
+ created,
297
+ overwritten,
298
+ skipped: [],
299
+ registered: plan.registrations.map((r) => r.procedureVarName),
300
+ errors: [],
301
+ };
302
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Sync Planner
3
+ *
4
+ * Transforms user choices from the prompter stage into a concrete
5
+ * generation plan consumed by the generator stage.
6
+ *
7
+ * For each model the user chose to generate or regenerate, the planner
8
+ * computes output paths, import paths, field/relation filtering, and
9
+ * router registration metadata.
10
+ */
11
+ import type { ModelChoices, SyncModelInfo, SyncPlan } from './types.js';
12
+ /**
13
+ * Build a complete generation plan from analyzer models and user choices.
14
+ *
15
+ * Skipped models produce no plan entries. For each non-skipped model,
16
+ * the planner produces a schema file plan, a procedure file plan,
17
+ * and a router registration plan.
18
+ *
19
+ * @param models - Model metadata from the analyzer stage
20
+ * @param choices - User selections from the prompter stage
21
+ * @param projectRoot - Absolute path to the project root directory
22
+ * @returns A complete plan ready for the generator stage
23
+ */
24
+ export declare function buildPlan(models: readonly SyncModelInfo[], choices: readonly ModelChoices[], projectRoot: string): SyncPlan;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Sync Planner
3
+ *
4
+ * Transforms user choices from the prompter stage into a concrete
5
+ * generation plan consumed by the generator stage.
6
+ *
7
+ * For each model the user chose to generate or regenerate, the planner
8
+ * computes output paths, import paths, field/relation filtering, and
9
+ * router registration metadata.
10
+ */
11
+ import { join } from 'node:path';
12
+ import { deriveEntityNames, toKebabCase } from '../generators/utils/naming.js';
13
+ /**
14
+ * Build a complete generation plan from analyzer models and user choices.
15
+ *
16
+ * Skipped models produce no plan entries. For each non-skipped model,
17
+ * the planner produces a schema file plan, a procedure file plan,
18
+ * and a router registration plan.
19
+ *
20
+ * @param models - Model metadata from the analyzer stage
21
+ * @param choices - User selections from the prompter stage
22
+ * @param projectRoot - Absolute path to the project root directory
23
+ * @returns A complete plan ready for the generator stage
24
+ */
25
+ export function buildPlan(models, choices, projectRoot) {
26
+ const schemas = [];
27
+ const procedures = [];
28
+ const registrations = [];
29
+ for (const choice of choices) {
30
+ if (choice.action === 'skip') {
31
+ continue;
32
+ }
33
+ const names = deriveEntityNames(choice.model);
34
+ const model = models.find((m) => m.name === choice.model);
35
+ if (!model) {
36
+ continue;
37
+ }
38
+ const fileAction = choice.action === 'regenerate' ? 'overwrite' : 'create';
39
+ const pluralKebab = toKebabCase(names.plural);
40
+ // ── Schema file plan ──────────────────────────────────────────────
41
+ const allFields = model.fields;
42
+ const fields = allFields.filter((f) => !f.isAutoManaged && !f.isSensitive);
43
+ const relations = model.relations.filter((r) => choice.schemaRelations.includes(r.name));
44
+ schemas.push({
45
+ model,
46
+ outputPath: join(projectRoot, 'src/schemas', `${names.kebab}.schema.ts`),
47
+ outputStrategy: choice.outputStrategy,
48
+ allFields,
49
+ fields,
50
+ relations,
51
+ fieldVisibility: choice.fieldVisibility,
52
+ action: fileAction,
53
+ });
54
+ // ── Procedure file plan ───────────────────────────────────────────
55
+ const includeRelations = model.relations
56
+ .filter((r) => choice.includeRelations.includes(r.name))
57
+ .map((r) => r.name);
58
+ procedures.push({
59
+ model,
60
+ outputPath: join(projectRoot, 'src/procedures', `${pluralKebab}.ts`),
61
+ schemaImportPath: `../schemas/${names.kebab}.schema.js`,
62
+ outputStrategy: choice.outputStrategy,
63
+ crud: choice.crud,
64
+ includeRelations,
65
+ action: fileAction,
66
+ });
67
+ // ── Registration plan ─────────────────────────────────────────────
68
+ registrations.push({
69
+ procedureVarName: `${names.camel}Procedures`,
70
+ entityName: pluralKebab,
71
+ importPath: `./procedures/${pluralKebab}.js`,
72
+ });
73
+ }
74
+ return { schemas, procedures, registrations };
75
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Sync Procedure Generator
3
+ *
4
+ * Generates complete TypeScript source for procedure files from a
5
+ * `ProcedureFilePlan`. Supports two output strategies:
6
+ *
7
+ * - `output`: Procedures use `.output()` with plain Zod schemas
8
+ * - `resource`: Procedures use `resource()` / `resourceCollection()` projection
9
+ *
10
+ * Both strategies generate CRUD procedures based on `plan.crud` flags.
11
+ */
12
+ import type { ProcedureFilePlan } from './types.js';
13
+ /**
14
+ * Generate a complete TypeScript source string for a procedure file.
15
+ *
16
+ * The output is a syntactically valid TypeScript module containing:
17
+ * - Imports (router, zod, schemas)
18
+ * - A procedures collection with CRUD operations based on plan.crud
19
+ */
20
+ export declare function generateProcedureFile(plan: ProcedureFilePlan): string;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Sync Procedure Generator
3
+ *
4
+ * Generates complete TypeScript source for procedure files from a
5
+ * `ProcedureFilePlan`. Supports two output strategies:
6
+ *
7
+ * - `output`: Procedures use `.output()` with plain Zod schemas
8
+ * - `resource`: Procedures use `resource()` / `resourceCollection()` projection
9
+ *
10
+ * Both strategies generate CRUD procedures based on `plan.crud` flags.
11
+ */
12
+ import { deriveEntityNames, toKebabCase } from '../generators/utils/naming.js';
13
+ // ============================================================================
14
+ // Public API
15
+ // ============================================================================
16
+ /**
17
+ * Generate a complete TypeScript source string for a procedure file.
18
+ *
19
+ * The output is a syntactically valid TypeScript module containing:
20
+ * - Imports (router, zod, schemas)
21
+ * - A procedures collection with CRUD operations based on plan.crud
22
+ */
23
+ export function generateProcedureFile(plan) {
24
+ const names = deriveEntityNames(plan.model.name);
25
+ const lines = [];
26
+ // ── Imports ──────────────────────────────────────────
27
+ lines.push(generateImports(plan, names.pascal, names.pascalPlural));
28
+ lines.push('');
29
+ // ── Procedures Collection ───────────────────────────
30
+ lines.push(generateProceduresCollection(plan, names));
31
+ // Ensure trailing newline
32
+ return `${lines.join('\n')}\n`;
33
+ }
34
+ function generateImports(plan, pascal, pascalPlural) {
35
+ const lines = [];
36
+ // Router imports
37
+ const routerImports = ['procedure', 'procedures'];
38
+ if (plan.outputStrategy === 'resource') {
39
+ if (plan.crud.get) {
40
+ routerImports.push('resource');
41
+ }
42
+ if (plan.crud.list) {
43
+ routerImports.push('resourceCollection');
44
+ }
45
+ }
46
+ lines.push(`import { ${routerImports.join(', ')} } from '@veloxts/router';`);
47
+ // Zod import
48
+ lines.push("import { z } from 'zod';");
49
+ // Schema imports
50
+ const schemaNames = buildSchemaImportNames(plan, pascal, pascalPlural);
51
+ if (schemaNames.length > 0) {
52
+ lines.push(`import {\n ${schemaNames.join(',\n ')},\n} from '${plan.schemaImportPath}';`);
53
+ }
54
+ return lines.join('\n');
55
+ }
56
+ function buildSchemaImportNames(plan, pascal, pascalPlural) {
57
+ const schemas = [];
58
+ if (plan.outputStrategy === 'output') {
59
+ // Output strategy: import base or WithRelations schema
60
+ if (plan.crud.get || plan.crud.list) {
61
+ if (plan.includeRelations.length > 0) {
62
+ schemas.push(`${pascal}WithRelationsSchema`);
63
+ }
64
+ else {
65
+ schemas.push(`${pascal}Schema`);
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ // Resource strategy: import base schema (for resource projection)
71
+ if (plan.crud.get || plan.crud.list) {
72
+ schemas.push(`${pascal}Schema`);
73
+ }
74
+ }
75
+ if (plan.crud.create) {
76
+ schemas.push(`Create${pascal}Schema`);
77
+ }
78
+ if (plan.crud.update) {
79
+ schemas.push(`Update${pascal}Schema`);
80
+ }
81
+ if (plan.crud.list) {
82
+ schemas.push(`List${pascalPlural}Schema`);
83
+ }
84
+ return schemas;
85
+ }
86
+ // ============================================================================
87
+ // Procedures Collection
88
+ // ============================================================================
89
+ function generateProceduresCollection(plan, names) {
90
+ const routerName = toKebabCase(names.plural);
91
+ const collectionVar = `${names.camel}Procedures`;
92
+ const procedureEntries = [];
93
+ if (plan.crud.get) {
94
+ procedureEntries.push(generateGetProcedure(plan, names));
95
+ }
96
+ if (plan.crud.list) {
97
+ procedureEntries.push(generateListProcedure(plan, names));
98
+ }
99
+ if (plan.crud.create) {
100
+ procedureEntries.push(generateCreateProcedure(plan, names));
101
+ }
102
+ if (plan.crud.update) {
103
+ procedureEntries.push(generateUpdateProcedure(plan, names));
104
+ }
105
+ if (plan.crud.delete) {
106
+ procedureEntries.push(generateDeleteProcedure(plan, names));
107
+ }
108
+ const lines = [];
109
+ lines.push(`export const ${collectionVar} = procedures('${routerName}', {`);
110
+ lines.push(procedureEntries.join('\n\n'));
111
+ lines.push('});');
112
+ return lines.join('\n');
113
+ }
114
+ // ============================================================================
115
+ // Individual Procedure Generators
116
+ // ============================================================================
117
+ function generateGetProcedure(plan, names) {
118
+ const procName = `get${names.pascal}`;
119
+ const hasIncludes = plan.includeRelations.length > 0;
120
+ const lines = [];
121
+ lines.push(` ${procName}: procedure()`);
122
+ lines.push(` .input(z.object({ id: z.string().uuid() }))`);
123
+ if (plan.outputStrategy === 'output') {
124
+ const schemaName = hasIncludes ? `${names.pascal}WithRelationsSchema` : `${names.pascal}Schema`;
125
+ lines.push(` .output(${schemaName})`);
126
+ lines.push(` .query(async ({ input, ctx }) => {`);
127
+ lines.push(` return ctx.db.${names.camel}.findUniqueOrThrow({`);
128
+ lines.push(` where: { id: input.id },`);
129
+ if (hasIncludes) {
130
+ lines.push(generateIncludeBlock(plan.includeRelations, 8));
131
+ }
132
+ lines.push(` });`);
133
+ lines.push(` }),`);
134
+ }
135
+ else {
136
+ // Resource strategy: no .output(), use resource()
137
+ lines.push(` .query(async ({ input, ctx }) => {`);
138
+ lines.push(` const ${names.camel} = await ctx.db.${names.camel}.findUniqueOrThrow({`);
139
+ lines.push(` where: { id: input.id },`);
140
+ if (hasIncludes) {
141
+ lines.push(generateIncludeBlock(plan.includeRelations, 8));
142
+ }
143
+ lines.push(` });`);
144
+ lines.push(` return resource(${names.camel}, ${names.pascal}Schema.public);`);
145
+ lines.push(` }),`);
146
+ }
147
+ return lines.join('\n');
148
+ }
149
+ function generateListProcedure(plan, names) {
150
+ const procName = `list${names.pascalPlural}`;
151
+ const hasIncludes = plan.includeRelations.length > 0;
152
+ const lines = [];
153
+ lines.push(` ${procName}: procedure()`);
154
+ lines.push(` .input(List${names.pascalPlural}Schema)`);
155
+ if (plan.outputStrategy === 'output') {
156
+ lines.push(` .query(async ({ input, ctx }) => {`);
157
+ }
158
+ else {
159
+ lines.push(` .query(async ({ input, ctx }) => {`);
160
+ }
161
+ lines.push(` const { page, perPage } = input;`);
162
+ lines.push(` const [items, total] = await Promise.all([`);
163
+ lines.push(` ctx.db.${names.camel}.findMany({`);
164
+ lines.push(` skip: (page - 1) * perPage,`);
165
+ lines.push(` take: perPage,`);
166
+ lines.push(` orderBy: { createdAt: 'desc' },`);
167
+ if (hasIncludes) {
168
+ lines.push(generateIncludeBlock(plan.includeRelations, 10));
169
+ }
170
+ lines.push(` }),`);
171
+ lines.push(` ctx.db.${names.camel}.count(),`);
172
+ lines.push(` ]);`);
173
+ if (plan.outputStrategy === 'resource') {
174
+ lines.push(` return {`);
175
+ lines.push(` items: resourceCollection(items, ${names.pascal}Schema.public),`);
176
+ lines.push(` total, page, perPage,`);
177
+ lines.push(` };`);
178
+ }
179
+ else {
180
+ lines.push(` return { items, total, page, perPage };`);
181
+ }
182
+ lines.push(` }),`);
183
+ return lines.join('\n');
184
+ }
185
+ function generateCreateProcedure(plan, names) {
186
+ const procName = `create${names.pascal}`;
187
+ const userFkField = plan.model.fields.find((f) => f.isUserForeignKey);
188
+ const lines = [];
189
+ lines.push(` ${procName}: procedure()`);
190
+ lines.push(` .input(Create${names.pascal}Schema)`);
191
+ lines.push(` .mutation(async ({ input, ctx }) => {`);
192
+ if (userFkField) {
193
+ lines.push(` return ctx.db.${names.camel}.create({`);
194
+ lines.push(` data: { ...input, ${userFkField.name}: ctx.user.id },`);
195
+ lines.push(` });`);
196
+ }
197
+ else {
198
+ lines.push(` return ctx.db.${names.camel}.create({ data: input });`);
199
+ }
200
+ lines.push(` }),`);
201
+ return lines.join('\n');
202
+ }
203
+ function generateUpdateProcedure(_plan, names) {
204
+ const procName = `update${names.pascal}`;
205
+ const lines = [];
206
+ lines.push(` ${procName}: procedure()`);
207
+ lines.push(` .input(z.object({ id: z.string().uuid() }).merge(Update${names.pascal}Schema))`);
208
+ lines.push(` .mutation(async ({ input, ctx }) => {`);
209
+ lines.push(` const { id, ...data } = input;`);
210
+ lines.push(` return ctx.db.${names.camel}.update({ where: { id }, data });`);
211
+ lines.push(` }),`);
212
+ return lines.join('\n');
213
+ }
214
+ function generateDeleteProcedure(plan, names) {
215
+ const procName = `delete${names.pascal}`;
216
+ const lines = [];
217
+ lines.push(` ${procName}: procedure()`);
218
+ if (plan.model.isJoinTable && plan.model.uniqueConstraints.length > 0) {
219
+ // Join table: use compound unique key for delete
220
+ const constraintFields = plan.model.uniqueConstraints[0];
221
+ const compoundKeyName = constraintFields.join('_');
222
+ const inputFields = constraintFields.map((f) => `${f}: z.string().uuid()`).join(', ');
223
+ const whereFields = constraintFields.map((f) => `${f}: input.${f}`).join(', ');
224
+ lines.push(` .input(z.object({ ${inputFields} }))`);
225
+ lines.push(` .mutation(async ({ input, ctx }) => {`);
226
+ lines.push(` await ctx.db.${names.camel}.delete({`);
227
+ lines.push(` where: { ${compoundKeyName}: { ${whereFields} } },`);
228
+ lines.push(` });`);
229
+ lines.push(` return { success: true };`);
230
+ lines.push(` }),`);
231
+ }
232
+ else {
233
+ lines.push(` .input(z.object({ id: z.string().uuid() }))`);
234
+ lines.push(` .mutation(async ({ input, ctx }) => {`);
235
+ lines.push(` await ctx.db.${names.camel}.delete({ where: { id: input.id } });`);
236
+ lines.push(` return { success: true };`);
237
+ lines.push(` }),`);
238
+ }
239
+ return lines.join('\n');
240
+ }
241
+ // ============================================================================
242
+ // Include Block Helper
243
+ // ============================================================================
244
+ function generateIncludeBlock(relations, indent) {
245
+ const pad = ' '.repeat(indent);
246
+ const lines = [];
247
+ lines.push(`${pad}include: {`);
248
+ for (const rel of relations) {
249
+ lines.push(`${pad} ${rel}: true,`);
250
+ }
251
+ lines.push(`${pad}},`);
252
+ return lines.join('\n');
253
+ }