@specverse/engines 4.1.9 → 4.1.11
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/libs/instance-factories/applications/templates/generic/main-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +8 -5
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +6 -44
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +2 -52
- package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +13 -2
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +3 -18
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +2 -12
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +9 -1
- package/dist/parser/processors/AttributeProcessor.d.ts.map +1 -1
- package/dist/parser/processors/AttributeProcessor.js +10 -2
- package/dist/parser/processors/AttributeProcessor.js.map +1 -1
- package/dist/parser/processors/ModelProcessor.d.ts.map +1 -1
- package/dist/parser/processors/ModelProcessor.js +30 -16
- package/dist/parser/processors/ModelProcessor.js.map +1 -1
- package/dist/parser/types/ast.d.ts +3 -514
- package/dist/parser/types/ast.d.ts.map +1 -1
- package/dist/parser/types/ast.js +0 -6
- package/dist/parser/types/ast.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/main-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +8 -5
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +11 -60
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +2 -70
- package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +18 -2
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +5 -24
- package/libs/instance-factories/views/templates/react/forms-generator.ts +4 -22
- package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +9 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TemplateContext } from '@specverse/types';
|
|
8
|
+
import { inferHttpMethod as sharedInferHttpMethod, inferPath as sharedInferPath, curedToEndpoints as sharedCuredToEndpoints } from '@specverse/types/spec-rules';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Generate Fastify routes for a controller
|
|
@@ -309,81 +310,31 @@ function inferOperationFromMethodAndPath(method: string, path: string): string {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
/**
|
|
312
|
-
* Infer HTTP method from operation name
|
|
313
|
+
* Infer HTTP method from operation name (delegates to shared spec-rules)
|
|
313
314
|
*/
|
|
314
315
|
function inferHttpMethod(operation: string): string {
|
|
315
|
-
|
|
316
|
-
console.warn('Warning: undefined operation in inferHttpMethod');
|
|
317
|
-
return 'post'; // default
|
|
318
|
-
}
|
|
319
|
-
const opLower = operation.toLowerCase();
|
|
320
|
-
|
|
321
|
-
if (opLower === 'create' || opLower === 'validate') return 'post';
|
|
322
|
-
if (opLower === 'retrieve' || opLower === 'list') return 'get';
|
|
323
|
-
if (opLower === 'update' || opLower === 'evolve') return 'put';
|
|
324
|
-
if (opLower === 'delete') return 'delete';
|
|
325
|
-
|
|
326
|
-
return 'post'; // default
|
|
316
|
+
return sharedInferHttpMethod(operation);
|
|
327
317
|
}
|
|
328
318
|
|
|
329
319
|
/**
|
|
330
|
-
* Infer path from operation name
|
|
331
|
-
*
|
|
320
|
+
* Infer path from operation name.
|
|
321
|
+
* Delegates to shared spec-rules for standard CURVED ops,
|
|
322
|
+
* handles custom service operation paths locally.
|
|
332
323
|
*/
|
|
333
324
|
function inferPath(operation: string, endpoint: any): string {
|
|
334
|
-
// Endpoint paths from AI view generator are placeholders - ignore them
|
|
335
|
-
// Always generate relative paths from operation for Fastify route registration
|
|
336
|
-
// (The prefix is set in main.ts registration, e.g., prefix: '/api/users')
|
|
337
|
-
|
|
338
|
-
const opLower = operation.toLowerCase();
|
|
339
|
-
|
|
340
|
-
if (opLower === 'create') return '/';
|
|
341
|
-
if (opLower === 'list') return '/';
|
|
342
|
-
if (opLower === 'retrieve') return '/:id';
|
|
343
|
-
if (opLower === 'update') return '/:id';
|
|
344
|
-
if (opLower === 'evolve') return '/:id/evolve';
|
|
345
|
-
if (opLower === 'delete') return '/:id';
|
|
346
|
-
if (opLower === 'validate') return '/validate';
|
|
347
|
-
|
|
348
325
|
// For custom actions, extract relative path from endpoint.path
|
|
349
|
-
|
|
350
|
-
if (endpoint.path && endpoint.serviceOperation?.type === 'custom') {
|
|
326
|
+
if (endpoint?.path && endpoint.serviceOperation?.type === 'custom') {
|
|
351
327
|
const pathParts = endpoint.path.split('/').filter((p: string) => p);
|
|
352
328
|
const lastPart = pathParts[pathParts.length - 1];
|
|
353
329
|
return `/${lastPart}`;
|
|
354
330
|
}
|
|
355
331
|
|
|
356
|
-
return
|
|
332
|
+
return sharedInferPath(operation);
|
|
357
333
|
}
|
|
358
334
|
|
|
359
335
|
/**
|
|
360
|
-
* Convert CURED operations to endpoints
|
|
336
|
+
* Convert CURED operations to endpoints (delegates to shared spec-rules)
|
|
361
337
|
*/
|
|
362
|
-
function curedToEndpoints(cured: any,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// Map CURED operations to endpoints with correct paths
|
|
366
|
-
if (cured.create) {
|
|
367
|
-
endpoints.push({ operation: 'create', method: 'POST' });
|
|
368
|
-
}
|
|
369
|
-
if (cured.retrieve) {
|
|
370
|
-
endpoints.push({ operation: 'retrieve', method: 'GET' });
|
|
371
|
-
}
|
|
372
|
-
if (cured.retrieve_many) {
|
|
373
|
-
endpoints.push({ operation: 'list', method: 'GET' });
|
|
374
|
-
}
|
|
375
|
-
if (cured.update) {
|
|
376
|
-
endpoints.push({ operation: 'update', method: 'PUT' });
|
|
377
|
-
}
|
|
378
|
-
if (cured.evolve) {
|
|
379
|
-
endpoints.push({ operation: 'evolve', method: 'PATCH' });
|
|
380
|
-
}
|
|
381
|
-
if (cured.delete) {
|
|
382
|
-
endpoints.push({ operation: 'delete', method: 'DELETE' });
|
|
383
|
-
}
|
|
384
|
-
if (cured.validate) {
|
|
385
|
-
endpoints.push({ operation: 'validate', method: 'POST' });
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return endpoints;
|
|
338
|
+
function curedToEndpoints(cured: any, _modelName: string): any[] {
|
|
339
|
+
return sharedCuredToEndpoints(cured);
|
|
389
340
|
}
|
|
@@ -5,75 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TemplateContext } from '@specverse/types';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate Fastify server bootstrap
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Normalize spec data for frontend consumption.
|
|
14
|
-
* Converts array-format attributes/relationships/lifecycles to object format.
|
|
15
|
-
* Ensures model references are single strings, not arrays.
|
|
16
|
-
*/
|
|
17
|
-
function normalizeSpec(spec: any): any {
|
|
18
|
-
if (!spec) return spec;
|
|
19
|
-
const normalized = { ...spec };
|
|
20
|
-
|
|
21
|
-
// Normalize models
|
|
22
|
-
if (normalized.models && typeof normalized.models === 'object') {
|
|
23
|
-
for (const [name, model] of Object.entries(normalized.models) as [string, any][]) {
|
|
24
|
-
// Attributes: [{name:'id', type:'UUID'}] → {id: {type:'UUID'}}
|
|
25
|
-
if (Array.isArray(model.attributes)) {
|
|
26
|
-
const obj: Record<string, any> = {};
|
|
27
|
-
for (const attr of model.attributes) {
|
|
28
|
-
if (attr.name) obj[attr.name] = attr;
|
|
29
|
-
}
|
|
30
|
-
model.attributes = obj;
|
|
31
|
-
}
|
|
32
|
-
// Relationships: [{name:'author', type:'belongsTo', target:'Author'}] → {author: {..., targetModel:'Author'}}
|
|
33
|
-
if (Array.isArray(model.relationships)) {
|
|
34
|
-
const obj: Record<string, any> = {};
|
|
35
|
-
for (const rel of model.relationships) {
|
|
36
|
-
if (rel.name) obj[rel.name] = { ...rel, targetModel: rel.target || rel.targetModel };
|
|
37
|
-
}
|
|
38
|
-
model.relationships = obj;
|
|
39
|
-
}
|
|
40
|
-
// Lifecycles: [{name:'status', states:[...]}] → {status: {states:[...]}}
|
|
41
|
-
if (Array.isArray(model.lifecycles)) {
|
|
42
|
-
const obj: Record<string, any> = {};
|
|
43
|
-
for (const lc of model.lifecycles) {
|
|
44
|
-
if (lc.name) obj[lc.name] = lc;
|
|
45
|
-
}
|
|
46
|
-
model.lifecycles = obj;
|
|
47
|
-
}
|
|
48
|
-
// Parse flow strings in lifecycles: "draft -> open -> closed" → states + transitions
|
|
49
|
-
if (model.lifecycles && typeof model.lifecycles === 'object') {
|
|
50
|
-
for (const lc of Object.values(model.lifecycles) as any[]) {
|
|
51
|
-
if (lc.flow && typeof lc.flow === 'string' && !lc.states) {
|
|
52
|
-
const stateNames = lc.flow.split('->').map((s: string) => s.trim()).filter(Boolean);
|
|
53
|
-
lc.states = stateNames.map((s: string) => ({ name: s }));
|
|
54
|
-
lc.transitions = [];
|
|
55
|
-
lc.initialState = stateNames[0];
|
|
56
|
-
for (let i = 0; i < stateNames.length - 1; i++) {
|
|
57
|
-
lc.transitions.push({ from: stateNames[i], to: stateNames[i + 1] });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Normalize views — ensure model is a single string
|
|
66
|
-
if (normalized.views && typeof normalized.views === 'object') {
|
|
67
|
-
for (const view of Object.values(normalized.views) as any[]) {
|
|
68
|
-
if (Array.isArray(view.model)) {
|
|
69
|
-
view.models = view.model; // preserve full list
|
|
70
|
-
view.model = view.model[0]; // primary model is first
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return normalized;
|
|
76
|
-
}
|
|
8
|
+
import { normalizeSpec, deriveBasePath } from '@specverse/types/spec-rules';
|
|
77
9
|
|
|
78
10
|
export default function generateFastifyServer(context: TemplateContext): string {
|
|
79
11
|
const { spec, models } = context;
|
|
@@ -88,7 +20,7 @@ export default function generateFastifyServer(context: TemplateContext): string
|
|
|
88
20
|
).join('\n');
|
|
89
21
|
|
|
90
22
|
const routeRegistrations = modelNames.map((name: string) => {
|
|
91
|
-
const path =
|
|
23
|
+
const path = deriveBasePath(name);
|
|
92
24
|
return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
|
|
93
25
|
}).join('\n');
|
|
94
26
|
|
|
@@ -479,8 +479,24 @@ function generateRelationship(rel: any, model: any, relationMap: Map<string, str
|
|
|
479
479
|
}
|
|
480
480
|
relationDef += `fields: [${fkBase}], references: [id]`;
|
|
481
481
|
|
|
482
|
-
if
|
|
483
|
-
|
|
482
|
+
// Check if parent model declares cascade on its hasMany/hasOne side
|
|
483
|
+
let onDelete = rel.onDelete;
|
|
484
|
+
if (!onDelete && allModels) {
|
|
485
|
+
const parentModel = allModels.find((m: any) => m.name === rel.target);
|
|
486
|
+
if (parentModel) {
|
|
487
|
+
const parentRels = Array.isArray(parentModel.relationships)
|
|
488
|
+
? parentModel.relationships
|
|
489
|
+
: Object.values(parentModel.relationships || {});
|
|
490
|
+
const parentRel = parentRels.find((pr: any) =>
|
|
491
|
+
(pr.type === 'hasMany' || pr.type === 'hasOne') &&
|
|
492
|
+
(pr.target === model.name || pr.targetModel === model.name));
|
|
493
|
+
if (parentRel?.cascade || parentRel?.cascadeDelete) {
|
|
494
|
+
onDelete = 'Cascade';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (onDelete) {
|
|
499
|
+
relationDef += `, onDelete: ${onDelete}`;
|
|
484
500
|
}
|
|
485
501
|
if (rel.onUpdate) {
|
|
486
502
|
relationDef += `, onUpdate: ${rel.onUpdate}`;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TemplateContext } from '@specverse/types';
|
|
9
|
+
import { buildTransitionMap, isAutoField } from '@specverse/types/spec-rules';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Generate Prisma controller for a model
|
|
@@ -106,12 +107,9 @@ function generateValidationLogic(model: any, dataParam: string = '_data', contex
|
|
|
106
107
|
const attrList = Array.isArray(model.attributes)
|
|
107
108
|
? model.attributes.map((a: any) => [a.name, a])
|
|
108
109
|
: Object.entries(model.attributes);
|
|
109
|
-
// Fields that are auto-generated and should not be required on create
|
|
110
|
-
const AUTO_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'created_at', 'updated_at', 'createdBy', 'updatedBy']);
|
|
111
|
-
|
|
112
110
|
attrList.forEach(([name, attr]: [string, any]) => {
|
|
113
|
-
// Required validation — skip auto-generated fields
|
|
114
|
-
if (attr.required && !
|
|
111
|
+
// Required validation — skip auto-generated fields (uses shared spec-rules)
|
|
112
|
+
if (attr.required && !isAutoField(name, attr)) {
|
|
115
113
|
validations.push(`
|
|
116
114
|
// ${name} is required
|
|
117
115
|
if (${contextParam}.operation === 'create' && !${dataParam}.${name}) {
|
|
@@ -288,25 +286,8 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
288
286
|
const lifecycleName = lifecycle?.name || 'status';
|
|
289
287
|
const states = lifecycle?.states || [];
|
|
290
288
|
|
|
291
|
-
// Build transition map
|
|
292
|
-
const validTransitions
|
|
293
|
-
if (states.length > 1) {
|
|
294
|
-
for (let i = 0; i < states.length - 1; i++) {
|
|
295
|
-
validTransitions[states[i]] = [states[i + 1]];
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// Add cancel transitions if spec defines them
|
|
299
|
-
if (lifecycle?.transitions) {
|
|
300
|
-
const transitions = Array.isArray(lifecycle.transitions) ? lifecycle.transitions :
|
|
301
|
-
Object.entries(lifecycle.transitions).map(([name, t]: [string, any]) => ({ name, ...t }));
|
|
302
|
-
for (const t of transitions) {
|
|
303
|
-
const fromStates = Array.isArray(t.from) ? t.from : [t.from];
|
|
304
|
-
for (const from of fromStates) {
|
|
305
|
-
if (!validTransitions[from]) validTransitions[from] = [];
|
|
306
|
-
if (!validTransitions[from].includes(t.to)) validTransitions[from].push(t.to);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
289
|
+
// Build transition map using shared spec-rules
|
|
290
|
+
const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
|
|
310
291
|
|
|
311
292
|
return `
|
|
312
293
|
/**
|
|
@@ -158,19 +158,10 @@ function generateSchemaFields(model: any): string {
|
|
|
158
158
|
const fields: string[] = [];
|
|
159
159
|
|
|
160
160
|
for (const [name, config] of Object.entries(attributes)) {
|
|
161
|
-
// Skip standard auto-generated fields
|
|
162
|
-
if (name === 'id' || name === 'createdAt' || name === 'updatedAt') continue;
|
|
163
|
-
|
|
164
161
|
const attr: any = config;
|
|
165
162
|
|
|
166
|
-
// Skip fields
|
|
167
|
-
if (attr.
|
|
168
|
-
|
|
169
|
-
// Skip common auto-timestamp fields (workaround for parser not preserving auto directive)
|
|
170
|
-
const autoTimestampFields = ['joinedAt', 'registeredAt', 'enrolledAt', 'startedAt', 'completedAt', 'verifiedAt'];
|
|
171
|
-
if (autoTimestampFields.includes(name) && (attr.type === 'DateTime' || attr.type === 'Timestamp')) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
163
|
+
// Skip metadata fields (category set at parse time)
|
|
164
|
+
if (attr.category === 'metadata') continue;
|
|
174
165
|
|
|
175
166
|
const type = attr.type || 'String';
|
|
176
167
|
let zodType = 'z.string()';
|
|
@@ -229,19 +220,10 @@ function generateFormFields(model: any): string {
|
|
|
229
220
|
const textAreaFields: string[] = [];
|
|
230
221
|
|
|
231
222
|
for (const [name, config] of Object.entries(attributes)) {
|
|
232
|
-
// Skip standard auto-generated fields
|
|
233
|
-
if (name === 'id' || name === 'createdAt' || name === 'updatedAt') continue;
|
|
234
|
-
|
|
235
223
|
const attr: any = config;
|
|
236
224
|
|
|
237
|
-
// Skip fields
|
|
238
|
-
if (attr.
|
|
239
|
-
|
|
240
|
-
// Skip common auto-timestamp fields (workaround for parser not preserving auto directive)
|
|
241
|
-
const autoTimestampFields = ['joinedAt', 'registeredAt', 'enrolledAt', 'startedAt', 'completedAt', 'verifiedAt'];
|
|
242
|
-
if (autoTimestampFields.includes(name) && (attr.type === 'DateTime' || attr.type === 'Timestamp')) {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
225
|
+
// Skip metadata fields (category set at parse time)
|
|
226
|
+
if (attr.category === 'metadata') continue;
|
|
245
227
|
|
|
246
228
|
const type = attr.type || 'String';
|
|
247
229
|
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -51,9 +51,17 @@ export function isMetadataField(name: string): boolean {
|
|
|
51
51
|
*/
|
|
52
52
|
export function getEntityDisplayName(entity: any): string {
|
|
53
53
|
if (!entity) return 'Unknown';
|
|
54
|
-
|
|
54
|
+
const METADATA = new Set(['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt', 'version']);
|
|
55
|
+
// Check common name fields first
|
|
56
|
+
for (const field of ['name', 'title', 'displayName', 'label', 'username', 'email', 'subject', 'message', 'description']) {
|
|
55
57
|
if (entity[field]) return String(entity[field]);
|
|
56
58
|
}
|
|
59
|
+
// Fall back to first non-metadata string value
|
|
60
|
+
for (const [key, val] of Object.entries(entity)) {
|
|
61
|
+
if (!METADATA.has(key) && val && typeof val === 'string' && !String(val).match(/^[0-9a-f-]{36}$/i)) {
|
|
62
|
+
return String(val);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
57
65
|
return entity.id ? String(entity.id).slice(0, 8) + '...' : 'Unknown';
|
|
58
66
|
}
|
|
59
67
|
|