@specverse/engines 4.1.5 → 4.1.6
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/backend-env-generator.js +22 -0
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +66 -0
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +54 -0
- package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +290 -0
- package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +530 -0
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +437 -0
- package/dist/libs/instance-factories/applications/templates/react/api-types-generator.js +146 -0
- package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +73 -0
- package/dist/libs/instance-factories/applications/templates/react/env-example-generator.js +18 -0
- package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +99 -0
- package/dist/libs/instance-factories/applications/templates/react/gitignore-generator.js +35 -0
- package/dist/libs/instance-factories/applications/templates/react/index-css-generator.js +9 -0
- package/dist/libs/instance-factories/applications/templates/react/index-html-generator.js +23 -0
- package/dist/libs/instance-factories/applications/templates/react/main-tsx-generator.js +29 -0
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +49 -0
- package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +156 -0
- package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +935 -0
- package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +143 -0
- package/dist/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.js +101 -0
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +50 -0
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +646 -0
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +65 -0
- package/dist/libs/instance-factories/applications/templates/react/tsconfig-generator.js +28 -0
- package/dist/libs/instance-factories/applications/templates/react/use-api-hooks-generator.js +132 -0
- package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +143 -0
- package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +143 -0
- package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +355 -0
- package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +91 -0
- package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +79 -0
- package/dist/libs/instance-factories/applications/templates/react/vite-config-generator.js +42 -0
- package/dist/libs/instance-factories/cli/templates/commander/cli-bin-wrapper-generator.js +11 -0
- package/dist/libs/instance-factories/cli/templates/commander/cli-entry-generator.js +111 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +928 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +83 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/publisher-generator.js +91 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/subscriber-generator.js +86 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/meta-routes-generator.js +93 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +280 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +125 -0
- package/dist/libs/instance-factories/infrastructure/templates/docker-k8s/infrastructure-generator.js +25 -0
- package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +371 -0
- package/dist/libs/instance-factories/orms/templates/prisma/services-generator.js +266 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/env-example-generator.js +51 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/env-generator.js +61 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/gitignore-generator.js +59 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +126 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/readme-generator.js +159 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +56 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-react-generator.js +37 -0
- package/dist/libs/instance-factories/sdks/templates/python/sdk-generator.js +29 -0
- package/dist/libs/instance-factories/sdks/templates/typescript/sdk-generator.js +28 -0
- package/dist/libs/instance-factories/services/templates/memory/generate-interpreter.js +14 -0
- package/dist/libs/instance-factories/services/templates/memory/step-conventions-memory.js +415 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +177 -0
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +413 -0
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +243 -0
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +264 -0
- package/dist/libs/instance-factories/services/templates/shared-patterns.js +24 -0
- package/dist/libs/instance-factories/shared/path-resolver.js +59 -0
- package/dist/libs/instance-factories/storage/templates/mongodb/config-generator.js +13 -0
- package/dist/libs/instance-factories/storage/templates/mongodb/docker-generator.js +16 -0
- package/dist/libs/instance-factories/storage/templates/postgresql/config-generator.js +45 -0
- package/dist/libs/instance-factories/storage/templates/postgresql/docker-generator.js +46 -0
- package/dist/libs/instance-factories/storage/templates/redis/config-generator.js +14 -0
- package/dist/libs/instance-factories/storage/templates/redis/docker-generator.js +16 -0
- package/dist/libs/instance-factories/test-generation.js +145 -0
- package/dist/libs/instance-factories/testing/templates/vitest/tests-generator.js +30 -0
- package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +149 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +232 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +49 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +18 -0
- 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 +97 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +64 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +182 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +1210 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +172 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +240 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +147 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +281 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +409 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +414 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +467 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +135 -0
- 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 +965 -0
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +238 -0
- package/dist/libs/instance-factories/validation/templates/zod/validation-generator.js +25 -0
- package/dist/libs/instance-factories/views/index.js +48 -0
- package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +742 -0
- package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +824 -0
- package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +719 -0
- package/dist/libs/instance-factories/views/templates/react/app-generator.js +45 -0
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +779 -0
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +285 -0
- package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +46 -0
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +111 -0
- package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +9 -0
- package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +23 -0
- package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +21 -0
- package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +299 -0
- package/dist/libs/instance-factories/views/templates/react/router-generator.js +136 -0
- package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +107 -0
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +179 -0
- package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +7 -0
- package/dist/libs/instance-factories/views/templates/react/types-generator.js +56 -0
- package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +27 -0
- package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +29 -0
- package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +261 -0
- package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +34 -0
- package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +800 -0
- package/dist/libs/instance-factories/views/templates/shared/base-generator.js +305 -0
- package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +517 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +445 -0
- package/dist/libs/instance-factories/views/templates/shared/index.js +80 -0
- package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +210 -0
- package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +492 -0
- package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +321 -0
- package/package.json +3 -2
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
function generatePrismaController(context) {
|
|
2
|
+
const { controller, model, spec, models: allModels } = context;
|
|
3
|
+
if (!controller) {
|
|
4
|
+
throw new Error("Controller is required in template context");
|
|
5
|
+
}
|
|
6
|
+
if (!model) {
|
|
7
|
+
throw new Error("Model is required for controller generation");
|
|
8
|
+
}
|
|
9
|
+
const controllerName = controller.name;
|
|
10
|
+
const modelName = model.name;
|
|
11
|
+
const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
12
|
+
const RESERVED_WORDS = /* @__PURE__ */ 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"]);
|
|
13
|
+
const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
|
|
14
|
+
const curedOps = controller.cured || {};
|
|
15
|
+
const idAttr = (Array.isArray(model.attributes) ? model.attributes : Object.values(model.attributes || {})).find((a) => a.name === "id");
|
|
16
|
+
const idType = idAttr?.type || "UUID";
|
|
17
|
+
const needsIntParse = idType === "Integer" || idType === "Int" || idType === "Number";
|
|
18
|
+
return `/**
|
|
19
|
+
* ${controllerName}
|
|
20
|
+
* Model-specific business logic for ${modelName}
|
|
21
|
+
* ${controller.description || ""}
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { PrismaClient } from '@prisma/client';
|
|
25
|
+
${hasEventPublishing(curedOps, controller) ? `import { eventBus, EventName } from '../events/eventBus.js';` : ""}
|
|
26
|
+
|
|
27
|
+
const prisma = new PrismaClient();
|
|
28
|
+
|
|
29
|
+
/** Parse ID from string to the correct type for this model */
|
|
30
|
+
function parseId(id: string): ${needsIntParse ? "number" : "string"} {
|
|
31
|
+
${needsIntParse ? "return parseInt(id, 10);" : "return id;"}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ${controllerName} class
|
|
36
|
+
*/
|
|
37
|
+
export class ${controllerName} {
|
|
38
|
+
${generateValidateMethod(model, modelName)}
|
|
39
|
+
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, controller, allModels) : ""}
|
|
40
|
+
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar) : ""}
|
|
41
|
+
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ""}
|
|
42
|
+
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ""}
|
|
43
|
+
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ""}
|
|
44
|
+
${generateCustomActions(controller, modelName, modelVar)}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Export singleton instance
|
|
48
|
+
export const ${modelVar}Controller = new ${controllerName}();
|
|
49
|
+
export default ${modelVar}Controller;
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
function generateValidateMethod(model, modelName) {
|
|
53
|
+
return `
|
|
54
|
+
/**
|
|
55
|
+
* Validate ${modelName} data
|
|
56
|
+
* Unified validation method for all operations
|
|
57
|
+
*/
|
|
58
|
+
public validate(
|
|
59
|
+
_data: any,
|
|
60
|
+
_context: { operation: 'create' | 'update' | 'evolve' }
|
|
61
|
+
): { valid: boolean; errors: string[] } {
|
|
62
|
+
const errors: string[] = [];
|
|
63
|
+
|
|
64
|
+
${generateValidationLogic(model, "_data", "_context")}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
valid: errors.length === 0,
|
|
68
|
+
errors
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
function generateValidationLogic(model, dataParam = "_data", contextParam = "_context") {
|
|
74
|
+
if (!model.attributes) return "// No validation rules defined";
|
|
75
|
+
const validations = [];
|
|
76
|
+
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
|
+
attrList.forEach(([name, attr]) => {
|
|
79
|
+
if (attr.required && !attr.auto && !AUTO_FIELDS.has(name)) {
|
|
80
|
+
validations.push(`
|
|
81
|
+
// ${name} is required
|
|
82
|
+
if (${contextParam}.operation === 'create' && !${dataParam}.${name}) {
|
|
83
|
+
errors.push('${name} is required');
|
|
84
|
+
}`);
|
|
85
|
+
}
|
|
86
|
+
if (attr.type === "String" || attr.type === "string") {
|
|
87
|
+
if (attr.min) {
|
|
88
|
+
validations.push(`
|
|
89
|
+
if (${dataParam}.${name} && ${dataParam}.${name}.length < ${attr.min}) {
|
|
90
|
+
errors.push('${name} must be at least ${attr.min} characters');
|
|
91
|
+
}`);
|
|
92
|
+
}
|
|
93
|
+
if (attr.max) {
|
|
94
|
+
validations.push(`
|
|
95
|
+
if (${dataParam}.${name} && ${dataParam}.${name}.length > ${attr.max}) {
|
|
96
|
+
errors.push('${name} must be at most ${attr.max} characters');
|
|
97
|
+
}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (attr.values && Array.isArray(attr.values)) {
|
|
101
|
+
const values = attr.values.map((v) => `'${v}'`).join(", ");
|
|
102
|
+
validations.push(`
|
|
103
|
+
if (${dataParam}.${name} && ![${values}].includes(${dataParam}.${name})) {
|
|
104
|
+
errors.push('${name} must be one of: ${attr.values.join(", ")}');
|
|
105
|
+
}`);
|
|
106
|
+
}
|
|
107
|
+
if (attr.format === "email") {
|
|
108
|
+
validations.push(`
|
|
109
|
+
if (${dataParam}.${name} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${dataParam}.${name})) {
|
|
110
|
+
errors.push('${name} must be a valid email address');
|
|
111
|
+
}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return validations.join("\n") || "// No validation rules defined";
|
|
115
|
+
}
|
|
116
|
+
function generateCreateMethod(model, modelName, modelVar, controller, allModels) {
|
|
117
|
+
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
118
|
+
const createEvent = hasEvents ? controller.publishes.find((e) => e.includes("Created")) : null;
|
|
119
|
+
return `
|
|
120
|
+
/**
|
|
121
|
+
* Create a new ${modelName}
|
|
122
|
+
*/
|
|
123
|
+
public async create(data: any): Promise<any> {
|
|
124
|
+
// Validate input
|
|
125
|
+
const validationResult = this.validate(data, { operation: 'create' });
|
|
126
|
+
if (!validationResult.valid) {
|
|
127
|
+
throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Transform FK fields to Prisma connect format
|
|
131
|
+
const prismaData = { ...data };
|
|
132
|
+
${generateFKTransform(model, "prismaData", allModels)}
|
|
133
|
+
|
|
134
|
+
// Create record
|
|
135
|
+
const ${modelVar} = await prisma.${modelVar}.create({
|
|
136
|
+
data: prismaData${generateIncludeRelationships(model)}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
${createEvent ? `
|
|
140
|
+
// Publish event
|
|
141
|
+
eventBus.publish(EventName.${createEvent}, {
|
|
142
|
+
${modelVar},
|
|
143
|
+
timestamp: new Date().toISOString()
|
|
144
|
+
});
|
|
145
|
+
` : ""}
|
|
146
|
+
|
|
147
|
+
return ${modelVar};
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
function generateRetrieveMethod(model, modelName, modelVar) {
|
|
152
|
+
return `
|
|
153
|
+
/**
|
|
154
|
+
* Retrieve ${modelName} by ID
|
|
155
|
+
*/
|
|
156
|
+
public async retrieve(id: string): Promise<any> {
|
|
157
|
+
const ${modelVar} = await prisma.${modelVar}.findUnique({
|
|
158
|
+
where: { id: parseId(id) }${generateIncludeRelationships(model)}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!${modelVar}) {
|
|
162
|
+
throw new Error('${modelName} not found');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return ${modelVar};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Retrieve all ${modelName}s
|
|
170
|
+
*/
|
|
171
|
+
public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
|
|
172
|
+
return await prisma.${modelVar}.findMany({
|
|
173
|
+
skip: options.skip,
|
|
174
|
+
take: options.take${generateIncludeRelationships(model)}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
function generateUpdateMethod(model, modelName, modelVar, controller, allModels) {
|
|
180
|
+
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
181
|
+
const updateEvent = hasEvents ? controller.publishes.find((e) => e.includes("Updated")) : null;
|
|
182
|
+
return `
|
|
183
|
+
/**
|
|
184
|
+
* Update ${modelName}
|
|
185
|
+
*/
|
|
186
|
+
public async update(id: string, data: any): Promise<any> {
|
|
187
|
+
// Validate input
|
|
188
|
+
const validationResult = this.validate(data, { operation: 'update' });
|
|
189
|
+
if (!validationResult.valid) {
|
|
190
|
+
throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Strip nested relations and id \u2014 only send scalar fields to Prisma
|
|
194
|
+
const updateData: any = {};
|
|
195
|
+
for (const [key, value] of Object.entries(data)) {
|
|
196
|
+
if (key === 'id') continue;
|
|
197
|
+
if (Array.isArray(value)) continue;
|
|
198
|
+
if (value !== null && typeof value === 'object' && !(value instanceof Date)) continue;
|
|
199
|
+
updateData[key] = value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Transform FK fields to Prisma connect format
|
|
203
|
+
${generateFKTransform(model, "updateData", allModels)}
|
|
204
|
+
|
|
205
|
+
// Update record
|
|
206
|
+
const ${modelVar} = await prisma.${modelVar}.update({
|
|
207
|
+
where: { id: parseId(id) },
|
|
208
|
+
data: updateData${generateIncludeRelationships(model)}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
${updateEvent ? `
|
|
212
|
+
// Publish event
|
|
213
|
+
eventBus.publish(EventName.${updateEvent}, {
|
|
214
|
+
${modelVar},
|
|
215
|
+
timestamp: new Date().toISOString()
|
|
216
|
+
});
|
|
217
|
+
` : ""}
|
|
218
|
+
|
|
219
|
+
return ${modelVar};
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function generateEvolveMethod(model, modelName, modelVar, controller) {
|
|
224
|
+
const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
|
|
225
|
+
const lifecycle = lifecycles[0];
|
|
226
|
+
const lifecycleName = lifecycle?.name || "status";
|
|
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
|
+
}
|
|
244
|
+
return `
|
|
245
|
+
/**
|
|
246
|
+
* Evolve ${modelName} through lifecycle
|
|
247
|
+
* States: ${states.join(" \u2192 ")}
|
|
248
|
+
*/
|
|
249
|
+
public async evolve(id: string, data: any): Promise<any> {
|
|
250
|
+
// Validate input
|
|
251
|
+
const validationResult = this.validate(data, { operation: 'evolve' });
|
|
252
|
+
if (!validationResult.valid) {
|
|
253
|
+
throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get current record to check lifecycle state
|
|
257
|
+
const current = await prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
|
|
258
|
+
if (!current) {
|
|
259
|
+
throw new Error('${modelName} not found');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
${states.length > 0 ? `
|
|
263
|
+
// Validate lifecycle transition
|
|
264
|
+
const currentState = (current as any).${lifecycleName};
|
|
265
|
+
const newState = data.${lifecycleName};
|
|
266
|
+
if (newState) {
|
|
267
|
+
const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
|
|
268
|
+
const allowed = validTransitions[currentState] || [];
|
|
269
|
+
if (!allowed.includes(newState)) {
|
|
270
|
+
throw new Error(\`Invalid transition: \${currentState} \u2192 \${newState}. Allowed: \${allowed.join(', ') || 'none'}\`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
` : ""}
|
|
274
|
+
|
|
275
|
+
// Update record
|
|
276
|
+
const ${modelVar} = await prisma.${modelVar}.update({
|
|
277
|
+
where: { id: parseId(id) },
|
|
278
|
+
data${generateIncludeRelationships(model)}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return ${modelVar};
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
function generateDeleteMethod(model, modelName, modelVar, controller) {
|
|
286
|
+
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
287
|
+
const deleteEvent = hasEvents ? controller.publishes.find((e) => e.includes("Deleted")) : null;
|
|
288
|
+
return `
|
|
289
|
+
/**
|
|
290
|
+
* Delete ${modelName}
|
|
291
|
+
*/
|
|
292
|
+
public async delete(id: string): Promise<void> {
|
|
293
|
+
${deleteEvent ? `
|
|
294
|
+
// Get record before deletion for event
|
|
295
|
+
const ${modelVar} = await prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
|
|
296
|
+
` : ""}
|
|
297
|
+
|
|
298
|
+
await prisma.${modelVar}.delete({
|
|
299
|
+
where: { id: parseId(id) }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
${deleteEvent ? `
|
|
303
|
+
// Publish event
|
|
304
|
+
if (${modelVar}) {
|
|
305
|
+
eventBus.publish(EventName.${deleteEvent}, {
|
|
306
|
+
${modelVar},
|
|
307
|
+
timestamp: new Date().toISOString()
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
` : ""}
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function generateCustomActions(controller, modelName, modelVar) {
|
|
315
|
+
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
const actions = [];
|
|
319
|
+
Object.entries(controller.actions).forEach(([actionName, action]) => {
|
|
320
|
+
actions.push(`
|
|
321
|
+
/**
|
|
322
|
+
* ${actionName}
|
|
323
|
+
* ${action.description || ""}
|
|
324
|
+
*/
|
|
325
|
+
public async ${actionName}(${generateActionParams(action)}): Promise<any> {
|
|
326
|
+
// TODO: Implement ${actionName} logic
|
|
327
|
+
throw new Error('${actionName} not implemented');
|
|
328
|
+
}`);
|
|
329
|
+
});
|
|
330
|
+
return actions.join("\n");
|
|
331
|
+
}
|
|
332
|
+
function generateActionParams(action) {
|
|
333
|
+
if (!action.parameters || Object.keys(action.parameters).length === 0) {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
const params = Object.entries(action.parameters).map(([name, param]) => {
|
|
337
|
+
const optional = !param.required;
|
|
338
|
+
return `${name}${optional ? "?" : ""}: ${mapTypeToTypeScript(param.type)}`;
|
|
339
|
+
});
|
|
340
|
+
return params.join(", ");
|
|
341
|
+
}
|
|
342
|
+
function mapTypeToTypeScript(type) {
|
|
343
|
+
const typeMap = {
|
|
344
|
+
String: "string",
|
|
345
|
+
Integer: "number",
|
|
346
|
+
Float: "number",
|
|
347
|
+
Boolean: "boolean",
|
|
348
|
+
Date: "Date",
|
|
349
|
+
DateTime: "Date",
|
|
350
|
+
UUID: "string",
|
|
351
|
+
JSON: "any"
|
|
352
|
+
};
|
|
353
|
+
return typeMap[type] || "any";
|
|
354
|
+
}
|
|
355
|
+
function generateFKTransform(model, varName = "prismaData", allModels) {
|
|
356
|
+
const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
|
|
357
|
+
const belongsToRels = rels.filter((r) => r.type === "belongsTo");
|
|
358
|
+
if (belongsToRels.length === 0) return "";
|
|
359
|
+
return belongsToRels.map((rel) => {
|
|
360
|
+
const relName = rel.name;
|
|
361
|
+
const fkField = `${relName}Id`;
|
|
362
|
+
let parseExpr = `${varName}.${fkField}`;
|
|
363
|
+
if (allModels) {
|
|
364
|
+
const targetModel = allModels.find((m) => m.name === rel.target);
|
|
365
|
+
if (targetModel) {
|
|
366
|
+
const idAttr = (Array.isArray(targetModel.attributes) ? targetModel.attributes : Object.values(targetModel.attributes || {})).find((a) => a.name === "id");
|
|
367
|
+
const idType = idAttr?.type || "String";
|
|
368
|
+
if (idType === "Integer" || idType === "Int" || idType === "Number") {
|
|
369
|
+
parseExpr = `parseInt(${varName}.${fkField}, 10)`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return `if (${varName}.${fkField}) {
|
|
374
|
+
${varName}.${relName} = { connect: { id: ${parseExpr} } };
|
|
375
|
+
delete ${varName}.${fkField};
|
|
376
|
+
}`;
|
|
377
|
+
}).join("\n ");
|
|
378
|
+
}
|
|
379
|
+
function generateIncludeRelationships(model) {
|
|
380
|
+
if (!model.relationships) {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
let relationshipKeys;
|
|
384
|
+
if (Array.isArray(model.relationships)) {
|
|
385
|
+
relationshipKeys = model.relationships.map((rel) => {
|
|
386
|
+
if (typeof rel === "string") {
|
|
387
|
+
return rel;
|
|
388
|
+
} else if (typeof rel === "object" && rel.name) {
|
|
389
|
+
return rel.name;
|
|
390
|
+
} else {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}).filter((rel) => rel !== null);
|
|
394
|
+
} else if (typeof model.relationships === "object") {
|
|
395
|
+
relationshipKeys = Object.keys(model.relationships);
|
|
396
|
+
} else {
|
|
397
|
+
return "";
|
|
398
|
+
}
|
|
399
|
+
if (relationshipKeys.length === 0) {
|
|
400
|
+
return "";
|
|
401
|
+
}
|
|
402
|
+
const includes = relationshipKeys.map((rel) => ` ${rel}: true`).join(",\n");
|
|
403
|
+
return `,
|
|
404
|
+
include: {
|
|
405
|
+
${includes}
|
|
406
|
+
}`;
|
|
407
|
+
}
|
|
408
|
+
function hasEventPublishing(curedOps, controller) {
|
|
409
|
+
return controller.publishes && Array.isArray(controller.publishes) && controller.publishes.length > 0;
|
|
410
|
+
}
|
|
411
|
+
export {
|
|
412
|
+
generatePrismaController as default
|
|
413
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { generateBehaviorWithHelpers } from "./behavior-generator.js";
|
|
2
|
+
function generatePrismaService(context) {
|
|
3
|
+
const { service, spec } = context;
|
|
4
|
+
if (!service) {
|
|
5
|
+
throw new Error("Service is required in template context");
|
|
6
|
+
}
|
|
7
|
+
const serviceName = service.name;
|
|
8
|
+
const hasEvents = service.publishes && service.publishes.length > 0 || service.subscribes && service.subscribes.length > 0;
|
|
9
|
+
return `/**
|
|
10
|
+
* ${serviceName}
|
|
11
|
+
* Abstract business logic service
|
|
12
|
+
* ${service.description || ""}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { PrismaClient } from '@prisma/client';
|
|
16
|
+
${hasEvents ? `import { eventBus, EventName } from '../events/eventBus.js';` : ""}
|
|
17
|
+
|
|
18
|
+
const prisma = new PrismaClient();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ${serviceName} class
|
|
22
|
+
*/
|
|
23
|
+
export class ${serviceName} {
|
|
24
|
+
${generateConstructor(service)}
|
|
25
|
+
${generateOperationsWithHelpers(service)}
|
|
26
|
+
${generateEventSubscriptions(service)}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Export singleton instance
|
|
30
|
+
export const ${serviceName.charAt(0).toLowerCase() + serviceName.slice(1)} = new ${serviceName}();
|
|
31
|
+
export default ${serviceName.charAt(0).toLowerCase() + serviceName.slice(1)};
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
function generateConstructor(service) {
|
|
35
|
+
const hasSubscriptions = service.subscribes && service.subscribes.length > 0;
|
|
36
|
+
if (!hasSubscriptions) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
return `
|
|
40
|
+
private unsubscribers: Array<() => void> = [];
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
// Subscribe to events
|
|
44
|
+
this.setupEventSubscriptions();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Setup event subscriptions
|
|
49
|
+
*/
|
|
50
|
+
private setupEventSubscriptions(): void {
|
|
51
|
+
${service.subscribes.map((eventName) => `
|
|
52
|
+
// Subscribe to ${eventName}
|
|
53
|
+
const unsubscribe${eventName} = eventBus.subscribe(
|
|
54
|
+
EventName.${eventName},
|
|
55
|
+
this.on${eventName}.bind(this)
|
|
56
|
+
);
|
|
57
|
+
this.unsubscribers.push(unsubscribe${eventName});
|
|
58
|
+
`).join("\n")}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Cleanup subscriptions
|
|
63
|
+
*/
|
|
64
|
+
public destroy(): void {
|
|
65
|
+
this.unsubscribers.forEach(unsubscribe => unsubscribe());
|
|
66
|
+
this.unsubscribers = [];
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function generateOperationsWithHelpers(service) {
|
|
71
|
+
const ops = generateOperations(service);
|
|
72
|
+
if (_collectedHelpers.length > 0) {
|
|
73
|
+
const helpers = [...new Set(_collectedHelpers)].join("\n");
|
|
74
|
+
_collectedHelpers.length = 0;
|
|
75
|
+
return ops + "\n" + helpers;
|
|
76
|
+
}
|
|
77
|
+
return ops;
|
|
78
|
+
}
|
|
79
|
+
const _collectedHelpers = [];
|
|
80
|
+
function generateOperations(service) {
|
|
81
|
+
if (!service.operations || Array.isArray(service.operations) && service.operations.length === 0 || !Array.isArray(service.operations) && Object.keys(service.operations).length === 0) {
|
|
82
|
+
return `
|
|
83
|
+
/**
|
|
84
|
+
* Execute service logic
|
|
85
|
+
*/
|
|
86
|
+
public async execute(params: any): Promise<any> {
|
|
87
|
+
// TODO: Implement service logic
|
|
88
|
+
throw new Error('Service logic not implemented');
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
const entries = Array.isArray(service.operations) ? service.operations.map((op) => [op.name, op]) : Object.entries(service.operations);
|
|
93
|
+
const operations = [];
|
|
94
|
+
entries.forEach(([operationName, operation]) => {
|
|
95
|
+
operations.push(generateOperation(operationName, operation, service));
|
|
96
|
+
});
|
|
97
|
+
return operations.join("\n");
|
|
98
|
+
}
|
|
99
|
+
function generateOperation(operationName, operation, service) {
|
|
100
|
+
const params = generateOperationParams(operation);
|
|
101
|
+
const hasPublish = service.publishes && service.publishes.length > 0;
|
|
102
|
+
return `
|
|
103
|
+
/**
|
|
104
|
+
* ${operationName}
|
|
105
|
+
* ${operation.description || ""}
|
|
106
|
+
*/
|
|
107
|
+
public async ${operationName}(${params}): Promise<any> {
|
|
108
|
+
try {
|
|
109
|
+
${generateOperationLogic(operation, service)}
|
|
110
|
+
|
|
111
|
+
${hasPublish ? `
|
|
112
|
+
// Publish event (example)
|
|
113
|
+
// eventBus.publish(EventName.SomeEvent, {
|
|
114
|
+
// operation: '${operationName}',
|
|
115
|
+
// timestamp: new Date().toISOString()
|
|
116
|
+
// });
|
|
117
|
+
` : ""}
|
|
118
|
+
|
|
119
|
+
// Return result
|
|
120
|
+
return { success: true };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(\`[${service.name}] ${operationName} failed:\`, error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
function generateOperationParams(operation) {
|
|
129
|
+
if (!operation.parameters || Object.keys(operation.parameters).length === 0) {
|
|
130
|
+
return "params: any = {}";
|
|
131
|
+
}
|
|
132
|
+
const params = Object.entries(operation.parameters).map(([name, param]) => {
|
|
133
|
+
const optional = !param.required;
|
|
134
|
+
return `${name}${optional ? "?" : ""}: ${mapTypeToTypeScript(param.type)}`;
|
|
135
|
+
});
|
|
136
|
+
return params.join(", ");
|
|
137
|
+
}
|
|
138
|
+
function generateOperationLogic(operation, service) {
|
|
139
|
+
const impl = operation.implementation || {};
|
|
140
|
+
const meta = operation.metadata;
|
|
141
|
+
if (!impl.preconditions && operation.requires) {
|
|
142
|
+
impl.preconditions = Array.isArray(operation.requires) ? operation.requires : [operation.requires];
|
|
143
|
+
}
|
|
144
|
+
if (!impl.postconditions && operation.ensures) {
|
|
145
|
+
impl.postconditions = Array.isArray(operation.ensures) ? operation.ensures : [operation.ensures];
|
|
146
|
+
}
|
|
147
|
+
if (!impl.sideEffects && operation.publishes) {
|
|
148
|
+
impl.sideEffects = (Array.isArray(operation.publishes) ? operation.publishes : [operation.publishes]).map((e) => `Publish event: ${e}`);
|
|
149
|
+
}
|
|
150
|
+
if (impl.preconditions?.length || impl.postconditions?.length || impl.steps?.length || impl.transactional) {
|
|
151
|
+
const modelName = inferModelFromServiceName(service.name);
|
|
152
|
+
const context = {
|
|
153
|
+
modelName,
|
|
154
|
+
serviceName: service.name,
|
|
155
|
+
operationName: operation.name || "execute",
|
|
156
|
+
prismaModel: modelName
|
|
157
|
+
};
|
|
158
|
+
const behavior = {
|
|
159
|
+
preconditions: impl.preconditions || [],
|
|
160
|
+
postconditions: impl.postconditions || [],
|
|
161
|
+
sideEffects: impl.sideEffects || [],
|
|
162
|
+
steps: impl.steps || [],
|
|
163
|
+
transactional: impl.transactional || false
|
|
164
|
+
};
|
|
165
|
+
const opMeta = {
|
|
166
|
+
async: meta?.async ?? true,
|
|
167
|
+
cacheable: meta?.cacheable ?? false,
|
|
168
|
+
idempotent: meta?.idempotent ?? false
|
|
169
|
+
};
|
|
170
|
+
const result = generateBehaviorWithHelpers(behavior, opMeta, context);
|
|
171
|
+
_collectedHelpers.push(...result.helperMethods);
|
|
172
|
+
return result.body;
|
|
173
|
+
}
|
|
174
|
+
if (operation.description) {
|
|
175
|
+
return `// ${operation.description}`;
|
|
176
|
+
}
|
|
177
|
+
return `// TODO: Implement operation logic`;
|
|
178
|
+
}
|
|
179
|
+
function inferModelFromServiceName(serviceName) {
|
|
180
|
+
return serviceName.replace(/RelationshipService$/, "").replace(/Service$/, "") || "Entity";
|
|
181
|
+
}
|
|
182
|
+
function generateEventSubscriptions(service) {
|
|
183
|
+
if (!service.subscribes || service.subscribes.length === 0) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
const handlers = [];
|
|
187
|
+
service.subscribes.forEach((eventName) => {
|
|
188
|
+
handlers.push(`
|
|
189
|
+
/**
|
|
190
|
+
* Handle ${eventName} event
|
|
191
|
+
*/
|
|
192
|
+
private async on${eventName}(payload: any): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
console.log(\`[${service.name}] Received ${eventName} event\`, payload);
|
|
195
|
+
|
|
196
|
+
// TODO: Implement event handling logic
|
|
197
|
+
${generateEventHandlerLogic(eventName, service)}
|
|
198
|
+
|
|
199
|
+
console.log(\`[${service.name}] ${eventName} event processed successfully\`);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(\`[${service.name}] Failed to process ${eventName} event\`, error);
|
|
202
|
+
// TODO: Add error handling (retry, dead letter queue, etc.)
|
|
203
|
+
}
|
|
204
|
+
}`);
|
|
205
|
+
});
|
|
206
|
+
return handlers.join("\n");
|
|
207
|
+
}
|
|
208
|
+
function generateEventHandlerLogic(eventName, service) {
|
|
209
|
+
return `
|
|
210
|
+
// Example: Update related records
|
|
211
|
+
// await prisma.someModel.update({
|
|
212
|
+
// where: { id: payload.id },
|
|
213
|
+
// data: { ... }
|
|
214
|
+
// });
|
|
215
|
+
|
|
216
|
+
// Example: Trigger workflows
|
|
217
|
+
// await this.triggerWorkflow(payload);
|
|
218
|
+
|
|
219
|
+
// Example: Publish follow-up events
|
|
220
|
+
// eventBus.publish(EventName.AnotherEvent, {
|
|
221
|
+
// original: payload,
|
|
222
|
+
// processed: true
|
|
223
|
+
// });
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
function mapTypeToTypeScript(type) {
|
|
227
|
+
const typeMap = {
|
|
228
|
+
String: "string",
|
|
229
|
+
Integer: "number",
|
|
230
|
+
Float: "number",
|
|
231
|
+
Boolean: "boolean",
|
|
232
|
+
Date: "Date",
|
|
233
|
+
DateTime: "Date",
|
|
234
|
+
UUID: "string",
|
|
235
|
+
JSON: "any",
|
|
236
|
+
Array: "any[]",
|
|
237
|
+
Object: "Record<string, any>"
|
|
238
|
+
};
|
|
239
|
+
return typeMap[type] || "any";
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
generatePrismaService as default
|
|
243
|
+
};
|