@specverse/engines 4.1.13 → 4.1.15
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/assets/prompts/core/standard/v9/behavior.prompt.yaml +126 -0
- package/dist/ai/behavior-ai-service.d.ts +65 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -0
- package/dist/ai/behavior-ai-service.js +205 -0
- package/dist/ai/behavior-ai-service.js.map +1 -0
- package/dist/ai/index.d.ts +27 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +30 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/prompt-loader.js +2 -2
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +204 -4
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +97 -22
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
- package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +249 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -54
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +192 -23
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +115 -22
- package/libs/instance-factories/communication/event-emitter.yaml +16 -12
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
- package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
- package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
- package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
- package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
|
@@ -24,13 +24,28 @@ export interface BehaviorContext {
|
|
|
24
24
|
serviceName: string;
|
|
25
25
|
operationName: string;
|
|
26
26
|
prismaModel?: string;
|
|
27
|
+
/** Parameter names from the action definition (e.g., ['pollId', 'optionId', 'voterName']) */
|
|
28
|
+
parameterNames?: string[];
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A step can be a plain string or an object with extra metadata.
|
|
33
|
+
* String form: "Find Order by id"
|
|
34
|
+
* Object form: { step: "Apply discount", as: "discount", returns: { amount: "number" } }
|
|
35
|
+
*/
|
|
36
|
+
export type Step = string | {
|
|
37
|
+
step: string;
|
|
38
|
+
/** Name the result variable (default: stepNResult) */
|
|
39
|
+
as?: string;
|
|
40
|
+
/** Declare return type for AI functions so types flow through */
|
|
41
|
+
returns?: Record<string, string> | string;
|
|
42
|
+
};
|
|
43
|
+
|
|
29
44
|
export interface BehaviorMetadata {
|
|
30
45
|
preconditions?: string[];
|
|
31
46
|
postconditions?: string[];
|
|
32
47
|
sideEffects?: string[];
|
|
33
|
-
steps?:
|
|
48
|
+
steps?: Step[];
|
|
34
49
|
transactional?: boolean;
|
|
35
50
|
}
|
|
36
51
|
|
|
@@ -43,6 +58,18 @@ export interface OperationMetadata {
|
|
|
43
58
|
export interface BehaviorResult {
|
|
44
59
|
body: string;
|
|
45
60
|
helperMethods: string[];
|
|
61
|
+
/** Steps that couldn't be matched by conventions — need AI generation */
|
|
62
|
+
unmatchedSteps: Array<{
|
|
63
|
+
step: string;
|
|
64
|
+
functionName: string;
|
|
65
|
+
operationName: string;
|
|
66
|
+
/** Names of variables passed as the typed input object */
|
|
67
|
+
inputs: string[];
|
|
68
|
+
/** Declared return type from spec ({field: type} or a single type string) */
|
|
69
|
+
returns?: Record<string, string> | string;
|
|
70
|
+
/** Named result variable (default: stepNResult) */
|
|
71
|
+
resultName?: string;
|
|
72
|
+
}>;
|
|
46
73
|
}
|
|
47
74
|
|
|
48
75
|
/**
|
|
@@ -71,15 +98,18 @@ export function generateBehaviorWithHelpers(
|
|
|
71
98
|
const parts: string[] = [];
|
|
72
99
|
const helperMethods: string[] = [];
|
|
73
100
|
|
|
101
|
+
// Track variables declared by preconditions so steps can reuse them
|
|
102
|
+
const preconditionDeclared = new Set<string>();
|
|
103
|
+
|
|
74
104
|
// Precondition guards
|
|
75
105
|
const preconditions = generatePreconditionChecks(
|
|
76
|
-
behavior.preconditions || [], context
|
|
106
|
+
behavior.preconditions || [], context, preconditionDeclared
|
|
77
107
|
);
|
|
78
108
|
if (preconditions) parts.push(preconditions);
|
|
79
109
|
|
|
80
110
|
// Main logic (from steps or inferred from operation semantics)
|
|
81
|
-
const { code, helpers } = generateStepLogic(
|
|
82
|
-
behavior.steps || [], context
|
|
111
|
+
const { code, helpers, unmatchedSteps } = generateStepLogic(
|
|
112
|
+
behavior.steps || [], context, preconditionDeclared
|
|
83
113
|
);
|
|
84
114
|
parts.push(code);
|
|
85
115
|
helperMethods.push(...helpers);
|
|
@@ -92,7 +122,7 @@ export function generateBehaviorWithHelpers(
|
|
|
92
122
|
|
|
93
123
|
// Event publishing
|
|
94
124
|
const events = generateEventPublishing(
|
|
95
|
-
behavior.sideEffects || [], context.operationName
|
|
125
|
+
behavior.sideEffects || [], context.operationName, context.parameterNames
|
|
96
126
|
);
|
|
97
127
|
if (events) parts.push(events);
|
|
98
128
|
|
|
@@ -103,39 +133,56 @@ export function generateBehaviorWithHelpers(
|
|
|
103
133
|
body = generateTransactionWrapper(body, context);
|
|
104
134
|
}
|
|
105
135
|
|
|
106
|
-
return { body, helperMethods };
|
|
136
|
+
return { body, helperMethods, unmatchedSteps };
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
/**
|
|
110
140
|
* Generate precondition checks from natural-language strings.
|
|
141
|
+
* Tracks declared variables to avoid duplicates and reuses fetched entities.
|
|
111
142
|
*/
|
|
112
143
|
export function generatePreconditionChecks(
|
|
113
144
|
preconditions: string[],
|
|
114
|
-
context: BehaviorContext
|
|
145
|
+
context: BehaviorContext,
|
|
146
|
+
declared?: Set<string>
|
|
115
147
|
): string {
|
|
116
148
|
if (preconditions.length === 0) return '';
|
|
117
149
|
|
|
118
|
-
|
|
150
|
+
if (!declared) declared = new Set<string>();
|
|
151
|
+
const checks = preconditions.map(pc => matchPreconditionPattern(pc, context, declared));
|
|
119
152
|
return ` // === PRECONDITIONS ===\n${checks.join('\n')}`;
|
|
120
153
|
}
|
|
121
154
|
|
|
155
|
+
/** Find the parameter name that refers to a model (e.g., 'pollId' for 'Poll') */
|
|
156
|
+
function findIdParam(modelName: string, paramNames: string[]): string {
|
|
157
|
+
const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
158
|
+
// Check for modelId (e.g., pollId)
|
|
159
|
+
const idParam = paramNames.find(p => p === `${modelVar}Id`);
|
|
160
|
+
if (idParam) return idParam;
|
|
161
|
+
// Check for just 'id'
|
|
162
|
+
if (paramNames.includes('id')) return 'id';
|
|
163
|
+
return `${modelVar}Id`;
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
function matchPreconditionPattern(
|
|
123
167
|
precondition: string,
|
|
124
|
-
context: BehaviorContext
|
|
168
|
+
context: BehaviorContext,
|
|
169
|
+
declared: Set<string>
|
|
125
170
|
): string {
|
|
126
171
|
const pc = precondition.toLowerCase();
|
|
127
|
-
const
|
|
172
|
+
const params = context.parameterNames || [];
|
|
128
173
|
|
|
129
174
|
// Pattern: "{Model} exists"
|
|
130
175
|
const existsMatch = precondition.match(/^(\w+)\s+exists/i);
|
|
131
176
|
if (existsMatch) {
|
|
132
177
|
const entity = existsMatch[1];
|
|
133
178
|
const entityVar = entity.charAt(0).toLowerCase() + entity.slice(1);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
179
|
+
const idParam = findIdParam(entity, params);
|
|
180
|
+
if (declared.has(entityVar)) {
|
|
181
|
+
// Already fetched — just check it
|
|
182
|
+
return ` // Guard: ${precondition} (already fetched)`;
|
|
183
|
+
}
|
|
184
|
+
declared.add(entityVar);
|
|
185
|
+
return ` const ${entityVar} = await prisma.${entityVar}.findUniqueOrThrow({ where: { id: ${idParam} } });`;
|
|
139
186
|
}
|
|
140
187
|
|
|
141
188
|
// Pattern: "{field} is not empty" / "{field} is required"
|
|
@@ -143,31 +190,22 @@ function matchPreconditionPattern(
|
|
|
143
190
|
const fieldMatch = precondition.match(/^(\w+)\s+is/i);
|
|
144
191
|
if (fieldMatch) {
|
|
145
192
|
const field = fieldMatch[1];
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
throw new Error('Precondition failed: ${precondition}');
|
|
149
|
-
}`;
|
|
193
|
+
const paramRef = params.includes(field) ? field : `params.${field}`;
|
|
194
|
+
return ` if (!${paramRef}) throw new Error('${field} is required');`;
|
|
150
195
|
}
|
|
151
196
|
}
|
|
152
197
|
|
|
153
198
|
// Pattern: "{field} is valid"
|
|
154
199
|
if (pc.includes('is valid')) {
|
|
155
|
-
return ` //
|
|
156
|
-
const validation = this.validate(params, { operation: '${context.operationName}' });
|
|
157
|
-
if (!validation.valid) {
|
|
158
|
-
throw new Error('Precondition failed: ${precondition} — ' + validation.errors.join(', '));
|
|
159
|
-
}`;
|
|
200
|
+
return ` // TODO: Implement validation: ${precondition}`;
|
|
160
201
|
}
|
|
161
202
|
|
|
162
203
|
// Pattern: "{X} matches {Y}" / "{X} equals {Y}"
|
|
163
204
|
const matchesMatch = precondition.match(/^(\w+)\s+(?:matches|equals)\s+(.+)/i);
|
|
164
205
|
if (matchesMatch) {
|
|
165
|
-
const left = matchesMatch[1];
|
|
166
|
-
const right = matchesMatch[2];
|
|
167
|
-
return `
|
|
168
|
-
if (params.${left.charAt(0).toLowerCase() + left.slice(1)} !== params.${right.charAt(0).toLowerCase() + right.slice(1)}) {
|
|
169
|
-
throw new Error('Precondition failed: ${precondition}');
|
|
170
|
-
}`;
|
|
206
|
+
const left = matchesMatch[1].charAt(0).toLowerCase() + matchesMatch[1].slice(1);
|
|
207
|
+
const right = matchesMatch[2].charAt(0).toLowerCase() + matchesMatch[2].slice(1);
|
|
208
|
+
return ` if (${left} !== ${right}) throw new Error('${matchesMatch[1]} must match ${matchesMatch[2]}');`;
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
// Pattern: "{Model} is {state}"
|
|
@@ -176,30 +214,43 @@ function matchPreconditionPattern(
|
|
|
176
214
|
const model = stateMatch[1];
|
|
177
215
|
const state = stateMatch[2];
|
|
178
216
|
const modelVar = model.charAt(0).toLowerCase() + model.slice(1);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
217
|
+
const idParam = findIdParam(model, params);
|
|
218
|
+
|
|
219
|
+
// If already declared, reuse; otherwise fetch
|
|
220
|
+
if (!declared.has(modelVar)) {
|
|
221
|
+
declared.add(modelVar);
|
|
222
|
+
return ` const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: ${idParam} } });
|
|
223
|
+
if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
|
|
224
|
+
}
|
|
225
|
+
return ` if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
|
|
184
226
|
}
|
|
185
227
|
|
|
186
228
|
// Unrecognized
|
|
187
|
-
return ` //
|
|
188
|
-
// TODO: Implement precondition check`;
|
|
229
|
+
return ` // TODO: Implement precondition: ${precondition}`;
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
/**
|
|
192
233
|
* Generate step logic using convention-based pattern matching.
|
|
193
234
|
*/
|
|
194
235
|
function generateStepLogic(
|
|
195
|
-
steps:
|
|
196
|
-
context: BehaviorContext
|
|
197
|
-
|
|
236
|
+
steps: Step[],
|
|
237
|
+
context: BehaviorContext,
|
|
238
|
+
preconditionDeclared?: Set<string>
|
|
239
|
+
): { code: string; helpers: string[]; unmatchedSteps: BehaviorResult['unmatchedSteps'] } {
|
|
198
240
|
const helpers: string[] = [];
|
|
241
|
+
const unmatchedSteps: BehaviorResult['unmatchedSteps'] = [];
|
|
242
|
+
|
|
243
|
+
// Shared declared variables across steps — carried through from preconditions
|
|
244
|
+
const declaredVars = preconditionDeclared || new Set<string>();
|
|
199
245
|
|
|
200
246
|
if (steps && steps.length > 0) {
|
|
201
|
-
const stepCode = steps.map((
|
|
202
|
-
|
|
247
|
+
const stepCode = steps.map((stepInput, i) => {
|
|
248
|
+
// Normalize to { text, as, returns }
|
|
249
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput.step;
|
|
250
|
+
const resultName = typeof stepInput === 'object' ? stepInput.as : undefined;
|
|
251
|
+
const returns = typeof stepInput === 'object' ? stepInput.returns : undefined;
|
|
252
|
+
|
|
253
|
+
if (typeof stepText !== 'string') {
|
|
203
254
|
return ` // Step ${i + 1}: Complex operation — see expanded definition`;
|
|
204
255
|
}
|
|
205
256
|
|
|
@@ -209,18 +260,32 @@ function generateStepLogic(
|
|
|
209
260
|
serviceName: context.serviceName,
|
|
210
261
|
operationName: context.operationName,
|
|
211
262
|
stepNum: i + 1,
|
|
263
|
+
parameterNames: context.parameterNames,
|
|
264
|
+
declaredVars,
|
|
265
|
+
resultName, // Pass through the named result
|
|
212
266
|
};
|
|
213
267
|
|
|
214
|
-
const result = matchStep(
|
|
268
|
+
const result = matchStep(stepText, ctx);
|
|
215
269
|
if (result.helperMethod) {
|
|
216
270
|
helpers.push(result.helperMethod);
|
|
217
271
|
}
|
|
272
|
+
if (!result.matched && result.functionName) {
|
|
273
|
+
unmatchedSteps.push({
|
|
274
|
+
step: stepText,
|
|
275
|
+
functionName: result.functionName,
|
|
276
|
+
operationName: context.operationName,
|
|
277
|
+
inputs: result.inputs || [],
|
|
278
|
+
returns,
|
|
279
|
+
resultName: resultName || `step${i + 1}Result`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
218
282
|
return result.call;
|
|
219
283
|
});
|
|
220
284
|
|
|
221
285
|
return {
|
|
222
286
|
code: ` // === EXECUTE ===\n${stepCode.join('\n\n')}`,
|
|
223
287
|
helpers,
|
|
288
|
+
unmatchedSteps,
|
|
224
289
|
};
|
|
225
290
|
}
|
|
226
291
|
|
|
@@ -228,6 +293,7 @@ function generateStepLogic(
|
|
|
228
293
|
return {
|
|
229
294
|
code: ` // === EXECUTE ===\n${inferLogicFromOperationName(context)}`,
|
|
230
295
|
helpers,
|
|
296
|
+
unmatchedSteps,
|
|
231
297
|
};
|
|
232
298
|
}
|
|
233
299
|
|
|
@@ -280,12 +346,17 @@ ${checks.join('\n')}
|
|
|
280
346
|
*/
|
|
281
347
|
export function generateEventPublishing(
|
|
282
348
|
sideEffects: string[],
|
|
283
|
-
operationName: string
|
|
349
|
+
operationName: string,
|
|
350
|
+
parameterNames?: string[]
|
|
284
351
|
): string {
|
|
285
352
|
if (!sideEffects || sideEffects.length === 0) return '';
|
|
286
353
|
|
|
354
|
+
const paramFields = parameterNames?.length
|
|
355
|
+
? parameterNames.join(', ') + ', '
|
|
356
|
+
: '';
|
|
357
|
+
|
|
287
358
|
const publishes = sideEffects.map(event =>
|
|
288
|
-
`
|
|
359
|
+
` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() } as any);`
|
|
289
360
|
);
|
|
290
361
|
|
|
291
362
|
return ` // === EVENTS ===\n${publishes.join('\n')}`;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TemplateContext } from '@specverse/types';
|
|
9
9
|
import { buildTransitionMap, isAutoField } from '@specverse/types/spec-rules';
|
|
10
|
+
import { generateBehaviorWithHelpers, type BehaviorContext, type BehaviorMetadata } from './behavior-generator.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Generate Prisma controller for a model
|
|
@@ -25,9 +26,15 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
25
26
|
const controllerName = controller.name;
|
|
26
27
|
const modelName = model.name;
|
|
27
28
|
const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
28
|
-
// Avoid JavaScript reserved words as variable names
|
|
29
|
+
// Avoid JavaScript reserved words as local variable names
|
|
29
30
|
const RESERVED_WORDS = new Set(['import', 'export', 'default', 'class', 'function', 'return', 'delete', 'new', 'this', 'switch', 'case', 'break', 'continue', 'for', 'while', 'do', 'if', 'else', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'let', 'const', 'var', 'void', 'with', 'yield', 'async', 'await', 'enum', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'super', 'extends']);
|
|
30
31
|
const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
|
|
32
|
+
// Prisma delegate name always matches the Prisma model's camelCase form —
|
|
33
|
+
// even when the model name is a JS reserved word (prisma.import, prisma.export).
|
|
34
|
+
// Access these via bracket notation so tsc doesn't choke on the keyword.
|
|
35
|
+
const prismaDelegate = RESERVED_WORDS.has(rawModelVar)
|
|
36
|
+
? `prisma['${rawModelVar}']`
|
|
37
|
+
: `prisma.${rawModelVar}`;
|
|
31
38
|
const curedOps = controller.cured || {};
|
|
32
39
|
|
|
33
40
|
// Determine ID type for proper parsing
|
|
@@ -36,6 +43,9 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
36
43
|
const idType = idAttr?.type || 'UUID';
|
|
37
44
|
const needsIntParse = idType === 'Integer' || idType === 'Int' || idType === 'Number';
|
|
38
45
|
|
|
46
|
+
// Generate custom actions and collect AI behavior stubs
|
|
47
|
+
const customActions = generateCustomActions(controller, modelName, modelVar);
|
|
48
|
+
|
|
39
49
|
return `/**
|
|
40
50
|
* ${controllerName}
|
|
41
51
|
* Model-specific business logic for ${modelName}
|
|
@@ -43,7 +53,8 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
43
53
|
*/
|
|
44
54
|
|
|
45
55
|
import { PrismaClient } from '@prisma/client';
|
|
46
|
-
${hasEventPublishing(curedOps, controller) ? `import { eventBus
|
|
56
|
+
${hasEventPublishing(curedOps, controller) ? `import { eventBus } from '../events/eventBus.js';` : ''}
|
|
57
|
+
${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : ''}
|
|
47
58
|
|
|
48
59
|
const prisma = new PrismaClient();
|
|
49
60
|
|
|
@@ -57,18 +68,19 @@ function parseId(id: string): ${needsIntParse ? 'number' : 'string'} {
|
|
|
57
68
|
*/
|
|
58
69
|
export class ${controllerName} {
|
|
59
70
|
${generateValidateMethod(model, modelName)}
|
|
60
|
-
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, controller, allModels) : ''}
|
|
61
|
-
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar) : ''}
|
|
62
|
-
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ''}
|
|
63
|
-
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ''}
|
|
64
|
-
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ''}
|
|
65
|
-
${
|
|
71
|
+
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ''}
|
|
72
|
+
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : ''}
|
|
73
|
+
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ''}
|
|
74
|
+
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) : ''}
|
|
75
|
+
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) : ''}
|
|
76
|
+
${customActions.code}
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
// Export singleton instance
|
|
69
80
|
export const ${modelVar}Controller = new ${controllerName}();
|
|
70
81
|
export default ${modelVar}Controller;
|
|
71
82
|
`;
|
|
83
|
+
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
/**
|
|
@@ -157,10 +169,7 @@ function generateValidationLogic(model: any, dataParam: string = '_data', contex
|
|
|
157
169
|
/**
|
|
158
170
|
* Generate create method
|
|
159
171
|
*/
|
|
160
|
-
function generateCreateMethod(model: any, modelName: string, modelVar: string, controller: any, allModels?: any[]): string {
|
|
161
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
162
|
-
const createEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Created')) : null;
|
|
163
|
-
|
|
172
|
+
function generateCreateMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any, allModels?: any[]): string {
|
|
164
173
|
return `
|
|
165
174
|
/**
|
|
166
175
|
* Create a new ${modelName}
|
|
@@ -177,17 +186,12 @@ function generateCreateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
177
186
|
${generateFKTransform(model, 'prismaData', allModels)}
|
|
178
187
|
|
|
179
188
|
// Create record
|
|
180
|
-
const ${modelVar} = await
|
|
189
|
+
const ${modelVar} = await ${prismaDelegate}.create({
|
|
181
190
|
data: prismaData${generateIncludeRelationships(model)}
|
|
182
191
|
});
|
|
183
192
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
eventBus.publish(EventName.${createEvent}, {
|
|
187
|
-
${modelVar},
|
|
188
|
-
timestamp: new Date().toISOString()
|
|
189
|
-
});
|
|
190
|
-
` : ''}
|
|
193
|
+
// Publish CURED event
|
|
194
|
+
await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
191
195
|
|
|
192
196
|
return ${modelVar};
|
|
193
197
|
}
|
|
@@ -197,13 +201,13 @@ function generateCreateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
197
201
|
/**
|
|
198
202
|
* Generate retrieve method
|
|
199
203
|
*/
|
|
200
|
-
function generateRetrieveMethod(model: any, modelName: string, modelVar: string): string {
|
|
204
|
+
function generateRetrieveMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string): string {
|
|
201
205
|
return `
|
|
202
206
|
/**
|
|
203
207
|
* Retrieve ${modelName} by ID
|
|
204
208
|
*/
|
|
205
209
|
public async retrieve(id: string): Promise<any> {
|
|
206
|
-
const ${modelVar} = await
|
|
210
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({
|
|
207
211
|
where: { id: parseId(id) }${generateIncludeRelationships(model)}
|
|
208
212
|
});
|
|
209
213
|
|
|
@@ -218,7 +222,7 @@ function generateRetrieveMethod(model: any, modelName: string, modelVar: string)
|
|
|
218
222
|
* Retrieve all ${modelName}s
|
|
219
223
|
*/
|
|
220
224
|
public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
|
|
221
|
-
return await
|
|
225
|
+
return await ${prismaDelegate}.findMany({
|
|
222
226
|
skip: options.skip,
|
|
223
227
|
take: options.take${generateIncludeRelationships(model)}
|
|
224
228
|
});
|
|
@@ -229,10 +233,7 @@ function generateRetrieveMethod(model: any, modelName: string, modelVar: string)
|
|
|
229
233
|
/**
|
|
230
234
|
* Generate update method
|
|
231
235
|
*/
|
|
232
|
-
function generateUpdateMethod(model: any, modelName: string, modelVar: string, controller: any, allModels?: any[]): string {
|
|
233
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
234
|
-
const updateEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Updated')) : null;
|
|
235
|
-
|
|
236
|
+
function generateUpdateMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any, allModels?: any[]): string {
|
|
236
237
|
return `
|
|
237
238
|
/**
|
|
238
239
|
* Update ${modelName}
|
|
@@ -257,18 +258,13 @@ function generateUpdateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
257
258
|
${generateFKTransform(model, 'updateData', allModels)}
|
|
258
259
|
|
|
259
260
|
// Update record
|
|
260
|
-
const ${modelVar} = await
|
|
261
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
261
262
|
where: { id: parseId(id) },
|
|
262
263
|
data: updateData${generateIncludeRelationships(model)}
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
eventBus.publish(EventName.${updateEvent}, {
|
|
268
|
-
${modelVar},
|
|
269
|
-
timestamp: new Date().toISOString()
|
|
270
|
-
});
|
|
271
|
-
` : ''}
|
|
266
|
+
// Publish CURED event
|
|
267
|
+
await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
272
268
|
|
|
273
269
|
return ${modelVar};
|
|
274
270
|
}
|
|
@@ -278,7 +274,7 @@ function generateUpdateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
278
274
|
/**
|
|
279
275
|
* Generate evolve method (lifecycle-aware updates)
|
|
280
276
|
*/
|
|
281
|
-
function generateEvolveMethod(model: any, modelName: string, modelVar: string, controller: any): string {
|
|
277
|
+
function generateEvolveMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any): string {
|
|
282
278
|
// Extract lifecycle — handle both array and object format
|
|
283
279
|
const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles :
|
|
284
280
|
(model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]: [string, any]) => ({ name, ...lc })) : []);
|
|
@@ -302,7 +298,7 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
302
298
|
}
|
|
303
299
|
|
|
304
300
|
// Get current record to check lifecycle state
|
|
305
|
-
const current = await
|
|
301
|
+
const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
306
302
|
if (!current) {
|
|
307
303
|
throw new Error('${modelName} not found');
|
|
308
304
|
}
|
|
@@ -321,11 +317,14 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
321
317
|
` : ''}
|
|
322
318
|
|
|
323
319
|
// Update record
|
|
324
|
-
const ${modelVar} = await
|
|
320
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
325
321
|
where: { id: parseId(id) },
|
|
326
322
|
data${generateIncludeRelationships(model)}
|
|
327
323
|
});
|
|
328
324
|
|
|
325
|
+
// Publish CURED event
|
|
326
|
+
await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
327
|
+
|
|
329
328
|
return ${modelVar};
|
|
330
329
|
}
|
|
331
330
|
`;
|
|
@@ -334,33 +333,23 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
334
333
|
/**
|
|
335
334
|
* Generate delete method
|
|
336
335
|
*/
|
|
337
|
-
function generateDeleteMethod(model: any, modelName: string, modelVar: string, controller: any): string {
|
|
338
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
339
|
-
const deleteEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Deleted')) : null;
|
|
340
|
-
|
|
336
|
+
function generateDeleteMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any): string {
|
|
341
337
|
return `
|
|
342
338
|
/**
|
|
343
339
|
* Delete ${modelName}
|
|
344
340
|
*/
|
|
345
341
|
public async delete(id: string): Promise<void> {
|
|
346
|
-
${deleteEvent ? `
|
|
347
342
|
// Get record before deletion for event
|
|
348
|
-
const ${modelVar} = await
|
|
349
|
-
` : ''}
|
|
343
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
350
344
|
|
|
351
|
-
await
|
|
345
|
+
await ${prismaDelegate}.delete({
|
|
352
346
|
where: { id: parseId(id) }
|
|
353
347
|
});
|
|
354
348
|
|
|
355
|
-
|
|
356
|
-
// Publish event
|
|
349
|
+
// Publish CURED event
|
|
357
350
|
if (${modelVar}) {
|
|
358
|
-
eventBus.publish(
|
|
359
|
-
${modelVar},
|
|
360
|
-
timestamp: new Date().toISOString()
|
|
361
|
-
});
|
|
351
|
+
await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
362
352
|
}
|
|
363
|
-
` : ''}
|
|
364
353
|
}
|
|
365
354
|
`;
|
|
366
355
|
}
|
|
@@ -368,26 +357,59 @@ function generateDeleteMethod(model: any, modelName: string, modelVar: string, c
|
|
|
368
357
|
/**
|
|
369
358
|
* Generate custom actions defined in controller
|
|
370
359
|
*/
|
|
371
|
-
|
|
360
|
+
interface CustomActionsResult {
|
|
361
|
+
code: string;
|
|
362
|
+
unmatchedSteps: Array<{ step: string; functionName: string; operationName: string }>;
|
|
363
|
+
needsAiBehaviors: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function generateCustomActions(controller: any, modelName: string, modelVar: string): CustomActionsResult {
|
|
372
367
|
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
373
|
-
return '';
|
|
368
|
+
return { code: '', unmatchedSteps: [], needsAiBehaviors: false };
|
|
374
369
|
}
|
|
375
370
|
|
|
376
371
|
const actions: string[] = [];
|
|
372
|
+
const allUnmatchedSteps: Array<{ step: string; functionName: string; operationName: string }> = [];
|
|
377
373
|
|
|
378
374
|
Object.entries(controller.actions).forEach(([actionName, action]: [string, any]) => {
|
|
375
|
+
const behavior: BehaviorMetadata = {
|
|
376
|
+
preconditions: action.requires || action.preconditions || [],
|
|
377
|
+
steps: action.steps || [],
|
|
378
|
+
postconditions: action.ensures || action.postconditions || [],
|
|
379
|
+
sideEffects: action.publishes || action.events || [],
|
|
380
|
+
transactional: action.transactional,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const ctx: BehaviorContext = {
|
|
384
|
+
modelName,
|
|
385
|
+
serviceName: `${modelName}Controller`,
|
|
386
|
+
operationName: actionName,
|
|
387
|
+
prismaModel: modelVar,
|
|
388
|
+
parameterNames: Object.keys(action.parameters || {}),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const result = generateBehaviorWithHelpers(behavior, {}, ctx);
|
|
392
|
+
allUnmatchedSteps.push(...result.unmatchedSteps);
|
|
393
|
+
|
|
379
394
|
actions.push(`
|
|
380
395
|
/**
|
|
381
396
|
* ${actionName}
|
|
382
397
|
* ${action.description || ''}
|
|
383
398
|
*/
|
|
384
399
|
public async ${actionName}(${generateActionParams(action)}): Promise<any> {
|
|
385
|
-
|
|
386
|
-
throw new Error('${actionName} not implemented');
|
|
400
|
+
${result.body}
|
|
387
401
|
}`);
|
|
402
|
+
|
|
403
|
+
if (result.helperMethods.length > 0) {
|
|
404
|
+
actions.push(...result.helperMethods);
|
|
405
|
+
}
|
|
388
406
|
});
|
|
389
407
|
|
|
390
|
-
return
|
|
408
|
+
return {
|
|
409
|
+
code: actions.join('\n'),
|
|
410
|
+
unmatchedSteps: allUnmatchedSteps,
|
|
411
|
+
needsAiBehaviors: allUnmatchedSteps.length > 0,
|
|
412
|
+
};
|
|
391
413
|
}
|
|
392
414
|
|
|
393
415
|
/**
|
|
@@ -509,5 +531,7 @@ ${includes}
|
|
|
509
531
|
* Check if controller publishes events
|
|
510
532
|
*/
|
|
511
533
|
function hasEventPublishing(curedOps: any, controller: any): boolean {
|
|
512
|
-
|
|
534
|
+
// Controllers always publish CURED events (Created, Updated, Deleted, Evolved)
|
|
535
|
+
// so the event bus is always needed
|
|
536
|
+
return true;
|
|
513
537
|
}
|