@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.
- package/CHANGELOG.md +26 -5
- package/dist/cli.js +2 -0
- package/dist/commands/sync.d.ts +22 -0
- package/dist/commands/sync.js +96 -0
- package/dist/generators/fields/types.d.ts +7 -0
- package/dist/generators/fields/types.js +29 -0
- package/dist/generators/generators/namespace.js +7 -2
- package/dist/generators/templates/namespace.d.ts +3 -0
- package/dist/generators/templates/namespace.js +85 -1
- package/dist/generators/utils/prisma-schema.d.ts +18 -0
- package/dist/generators/utils/prisma-schema.js +53 -2
- package/dist/sync/analyzer.d.ts +20 -0
- package/dist/sync/analyzer.js +277 -0
- package/dist/sync/detector.d.ts +18 -0
- package/dist/sync/detector.js +94 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.js +302 -0
- package/dist/sync/planner.d.ts +24 -0
- package/dist/sync/planner.js +75 -0
- package/dist/sync/procedure-generator.d.ts +20 -0
- package/dist/sync/procedure-generator.js +253 -0
- package/dist/sync/prompter.d.ts +18 -0
- package/dist/sync/prompter.js +312 -0
- package/dist/sync/schema-generator.d.ts +24 -0
- package/dist/sync/schema-generator.js +213 -0
- package/dist/sync/types.d.ts +219 -0
- package/dist/sync/types.js +9 -0
- package/package.json +6 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Prompter
|
|
3
|
+
*
|
|
4
|
+
* Interactive prompts for the `velox sync` command.
|
|
5
|
+
* Collects user choices for each Prisma model: action, CRUD operations,
|
|
6
|
+
* output strategy, relation inclusion, and field visibility.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExistingCodeMap, ModelChoices, SyncModelInfo } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Run the full interactive flow for all models.
|
|
11
|
+
* Returns null if user cancels at any point.
|
|
12
|
+
*/
|
|
13
|
+
export declare function promptAllModels(models: readonly SyncModelInfo[], existing: ExistingCodeMap): Promise<readonly ModelChoices[] | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Run interactive prompts for a single model.
|
|
16
|
+
* Returns null if user cancels (Ctrl+C).
|
|
17
|
+
*/
|
|
18
|
+
export declare function promptForModel(model: SyncModelInfo, existing: ExistingCodeMap): Promise<ModelChoices | null>;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Prompter
|
|
3
|
+
*
|
|
4
|
+
* Interactive prompts for the `velox sync` command.
|
|
5
|
+
* Collects user choices for each Prisma model: action, CRUD operations,
|
|
6
|
+
* output strategy, relation inclusion, and field visibility.
|
|
7
|
+
*/
|
|
8
|
+
import * as p from '@clack/prompts';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/** Models that default to resource output strategy */
|
|
14
|
+
const RESOURCE_DEFAULT_MODELS = new Set(['User', 'Message']);
|
|
15
|
+
/** Horizontal rule width for model headers */
|
|
16
|
+
const HEADER_WIDTH = 54;
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Public API
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Run the full interactive flow for all models.
|
|
22
|
+
* Returns null if user cancels at any point.
|
|
23
|
+
*/
|
|
24
|
+
export async function promptAllModels(models, existing) {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const model of models) {
|
|
27
|
+
const choices = await promptForModel(model, existing);
|
|
28
|
+
if (choices === null) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
results.push(choices);
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run interactive prompts for a single model.
|
|
37
|
+
* Returns null if user cancels (Ctrl+C).
|
|
38
|
+
*/
|
|
39
|
+
export async function promptForModel(model, existing) {
|
|
40
|
+
// Step 1: Display model header
|
|
41
|
+
displayModelHeader(model, existing);
|
|
42
|
+
// Step 2: Action selection
|
|
43
|
+
const action = await promptAction(model, existing);
|
|
44
|
+
if (action === null)
|
|
45
|
+
return null;
|
|
46
|
+
if (action === 'skip') {
|
|
47
|
+
return buildSkipChoices(model.name);
|
|
48
|
+
}
|
|
49
|
+
// Step 3: Output strategy
|
|
50
|
+
const outputStrategy = await promptOutputStrategy(model);
|
|
51
|
+
if (outputStrategy === null)
|
|
52
|
+
return null;
|
|
53
|
+
// Step 4: CRUD operations
|
|
54
|
+
const crud = await promptCrudOperations(model);
|
|
55
|
+
if (crud === null)
|
|
56
|
+
return null;
|
|
57
|
+
// Step 5: Schema relations (only if model has relations)
|
|
58
|
+
let schemaRelations = [];
|
|
59
|
+
if (model.relations.length > 0) {
|
|
60
|
+
const selected = await promptSchemaRelations(model);
|
|
61
|
+
if (selected === null)
|
|
62
|
+
return null;
|
|
63
|
+
schemaRelations = selected;
|
|
64
|
+
}
|
|
65
|
+
// Step 6: Include relations (only if schema relations were selected)
|
|
66
|
+
let includeRelations = [];
|
|
67
|
+
if (schemaRelations.length > 0) {
|
|
68
|
+
const selected = await promptIncludeRelations(schemaRelations);
|
|
69
|
+
if (selected === null)
|
|
70
|
+
return null;
|
|
71
|
+
includeRelations = selected;
|
|
72
|
+
}
|
|
73
|
+
// Step 7: Field visibility (only if resource strategy)
|
|
74
|
+
let fieldVisibility;
|
|
75
|
+
if (outputStrategy === 'resource') {
|
|
76
|
+
const visibility = await promptFieldVisibility(model);
|
|
77
|
+
if (visibility === null)
|
|
78
|
+
return null;
|
|
79
|
+
fieldVisibility = visibility;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
model: model.name,
|
|
83
|
+
action,
|
|
84
|
+
outputStrategy,
|
|
85
|
+
crud,
|
|
86
|
+
schemaRelations,
|
|
87
|
+
includeRelations,
|
|
88
|
+
fieldVisibility,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Step 1: Model Header
|
|
93
|
+
// ============================================================================
|
|
94
|
+
/**
|
|
95
|
+
* Display a formatted header for the model being prompted.
|
|
96
|
+
*/
|
|
97
|
+
function displayModelHeader(model, existing) {
|
|
98
|
+
const titlePart = `── ${model.name} `;
|
|
99
|
+
const remainingWidth = Math.max(0, HEADER_WIDTH - titlePart.length);
|
|
100
|
+
const ruler = titlePart + '─'.repeat(remainingWidth);
|
|
101
|
+
const nonAutoFields = model.fields.filter((f) => !f.isAutoManaged);
|
|
102
|
+
const fieldNames = nonAutoFields.map((f) => f.name).join(', ');
|
|
103
|
+
const relationLabels = model.relations.map((r) => formatRelationLabel(r)).join(', ');
|
|
104
|
+
const lines = [pc.bold(ruler), ` Fields: ${fieldNames || pc.dim('(none)')}`];
|
|
105
|
+
if (model.relations.length > 0) {
|
|
106
|
+
lines.push(` Relations: ${relationLabels}`);
|
|
107
|
+
}
|
|
108
|
+
const existingPath = existing.procedures.get(model.name);
|
|
109
|
+
if (existingPath) {
|
|
110
|
+
lines.push(` Existing: ${pc.dim(existingPath)}`);
|
|
111
|
+
}
|
|
112
|
+
p.log.info(lines.join('\n'));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Format a relation for display: `name(->RelatedModel)` or `name(->RelatedModel[])`.
|
|
116
|
+
*/
|
|
117
|
+
function formatRelationLabel(relation) {
|
|
118
|
+
const suffix = relation.kind === 'hasMany' ? '[]' : '';
|
|
119
|
+
return `${relation.name}(→${relation.relatedModel}${suffix})`;
|
|
120
|
+
}
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Step 2: Action Selection
|
|
123
|
+
// ============================================================================
|
|
124
|
+
/**
|
|
125
|
+
* Prompt the user for the action to take on a model.
|
|
126
|
+
* Returns 'generate', 'regenerate', or 'skip', or null if cancelled.
|
|
127
|
+
*/
|
|
128
|
+
async function promptAction(model, existing) {
|
|
129
|
+
const hasExisting = existing.procedures.has(model.name);
|
|
130
|
+
if (hasExisting) {
|
|
131
|
+
const action = await p.select({
|
|
132
|
+
message: `Action for ${model.name}?`,
|
|
133
|
+
options: [
|
|
134
|
+
{ value: 'skip', label: 'Skip (keep existing)' },
|
|
135
|
+
{ value: 'regenerate', label: 'Regenerate (overwrite)' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
if (p.isCancel(action))
|
|
139
|
+
return null;
|
|
140
|
+
return action;
|
|
141
|
+
}
|
|
142
|
+
const action = await p.select({
|
|
143
|
+
message: `Generate procedures for ${model.name}?`,
|
|
144
|
+
options: [
|
|
145
|
+
{ value: 'generate', label: 'Yes, generate' },
|
|
146
|
+
{ value: 'skip', label: 'Skip' },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
if (p.isCancel(action))
|
|
150
|
+
return null;
|
|
151
|
+
return action;
|
|
152
|
+
}
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Step 3: Output Strategy
|
|
155
|
+
// ============================================================================
|
|
156
|
+
/**
|
|
157
|
+
* Prompt for output strategy (plain .output() vs resource schema).
|
|
158
|
+
*/
|
|
159
|
+
async function promptOutputStrategy(model) {
|
|
160
|
+
const hasSensitiveFields = model.fields.some((f) => f.isSensitive);
|
|
161
|
+
const defaultToResource = hasSensitiveFields || RESOURCE_DEFAULT_MODELS.has(model.name);
|
|
162
|
+
const outputStrategy = await p.select({
|
|
163
|
+
message: 'Output strategy?',
|
|
164
|
+
options: [
|
|
165
|
+
{ value: 'output', label: '.output() \u2014 Same fields for all users' },
|
|
166
|
+
{ value: 'resource', label: '.resource() \u2014 Different fields per access level' },
|
|
167
|
+
],
|
|
168
|
+
initialValue: defaultToResource ? 'resource' : 'output',
|
|
169
|
+
});
|
|
170
|
+
if (p.isCancel(outputStrategy))
|
|
171
|
+
return null;
|
|
172
|
+
return outputStrategy;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Prompt for which CRUD operations to generate.
|
|
176
|
+
*/
|
|
177
|
+
async function promptCrudOperations(model) {
|
|
178
|
+
const allOps = ['get', 'list', 'create', 'update', 'delete'];
|
|
179
|
+
const crudOps = await p.multiselect({
|
|
180
|
+
message: 'CRUD operations:',
|
|
181
|
+
options: [
|
|
182
|
+
{ value: 'get', label: 'get' },
|
|
183
|
+
{ value: 'list', label: 'list' },
|
|
184
|
+
{ value: 'create', label: 'create' },
|
|
185
|
+
{ value: 'update', label: 'update' },
|
|
186
|
+
{ value: 'delete', label: 'delete' },
|
|
187
|
+
],
|
|
188
|
+
initialValues: model.isJoinTable ? ['create', 'delete'] : allOps,
|
|
189
|
+
required: true,
|
|
190
|
+
});
|
|
191
|
+
if (p.isCancel(crudOps))
|
|
192
|
+
return null;
|
|
193
|
+
const selected = new Set(crudOps);
|
|
194
|
+
return {
|
|
195
|
+
get: selected.has('get'),
|
|
196
|
+
list: selected.has('list'),
|
|
197
|
+
create: selected.has('create'),
|
|
198
|
+
update: selected.has('update'),
|
|
199
|
+
delete: selected.has('delete'),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Step 5: Schema Relations
|
|
204
|
+
// ============================================================================
|
|
205
|
+
/**
|
|
206
|
+
* Prompt for which relations to include in the Zod schema.
|
|
207
|
+
*/
|
|
208
|
+
async function promptSchemaRelations(model) {
|
|
209
|
+
const schemaRelations = await p.multiselect({
|
|
210
|
+
message: 'Include relations in Zod schema?',
|
|
211
|
+
options: model.relations.map((r) => ({
|
|
212
|
+
value: r.name,
|
|
213
|
+
label: `${r.name} (→${r.relatedModel}${r.kind === 'hasMany' ? '[]' : ''})`,
|
|
214
|
+
})),
|
|
215
|
+
initialValues: model.relations.filter((r) => r.kind === 'belongsTo').map((r) => r.name),
|
|
216
|
+
required: false,
|
|
217
|
+
});
|
|
218
|
+
if (p.isCancel(schemaRelations))
|
|
219
|
+
return null;
|
|
220
|
+
return schemaRelations;
|
|
221
|
+
}
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Step 6: Include Relations
|
|
224
|
+
// ============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Prompt for which of the selected schema relations to also fetch via Prisma includes.
|
|
227
|
+
*/
|
|
228
|
+
async function promptIncludeRelations(schemaRelations) {
|
|
229
|
+
const includeRelations = await p.multiselect({
|
|
230
|
+
message: 'Fetch relations in queries? (Prisma includes)',
|
|
231
|
+
options: schemaRelations.map((name) => ({
|
|
232
|
+
value: name,
|
|
233
|
+
label: name,
|
|
234
|
+
})),
|
|
235
|
+
initialValues: [...schemaRelations],
|
|
236
|
+
required: false,
|
|
237
|
+
});
|
|
238
|
+
if (p.isCancel(includeRelations))
|
|
239
|
+
return null;
|
|
240
|
+
return includeRelations;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Prompt for field visibility configuration (resource strategy only).
|
|
244
|
+
* Returns a map of field name -> visibility level for ALL non-auto fields.
|
|
245
|
+
*/
|
|
246
|
+
async function promptFieldVisibility(model) {
|
|
247
|
+
const nonAutoFields = model.fields.filter((f) => !f.isAutoManaged);
|
|
248
|
+
const fieldsToRestrict = await p.multiselect({
|
|
249
|
+
message: 'Any fields to restrict from public access?',
|
|
250
|
+
options: nonAutoFields.map((f) => ({
|
|
251
|
+
value: f.name,
|
|
252
|
+
label: f.name,
|
|
253
|
+
hint: f.isSensitive ? 'sensitive' : undefined,
|
|
254
|
+
})),
|
|
255
|
+
initialValues: nonAutoFields.filter((f) => f.isSensitive).map((f) => f.name),
|
|
256
|
+
required: false,
|
|
257
|
+
});
|
|
258
|
+
if (p.isCancel(fieldsToRestrict))
|
|
259
|
+
return null;
|
|
260
|
+
const restrictedSet = new Set(fieldsToRestrict);
|
|
261
|
+
// For each restricted field, ask for visibility level
|
|
262
|
+
const restrictedLevels = new Map();
|
|
263
|
+
for (const fieldName of fieldsToRestrict) {
|
|
264
|
+
const level = await p.select({
|
|
265
|
+
message: `Visibility for "${fieldName}"?`,
|
|
266
|
+
options: [
|
|
267
|
+
{ value: 'authenticated', label: 'authenticated \u2014 Logged-in users' },
|
|
268
|
+
{ value: 'admin', label: 'admin \u2014 Admin only' },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
if (p.isCancel(level))
|
|
272
|
+
return null;
|
|
273
|
+
restrictedLevels.set(fieldName, level);
|
|
274
|
+
}
|
|
275
|
+
// Build full visibility map: restricted fields get their level, others get 'public'
|
|
276
|
+
const visibility = new Map();
|
|
277
|
+
for (const field of nonAutoFields) {
|
|
278
|
+
if (restrictedSet.has(field.name)) {
|
|
279
|
+
const level = restrictedLevels.get(field.name);
|
|
280
|
+
if (level !== undefined) {
|
|
281
|
+
visibility.set(field.name, level);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
visibility.set(field.name, 'public');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return visibility;
|
|
289
|
+
}
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Helpers
|
|
292
|
+
// ============================================================================
|
|
293
|
+
/**
|
|
294
|
+
* Build a ModelChoices object for a skipped model.
|
|
295
|
+
*/
|
|
296
|
+
function buildSkipChoices(modelName) {
|
|
297
|
+
return {
|
|
298
|
+
model: modelName,
|
|
299
|
+
action: 'skip',
|
|
300
|
+
outputStrategy: 'output',
|
|
301
|
+
crud: {
|
|
302
|
+
get: false,
|
|
303
|
+
list: false,
|
|
304
|
+
create: false,
|
|
305
|
+
update: false,
|
|
306
|
+
delete: false,
|
|
307
|
+
},
|
|
308
|
+
schemaRelations: [],
|
|
309
|
+
includeRelations: [],
|
|
310
|
+
fieldVisibility: undefined,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates complete TypeScript source for Zod schema files from a
|
|
5
|
+
* `SchemaFilePlan`. Supports two output strategies:
|
|
6
|
+
*
|
|
7
|
+
* - `output`: Plain `z.object()` schemas with optional `WithRelationsSchema`
|
|
8
|
+
* - `resource`: `resourceSchema()` builder with visibility tiers
|
|
9
|
+
*
|
|
10
|
+
* Both strategies generate Create, Update, and List input schemas.
|
|
11
|
+
*/
|
|
12
|
+
import type { SchemaFilePlan } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Generate a complete TypeScript source string for a Zod schema file.
|
|
15
|
+
*
|
|
16
|
+
* The output is a syntactically valid TypeScript module containing:
|
|
17
|
+
* - Imports (zod, optionally resourceSchema from @veloxts/router)
|
|
18
|
+
* - Base/resource schema for all non-sensitive fields
|
|
19
|
+
* - WithRelationsSchema (output strategy only, when relations exist)
|
|
20
|
+
* - CreateSchema (excludes auto-managed, sensitive, and user FK fields)
|
|
21
|
+
* - UpdateSchema (same fields as Create, all optional)
|
|
22
|
+
* - ListSchema (page + perPage with defaults)
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateSchemaFile(plan: SchemaFilePlan): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates complete TypeScript source for Zod schema files from a
|
|
5
|
+
* `SchemaFilePlan`. Supports two output strategies:
|
|
6
|
+
*
|
|
7
|
+
* - `output`: Plain `z.object()` schemas with optional `WithRelationsSchema`
|
|
8
|
+
* - `resource`: `resourceSchema()` builder with visibility tiers
|
|
9
|
+
*
|
|
10
|
+
* Both strategies generate Create, Update, and List input schemas.
|
|
11
|
+
*/
|
|
12
|
+
import { deriveEntityNames } from '../generators/utils/naming.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Public API
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Generate a complete TypeScript source string for a Zod schema file.
|
|
18
|
+
*
|
|
19
|
+
* The output is a syntactically valid TypeScript module containing:
|
|
20
|
+
* - Imports (zod, optionally resourceSchema from @veloxts/router)
|
|
21
|
+
* - Base/resource schema for all non-sensitive fields
|
|
22
|
+
* - WithRelationsSchema (output strategy only, when relations exist)
|
|
23
|
+
* - CreateSchema (excludes auto-managed, sensitive, and user FK fields)
|
|
24
|
+
* - UpdateSchema (same fields as Create, all optional)
|
|
25
|
+
* - ListSchema (page + perPage with defaults)
|
|
26
|
+
*/
|
|
27
|
+
export function generateSchemaFile(plan) {
|
|
28
|
+
const names = deriveEntityNames(plan.model.name);
|
|
29
|
+
const lines = [];
|
|
30
|
+
// ── Imports ──────────────────────────────────────────
|
|
31
|
+
lines.push(generateImports(plan));
|
|
32
|
+
lines.push('');
|
|
33
|
+
// ── Base / Resource Schema ──────────────────────────
|
|
34
|
+
if (plan.outputStrategy === 'resource') {
|
|
35
|
+
lines.push(generateResourceSchema(plan, names.pascal));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
lines.push(generateBaseSchema(plan, names.pascal));
|
|
39
|
+
}
|
|
40
|
+
// ── WithRelationsSchema (output strategy only) ──────
|
|
41
|
+
if (plan.outputStrategy === 'output' && plan.relations.length > 0) {
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push(generateWithRelationsSchema(plan.relations, names.pascal));
|
|
44
|
+
}
|
|
45
|
+
// ── Input Schemas ───────────────────────────────────
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(generateInputSchemas(plan, names.pascal, names.pascalPlural));
|
|
48
|
+
// Ensure trailing newline
|
|
49
|
+
return `${lines.join('\n')}\n`;
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Import Generation
|
|
53
|
+
// ============================================================================
|
|
54
|
+
function generateImports(plan) {
|
|
55
|
+
const imports = ["import { z } from 'zod';"];
|
|
56
|
+
if (plan.outputStrategy === 'resource') {
|
|
57
|
+
imports.push("import { resourceSchema } from '@veloxts/router';");
|
|
58
|
+
}
|
|
59
|
+
return imports.join('\n');
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Base Schema (output strategy)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
function generateBaseSchema(plan, pascal) {
|
|
65
|
+
const nonSensitiveFields = plan.allFields.filter((f) => !f.isSensitive);
|
|
66
|
+
const fieldEntries = nonSensitiveFields.map((f) => ` ${f.name}: ${fieldToZod(f)},`);
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(`// ${SECTION_CHAR} Base Schema ${SECTION_PAD}`);
|
|
69
|
+
lines.push(`export const ${pascal}Schema = z.object({`);
|
|
70
|
+
lines.push(...fieldEntries);
|
|
71
|
+
lines.push('});');
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Resource Schema (resource strategy)
|
|
76
|
+
// ============================================================================
|
|
77
|
+
function generateResourceSchema(plan, pascal) {
|
|
78
|
+
const nonSensitiveFields = plan.allFields.filter((f) => !f.isSensitive);
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push(`// ${SECTION_CHAR} Resource Schema ${SECTION_PAD}`);
|
|
81
|
+
lines.push(`export const ${pascal}Schema = resourceSchema()`);
|
|
82
|
+
for (const field of nonSensitiveFields) {
|
|
83
|
+
const level = getVisibilityLevel(field.name, plan.fieldVisibility);
|
|
84
|
+
lines.push(` .${level}('${field.name}', ${fieldToZod(field)})`);
|
|
85
|
+
}
|
|
86
|
+
lines.push(' .build();');
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// WithRelationsSchema
|
|
91
|
+
// ============================================================================
|
|
92
|
+
function generateWithRelationsSchema(relations, pascal) {
|
|
93
|
+
const relEntries = relations.map((r) => {
|
|
94
|
+
if (r.kind === 'hasMany') {
|
|
95
|
+
return ` ${r.name}: z.array(z.object({ id: z.string() })),`;
|
|
96
|
+
}
|
|
97
|
+
return ` ${r.name}: z.object({ id: z.string() }),`;
|
|
98
|
+
});
|
|
99
|
+
const lines = [];
|
|
100
|
+
lines.push(`// ${SECTION_CHAR} With Relations Schema ${SECTION_PAD}`);
|
|
101
|
+
lines.push(`export const ${pascal}WithRelationsSchema = ${pascal}Schema.extend({`);
|
|
102
|
+
lines.push(...relEntries);
|
|
103
|
+
lines.push('});');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Input Schemas (Create, Update, List)
|
|
108
|
+
// ============================================================================
|
|
109
|
+
function generateInputSchemas(plan, pascal, pascalPlural) {
|
|
110
|
+
const inputFields = plan.fields.filter((f) => !f.isUserForeignKey);
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`// ${SECTION_CHAR} Input Schemas ${SECTION_PAD}`);
|
|
113
|
+
// ── CreateSchema ──
|
|
114
|
+
const createEntries = inputFields.map((f) => ` ${f.name}: ${fieldToCreateZod(f)},`);
|
|
115
|
+
lines.push(`export const Create${pascal}Schema = z.object({`);
|
|
116
|
+
lines.push(...createEntries);
|
|
117
|
+
lines.push('});');
|
|
118
|
+
lines.push('');
|
|
119
|
+
// ── UpdateSchema ──
|
|
120
|
+
const updateEntries = inputFields.map((f) => ` ${f.name}: ${fieldToUpdateZod(f)},`);
|
|
121
|
+
lines.push(`export const Update${pascal}Schema = z.object({`);
|
|
122
|
+
lines.push(...updateEntries);
|
|
123
|
+
lines.push('});');
|
|
124
|
+
lines.push('');
|
|
125
|
+
// ── ListSchema ──
|
|
126
|
+
lines.push(`export const List${pascalPlural}Schema = z.object({`);
|
|
127
|
+
lines.push(' page: z.number().int().positive().optional().default(1),');
|
|
128
|
+
lines.push(' perPage: z.number().int().min(1).max(100).optional().default(20),');
|
|
129
|
+
lines.push('});');
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Prisma-to-Zod Type Mapping
|
|
134
|
+
// ============================================================================
|
|
135
|
+
/** Map a field to its base Zod schema string (for output/resource schemas). */
|
|
136
|
+
function fieldToZod(field) {
|
|
137
|
+
let zod = prismaTypeToZod(field);
|
|
138
|
+
if (field.isOptional) {
|
|
139
|
+
zod += '.nullable()';
|
|
140
|
+
}
|
|
141
|
+
return zod;
|
|
142
|
+
}
|
|
143
|
+
/** Map a field to its Create input Zod schema string. */
|
|
144
|
+
function fieldToCreateZod(field) {
|
|
145
|
+
let zod = prismaTypeToZod(field);
|
|
146
|
+
if (!field.isOptional && field.type === 'String' && !field.isId && !isEmailField(field)) {
|
|
147
|
+
zod += '.min(1)';
|
|
148
|
+
}
|
|
149
|
+
if (field.isOptional) {
|
|
150
|
+
zod += '.nullable().optional()';
|
|
151
|
+
}
|
|
152
|
+
return zod;
|
|
153
|
+
}
|
|
154
|
+
/** Map a field to its Update input Zod schema string. */
|
|
155
|
+
function fieldToUpdateZod(field) {
|
|
156
|
+
let zod = prismaTypeToZod(field);
|
|
157
|
+
if (!field.isOptional && field.type === 'String' && !field.isId && !isEmailField(field)) {
|
|
158
|
+
zod += '.min(1)';
|
|
159
|
+
}
|
|
160
|
+
if (field.isOptional) {
|
|
161
|
+
zod += '.nullable().optional()';
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
zod += '.optional()';
|
|
165
|
+
}
|
|
166
|
+
return zod;
|
|
167
|
+
}
|
|
168
|
+
/** Convert a Prisma type to the base Zod schema call. */
|
|
169
|
+
function prismaTypeToZod(field) {
|
|
170
|
+
switch (field.type) {
|
|
171
|
+
case 'String': {
|
|
172
|
+
if (field.isId && field.defaultValue === 'uuid()') {
|
|
173
|
+
return 'z.string().uuid()';
|
|
174
|
+
}
|
|
175
|
+
if (isEmailField(field)) {
|
|
176
|
+
return 'z.string().email()';
|
|
177
|
+
}
|
|
178
|
+
return 'z.string()';
|
|
179
|
+
}
|
|
180
|
+
case 'Int':
|
|
181
|
+
return 'z.number().int()';
|
|
182
|
+
case 'Float':
|
|
183
|
+
case 'Decimal':
|
|
184
|
+
case 'BigInt':
|
|
185
|
+
return 'z.number()';
|
|
186
|
+
case 'Boolean':
|
|
187
|
+
return 'z.boolean()';
|
|
188
|
+
case 'DateTime':
|
|
189
|
+
return 'z.date()';
|
|
190
|
+
case 'Json':
|
|
191
|
+
return 'z.record(z.string(), z.unknown())';
|
|
192
|
+
default:
|
|
193
|
+
return 'z.string()';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Check if a field should use `.email()` validation. */
|
|
197
|
+
function isEmailField(field) {
|
|
198
|
+
return field.name.toLowerCase().includes('email') && field.isUnique;
|
|
199
|
+
}
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Visibility Helpers
|
|
202
|
+
// ============================================================================
|
|
203
|
+
function getVisibilityLevel(fieldName, visibility) {
|
|
204
|
+
if (!visibility) {
|
|
205
|
+
return 'public';
|
|
206
|
+
}
|
|
207
|
+
return visibility.get(fieldName) ?? 'public';
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Section Comment Constants
|
|
211
|
+
// ============================================================================
|
|
212
|
+
const SECTION_CHAR = '\u2500\u2500';
|
|
213
|
+
const SECTION_PAD = '\u2500'.repeat(40);
|