@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
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
function generateEventBus(context) {
|
|
2
2
|
const { spec } = context;
|
|
3
|
-
const
|
|
3
|
+
const hasEvents = spec.events && Object.keys(spec.events).length > 0;
|
|
4
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
4
5
|
return `/**
|
|
5
6
|
* Event Bus
|
|
6
|
-
*
|
|
7
|
+
* Typed in-memory event bus using EventEmitter3
|
|
7
8
|
* Generated from SpecVerse specification
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import EventEmitter from 'eventemitter3';
|
|
11
|
+
import { EventEmitter } from 'eventemitter3';
|
|
12
|
+
${hasEvents || models.length > 0 ? `import type { EventPayloads } from './event-types.js';` : ""}
|
|
11
13
|
|
|
12
|
-
//
|
|
13
|
-
${
|
|
14
|
+
// Re-export types for consumers
|
|
15
|
+
${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ""}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Event history entry
|
|
19
|
+
*/
|
|
20
|
+
interface EventHistoryEntry {
|
|
21
|
+
event: string;
|
|
22
|
+
payload: any;
|
|
23
|
+
timestamp: Date;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
/**
|
|
@@ -22,15 +28,13 @@ ${events.map((event) => ` ${event} = '${event}',`).join("\n")}
|
|
|
22
28
|
*/
|
|
23
29
|
class EventBus extends EventEmitter {
|
|
24
30
|
private static instance: EventBus;
|
|
31
|
+
private history: EventHistoryEntry[] = [];
|
|
32
|
+
private maxHistory = 1000;
|
|
25
33
|
|
|
26
34
|
private constructor() {
|
|
27
35
|
super();
|
|
28
|
-
this.setMaxListeners(100); // Configure max listeners
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
/**
|
|
32
|
-
* Get singleton instance
|
|
33
|
-
*/
|
|
34
38
|
public static getInstance(): EventBus {
|
|
35
39
|
if (!EventBus.instance) {
|
|
36
40
|
EventBus.instance = new EventBus();
|
|
@@ -39,37 +43,33 @@ class EventBus extends EventEmitter {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
|
-
* Publish
|
|
46
|
+
* Publish a typed event
|
|
43
47
|
*/
|
|
44
|
-
public publish<
|
|
45
|
-
|
|
48
|
+
public publish<K extends string>(event: K, payload: K extends keyof EventPayloads ? EventPayloads[K] : any): void {
|
|
49
|
+
this.history.push({ event, payload, timestamp: new Date() });
|
|
50
|
+
if (this.history.length > this.maxHistory) this.history.shift();
|
|
46
51
|
this.emit(event, payload);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* Subscribe to
|
|
55
|
+
* Subscribe to a typed event
|
|
51
56
|
*/
|
|
52
|
-
public subscribe<
|
|
53
|
-
event:
|
|
54
|
-
handler: (payload:
|
|
57
|
+
public subscribe<K extends string>(
|
|
58
|
+
event: K,
|
|
59
|
+
handler: (payload: K extends keyof EventPayloads ? EventPayloads[K] : any) => void | Promise<void>
|
|
55
60
|
): () => void {
|
|
56
|
-
console.log(\`[EventBus] Subscribing to event: \${event}\`);
|
|
57
61
|
this.on(event, handler);
|
|
58
|
-
|
|
59
|
-
// Return unsubscribe function
|
|
60
|
-
return () => {
|
|
61
|
-
this.off(event, handler);
|
|
62
|
-
};
|
|
62
|
+
return () => { this.off(event, handler); };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* Get event history
|
|
67
67
|
*/
|
|
68
|
-
public
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
public getHistory(eventName?: string, limit = 50): EventHistoryEntry[] {
|
|
69
|
+
const filtered = eventName
|
|
70
|
+
? this.history.filter(e => e.event === eventName)
|
|
71
|
+
: this.history;
|
|
72
|
+
return filtered.slice(-limit);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function tsType(specType) {
|
|
2
|
+
const map = {
|
|
3
|
+
String: "string",
|
|
4
|
+
Text: "string",
|
|
5
|
+
Email: "string",
|
|
6
|
+
URL: "string",
|
|
7
|
+
Integer: "number",
|
|
8
|
+
Float: "number",
|
|
9
|
+
Number: "number",
|
|
10
|
+
Decimal: "number",
|
|
11
|
+
Money: "number",
|
|
12
|
+
Boolean: "boolean",
|
|
13
|
+
Date: "string",
|
|
14
|
+
DateTime: "string",
|
|
15
|
+
Timestamp: "string",
|
|
16
|
+
UUID: "string",
|
|
17
|
+
Object: "Record<string, any>",
|
|
18
|
+
Array: "any[]"
|
|
19
|
+
};
|
|
20
|
+
return map[specType] || "any";
|
|
21
|
+
}
|
|
22
|
+
function generateEventTypes(context) {
|
|
23
|
+
const { spec } = context;
|
|
24
|
+
const events = spec.events && typeof spec.events === "object" ? Object.entries(spec.events) : [];
|
|
25
|
+
if (events.length === 0) {
|
|
26
|
+
return `/**
|
|
27
|
+
* Event Types \u2014 no events defined in specification
|
|
28
|
+
*/
|
|
29
|
+
export type EventPayloads = Record<string, any>;
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
const interfaces = [];
|
|
33
|
+
const payloadMapEntries = [];
|
|
34
|
+
for (const [eventName, eventDef] of events) {
|
|
35
|
+
const def = eventDef;
|
|
36
|
+
const attrs = def.attributes || def.payload || {};
|
|
37
|
+
const attrEntries = Array.isArray(attrs) ? attrs.map((a) => [a.name, a]) : Object.entries(attrs);
|
|
38
|
+
const fields = attrEntries.map(([name, attrDef]) => {
|
|
39
|
+
const type = typeof attrDef === "string" ? tsType(attrDef.split(" ")[0]) : tsType(attrDef?.type || "String");
|
|
40
|
+
const required = typeof attrDef === "string" ? attrDef.includes("required") : attrDef?.required;
|
|
41
|
+
return ` ${name}${required ? "" : "?"}: ${type};`;
|
|
42
|
+
});
|
|
43
|
+
interfaces.push(`export interface ${eventName}Payload {
|
|
44
|
+
${fields.join("\n")}
|
|
45
|
+
}`);
|
|
46
|
+
payloadMapEntries.push(` ${eventName}: ${eventName}Payload;`);
|
|
47
|
+
}
|
|
48
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
49
|
+
for (const model of models) {
|
|
50
|
+
for (const suffix of ["Created", "Updated", "Deleted", "Evolved"]) {
|
|
51
|
+
const name = `${model}${suffix}`;
|
|
52
|
+
if (!payloadMapEntries.some((e) => e.includes(name))) {
|
|
53
|
+
payloadMapEntries.push(` ${name}: { id: string; timestamp: string; [key: string]: any };`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return `/**
|
|
58
|
+
* Event Payload Types
|
|
59
|
+
* Generated from SpecVerse specification
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
${interfaces.join("\n\n")}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Map of all event names to their payload types
|
|
66
|
+
*/
|
|
67
|
+
export type EventPayloads = {
|
|
68
|
+
${payloadMapEntries.join("\n")}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* All known event names
|
|
73
|
+
*/
|
|
74
|
+
export type EventName = keyof EventPayloads;
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
generateEventTypes as default
|
|
79
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function generateWebSocketBridge(context) {
|
|
2
|
+
const { spec } = context;
|
|
3
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
4
|
+
const specEvents = spec.events ? Object.keys(spec.events) : [];
|
|
5
|
+
const curedEvents = models.flatMap(
|
|
6
|
+
(m) => ["Created", "Updated", "Deleted", "Evolved"].map((s) => `${m}${s}`)
|
|
7
|
+
);
|
|
8
|
+
const allEvents = [.../* @__PURE__ */ new Set([...specEvents, ...curedEvents])];
|
|
9
|
+
return `/**
|
|
10
|
+
* WebSocket Bridge
|
|
11
|
+
* Bridges the event bus to frontend clients for real-time updates.
|
|
12
|
+
* Generated from SpecVerse specification
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { FastifyInstance } from 'fastify';
|
|
16
|
+
import type { WebSocket } from '@fastify/websocket';
|
|
17
|
+
import { eventBus } from './eventBus.js';
|
|
18
|
+
|
|
19
|
+
interface ClientSubscription {
|
|
20
|
+
ws: WebSocket;
|
|
21
|
+
events: Set<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const clients = new Map<WebSocket, ClientSubscription>();
|
|
25
|
+
|
|
26
|
+
// All events this bridge knows about
|
|
27
|
+
const ALL_EVENTS = ${JSON.stringify(allEvents, null, 2)};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register WebSocket bridge with Fastify
|
|
31
|
+
*/
|
|
32
|
+
export async function registerWebSocketBridge(fastify: FastifyInstance): Promise<void> {
|
|
33
|
+
// Register @fastify/websocket plugin
|
|
34
|
+
const websocketPlugin = await import('@fastify/websocket');
|
|
35
|
+
await fastify.register(websocketPlugin.default || websocketPlugin);
|
|
36
|
+
|
|
37
|
+
// WebSocket route
|
|
38
|
+
fastify.get('/ws', { websocket: true }, (socket: WebSocket) => {
|
|
39
|
+
const subscription: ClientSubscription = { ws: socket, events: new Set() };
|
|
40
|
+
clients.set(socket, subscription);
|
|
41
|
+
|
|
42
|
+
socket.on('message', (raw: Buffer) => {
|
|
43
|
+
try {
|
|
44
|
+
const msg = JSON.parse(raw.toString());
|
|
45
|
+
|
|
46
|
+
if (msg.type === 'subscribe' && msg.event) {
|
|
47
|
+
subscription.events.add(msg.event);
|
|
48
|
+
socket.send(JSON.stringify({ type: 'subscribed', event: msg.event }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (msg.type === 'unsubscribe' && msg.event) {
|
|
52
|
+
subscription.events.delete(msg.event);
|
|
53
|
+
socket.send(JSON.stringify({ type: 'unsubscribed', event: msg.event }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (msg.type === 'subscribe-all') {
|
|
57
|
+
ALL_EVENTS.forEach(e => subscription.events.add(e));
|
|
58
|
+
socket.send(JSON.stringify({ type: 'subscribed-all', events: ALL_EVENTS }));
|
|
59
|
+
}
|
|
60
|
+
} catch { /* ignore malformed messages */ }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
socket.on('close', () => {
|
|
64
|
+
clients.delete(socket);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Send initial connection confirmation
|
|
68
|
+
socket.send(JSON.stringify({
|
|
69
|
+
type: 'connected',
|
|
70
|
+
availableEvents: ALL_EVENTS,
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Subscribe to all events and broadcast to interested clients
|
|
75
|
+
for (const eventName of ALL_EVENTS) {
|
|
76
|
+
eventBus.subscribe(eventName, (payload: any) => {
|
|
77
|
+
const message = JSON.stringify({
|
|
78
|
+
type: 'event',
|
|
79
|
+
event: eventName,
|
|
80
|
+
payload,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for (const [ws, sub] of clients) {
|
|
85
|
+
if (sub.events.has(eventName) && ws.readyState === 1) {
|
|
86
|
+
try { ws.send(message); } catch { /* client disconnected */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
generateWebSocketBridge as default
|
|
96
|
+
};
|
|
@@ -16,6 +16,20 @@ function generateFastifyRoutes(context) {
|
|
|
16
16
|
endpoints = curedToEndpoints(controller.cured, modelName);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
if (controller.actions) {
|
|
20
|
+
if (!endpoints) endpoints = [];
|
|
21
|
+
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
22
|
+
const params = action.parameters || {};
|
|
23
|
+
const hasIdParam = Object.keys(params).some((p) => p === "id" || p === `${modelName?.charAt(0).toLowerCase()}${modelName?.slice(1)}Id`);
|
|
24
|
+
endpoints.push({
|
|
25
|
+
operation: actionName,
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: hasIdParam ? `/:id/${actionName}` : `/${actionName}`,
|
|
28
|
+
parameters: params,
|
|
29
|
+
description: action.description || `Custom action: ${actionName}`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
if (!endpoints || endpoints.length === 0) {
|
|
20
34
|
console.warn(`Warning: Controller ${controllerName} has no endpoints. Generating empty routes file.`);
|
|
21
35
|
}
|
|
@@ -76,7 +90,7 @@ function generateRouteHandler(endpoint, modelName, handlerName, isModelControlle
|
|
|
76
90
|
}
|
|
77
91
|
const method = endpoint.method?.toLowerCase() || inferHttpMethod(operation);
|
|
78
92
|
const path = inferPath(operation, endpoint);
|
|
79
|
-
const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType);
|
|
93
|
+
const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint);
|
|
80
94
|
let route = `// ${operation} ${modelName}
|
|
81
95
|
`;
|
|
82
96
|
route += `fastify.${method}('${path}', {
|
|
@@ -111,7 +125,7 @@ function generateRouteHandler(endpoint, modelName, handlerName, isModelControlle
|
|
|
111
125
|
route += `});`;
|
|
112
126
|
return route;
|
|
113
127
|
}
|
|
114
|
-
function generateHandlerBody(operation, modelName, handlerName, isModelController, implType) {
|
|
128
|
+
function generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint) {
|
|
115
129
|
const rawLowerModel = modelName?.toLowerCase() || "item";
|
|
116
130
|
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"]);
|
|
117
131
|
const lowerModel = RESERVED_WORDS.has(rawLowerModel) ? `${rawLowerModel}Item` : rawLowerModel;
|
|
@@ -199,16 +213,40 @@ function generateHandlerBody(operation, modelName, handlerName, isModelControlle
|
|
|
199
213
|
message: error instanceof Error ? error.message : String(error)
|
|
200
214
|
});
|
|
201
215
|
}`;
|
|
202
|
-
default:
|
|
216
|
+
default: {
|
|
217
|
+
const params = endpoint?.parameters || {};
|
|
218
|
+
const paramNames = Object.keys(params);
|
|
219
|
+
const callArgs = paramNames.length > 0 ? paramNames.map((p) => `body.${p}`).join(", ") : "body";
|
|
220
|
+
const validations = [];
|
|
221
|
+
for (const [pName, pDef] of Object.entries(params)) {
|
|
222
|
+
const required = typeof pDef === "string" ? pDef.includes("required") : pDef?.required;
|
|
223
|
+
const typeStr = typeof pDef === "string" ? pDef.split(" ")[0] : pDef?.type || "String";
|
|
224
|
+
if (required) {
|
|
225
|
+
validations.push(` if (body.${pName} === undefined || body.${pName} === null) errors.push({ field: '${pName}', message: '${pName} is required' });`);
|
|
226
|
+
}
|
|
227
|
+
if (typeStr === "UUID" || typeStr === "String" || typeStr === "Email") {
|
|
228
|
+
validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'string') errors.push({ field: '${pName}', message: '${pName} must be a string' });`);
|
|
229
|
+
} else if (typeStr === "Integer" || typeStr === "Number" || typeStr === "Float") {
|
|
230
|
+
validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'number') errors.push({ field: '${pName}', message: '${pName} must be a number' });`);
|
|
231
|
+
} else if (typeStr === "Boolean") {
|
|
232
|
+
validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'boolean') errors.push({ field: '${pName}', message: '${pName} must be a boolean' });`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const validationBlock = validations.length > 0 ? ` const errors: Array<{ field: string; message: string }> = [];
|
|
236
|
+
${validations.join("\n")}
|
|
237
|
+
if (errors.length > 0) return reply.status(400).send({ error: 'Validation failed', details: errors });` : "";
|
|
203
238
|
return `try {
|
|
204
|
-
const
|
|
205
|
-
|
|
239
|
+
const body = (request.body || {}) as Record<string, any>;
|
|
240
|
+
${validationBlock}
|
|
241
|
+
const result = await handler.${operation}(${callArgs});
|
|
242
|
+
return reply.send(result || { success: true });
|
|
206
243
|
} catch (error) {
|
|
207
244
|
return reply.status(400).send({
|
|
208
245
|
error: 'Failed to execute ${operation}',
|
|
209
246
|
message: error instanceof Error ? error.message : String(error)
|
|
210
247
|
});
|
|
211
248
|
}`;
|
|
249
|
+
}
|
|
212
250
|
}
|
|
213
251
|
}
|
|
214
252
|
function inferOperationFromMethodAndPath(method, path) {
|
|
@@ -227,10 +265,8 @@ function inferHttpMethod(operation) {
|
|
|
227
265
|
return sharedInferHttpMethod(operation);
|
|
228
266
|
}
|
|
229
267
|
function inferPath(operation, endpoint) {
|
|
230
|
-
if (endpoint?.path
|
|
231
|
-
|
|
232
|
-
const lastPart = pathParts[pathParts.length - 1];
|
|
233
|
-
return `/${lastPart}`;
|
|
268
|
+
if (endpoint?.path) {
|
|
269
|
+
return endpoint.path;
|
|
234
270
|
}
|
|
235
271
|
return sharedInferPath(operation);
|
|
236
272
|
}
|
|
@@ -10,6 +10,12 @@ function generateFastifyServer(context) {
|
|
|
10
10
|
const path = deriveBasePath(name);
|
|
11
11
|
return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
|
|
12
12
|
}).join("\n");
|
|
13
|
+
const specEvents = spec.events ? Object.keys(spec.events) : [];
|
|
14
|
+
const hasEvents = specEvents.length > 0 || modelNames.length > 0;
|
|
15
|
+
const servicesList = spec.services ? Object.keys(spec.services) : [];
|
|
16
|
+
const serviceImports = servicesList.map(
|
|
17
|
+
(name) => `import './${name.charAt(0).toLowerCase() + name.slice(1)}.js'; // Initialize ${name} event subscriptions`
|
|
18
|
+
);
|
|
13
19
|
return `/**
|
|
14
20
|
* Fastify Server
|
|
15
21
|
* Generated from SpecVerse specification
|
|
@@ -18,6 +24,8 @@ function generateFastifyServer(context) {
|
|
|
18
24
|
import Fastify from 'fastify';
|
|
19
25
|
import cors from '@fastify/cors';
|
|
20
26
|
import { PrismaClient } from '@prisma/client';
|
|
27
|
+
${hasEvents ? `import { eventBus } from './events/eventBus.js';
|
|
28
|
+
import { registerWebSocketBridge } from './events/websocket-bridge.js';` : ""}
|
|
21
29
|
|
|
22
30
|
// Initialize Prisma
|
|
23
31
|
export const prisma = new PrismaClient();
|
|
@@ -42,13 +50,19 @@ fastify.get('/api/spec', async () => embeddedSpec);
|
|
|
42
50
|
fastify.get('/api/runtime/info', async () => ({
|
|
43
51
|
controllers: ${JSON.stringify(modelNames.map((n) => `${n}Controller`))},
|
|
44
52
|
models: ${JSON.stringify(modelNames)},
|
|
45
|
-
events:
|
|
46
|
-
services:
|
|
53
|
+
events: ${JSON.stringify(specEvents)},
|
|
54
|
+
services: ${JSON.stringify(servicesList)}
|
|
47
55
|
}));
|
|
48
56
|
|
|
57
|
+
${hasEvents ? `// Event history endpoint
|
|
58
|
+
fastify.get('/api/events', async () => eventBus.getHistory());` : ""}
|
|
59
|
+
|
|
49
60
|
// Register routes
|
|
50
61
|
${routeImports}
|
|
51
62
|
|
|
63
|
+
${serviceImports.length > 0 ? `// Initialize services (registers event subscriptions)
|
|
64
|
+
${serviceImports.join("\n")}` : ""}
|
|
65
|
+
|
|
52
66
|
async function registerRoutes() {
|
|
53
67
|
${routeRegistrations}
|
|
54
68
|
}
|
|
@@ -57,10 +71,14 @@ ${routeRegistrations}
|
|
|
57
71
|
const start = async () => {
|
|
58
72
|
try {
|
|
59
73
|
await registerRoutes();
|
|
74
|
+
${hasEvents ? ` // Register WebSocket bridge for real-time frontend events
|
|
75
|
+
await registerWebSocketBridge(fastify);` : ""}
|
|
60
76
|
const port = parseInt(process.env.PORT || '3000');
|
|
61
77
|
await fastify.listen({ port, host: '0.0.0.0' });
|
|
62
78
|
console.log(\`Server running at http://localhost:\${port}\`);
|
|
63
79
|
console.log(\`API endpoints: ${modelNames.map((n) => `/api/${n.toLowerCase()}s`).join(", ")}\`);
|
|
80
|
+
${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
|
|
81
|
+
console.log(\`Events: ${specEvents.join(", ")}\`);` : ""}
|
|
64
82
|
} catch (err) {
|
|
65
83
|
fastify.log.error(err);
|
|
66
84
|
process.exit(1);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
function generateTsConfig(context) {
|
|
2
2
|
const { manifest, spec } = context;
|
|
3
|
-
const
|
|
3
|
+
const implementationTypes = context.implementationTypes || [];
|
|
4
|
+
const isMonorepo = !implementationTypes.some((it) => {
|
|
5
|
+
const cfg = it?.configuration || it?.instanceFactory?.configuration || {};
|
|
6
|
+
return cfg.outputStructure === "standalone";
|
|
7
|
+
});
|
|
8
|
+
if (isMonorepo) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
const mergedOptions = extractTsConfigOptions(implementationTypes);
|
|
4
12
|
const hasViews = spec && (spec.views || Array.isArray(spec.views) && spec.views.length > 0);
|
|
5
|
-
const usesReact = hasViews &&
|
|
13
|
+
const usesReact = hasViews && implementationTypes.some(
|
|
6
14
|
(implType) => implType.capabilities?.provides?.includes("ui.components") && implType.technology?.framework === "react"
|
|
7
15
|
);
|
|
8
16
|
const tsconfig = {
|