@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.
Files changed (27) hide show
  1. package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +2 -2
  2. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +8 -5
  3. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +6 -44
  4. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +2 -52
  5. package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +13 -2
  6. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +3 -18
  7. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +2 -12
  8. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +9 -1
  9. package/dist/parser/processors/AttributeProcessor.d.ts.map +1 -1
  10. package/dist/parser/processors/AttributeProcessor.js +10 -2
  11. package/dist/parser/processors/AttributeProcessor.js.map +1 -1
  12. package/dist/parser/processors/ModelProcessor.d.ts.map +1 -1
  13. package/dist/parser/processors/ModelProcessor.js +30 -16
  14. package/dist/parser/processors/ModelProcessor.js.map +1 -1
  15. package/dist/parser/types/ast.d.ts +3 -514
  16. package/dist/parser/types/ast.d.ts.map +1 -1
  17. package/dist/parser/types/ast.js +0 -6
  18. package/dist/parser/types/ast.js.map +1 -1
  19. package/libs/instance-factories/applications/templates/generic/main-generator.ts +2 -2
  20. package/libs/instance-factories/applications/templates/react/api-client-generator.ts +8 -5
  21. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +11 -60
  22. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +2 -70
  23. package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +18 -2
  24. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +5 -24
  25. package/libs/instance-factories/views/templates/react/forms-generator.ts +4 -22
  26. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +9 -1
  27. 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
- if (!operation) {
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
- * Returns relative path (without base path) for use with Fastify route prefix
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
- // e.g., "/user/attach-profile" "/attach-profile"
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 `/${opLower}`;
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, modelName: string): any[] {
363
- const endpoints: any[] = [];
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 = `/api/${name.toLowerCase()}s`;
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 (rel.onDelete) {
483
- relationDef += `, onDelete: ${rel.onDelete}`;
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 && !attr.auto && !AUTO_FIELDS.has(name)) {
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 from the lifecycle flow
292
- const validTransitions: Record<string, string[]> = {};
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 with auto directive (auto=uuid4, auto=now, etc.)
167
- if (attr.auto || attr.autoGenerate) continue;
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 with auto directive (auto=uuid4, auto=now, etc.)
238
- if (attr.auto || attr.autoGenerate) continue;
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
- for (const field of ['name', 'title', 'displayName', 'label', 'username', 'email', 'subject']) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "4.1.9",
3
+ "version": "4.1.11",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",