@specverse/engines 4.1.9 → 4.1.10

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.
@@ -1,3 +1,4 @@
1
+ import { deriveBasePath } from "@specverse/types/spec-rules";
1
2
  function generateMain(context) {
2
3
  const { spec, manifest } = context;
3
4
  let framework = "fastify";
@@ -52,8 +53,7 @@ function generateFastifyMain(spec) {
52
53
  let prefix = ctrl.basePath;
53
54
  if (!prefix) {
54
55
  const modelName = ctrl.model || ctrl.name.replace("Controller", "");
55
- const pluralModel = modelName.toLowerCase() + "s";
56
- prefix = `/api/${pluralModel}`;
56
+ prefix = deriveBasePath(modelName);
57
57
  }
58
58
  return ` await app.register(${ctrl.name.replace("Controller", "")}Routes, {
59
59
  prefix: '${prefix}',
@@ -50,12 +50,10 @@ async function apiRequest<T = any>(
50
50
  path: string,
51
51
  body: any = null
52
52
  ): Promise<T> {
53
- const options: RequestInit = {
54
- method,
55
- headers: { 'Content-Type': 'application/json' }
56
- };
53
+ const options: RequestInit = { method };
57
54
 
58
- if (body) {
55
+ if (body && Object.keys(body).length > 0) {
56
+ options.headers = { 'Content-Type': 'application/json' };
59
57
  options.body = JSON.stringify(body);
60
58
  }
61
59
 
@@ -65,6 +63,11 @@ async function apiRequest<T = any>(
65
63
  throw new Error(\`API request failed: \${response.statusText}\`);
66
64
  }
67
65
 
66
+ // Handle 204 No Content (e.g., delete) \u2014 no body to parse
67
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
68
+ return {} as T;
69
+ }
70
+
68
71
  return await response.json();
69
72
  }
70
73
 
@@ -1,3 +1,4 @@
1
+ import { inferHttpMethod as sharedInferHttpMethod, inferPath as sharedInferPath, curedToEndpoints as sharedCuredToEndpoints } from "@specverse/types/spec-rules";
1
2
  function generateFastifyRoutes(context) {
2
3
  const { controller, model, spec, implType } = context;
3
4
  if (!controller) {
@@ -223,57 +224,18 @@ function inferOperationFromMethodAndPath(method, path) {
223
224
  return "unknown";
224
225
  }
225
226
  function inferHttpMethod(operation) {
226
- if (!operation) {
227
- console.warn("Warning: undefined operation in inferHttpMethod");
228
- return "post";
229
- }
230
- const opLower = operation.toLowerCase();
231
- if (opLower === "create" || opLower === "validate") return "post";
232
- if (opLower === "retrieve" || opLower === "list") return "get";
233
- if (opLower === "update" || opLower === "evolve") return "put";
234
- if (opLower === "delete") return "delete";
235
- return "post";
227
+ return sharedInferHttpMethod(operation);
236
228
  }
237
229
  function inferPath(operation, endpoint) {
238
- const opLower = operation.toLowerCase();
239
- if (opLower === "create") return "/";
240
- if (opLower === "list") return "/";
241
- if (opLower === "retrieve") return "/:id";
242
- if (opLower === "update") return "/:id";
243
- if (opLower === "evolve") return "/:id/evolve";
244
- if (opLower === "delete") return "/:id";
245
- if (opLower === "validate") return "/validate";
246
- if (endpoint.path && endpoint.serviceOperation?.type === "custom") {
230
+ if (endpoint?.path && endpoint.serviceOperation?.type === "custom") {
247
231
  const pathParts = endpoint.path.split("/").filter((p) => p);
248
232
  const lastPart = pathParts[pathParts.length - 1];
249
233
  return `/${lastPart}`;
250
234
  }
251
- return `/${opLower}`;
235
+ return sharedInferPath(operation);
252
236
  }
253
- function curedToEndpoints(cured, modelName) {
254
- const endpoints = [];
255
- if (cured.create) {
256
- endpoints.push({ operation: "create", method: "POST" });
257
- }
258
- if (cured.retrieve) {
259
- endpoints.push({ operation: "retrieve", method: "GET" });
260
- }
261
- if (cured.retrieve_many) {
262
- endpoints.push({ operation: "list", method: "GET" });
263
- }
264
- if (cured.update) {
265
- endpoints.push({ operation: "update", method: "PUT" });
266
- }
267
- if (cured.evolve) {
268
- endpoints.push({ operation: "evolve", method: "PATCH" });
269
- }
270
- if (cured.delete) {
271
- endpoints.push({ operation: "delete", method: "DELETE" });
272
- }
273
- if (cured.validate) {
274
- endpoints.push({ operation: "validate", method: "POST" });
275
- }
276
- return endpoints;
237
+ function curedToEndpoints(cured, _modelName) {
238
+ return sharedCuredToEndpoints(cured);
277
239
  }
278
240
  export {
279
241
  generateFastifyRoutes as default
@@ -1,54 +1,4 @@
1
- function normalizeSpec(spec) {
2
- if (!spec) return spec;
3
- const normalized = { ...spec };
4
- if (normalized.models && typeof normalized.models === "object") {
5
- for (const [name, model] of Object.entries(normalized.models)) {
6
- if (Array.isArray(model.attributes)) {
7
- const obj = {};
8
- for (const attr of model.attributes) {
9
- if (attr.name) obj[attr.name] = attr;
10
- }
11
- model.attributes = obj;
12
- }
13
- if (Array.isArray(model.relationships)) {
14
- const obj = {};
15
- for (const rel of model.relationships) {
16
- if (rel.name) obj[rel.name] = { ...rel, targetModel: rel.target || rel.targetModel };
17
- }
18
- model.relationships = obj;
19
- }
20
- if (Array.isArray(model.lifecycles)) {
21
- const obj = {};
22
- for (const lc of model.lifecycles) {
23
- if (lc.name) obj[lc.name] = lc;
24
- }
25
- model.lifecycles = obj;
26
- }
27
- if (model.lifecycles && typeof model.lifecycles === "object") {
28
- for (const lc of Object.values(model.lifecycles)) {
29
- if (lc.flow && typeof lc.flow === "string" && !lc.states) {
30
- const stateNames = lc.flow.split("->").map((s) => s.trim()).filter(Boolean);
31
- lc.states = stateNames.map((s) => ({ name: s }));
32
- lc.transitions = [];
33
- lc.initialState = stateNames[0];
34
- for (let i = 0; i < stateNames.length - 1; i++) {
35
- lc.transitions.push({ from: stateNames[i], to: stateNames[i + 1] });
36
- }
37
- }
38
- }
39
- }
40
- }
41
- }
42
- if (normalized.views && typeof normalized.views === "object") {
43
- for (const view of Object.values(normalized.views)) {
44
- if (Array.isArray(view.model)) {
45
- view.models = view.model;
46
- view.model = view.model[0];
47
- }
48
- }
49
- }
50
- return normalized;
51
- }
1
+ import { normalizeSpec, deriveBasePath } from "@specverse/types/spec-rules";
52
2
  function generateFastifyServer(context) {
53
3
  const { spec, models } = context;
54
4
  const allModels = models || (spec?.models ? Object.values(spec.models) : []);
@@ -57,7 +7,7 @@ function generateFastifyServer(context) {
57
7
  (name) => `import ${name}Routes from './routes/${name}Controller.js';`
58
8
  ).join("\n");
59
9
  const routeRegistrations = modelNames.map((name) => {
60
- const path = `/api/${name.toLowerCase()}s`;
10
+ const path = deriveBasePath(name);
61
11
  return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
62
12
  }).join("\n");
63
13
  return `/**
@@ -291,8 +291,19 @@ function generateRelationship(rel, model, relationMap, hasOneTargets, allModels)
291
291
  relationDef += `"${relName}", `;
292
292
  }
293
293
  relationDef += `fields: [${fkBase}], references: [id]`;
294
- if (rel.onDelete) {
295
- relationDef += `, onDelete: ${rel.onDelete}`;
294
+ let onDelete = rel.onDelete;
295
+ if (!onDelete && allModels) {
296
+ const parentModel = allModels.find((m) => m.name === rel.target);
297
+ if (parentModel) {
298
+ const parentRels = Array.isArray(parentModel.relationships) ? parentModel.relationships : Object.values(parentModel.relationships || {});
299
+ const parentRel = parentRels.find((pr) => (pr.type === "hasMany" || pr.type === "hasOne") && (pr.target === model.name || pr.targetModel === model.name));
300
+ if (parentRel?.cascade || parentRel?.cascadeDelete) {
301
+ onDelete = "Cascade";
302
+ }
303
+ }
304
+ }
305
+ if (onDelete) {
306
+ relationDef += `, onDelete: ${onDelete}`;
296
307
  }
297
308
  if (rel.onUpdate) {
298
309
  relationDef += `, onUpdate: ${rel.onUpdate}`;
@@ -1,3 +1,4 @@
1
+ import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
1
2
  function generatePrismaController(context) {
2
3
  const { controller, model, spec, models: allModels } = context;
3
4
  if (!controller) {
@@ -74,9 +75,8 @@ function generateValidationLogic(model, dataParam = "_data", contextParam = "_co
74
75
  if (!model.attributes) return "// No validation rules defined";
75
76
  const validations = [];
76
77
  const attrList = Array.isArray(model.attributes) ? model.attributes.map((a) => [a.name, a]) : Object.entries(model.attributes);
77
- const AUTO_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "created_at", "updated_at", "createdBy", "updatedBy"]);
78
78
  attrList.forEach(([name, attr]) => {
79
- if (attr.required && !attr.auto && !AUTO_FIELDS.has(name)) {
79
+ if (attr.required && !isAutoField(name, attr)) {
80
80
  validations.push(`
81
81
  // ${name} is required
82
82
  if (${contextParam}.operation === 'create' && !${dataParam}.${name}) {
@@ -225,22 +225,7 @@ function generateEvolveMethod(model, modelName, modelVar, controller) {
225
225
  const lifecycle = lifecycles[0];
226
226
  const lifecycleName = lifecycle?.name || "status";
227
227
  const states = lifecycle?.states || [];
228
- const validTransitions = {};
229
- if (states.length > 1) {
230
- for (let i = 0; i < states.length - 1; i++) {
231
- validTransitions[states[i]] = [states[i + 1]];
232
- }
233
- }
234
- if (lifecycle?.transitions) {
235
- const transitions = Array.isArray(lifecycle.transitions) ? lifecycle.transitions : Object.entries(lifecycle.transitions).map(([name, t]) => ({ name, ...t }));
236
- for (const t of transitions) {
237
- const fromStates = Array.isArray(t.from) ? t.from : [t.from];
238
- for (const from of fromStates) {
239
- if (!validTransitions[from]) validTransitions[from] = [];
240
- if (!validTransitions[from].includes(t.to)) validTransitions[from].push(t.to);
241
- }
242
- }
243
- }
228
+ const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
244
229
  return `
245
230
  /**
246
231
  * Evolve ${modelName} through lifecycle
@@ -135,7 +135,8 @@ function generateSchemaFields(model) {
135
135
  }
136
136
  const fields = [];
137
137
  for (const [name, config] of Object.entries(attributes)) {
138
- if (name === "id" || name === "createdAt" || name === "updatedAt") continue;
138
+ const METADATA_FIELDS = ["id", "createdAt", "updatedAt", "createdBy", "updatedBy", "deletedAt", "version"];
139
+ if (METADATA_FIELDS.includes(name)) continue;
139
140
  const attr = config;
140
141
  if (attr.auto || attr.autoGenerate) continue;
141
142
  const autoTimestampFields = ["joinedAt", "registeredAt", "enrolledAt", "startedAt", "completedAt", "verifiedAt"];
@@ -182,7 +183,8 @@ function generateFormFields(model) {
182
183
  const regularFields = [];
183
184
  const textAreaFields = [];
184
185
  for (const [name, config] of Object.entries(attributes)) {
185
- if (name === "id" || name === "createdAt" || name === "updatedAt") continue;
186
+ const METADATA_FIELDS = ["id", "createdAt", "updatedAt", "createdBy", "updatedBy", "deletedAt", "version"];
187
+ if (METADATA_FIELDS.includes(name)) continue;
186
188
  const attr = config;
187
189
  if (attr.auto || attr.autoGenerate) continue;
188
190
  const autoTimestampFields = ["joinedAt", "registeredAt", "enrolledAt", "startedAt", "completedAt", "verifiedAt"];
@@ -40,9 +40,17 @@ export function isMetadataField(name: string): boolean {
40
40
  */
41
41
  export function getEntityDisplayName(entity: any): string {
42
42
  if (!entity) return 'Unknown';
43
- for (const field of ['name', 'title', 'displayName', 'label', 'username', 'email', 'subject']) {
43
+ const METADATA = new Set(['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt', 'version']);
44
+ // Check common name fields first
45
+ for (const field of ['name', 'title', 'displayName', 'label', 'username', 'email', 'subject', 'message', 'description']) {
44
46
  if (entity[field]) return String(entity[field]);
45
47
  }
48
+ // Fall back to first non-metadata string value
49
+ for (const [key, val] of Object.entries(entity)) {
50
+ if (!METADATA.has(key) && val && typeof val === 'string' && !String(val).match(/^[0-9a-f-]{36}$/i)) {
51
+ return String(val);
52
+ }
53
+ }
46
54
  return entity.id ? String(entity.id).slice(0, 8) + '...' : 'Unknown';
47
55
  }
48
56
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { TemplateContext } from '@specverse/types';
8
+ import { deriveBasePath, normalizeSpec } from '@specverse/types/spec-rules';
8
9
 
9
10
  export default function generateMain(context: TemplateContext): string {
10
11
  const { spec, manifest } = context;
@@ -82,8 +83,7 @@ function generateFastifyMain(spec: any): string {
82
83
  let prefix = ctrl.basePath;
83
84
  if (!prefix) {
84
85
  const modelName = ctrl.model || ctrl.name.replace('Controller', '');
85
- const pluralModel = modelName.toLowerCase() + 's';
86
- prefix = `/api/${pluralModel}`;
86
+ prefix = deriveBasePath(modelName);
87
87
  }
88
88
  return ` await app.register(${ctrl.name.replace('Controller', '')}Routes, {
89
89
  prefix: '${prefix}',
@@ -83,12 +83,10 @@ async function apiRequest<T = any>(
83
83
  path: string,
84
84
  body: any = null
85
85
  ): Promise<T> {
86
- const options: RequestInit = {
87
- method,
88
- headers: { 'Content-Type': 'application/json' }
89
- };
86
+ const options: RequestInit = { method };
90
87
 
91
- if (body) {
88
+ if (body && Object.keys(body).length > 0) {
89
+ options.headers = { 'Content-Type': 'application/json' };
92
90
  options.body = JSON.stringify(body);
93
91
  }
94
92
 
@@ -98,6 +96,11 @@ async function apiRequest<T = any>(
98
96
  throw new Error(\`API request failed: \${response.statusText}\`);
99
97
  }
100
98
 
99
+ // Handle 204 No Content (e.g., delete) — no body to parse
100
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
101
+ return {} as T;
102
+ }
103
+
101
104
  return await response.json();
102
105
  }
103
106
 
@@ -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,8 +158,9 @@ 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;
161
+ // Skip metadata and auto-generated fields
162
+ const METADATA_FIELDS = ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt', 'version'];
163
+ if (METADATA_FIELDS.includes(name)) continue;
163
164
 
164
165
  const attr: any = config;
165
166
 
@@ -229,8 +230,9 @@ function generateFormFields(model: any): string {
229
230
  const textAreaFields: string[] = [];
230
231
 
231
232
  for (const [name, config] of Object.entries(attributes)) {
232
- // Skip standard auto-generated fields
233
- if (name === 'id' || name === 'createdAt' || name === 'updatedAt') continue;
233
+ // Skip metadata and auto-generated fields
234
+ const METADATA_FIELDS = ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt', 'version'];
235
+ if (METADATA_FIELDS.includes(name)) continue;
234
236
 
235
237
  const attr: any = config;
236
238
 
@@ -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.10",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",