@specverse/engines 6.7.8 → 6.16.0
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/ai/behavior-ai-service.js +2 -2
- package/dist/ai/behavior-ai-service.js.map +1 -1
- package/dist/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +20 -0
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +72 -22
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/logical/generators/controller-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/controller-generator.js +26 -4
- package/dist/inference/logical/generators/controller-generator.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +22 -5
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +50 -15
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +26 -6
- package/dist/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +44 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +68 -13
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +515 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/client-generator.js +165 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/controller-generator.js +300 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/ddl-generator.js +169 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/service-generator.js +65 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +433 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +27 -4
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +7 -34
- package/dist/parser/processors/ExecutableProcessor.d.ts.map +1 -1
- package/dist/parser/processors/ExecutableProcessor.js +14 -1
- package/dist/parser/processors/ExecutableProcessor.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +30 -3
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +46 -24
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +80 -21
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +48 -7
- package/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +103 -0
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +97 -23
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +691 -0
- package/libs/instance-factories/services/templates/postgres-native/__tests__/controller-generator.test.ts +193 -0
- package/libs/instance-factories/services/templates/postgres-native/client-generator.ts +178 -0
- package/libs/instance-factories/services/templates/postgres-native/controller-generator.ts +372 -0
- package/libs/instance-factories/services/templates/postgres-native/ddl-generator.ts +236 -0
- package/libs/instance-factories/services/templates/postgres-native/service-generator.ts +84 -0
- package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +539 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +61 -7
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +21 -68
- package/package.json +4 -3
|
@@ -12,7 +12,10 @@ function generateBackendPackageJson(context) {
|
|
|
12
12
|
const appName = (spec.metadata?.component || "app").toLowerCase().replace(/\s+/g, "-");
|
|
13
13
|
const orm = resolveOrmName(manifest);
|
|
14
14
|
const isMongoNative = orm === "MongoDBNativeDriver";
|
|
15
|
-
const
|
|
15
|
+
const isPgNative = orm === "PostgresNativeDriver";
|
|
16
|
+
const dbScripts = isMongoNative ? {} : isPgNative ? {
|
|
17
|
+
"db:setup": 'psql "$POSTGRES_URL" -f src/db/schema.sql'
|
|
18
|
+
} : {
|
|
16
19
|
"db:setup": "prisma generate && prisma db push",
|
|
17
20
|
"db:generate": "prisma generate",
|
|
18
21
|
"db:push": "prisma db push",
|
|
@@ -20,8 +23,8 @@ function generateBackendPackageJson(context) {
|
|
|
20
23
|
"db:studio": "prisma studio",
|
|
21
24
|
"db:seed": "tsx prisma/seed.ts"
|
|
22
25
|
};
|
|
23
|
-
const ormDeps = isMongoNative ? { "mongodb": "^6.3.0" } : { "@prisma/client": "^5.7.0" };
|
|
24
|
-
const ormDevDeps = isMongoNative ? {} : { "prisma": "^5.7.0" };
|
|
26
|
+
const ormDeps = isMongoNative ? { "mongodb": "^6.3.0" } : isPgNative ? { "pg": "^8.11.0" } : { "@prisma/client": "^5.7.0" };
|
|
27
|
+
const ormDevDeps = isMongoNative ? {} : isPgNative ? { "@types/pg": "^8.11.0" } : { "prisma": "^5.7.0" };
|
|
25
28
|
const pkg = {
|
|
26
29
|
name: `${appName}-backend`,
|
|
27
30
|
version: spec.metadata?.version || "1.0.0",
|
|
@@ -61,7 +64,17 @@ function generateBackendPackageJson(context) {
|
|
|
61
64
|
"eventemitter3": "^5.0.0",
|
|
62
65
|
"zod": "^3.22.0",
|
|
63
66
|
"dotenv": "^16.3.0",
|
|
64
|
-
"commander": "^13.0.0"
|
|
67
|
+
"commander": "^13.0.0",
|
|
68
|
+
// AI-behavior whitelist — these libs are allowed to be dynamic-
|
|
69
|
+
// imported from generated `*.ai.ts` pure functions. They cover the
|
|
70
|
+
// common cases (JWT, hashing, formula eval) without giving the LLM
|
|
71
|
+
// unbounded library access. Listed unconditionally for now; revisit
|
|
72
|
+
// (TODO #43K-B-review) whether to gate per-spec when the spec
|
|
73
|
+
// doesn't actually use them.
|
|
74
|
+
"jsonwebtoken": "^9.0.0",
|
|
75
|
+
"bcryptjs": "^2.4.3",
|
|
76
|
+
"uuid": "^9.0.0",
|
|
77
|
+
"expr-eval": "^2.0.2"
|
|
65
78
|
},
|
|
66
79
|
devDependencies: {
|
|
67
80
|
"typescript": "^5.3.0",
|
|
@@ -72,7 +85,11 @@ function generateBackendPackageJson(context) {
|
|
|
72
85
|
"@vitest/coverage-v8": "^3.0.0",
|
|
73
86
|
"eslint": "^9.0.0",
|
|
74
87
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
75
|
-
"@typescript-eslint/parser": "^8.0.0"
|
|
88
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
89
|
+
// Type definitions for the AI-behavior whitelist libs (#43K-B).
|
|
90
|
+
"@types/jsonwebtoken": "^9.0.0",
|
|
91
|
+
"@types/bcryptjs": "^2.4.0",
|
|
92
|
+
"@types/uuid": "^9.0.0"
|
|
76
93
|
},
|
|
77
94
|
engines: {
|
|
78
95
|
node: ">=20.0.0"
|
|
@@ -21,10 +21,14 @@ function generateFastifyRoutes(context) {
|
|
|
21
21
|
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
22
22
|
const params = action.parameters || {};
|
|
23
23
|
const hasIdParam = Object.keys(params).some((p) => p === "id" || p === `${modelName?.charAt(0).toLowerCase()}${modelName?.slice(1)}Id`);
|
|
24
|
+
const explicitPath = action.path;
|
|
25
|
+
const explicitMethod = action.method;
|
|
26
|
+
const isExternal = !!(explicitPath && /^\/api\//.test(explicitPath));
|
|
24
27
|
endpoints.push({
|
|
25
28
|
operation: actionName,
|
|
26
|
-
method: "POST",
|
|
27
|
-
path: hasIdParam ? `/:id/${actionName}` : `/${actionName}
|
|
29
|
+
method: explicitMethod || "POST",
|
|
30
|
+
path: explicitPath || (hasIdParam ? `/:id/${actionName}` : `/${actionName}`),
|
|
31
|
+
external: isExternal,
|
|
28
32
|
parameters: params,
|
|
29
33
|
description: action.description || `Custom action: ${actionName}`
|
|
30
34
|
});
|
|
@@ -33,9 +37,27 @@ function generateFastifyRoutes(context) {
|
|
|
33
37
|
if (!endpoints || endpoints.length === 0) {
|
|
34
38
|
console.warn(`Warning: Controller ${controllerName} has no endpoints. Generating empty routes file.`);
|
|
35
39
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const internalEndpoints = endpoints?.filter((e) => !e.external) || [];
|
|
41
|
+
const externalEndpoints = endpoints?.filter((e) => e.external) || [];
|
|
42
|
+
const internalHandlers = internalEndpoints.map((endpoint) => generateRouteHandler(endpoint, modelName, handlerName, isModelController, implType, controllerName)).join("\n\n");
|
|
43
|
+
const externalHandlers = externalEndpoints.map((endpoint) => generateRouteHandler(endpoint, modelName, handlerName, isModelController, implType, controllerName)).join("\n\n");
|
|
44
|
+
const externalRoutesExport = externalEndpoints.length > 0 ? `
|
|
45
|
+
/**
|
|
46
|
+
* Mount the external (root-prefix) routes for this controller.
|
|
47
|
+
*
|
|
48
|
+
* Called by the generated main.ts at root scope (no prefix) so action.path
|
|
49
|
+
* declarations like '/api/v2/auth/register' land at exactly that URL,
|
|
50
|
+
* bypassing the controller's '/api/<plural>' prefix.
|
|
51
|
+
*/
|
|
52
|
+
export async function registerExternalRoutes(
|
|
53
|
+
fastify: FastifyInstance,
|
|
54
|
+
options: any
|
|
55
|
+
) {
|
|
56
|
+
const handler = ${isModelController ? "options.controllers" : "options.services"}.${handlerName};
|
|
57
|
+
|
|
58
|
+
${externalHandlers.split("\n").map((line) => " " + line).join("\n")}
|
|
59
|
+
}
|
|
60
|
+
` : "";
|
|
39
61
|
return `${imports}
|
|
40
62
|
|
|
41
63
|
/**
|
|
@@ -51,9 +73,9 @@ export default async function ${routeName.replace("Controller", "")}Routes(
|
|
|
51
73
|
) {
|
|
52
74
|
const handler = ${isModelController ? "options.controllers" : "options.services"}.${handlerName};
|
|
53
75
|
|
|
54
|
-
${
|
|
76
|
+
${internalHandlers.split("\n").map((line) => " " + line).join("\n")}
|
|
55
77
|
}
|
|
56
|
-
`;
|
|
78
|
+
${externalRoutesExport}`;
|
|
57
79
|
}
|
|
58
80
|
function generateImports(controller, modelName, handlerName, isModelController, implType) {
|
|
59
81
|
const imports = [
|
|
@@ -226,36 +248,49 @@ function generateHandlerBody(operation, modelName, handlerName, isModelControlle
|
|
|
226
248
|
});
|
|
227
249
|
}`;
|
|
228
250
|
default: {
|
|
251
|
+
const path = endpoint?.path || "";
|
|
252
|
+
const urlParamNames = /* @__PURE__ */ new Set();
|
|
253
|
+
const urlParamRegex = /:([A-Za-z_][\w]*)/g;
|
|
254
|
+
let urlMatch;
|
|
255
|
+
while ((urlMatch = urlParamRegex.exec(path)) !== null) {
|
|
256
|
+
urlParamNames.add(urlMatch[1]);
|
|
257
|
+
}
|
|
229
258
|
const params = endpoint?.parameters || {};
|
|
230
|
-
const paramNames = Object.keys(params);
|
|
231
|
-
const callArgs = paramNames.length > 0 ? paramNames.map((p) => `body.${p}`).join(", ") : "body";
|
|
232
259
|
const validations = [];
|
|
233
260
|
for (const [pName, pDef] of Object.entries(params)) {
|
|
261
|
+
if (urlParamNames.has(pName)) continue;
|
|
234
262
|
const required = typeof pDef === "string" ? pDef.includes("required") : pDef?.required;
|
|
235
263
|
const typeStr = typeof pDef === "string" ? pDef.split(" ")[0] : pDef?.type || "String";
|
|
236
264
|
if (required) {
|
|
237
|
-
validations.push(` if (
|
|
265
|
+
validations.push(` if (args.${pName} === undefined || args.${pName} === null) errors.push({ field: '${pName}', message: '${pName} is required' });`);
|
|
238
266
|
}
|
|
239
267
|
if (typeStr === "UUID" || typeStr === "String" || typeStr === "Email") {
|
|
240
|
-
validations.push(` if (
|
|
268
|
+
validations.push(` if (args.${pName} !== undefined && typeof args.${pName} !== 'string') errors.push({ field: '${pName}', message: '${pName} must be a string' });`);
|
|
241
269
|
} else if (typeStr === "Integer" || typeStr === "Number" || typeStr === "Float") {
|
|
242
|
-
validations.push(` if (
|
|
270
|
+
validations.push(` if (args.${pName} !== undefined && typeof args.${pName} !== 'number') errors.push({ field: '${pName}', message: '${pName} must be a number' });`);
|
|
243
271
|
} else if (typeStr === "Boolean") {
|
|
244
|
-
validations.push(` if (
|
|
272
|
+
validations.push(` if (args.${pName} !== undefined && typeof args.${pName} !== 'boolean') errors.push({ field: '${pName}', message: '${pName} must be a boolean' });`);
|
|
245
273
|
}
|
|
246
274
|
}
|
|
247
275
|
const validationBlock = validations.length > 0 ? ` const errors: Array<{ field: string; message: string }> = [];
|
|
248
276
|
${validations.join("\n")}
|
|
249
277
|
if (errors.length > 0) return reply.status(400).send({ error: 'Validation failed', details: errors });` : "";
|
|
250
278
|
return `try {
|
|
279
|
+
const params = (request.params || {}) as Record<string, any>;
|
|
251
280
|
const body = (request.body || {}) as Record<string, any>;
|
|
281
|
+
const args = { ...params, ...body };
|
|
252
282
|
${validationBlock}
|
|
253
|
-
const result = await handler.${operation}(
|
|
283
|
+
const result = await handler.${operation}(args);
|
|
254
284
|
return reply.send(result || { success: true });
|
|
255
285
|
} catch (error) {
|
|
286
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
287
|
+
// 501 for TODO-stubbed action bodies (#43F); 400 for real failures.
|
|
288
|
+
if (msg.includes('is not implemented')) {
|
|
289
|
+
return reply.status(501).send({ error: 'Not Implemented', message: msg });
|
|
290
|
+
}
|
|
256
291
|
return reply.status(400).send({
|
|
257
292
|
error: 'Failed to execute ${operation}',
|
|
258
|
-
message:
|
|
293
|
+
message: msg
|
|
259
294
|
});
|
|
260
295
|
}`;
|
|
261
296
|
}
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { normalizeSpec, deriveBasePath } from "@specverse/types/spec-rules";
|
|
2
|
+
function resolveOrmName(manifest) {
|
|
3
|
+
if (!manifest) return "PrismaORM";
|
|
4
|
+
const inner = manifest.manifests ? Object.values(manifest.manifests)[0] : manifest;
|
|
5
|
+
if (!inner) return "PrismaORM";
|
|
6
|
+
const caps = Array.isArray(inner.capabilityMappings) ? inner.capabilityMappings : [];
|
|
7
|
+
const ormCap = caps.find((m) => m?.capability === "orm.client") || caps.find((m) => m?.capability === "orm.schema");
|
|
8
|
+
if (ormCap) return ormCap.implementation || ormCap.instanceFactory || "PrismaORM";
|
|
9
|
+
return inner.defaultMappings?.orm || "PrismaORM";
|
|
10
|
+
}
|
|
2
11
|
function generateFastifyServer(context) {
|
|
3
|
-
const { spec, models } = context;
|
|
12
|
+
const { spec, models, manifest } = context;
|
|
13
|
+
const orm = resolveOrmName(manifest);
|
|
14
|
+
const isMongoNative = orm === "MongoDBNativeDriver";
|
|
4
15
|
const allModels = models || (spec?.models ? Object.values(spec.models) : []);
|
|
5
16
|
const modelNames = allModels.map((m) => m.name).filter(Boolean);
|
|
6
17
|
const routeImports = modelNames.map(
|
|
7
|
-
(name) => `import ${name}Routes from './routes/${name}Controller.js';`
|
|
18
|
+
(name) => `import ${name}Routes, * as ${name}RoutesNS from './routes/${name}Controller.js';`
|
|
8
19
|
).join("\n");
|
|
9
20
|
const routeRegistrations = modelNames.map((name) => {
|
|
10
21
|
const path = deriveBasePath(name);
|
|
11
22
|
return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
|
|
12
23
|
}).join("\n");
|
|
24
|
+
const externalRouteRegistrations = modelNames.map((name) => {
|
|
25
|
+
return ` if (typeof (${name}RoutesNS as any).registerExternalRoutes === 'function') {
|
|
26
|
+
await (${name}RoutesNS as any).registerExternalRoutes(fastify, { controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });
|
|
27
|
+
}`;
|
|
28
|
+
}).join("\n");
|
|
13
29
|
const specEvents = spec.events ? Object.keys(spec.events) : [];
|
|
14
30
|
const hasEvents = specEvents.length > 0 || modelNames.length > 0;
|
|
15
31
|
const servicesList = spec.services ? Object.keys(spec.services) : [];
|
|
@@ -52,12 +68,13 @@ import { fileURLToPath } from 'url';
|
|
|
52
68
|
|
|
53
69
|
import Fastify from 'fastify';
|
|
54
70
|
import cors from '@fastify/cors';
|
|
55
|
-
import { PrismaClient } from '@prisma/client'
|
|
71
|
+
${isMongoNative ? `import { disconnect as disconnectMongo } from './db/mongoClient.js';` : `import { PrismaClient } from '@prisma/client';`}
|
|
56
72
|
${hasEvents ? `import { eventBus } from './events/eventBus.js';
|
|
57
73
|
import { registerWebSocketBridge } from './events/websocket-bridge.js';` : ""}
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
${isMongoNative ? `// MongoDB native driver client lives in db/mongoClient.ts; nothing
|
|
76
|
+
// to initialize at module scope (lazy-connect on first getCollection).` : `// Initialize Prisma
|
|
77
|
+
export const prisma = new PrismaClient();`}
|
|
61
78
|
|
|
62
79
|
// Initialize Fastify
|
|
63
80
|
const fastify = Fastify({
|
|
@@ -99,6 +116,9 @@ ${serviceRouteImports}` : ""}
|
|
|
99
116
|
async function registerRoutes() {
|
|
100
117
|
${routeRegistrations}
|
|
101
118
|
${serviceRouteRegistrations ? "\n // Service operation routes (RPC-style under /api/services/{ServiceName})\n" + serviceRouteRegistrations : ""}
|
|
119
|
+
|
|
120
|
+
// External-prefix actions (action.path declarations like '/api/v2/auth/register')
|
|
121
|
+
${externalRouteRegistrations}
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
// Start server
|
|
@@ -127,7 +147,7 @@ ${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
|
|
|
127
147
|
try {
|
|
128
148
|
fastify.log.info(\`Received \${signal}, closing server...\`);
|
|
129
149
|
await fastify.close();
|
|
130
|
-
try { await (prisma as any).$disconnect?.(); } catch { /* ignore */ }
|
|
150
|
+
try { ${isMongoNative ? "await disconnectMongo();" : "await (prisma as any).$disconnect?.();"} } catch { /* ignore */ }
|
|
131
151
|
process.exit(0);
|
|
132
152
|
} catch (err) {
|
|
133
153
|
fastify.log.error({ err }, 'Error during shutdown');
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: PostgresNativeDriver
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
category: service
|
|
4
|
+
description: "Business logic services using the native node-postgres (pg) driver — raw SQL, no ORM layer"
|
|
5
|
+
|
|
6
|
+
metadata:
|
|
7
|
+
author: "SpecVerse Team"
|
|
8
|
+
license: "MIT"
|
|
9
|
+
tags: [services, business-logic, postgres, pg, native-driver, controllers]
|
|
10
|
+
|
|
11
|
+
compatibility:
|
|
12
|
+
specverse: ">=5.0.0"
|
|
13
|
+
node: ">=18.0.0"
|
|
14
|
+
|
|
15
|
+
# Same shape as MongoDBNativeDriver — the pg pool IS the data layer; no ORM
|
|
16
|
+
# layer to swap in independently. This single factory therefore provides
|
|
17
|
+
# both the orm.* capabilities and the service-layer capabilities.
|
|
18
|
+
capabilities:
|
|
19
|
+
provides:
|
|
20
|
+
- "orm.schema" # Emits the pg pool client + DDL (the connection IS the schema layer)
|
|
21
|
+
- "orm.client"
|
|
22
|
+
- "orm.postgres.native"
|
|
23
|
+
- "service.controller"
|
|
24
|
+
- "service.business"
|
|
25
|
+
- "service.crud"
|
|
26
|
+
requires:
|
|
27
|
+
- "storage.database.relational"
|
|
28
|
+
|
|
29
|
+
technology:
|
|
30
|
+
runtime: "node"
|
|
31
|
+
language: "typescript"
|
|
32
|
+
orm: "postgres-native"
|
|
33
|
+
version: "^8.11.0"
|
|
34
|
+
|
|
35
|
+
dependencies:
|
|
36
|
+
runtime:
|
|
37
|
+
- name: "pg"
|
|
38
|
+
version: "^8.11.0"
|
|
39
|
+
|
|
40
|
+
dev:
|
|
41
|
+
- name: "@types/pg"
|
|
42
|
+
version: "^8.11.0"
|
|
43
|
+
- name: "@types/node"
|
|
44
|
+
version: "^20.8.0"
|
|
45
|
+
- name: "typescript"
|
|
46
|
+
version: "^5.2.0"
|
|
47
|
+
|
|
48
|
+
codeTemplates:
|
|
49
|
+
# `schema` slot emits the pg pool singleton + a co-located CREATE TABLE
|
|
50
|
+
# script (schema.sql) so users can bootstrap a database without pulling
|
|
51
|
+
# in an external migrator. The pool generator owns the runtime
|
|
52
|
+
# connection; the DDL generator owns the static SQL.
|
|
53
|
+
schema:
|
|
54
|
+
engine: typescript
|
|
55
|
+
generator: "libs/instance-factories/services/templates/postgres-native/client-generator.ts"
|
|
56
|
+
outputPattern: "{backendDir}/src/db/pgClient.ts"
|
|
57
|
+
|
|
58
|
+
ddl:
|
|
59
|
+
engine: typescript
|
|
60
|
+
generator: "libs/instance-factories/services/templates/postgres-native/ddl-generator.ts"
|
|
61
|
+
outputPattern: "{backendDir}/src/db/schema.sql"
|
|
62
|
+
|
|
63
|
+
controllers:
|
|
64
|
+
engine: typescript
|
|
65
|
+
generator: "libs/instance-factories/services/templates/postgres-native/controller-generator.ts"
|
|
66
|
+
outputPattern: "{backendDir}/src/controllers/{model}Controller.ts"
|
|
67
|
+
|
|
68
|
+
services:
|
|
69
|
+
engine: typescript
|
|
70
|
+
generator: "libs/instance-factories/services/templates/postgres-native/service-generator.ts"
|
|
71
|
+
outputPattern: "{backendDir}/src/services/{service}.ts"
|
|
72
|
+
|
|
73
|
+
configuration:
|
|
74
|
+
outputStructure: "monorepo"
|
|
75
|
+
backendDir: "backend"
|
|
76
|
+
tableNaming: "lowercase-pluralized" # User → users, OrderItem → orderitems
|
|
77
|
+
validation: true
|
|
78
|
+
eventPublishing: true
|
|
79
|
+
errorHandling: "throw"
|
|
80
|
+
|
|
81
|
+
requirements:
|
|
82
|
+
dependencies:
|
|
83
|
+
npm:
|
|
84
|
+
dependencies:
|
|
85
|
+
"pg": "^8.11.0"
|
|
86
|
+
environment:
|
|
87
|
+
- name: "POSTGRES_URL"
|
|
88
|
+
description: "PostgreSQL connection string (e.g. postgres://user:pass@localhost:5432/myapp)"
|
|
89
|
+
required: true
|
|
90
|
+
configuration: {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function toMethod(words) {
|
|
2
|
+
const cleaned = words.trim().replace(/[^A-Za-z0-9\s]+/g, " ");
|
|
3
|
+
const camel = cleaned.replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
|
|
4
|
+
const safe = camel.replace(/[^A-Za-z0-9_$]/g, "");
|
|
5
|
+
return safe || "unnamedStep";
|
|
6
|
+
}
|
|
7
|
+
function toVar(name) {
|
|
8
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
9
|
+
}
|
|
10
|
+
function matchAgainstConventions(step, ctx, conventions, aiArgsExpr) {
|
|
11
|
+
for (const convention of conventions) {
|
|
12
|
+
const m = step.match(convention.pattern);
|
|
13
|
+
if (m) {
|
|
14
|
+
const call = convention.generateCall(m, ctx);
|
|
15
|
+
if (call) {
|
|
16
|
+
return {
|
|
17
|
+
matched: true,
|
|
18
|
+
call,
|
|
19
|
+
helperMethod: convention.generateMethod?.(m, ctx)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const functionName = toMethod(step);
|
|
25
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
26
|
+
const paramNames = ctx.parameterNames || [];
|
|
27
|
+
const inputs = [...paramNames, ...declared];
|
|
28
|
+
const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
|
|
29
|
+
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
30
|
+
const inputObj = aiArgsExpr(inputs, paramNames);
|
|
31
|
+
return {
|
|
32
|
+
matched: false,
|
|
33
|
+
call: ` // Step ${ctx.stepNum}: ${step} [AI-generated \u2014 pure transform]
|
|
34
|
+
const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
|
|
35
|
+
functionName,
|
|
36
|
+
inputs,
|
|
37
|
+
resultVar
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
matchAgainstConventions,
|
|
42
|
+
toMethod,
|
|
43
|
+
toVar
|
|
44
|
+
};
|
package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
|
|
2
2
|
function generateMongoNativeController(context) {
|
|
3
|
-
const { controller, model } = context;
|
|
3
|
+
const { controller, model, models } = context;
|
|
4
4
|
if (!controller) throw new Error("Controller is required in template context");
|
|
5
5
|
if (!model) throw new Error("Model is required for controller generation");
|
|
6
6
|
const controllerName = controller.name;
|
|
@@ -8,7 +8,13 @@ function generateMongoNativeController(context) {
|
|
|
8
8
|
const modelVar = lowerFirst(modelName);
|
|
9
9
|
const collection = collectionName(model);
|
|
10
10
|
const curedOps = controller.cured || {};
|
|
11
|
-
const
|
|
11
|
+
const modelRegistry = {};
|
|
12
|
+
if (Array.isArray(models)) {
|
|
13
|
+
for (const m of models) if (m?.name) modelRegistry[m.name] = m;
|
|
14
|
+
} else if (models && typeof models === "object") {
|
|
15
|
+
Object.assign(modelRegistry, models);
|
|
16
|
+
}
|
|
17
|
+
const customActions = generateCustomActions(controller, modelRegistry);
|
|
12
18
|
const validate = generateValidateMethod(model, modelName);
|
|
13
19
|
const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, collection) : "";
|
|
14
20
|
const retrieve = curedOps.retrieve ? generateRetrieveMethod(modelName, modelVar, collection) : "";
|
|
@@ -23,7 +29,8 @@ function generateMongoNativeController(context) {
|
|
|
23
29
|
*/
|
|
24
30
|
import { ObjectId, type Filter, type Document } from 'mongodb';
|
|
25
31
|
import { getCollection } from '../db/mongoClient.js';
|
|
26
|
-
${hasEventPublishing ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
32
|
+
${hasEventPublishing || customActions.needsAiBehaviors ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
33
|
+
${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${controllerName}.ai.js';` : ""}
|
|
27
34
|
|
|
28
35
|
const COLLECTION_NAME = '${collection}';
|
|
29
36
|
|
|
@@ -221,13 +228,65 @@ function generateDeleteMethod(modelName, modelVar, collection) {
|
|
|
221
228
|
}
|
|
222
229
|
`;
|
|
223
230
|
}
|
|
224
|
-
|
|
231
|
+
import { matchMongoStep } from "./step-conventions.js";
|
|
232
|
+
function generateCustomActions(controller, modelRegistry = {}) {
|
|
225
233
|
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
226
|
-
return { code: "" };
|
|
234
|
+
return { code: "", needsAiBehaviors: false };
|
|
227
235
|
}
|
|
236
|
+
const CRUD_NAMES = /* @__PURE__ */ new Set(["create", "retrieve", "retrieveAll", "update", "evolve", "delete", "validate"]);
|
|
237
|
+
const modelName = controller.model || (controller.name || "").replace(/Controller$/, "") || "Model";
|
|
238
|
+
const collectionName2 = modelName.toLowerCase() + "s";
|
|
228
239
|
const out = [];
|
|
240
|
+
let needsAiBehaviors = false;
|
|
229
241
|
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
230
|
-
|
|
242
|
+
if (CRUD_NAMES.has(actionName)) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`\u26A0\uFE0F ${controller.name || "Controller"}.${actionName} \u2014 behaviour-derived action collides with the auto-generated CURVED \`${actionName}\` op. Dropped to avoid TS2393 duplicate-implementation. Rename the behaviour (e.g. \`${actionName}Soft\` / \`hardDelete\`) if you need the custom logic.`
|
|
245
|
+
);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const steps = Array.isArray(action.steps) ? action.steps : [];
|
|
249
|
+
const stepsHeader = steps.length > 0 ? steps.map((s) => ` * - ${typeof s === "string" ? s : s.action || JSON.stringify(s)}`).join("\n") : " * (no spec steps declared)";
|
|
250
|
+
const declaredVars = /* @__PURE__ */ new Set();
|
|
251
|
+
const stepBodies = [];
|
|
252
|
+
let usesArgs = false;
|
|
253
|
+
let actionRefersToAi = false;
|
|
254
|
+
steps.forEach((rawStep, i) => {
|
|
255
|
+
const stepText = typeof rawStep === "string" ? rawStep : rawStep?.step || rawStep?.action;
|
|
256
|
+
if (typeof stepText !== "string") {
|
|
257
|
+
stepBodies.push(` // Step ${i + 1}: (non-string step ignored)`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const ctx = {
|
|
261
|
+
modelName,
|
|
262
|
+
collectionName: collectionName2,
|
|
263
|
+
serviceName: controller.name || "Controller",
|
|
264
|
+
operationName: actionName,
|
|
265
|
+
stepNum: i + 1,
|
|
266
|
+
parameterNames: Object.keys(action.parameters || {}),
|
|
267
|
+
declaredVars,
|
|
268
|
+
models: modelRegistry
|
|
269
|
+
};
|
|
270
|
+
const result = matchMongoStep(stepText, ctx);
|
|
271
|
+
stepBodies.push(result.call);
|
|
272
|
+
if (/\bargs\./.test(result.call)) usesArgs = true;
|
|
273
|
+
if (!result.matched) actionRefersToAi = true;
|
|
274
|
+
});
|
|
275
|
+
if (actionRefersToAi) needsAiBehaviors = true;
|
|
276
|
+
const argsParam = usesArgs ? "args: any = {}" : "_args: any = {}";
|
|
277
|
+
let combined = stepBodies.join("\n\n");
|
|
278
|
+
const stepResultRe = /const\s+(step\d+Result)\s*=/g;
|
|
279
|
+
let mres;
|
|
280
|
+
const declared = [];
|
|
281
|
+
while ((mres = stepResultRe.exec(combined)) !== null) declared.push(mres[1]);
|
|
282
|
+
for (const name of declared) {
|
|
283
|
+
const refCount = (combined.match(new RegExp(`\\b${name}\\b`, "g")) || []).length;
|
|
284
|
+
if (refCount <= 1) {
|
|
285
|
+
combined = combined.replace(new RegExp(`const\\s+${name}\\s*=\\s*`), "");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const body = steps.length > 0 ? combined + `
|
|
289
|
+
return { success: true };` : ` throw new Error('${controller.name || "Controller"}.${actionName} is not implemented');`;
|
|
231
290
|
out.push(`
|
|
232
291
|
/**
|
|
233
292
|
* ${actionName}
|
|
@@ -236,16 +295,12 @@ function generateCustomActions(controller) {
|
|
|
236
295
|
* Spec steps:
|
|
237
296
|
${stepsHeader}
|
|
238
297
|
*/
|
|
239
|
-
public async ${actionName}(
|
|
240
|
-
|
|
241
|
-
// via a mongodb-native step-conventions library (mirror of the prisma
|
|
242
|
-
// one). For now this is a stub so realize completes and the action
|
|
243
|
-
// surface is callable for parity tests.
|
|
244
|
-
throw new Error('${controller.name}.${actionName} is not implemented');
|
|
298
|
+
public async ${actionName}(${argsParam}): Promise<any> {
|
|
299
|
+
${body}
|
|
245
300
|
}
|
|
246
301
|
`);
|
|
247
302
|
}
|
|
248
|
-
return { code: out.join("\n") };
|
|
303
|
+
return { code: out.join("\n"), needsAiBehaviors };
|
|
249
304
|
}
|
|
250
305
|
export {
|
|
251
306
|
generateMongoNativeController as default
|