@kapeta/local-cluster-service 0.46.0 → 0.47.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/CHANGELOG.md +7 -0
- package/dist/cjs/src/storm/event-parser.d.ts +3 -3
- package/dist/cjs/src/storm/event-parser.js +42 -4
- package/dist/cjs/src/storm/events.d.ts +8 -1
- package/dist/cjs/src/storm/events.js +0 -4
- package/dist/cjs/src/storm/routes.js +20 -7
- package/dist/cjs/test/storm/event-parser.test.d.ts +5 -0
- package/dist/cjs/test/storm/event-parser.test.js +169 -0
- package/dist/esm/src/storm/event-parser.d.ts +3 -3
- package/dist/esm/src/storm/event-parser.js +42 -4
- package/dist/esm/src/storm/events.d.ts +8 -1
- package/dist/esm/src/storm/events.js +0 -4
- package/dist/esm/src/storm/routes.js +20 -7
- package/dist/esm/test/storm/event-parser.test.d.ts +5 -0
- package/dist/esm/test/storm/event-parser.test.js +169 -0
- package/package.json +3 -1
- package/src/storm/event-parser.ts +56 -15
- package/src/storm/events.ts +10 -1
- package/src/storm/routes.ts +44 -29
- package/test/storm/event-parser.test.ts +190 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.47.0](https://github.com/kapetacom/local-cluster-service/compare/v0.46.0...v0.47.0) (2024-05-29)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Added tests for event parsing and layouting of nodes ([#149](https://github.com/kapetacom/local-cluster-service/issues/149)) ([d36afa9](https://github.com/kapetacom/local-cluster-service/commit/d36afa905d0c66c4562b4eccf07c067efe6b3c9c))
|
7
|
+
|
1
8
|
# [0.46.0](https://github.com/kapetacom/local-cluster-service/compare/v0.45.0...v0.46.0) (2024-05-28)
|
2
9
|
|
3
10
|
|
@@ -10,7 +10,7 @@ export interface BlockDefinitionInfo {
|
|
10
10
|
content: BlockDefinition;
|
11
11
|
aiName: string;
|
12
12
|
}
|
13
|
-
export interface
|
13
|
+
export interface StormDefinitions {
|
14
14
|
plan: Plan;
|
15
15
|
blocks: BlockDefinitionInfo[];
|
16
16
|
}
|
@@ -51,12 +51,12 @@ export declare class StormEventParser {
|
|
51
51
|
private options;
|
52
52
|
constructor(options: StormOptions);
|
53
53
|
private reset;
|
54
|
-
addEvent(evt: StormEvent):
|
54
|
+
addEvent(handle: string, evt: StormEvent): StormDefinitions;
|
55
55
|
getEvents(): StormEvent[];
|
56
56
|
isValid(): boolean;
|
57
57
|
getError(): string;
|
58
58
|
private applyLayoutToBlocks;
|
59
|
-
toResult(handle: string):
|
59
|
+
toResult(handle: string): StormDefinitions;
|
60
60
|
private toSafeName;
|
61
61
|
private toRef;
|
62
62
|
toBlockDefinitions(handle: string): {
|
@@ -12,6 +12,8 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
|
12
12
|
const kaplang_core_1 = require("@kapeta/kaplang-core");
|
13
13
|
const node_uuid_1 = __importDefault(require("node-uuid"));
|
14
14
|
const definitionsManager_1 = require("../definitionsManager");
|
15
|
+
const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
|
16
|
+
const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
|
15
17
|
async function resolveOptions() {
|
16
18
|
// Predefined types for now - TODO: Allow user to select / change
|
17
19
|
const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
|
@@ -87,6 +89,7 @@ async function resolveOptions() {
|
|
87
89
|
};
|
88
90
|
}
|
89
91
|
exports.resolveOptions = resolveOptions;
|
92
|
+
const LAYOUT_MARGIN = 50;
|
90
93
|
class StormEventParser {
|
91
94
|
events = [];
|
92
95
|
planName = '';
|
@@ -105,7 +108,7 @@ class StormEventParser {
|
|
105
108
|
this.blocks = {};
|
106
109
|
this.connections = [];
|
107
110
|
}
|
108
|
-
addEvent(evt) {
|
111
|
+
addEvent(handle, evt) {
|
109
112
|
this.events.push(evt);
|
110
113
|
switch (evt.type) {
|
111
114
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -144,6 +147,7 @@ class StormEventParser {
|
|
144
147
|
case 'FILE':
|
145
148
|
break;
|
146
149
|
}
|
150
|
+
return this.toResult(handle);
|
147
151
|
}
|
148
152
|
getEvents() {
|
149
153
|
return this.events;
|
@@ -155,13 +159,47 @@ class StormEventParser {
|
|
155
159
|
return this.error;
|
156
160
|
}
|
157
161
|
applyLayoutToBlocks(result) {
|
162
|
+
const graph = (0, ngraph_graph_1.default)();
|
163
|
+
const blockInstances = {};
|
164
|
+
result.plan.spec.blocks.forEach((block, index) => {
|
165
|
+
graph.addNode(block.id, block);
|
166
|
+
blockInstances[block.id] = block;
|
167
|
+
});
|
168
|
+
result.plan.spec.connections.forEach((connection) => {
|
169
|
+
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
170
|
+
});
|
171
|
+
const layout = (0, ngraph_forcelayout_1.default)(graph, {
|
172
|
+
springLength: 150,
|
173
|
+
debug: true,
|
174
|
+
dimensions: 2,
|
175
|
+
gravity: 2,
|
176
|
+
springCoefficient: 0.0008,
|
177
|
+
});
|
178
|
+
for (let i = 0; i < 100; ++i) {
|
179
|
+
layout.step();
|
180
|
+
}
|
181
|
+
// Layout might place things in negative space. We move everything to positive space
|
182
|
+
const graphBox = layout.getGraphRect();
|
183
|
+
let yAdjust = 0;
|
184
|
+
let xAdjust = 0;
|
185
|
+
if (graphBox.y1 < 0) {
|
186
|
+
yAdjust = -graphBox.y1;
|
187
|
+
}
|
188
|
+
if (graphBox.x1 < 0) {
|
189
|
+
xAdjust = -graphBox.x1;
|
190
|
+
}
|
191
|
+
graph.forEachNode((node) => {
|
192
|
+
const position = layout.getNodePosition(node.id);
|
193
|
+
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
194
|
+
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
195
|
+
});
|
196
|
+
layout.dispose();
|
158
197
|
return result;
|
159
198
|
}
|
160
199
|
toResult(handle) {
|
161
200
|
const planRef = this.toRef(handle, this.planName);
|
162
201
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
163
202
|
const refIdMap = {};
|
164
|
-
const screens = {};
|
165
203
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
166
204
|
const id = node_uuid_1.default.v4();
|
167
205
|
refIdMap[ref] = id;
|
@@ -174,7 +212,7 @@ class StormEventParser {
|
|
174
212
|
dimensions: {
|
175
213
|
left: 0,
|
176
214
|
top: 0,
|
177
|
-
width:
|
215
|
+
width: 150,
|
178
216
|
height: 200,
|
179
217
|
},
|
180
218
|
};
|
@@ -377,7 +415,7 @@ class StormEventParser {
|
|
377
415
|
},
|
378
416
|
},
|
379
417
|
};
|
380
|
-
blockSpec.
|
418
|
+
blockSpec.consumers.push(dbResource);
|
381
419
|
break;
|
382
420
|
case 'JWTCONSUMER':
|
383
421
|
case 'WEBFRAGMENT':
|
@@ -2,6 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
+
import { StormDefinitions } from "./event-parser";
|
5
6
|
export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
|
6
7
|
export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
|
7
8
|
export interface StormBlockInfo {
|
@@ -123,4 +124,10 @@ export interface StormEventDone {
|
|
123
124
|
type: 'DONE';
|
124
125
|
created: number;
|
125
126
|
}
|
126
|
-
export
|
127
|
+
export interface StormEventDefinitionChange {
|
128
|
+
type: 'DEFINITION_CHANGE';
|
129
|
+
reason: string;
|
130
|
+
created: number;
|
131
|
+
payload: StormDefinitions;
|
132
|
+
}
|
133
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange;
|
@@ -25,12 +25,13 @@ router.post('/:handle/all', async (req, res) => {
|
|
25
25
|
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
26
26
|
res.set('Content-Type', 'application/x-ndjson');
|
27
27
|
metaStream.on('data', (data) => {
|
28
|
-
eventParser.addEvent(data);
|
28
|
+
const result = eventParser.addEvent(req.params.handle, data);
|
29
|
+
sendDefinitions(res, result);
|
29
30
|
});
|
30
31
|
await streamStormPartialResponse(metaStream, res);
|
31
32
|
if (!eventParser.isValid()) {
|
32
33
|
// We can't continue if the meta stream is invalid
|
33
|
-
res
|
34
|
+
sendEvent(res, {
|
34
35
|
type: 'ERROR_INTERNAL',
|
35
36
|
payload: { error: eventParser.getError() },
|
36
37
|
reason: 'Failed to generate system',
|
@@ -40,6 +41,7 @@ router.post('/:handle/all', async (req, res) => {
|
|
40
41
|
return;
|
41
42
|
}
|
42
43
|
const result = eventParser.toResult(handle);
|
44
|
+
sendDefinitions(res, result);
|
43
45
|
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
44
46
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
45
47
|
await stormCodegen.process();
|
@@ -50,22 +52,30 @@ router.post('/:handle/all', async (req, res) => {
|
|
50
52
|
sendError(err, res);
|
51
53
|
}
|
52
54
|
});
|
55
|
+
function sendDefinitions(res, result) {
|
56
|
+
sendEvent(res, {
|
57
|
+
type: 'DEFINITION_CHANGE',
|
58
|
+
payload: result,
|
59
|
+
reason: 'Updates to definition',
|
60
|
+
created: Date.now(),
|
61
|
+
});
|
62
|
+
}
|
53
63
|
function sendDone(res) {
|
54
|
-
res
|
64
|
+
sendEvent(res, {
|
55
65
|
type: 'DONE',
|
56
66
|
created: Date.now(),
|
57
|
-
})
|
67
|
+
});
|
58
68
|
res.end();
|
59
69
|
}
|
60
70
|
function sendError(err, res) {
|
61
71
|
console.error('Failed to send prompt', err);
|
62
72
|
if (res.headersSent) {
|
63
|
-
res
|
73
|
+
sendEvent(res, {
|
64
74
|
type: 'ERROR_INTERNAL',
|
65
75
|
created: Date.now(),
|
66
76
|
payload: { error: err.message },
|
67
77
|
reason: 'Failed while sending prompt',
|
68
|
-
})
|
78
|
+
});
|
69
79
|
}
|
70
80
|
else {
|
71
81
|
res.status(400).send({ error: err.message });
|
@@ -74,7 +84,7 @@ function sendError(err, res) {
|
|
74
84
|
function streamStormPartialResponse(result, res) {
|
75
85
|
return new Promise((resolve, reject) => {
|
76
86
|
result.on('data', (data) => {
|
77
|
-
res
|
87
|
+
sendEvent(res, data);
|
78
88
|
});
|
79
89
|
result.on('error', (err) => {
|
80
90
|
reject(err);
|
@@ -84,4 +94,7 @@ function streamStormPartialResponse(result, res) {
|
|
84
94
|
});
|
85
95
|
});
|
86
96
|
}
|
97
|
+
function sendEvent(res, evt) {
|
98
|
+
res.write(JSON.stringify(evt) + '\n');
|
99
|
+
}
|
87
100
|
exports.default = router;
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"use strict";
|
2
|
+
/**
|
3
|
+
* Copyright 2023 Kapeta Inc.
|
4
|
+
* SPDX-License-Identifier: BUSL-1.1
|
5
|
+
*/
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
7
|
+
const event_parser_1 = require("../../src/storm/event-parser");
|
8
|
+
const parserOptions = {
|
9
|
+
serviceKind: 'kapeta/block-service:local',
|
10
|
+
serviceLanguage: 'kapeta/language-target-nodejs-ts:local',
|
11
|
+
frontendKind: 'kapeta/block-type-frontend:local',
|
12
|
+
frontendLanguage: 'kapeta/language-target-react-ts:local',
|
13
|
+
cliKind: 'kapeta/block-type-cli:local',
|
14
|
+
cliLanguage: 'kapeta/language-target-nodejs-ts:local',
|
15
|
+
desktopKind: 'kapeta/block-type-desktop:local',
|
16
|
+
desktopLanguage: 'kapeta/language-target-electron-ts:local',
|
17
|
+
gatewayKind: 'kapeta/block-type-gateway:local',
|
18
|
+
mqKind: 'kapeta/block-type-mq:local',
|
19
|
+
exchangeKind: 'kapeta/resource-type-exchange:local',
|
20
|
+
queueKind: 'kapeta/resource-type-queue:local',
|
21
|
+
publisherKind: 'kapeta/resource-type-publisher:local',
|
22
|
+
subscriberKind: 'kapeta/resource-type-subscriber:local',
|
23
|
+
databaseKind: 'kapeta/block-type-database:local',
|
24
|
+
apiKind: 'kapeta/block-type-api:local',
|
25
|
+
clientKind: 'kapeta/block-type-client:local',
|
26
|
+
webPageKind: 'kapeta/block-type-web-page:local',
|
27
|
+
webFragmentKind: 'kapeta/block-type-web-fragment:local',
|
28
|
+
jwtProviderKind: 'kapeta/resource-type-jwt-provider:local',
|
29
|
+
jwtConsumerKind: 'kapeta/resource-type-jwt-consumer:local',
|
30
|
+
smtpKind: 'kapeta/resource-type-smtp:local',
|
31
|
+
externalApiKind: 'kapeta/resource-type-external-api:local',
|
32
|
+
};
|
33
|
+
const events = [
|
34
|
+
{
|
35
|
+
type: 'CREATE_PLAN_PROPERTIES',
|
36
|
+
created: Date.now(),
|
37
|
+
reason: 'create plan properties',
|
38
|
+
payload: {
|
39
|
+
name: 'my-plan',
|
40
|
+
description: 'my plan description',
|
41
|
+
},
|
42
|
+
},
|
43
|
+
{
|
44
|
+
type: 'CREATE_BLOCK',
|
45
|
+
reason: 'create backend',
|
46
|
+
created: Date.now(),
|
47
|
+
payload: {
|
48
|
+
name: 'service',
|
49
|
+
description: 'A service block',
|
50
|
+
type: 'BACKEND',
|
51
|
+
resources: [
|
52
|
+
{
|
53
|
+
name: 'entities',
|
54
|
+
type: 'DATABASE',
|
55
|
+
description: 'A database resource',
|
56
|
+
},
|
57
|
+
{
|
58
|
+
type: 'API',
|
59
|
+
name: 'entities',
|
60
|
+
description: 'An API resource',
|
61
|
+
},
|
62
|
+
],
|
63
|
+
},
|
64
|
+
},
|
65
|
+
{
|
66
|
+
type: 'CREATE_BLOCK',
|
67
|
+
reason: 'create frontend',
|
68
|
+
created: Date.now(),
|
69
|
+
payload: {
|
70
|
+
name: 'ui',
|
71
|
+
description: 'A frontend block',
|
72
|
+
type: 'FRONTEND',
|
73
|
+
resources: [
|
74
|
+
{
|
75
|
+
name: 'web',
|
76
|
+
type: 'WEBPAGE',
|
77
|
+
description: 'A web page',
|
78
|
+
},
|
79
|
+
{
|
80
|
+
type: 'CLIENT',
|
81
|
+
name: 'entities',
|
82
|
+
description: 'Client for backend',
|
83
|
+
},
|
84
|
+
],
|
85
|
+
},
|
86
|
+
},
|
87
|
+
{
|
88
|
+
type: 'CREATE_CONNECTION',
|
89
|
+
created: Date.now(),
|
90
|
+
reason: 'connect service to ui',
|
91
|
+
payload: {
|
92
|
+
fromComponent: 'service',
|
93
|
+
fromResource: 'entities',
|
94
|
+
fromResourceType: 'API',
|
95
|
+
toComponent: 'ui',
|
96
|
+
toResource: 'entities',
|
97
|
+
toResourceType: 'CLIENT',
|
98
|
+
},
|
99
|
+
},
|
100
|
+
{
|
101
|
+
type: 'CREATE_API',
|
102
|
+
reason: 'create api',
|
103
|
+
created: Date.now(),
|
104
|
+
payload: {
|
105
|
+
blockName: 'service',
|
106
|
+
content: `controller Entities('/entities') {
|
107
|
+
@GET('/')
|
108
|
+
list(): string[]
|
109
|
+
}`,
|
110
|
+
},
|
111
|
+
},
|
112
|
+
{
|
113
|
+
type: 'CREATE_MODEL',
|
114
|
+
created: Date.now(),
|
115
|
+
reason: 'create model',
|
116
|
+
payload: {
|
117
|
+
blockName: 'service',
|
118
|
+
content: `type Entity {
|
119
|
+
@Id
|
120
|
+
id: string
|
121
|
+
|
122
|
+
name: string
|
123
|
+
}`,
|
124
|
+
},
|
125
|
+
},
|
126
|
+
];
|
127
|
+
describe('event-parser', () => {
|
128
|
+
it('it can parse events into a plan and blocks with proper layout', () => {
|
129
|
+
const parser = new event_parser_1.StormEventParser(parserOptions);
|
130
|
+
events.forEach((event) => parser.addEvent('kapeta', event));
|
131
|
+
const result = parser.toResult('kapeta');
|
132
|
+
expect(result.plan.metadata.name).toBe('kapeta/my-plan');
|
133
|
+
expect(result.plan.metadata.description).toBe('my plan description');
|
134
|
+
expect(result.blocks.length).toBe(2);
|
135
|
+
expect(result.blocks[0].content.metadata.name).toBe('kapeta/service');
|
136
|
+
expect(result.blocks[1].content.metadata.name).toBe('kapeta/ui');
|
137
|
+
const dbResource = result.blocks[0].content.spec.consumers?.[0];
|
138
|
+
const apiResource = result.blocks[0].content.spec.providers?.[0];
|
139
|
+
const clientResource = result.blocks[1].content.spec.consumers?.[0];
|
140
|
+
const pageResource = result.blocks[1].content.spec.providers?.[0];
|
141
|
+
expect(apiResource).toBeDefined();
|
142
|
+
expect(clientResource).toBeDefined();
|
143
|
+
expect(dbResource).toBeDefined();
|
144
|
+
expect(pageResource).toBeDefined();
|
145
|
+
expect(apiResource?.kind).toBe(parserOptions.apiKind);
|
146
|
+
expect(clientResource?.kind).toBe(parserOptions.clientKind);
|
147
|
+
expect(dbResource?.kind).toBe(parserOptions.databaseKind);
|
148
|
+
expect(pageResource?.kind).toBe(parserOptions.webPageKind);
|
149
|
+
expect(apiResource?.spec).toEqual(clientResource?.spec);
|
150
|
+
expect(dbResource?.spec.source.value).toContain('type Entity');
|
151
|
+
const serviceBlockInstance = result.plan.spec.blocks[0];
|
152
|
+
expect(serviceBlockInstance.name).toBe('service');
|
153
|
+
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
154
|
+
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
155
|
+
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
156
|
+
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
157
|
+
const uiBlockInstance = result.plan.spec.blocks[1];
|
158
|
+
expect(uiBlockInstance.name).toBe('ui');
|
159
|
+
expect(uiBlockInstance.dimensions.width).toBe(150);
|
160
|
+
expect(uiBlockInstance.dimensions.height).toBe(200);
|
161
|
+
expect(uiBlockInstance.dimensions.top).toBe(107);
|
162
|
+
expect(uiBlockInstance.dimensions.left).toBe(112);
|
163
|
+
expect(result.plan.spec.connections.length).toBe(1);
|
164
|
+
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|
165
|
+
expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
|
166
|
+
expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
|
167
|
+
expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
|
168
|
+
});
|
169
|
+
});
|
@@ -10,7 +10,7 @@ export interface BlockDefinitionInfo {
|
|
10
10
|
content: BlockDefinition;
|
11
11
|
aiName: string;
|
12
12
|
}
|
13
|
-
export interface
|
13
|
+
export interface StormDefinitions {
|
14
14
|
plan: Plan;
|
15
15
|
blocks: BlockDefinitionInfo[];
|
16
16
|
}
|
@@ -51,12 +51,12 @@ export declare class StormEventParser {
|
|
51
51
|
private options;
|
52
52
|
constructor(options: StormOptions);
|
53
53
|
private reset;
|
54
|
-
addEvent(evt: StormEvent):
|
54
|
+
addEvent(handle: string, evt: StormEvent): StormDefinitions;
|
55
55
|
getEvents(): StormEvent[];
|
56
56
|
isValid(): boolean;
|
57
57
|
getError(): string;
|
58
58
|
private applyLayoutToBlocks;
|
59
|
-
toResult(handle: string):
|
59
|
+
toResult(handle: string): StormDefinitions;
|
60
60
|
private toSafeName;
|
61
61
|
private toRef;
|
62
62
|
toBlockDefinitions(handle: string): {
|
@@ -12,6 +12,8 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
|
12
12
|
const kaplang_core_1 = require("@kapeta/kaplang-core");
|
13
13
|
const node_uuid_1 = __importDefault(require("node-uuid"));
|
14
14
|
const definitionsManager_1 = require("../definitionsManager");
|
15
|
+
const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
|
16
|
+
const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
|
15
17
|
async function resolveOptions() {
|
16
18
|
// Predefined types for now - TODO: Allow user to select / change
|
17
19
|
const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
|
@@ -87,6 +89,7 @@ async function resolveOptions() {
|
|
87
89
|
};
|
88
90
|
}
|
89
91
|
exports.resolveOptions = resolveOptions;
|
92
|
+
const LAYOUT_MARGIN = 50;
|
90
93
|
class StormEventParser {
|
91
94
|
events = [];
|
92
95
|
planName = '';
|
@@ -105,7 +108,7 @@ class StormEventParser {
|
|
105
108
|
this.blocks = {};
|
106
109
|
this.connections = [];
|
107
110
|
}
|
108
|
-
addEvent(evt) {
|
111
|
+
addEvent(handle, evt) {
|
109
112
|
this.events.push(evt);
|
110
113
|
switch (evt.type) {
|
111
114
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -144,6 +147,7 @@ class StormEventParser {
|
|
144
147
|
case 'FILE':
|
145
148
|
break;
|
146
149
|
}
|
150
|
+
return this.toResult(handle);
|
147
151
|
}
|
148
152
|
getEvents() {
|
149
153
|
return this.events;
|
@@ -155,13 +159,47 @@ class StormEventParser {
|
|
155
159
|
return this.error;
|
156
160
|
}
|
157
161
|
applyLayoutToBlocks(result) {
|
162
|
+
const graph = (0, ngraph_graph_1.default)();
|
163
|
+
const blockInstances = {};
|
164
|
+
result.plan.spec.blocks.forEach((block, index) => {
|
165
|
+
graph.addNode(block.id, block);
|
166
|
+
blockInstances[block.id] = block;
|
167
|
+
});
|
168
|
+
result.plan.spec.connections.forEach((connection) => {
|
169
|
+
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
170
|
+
});
|
171
|
+
const layout = (0, ngraph_forcelayout_1.default)(graph, {
|
172
|
+
springLength: 150,
|
173
|
+
debug: true,
|
174
|
+
dimensions: 2,
|
175
|
+
gravity: 2,
|
176
|
+
springCoefficient: 0.0008,
|
177
|
+
});
|
178
|
+
for (let i = 0; i < 100; ++i) {
|
179
|
+
layout.step();
|
180
|
+
}
|
181
|
+
// Layout might place things in negative space. We move everything to positive space
|
182
|
+
const graphBox = layout.getGraphRect();
|
183
|
+
let yAdjust = 0;
|
184
|
+
let xAdjust = 0;
|
185
|
+
if (graphBox.y1 < 0) {
|
186
|
+
yAdjust = -graphBox.y1;
|
187
|
+
}
|
188
|
+
if (graphBox.x1 < 0) {
|
189
|
+
xAdjust = -graphBox.x1;
|
190
|
+
}
|
191
|
+
graph.forEachNode((node) => {
|
192
|
+
const position = layout.getNodePosition(node.id);
|
193
|
+
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
194
|
+
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
195
|
+
});
|
196
|
+
layout.dispose();
|
158
197
|
return result;
|
159
198
|
}
|
160
199
|
toResult(handle) {
|
161
200
|
const planRef = this.toRef(handle, this.planName);
|
162
201
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
163
202
|
const refIdMap = {};
|
164
|
-
const screens = {};
|
165
203
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
166
204
|
const id = node_uuid_1.default.v4();
|
167
205
|
refIdMap[ref] = id;
|
@@ -174,7 +212,7 @@ class StormEventParser {
|
|
174
212
|
dimensions: {
|
175
213
|
left: 0,
|
176
214
|
top: 0,
|
177
|
-
width:
|
215
|
+
width: 150,
|
178
216
|
height: 200,
|
179
217
|
},
|
180
218
|
};
|
@@ -377,7 +415,7 @@ class StormEventParser {
|
|
377
415
|
},
|
378
416
|
},
|
379
417
|
};
|
380
|
-
blockSpec.
|
418
|
+
blockSpec.consumers.push(dbResource);
|
381
419
|
break;
|
382
420
|
case 'JWTCONSUMER':
|
383
421
|
case 'WEBFRAGMENT':
|
@@ -2,6 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
+
import { StormDefinitions } from "./event-parser";
|
5
6
|
export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
|
6
7
|
export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
|
7
8
|
export interface StormBlockInfo {
|
@@ -123,4 +124,10 @@ export interface StormEventDone {
|
|
123
124
|
type: 'DONE';
|
124
125
|
created: number;
|
125
126
|
}
|
126
|
-
export
|
127
|
+
export interface StormEventDefinitionChange {
|
128
|
+
type: 'DEFINITION_CHANGE';
|
129
|
+
reason: string;
|
130
|
+
created: number;
|
131
|
+
payload: StormDefinitions;
|
132
|
+
}
|
133
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange;
|
@@ -25,12 +25,13 @@ router.post('/:handle/all', async (req, res) => {
|
|
25
25
|
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
26
26
|
res.set('Content-Type', 'application/x-ndjson');
|
27
27
|
metaStream.on('data', (data) => {
|
28
|
-
eventParser.addEvent(data);
|
28
|
+
const result = eventParser.addEvent(req.params.handle, data);
|
29
|
+
sendDefinitions(res, result);
|
29
30
|
});
|
30
31
|
await streamStormPartialResponse(metaStream, res);
|
31
32
|
if (!eventParser.isValid()) {
|
32
33
|
// We can't continue if the meta stream is invalid
|
33
|
-
res
|
34
|
+
sendEvent(res, {
|
34
35
|
type: 'ERROR_INTERNAL',
|
35
36
|
payload: { error: eventParser.getError() },
|
36
37
|
reason: 'Failed to generate system',
|
@@ -40,6 +41,7 @@ router.post('/:handle/all', async (req, res) => {
|
|
40
41
|
return;
|
41
42
|
}
|
42
43
|
const result = eventParser.toResult(handle);
|
44
|
+
sendDefinitions(res, result);
|
43
45
|
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
44
46
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
45
47
|
await stormCodegen.process();
|
@@ -50,22 +52,30 @@ router.post('/:handle/all', async (req, res) => {
|
|
50
52
|
sendError(err, res);
|
51
53
|
}
|
52
54
|
});
|
55
|
+
function sendDefinitions(res, result) {
|
56
|
+
sendEvent(res, {
|
57
|
+
type: 'DEFINITION_CHANGE',
|
58
|
+
payload: result,
|
59
|
+
reason: 'Updates to definition',
|
60
|
+
created: Date.now(),
|
61
|
+
});
|
62
|
+
}
|
53
63
|
function sendDone(res) {
|
54
|
-
res
|
64
|
+
sendEvent(res, {
|
55
65
|
type: 'DONE',
|
56
66
|
created: Date.now(),
|
57
|
-
})
|
67
|
+
});
|
58
68
|
res.end();
|
59
69
|
}
|
60
70
|
function sendError(err, res) {
|
61
71
|
console.error('Failed to send prompt', err);
|
62
72
|
if (res.headersSent) {
|
63
|
-
res
|
73
|
+
sendEvent(res, {
|
64
74
|
type: 'ERROR_INTERNAL',
|
65
75
|
created: Date.now(),
|
66
76
|
payload: { error: err.message },
|
67
77
|
reason: 'Failed while sending prompt',
|
68
|
-
})
|
78
|
+
});
|
69
79
|
}
|
70
80
|
else {
|
71
81
|
res.status(400).send({ error: err.message });
|
@@ -74,7 +84,7 @@ function sendError(err, res) {
|
|
74
84
|
function streamStormPartialResponse(result, res) {
|
75
85
|
return new Promise((resolve, reject) => {
|
76
86
|
result.on('data', (data) => {
|
77
|
-
res
|
87
|
+
sendEvent(res, data);
|
78
88
|
});
|
79
89
|
result.on('error', (err) => {
|
80
90
|
reject(err);
|
@@ -84,4 +94,7 @@ function streamStormPartialResponse(result, res) {
|
|
84
94
|
});
|
85
95
|
});
|
86
96
|
}
|
97
|
+
function sendEvent(res, evt) {
|
98
|
+
res.write(JSON.stringify(evt) + '\n');
|
99
|
+
}
|
87
100
|
exports.default = router;
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"use strict";
|
2
|
+
/**
|
3
|
+
* Copyright 2023 Kapeta Inc.
|
4
|
+
* SPDX-License-Identifier: BUSL-1.1
|
5
|
+
*/
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
7
|
+
const event_parser_1 = require("../../src/storm/event-parser");
|
8
|
+
const parserOptions = {
|
9
|
+
serviceKind: 'kapeta/block-service:local',
|
10
|
+
serviceLanguage: 'kapeta/language-target-nodejs-ts:local',
|
11
|
+
frontendKind: 'kapeta/block-type-frontend:local',
|
12
|
+
frontendLanguage: 'kapeta/language-target-react-ts:local',
|
13
|
+
cliKind: 'kapeta/block-type-cli:local',
|
14
|
+
cliLanguage: 'kapeta/language-target-nodejs-ts:local',
|
15
|
+
desktopKind: 'kapeta/block-type-desktop:local',
|
16
|
+
desktopLanguage: 'kapeta/language-target-electron-ts:local',
|
17
|
+
gatewayKind: 'kapeta/block-type-gateway:local',
|
18
|
+
mqKind: 'kapeta/block-type-mq:local',
|
19
|
+
exchangeKind: 'kapeta/resource-type-exchange:local',
|
20
|
+
queueKind: 'kapeta/resource-type-queue:local',
|
21
|
+
publisherKind: 'kapeta/resource-type-publisher:local',
|
22
|
+
subscriberKind: 'kapeta/resource-type-subscriber:local',
|
23
|
+
databaseKind: 'kapeta/block-type-database:local',
|
24
|
+
apiKind: 'kapeta/block-type-api:local',
|
25
|
+
clientKind: 'kapeta/block-type-client:local',
|
26
|
+
webPageKind: 'kapeta/block-type-web-page:local',
|
27
|
+
webFragmentKind: 'kapeta/block-type-web-fragment:local',
|
28
|
+
jwtProviderKind: 'kapeta/resource-type-jwt-provider:local',
|
29
|
+
jwtConsumerKind: 'kapeta/resource-type-jwt-consumer:local',
|
30
|
+
smtpKind: 'kapeta/resource-type-smtp:local',
|
31
|
+
externalApiKind: 'kapeta/resource-type-external-api:local',
|
32
|
+
};
|
33
|
+
const events = [
|
34
|
+
{
|
35
|
+
type: 'CREATE_PLAN_PROPERTIES',
|
36
|
+
created: Date.now(),
|
37
|
+
reason: 'create plan properties',
|
38
|
+
payload: {
|
39
|
+
name: 'my-plan',
|
40
|
+
description: 'my plan description',
|
41
|
+
},
|
42
|
+
},
|
43
|
+
{
|
44
|
+
type: 'CREATE_BLOCK',
|
45
|
+
reason: 'create backend',
|
46
|
+
created: Date.now(),
|
47
|
+
payload: {
|
48
|
+
name: 'service',
|
49
|
+
description: 'A service block',
|
50
|
+
type: 'BACKEND',
|
51
|
+
resources: [
|
52
|
+
{
|
53
|
+
name: 'entities',
|
54
|
+
type: 'DATABASE',
|
55
|
+
description: 'A database resource',
|
56
|
+
},
|
57
|
+
{
|
58
|
+
type: 'API',
|
59
|
+
name: 'entities',
|
60
|
+
description: 'An API resource',
|
61
|
+
},
|
62
|
+
],
|
63
|
+
},
|
64
|
+
},
|
65
|
+
{
|
66
|
+
type: 'CREATE_BLOCK',
|
67
|
+
reason: 'create frontend',
|
68
|
+
created: Date.now(),
|
69
|
+
payload: {
|
70
|
+
name: 'ui',
|
71
|
+
description: 'A frontend block',
|
72
|
+
type: 'FRONTEND',
|
73
|
+
resources: [
|
74
|
+
{
|
75
|
+
name: 'web',
|
76
|
+
type: 'WEBPAGE',
|
77
|
+
description: 'A web page',
|
78
|
+
},
|
79
|
+
{
|
80
|
+
type: 'CLIENT',
|
81
|
+
name: 'entities',
|
82
|
+
description: 'Client for backend',
|
83
|
+
},
|
84
|
+
],
|
85
|
+
},
|
86
|
+
},
|
87
|
+
{
|
88
|
+
type: 'CREATE_CONNECTION',
|
89
|
+
created: Date.now(),
|
90
|
+
reason: 'connect service to ui',
|
91
|
+
payload: {
|
92
|
+
fromComponent: 'service',
|
93
|
+
fromResource: 'entities',
|
94
|
+
fromResourceType: 'API',
|
95
|
+
toComponent: 'ui',
|
96
|
+
toResource: 'entities',
|
97
|
+
toResourceType: 'CLIENT',
|
98
|
+
},
|
99
|
+
},
|
100
|
+
{
|
101
|
+
type: 'CREATE_API',
|
102
|
+
reason: 'create api',
|
103
|
+
created: Date.now(),
|
104
|
+
payload: {
|
105
|
+
blockName: 'service',
|
106
|
+
content: `controller Entities('/entities') {
|
107
|
+
@GET('/')
|
108
|
+
list(): string[]
|
109
|
+
}`,
|
110
|
+
},
|
111
|
+
},
|
112
|
+
{
|
113
|
+
type: 'CREATE_MODEL',
|
114
|
+
created: Date.now(),
|
115
|
+
reason: 'create model',
|
116
|
+
payload: {
|
117
|
+
blockName: 'service',
|
118
|
+
content: `type Entity {
|
119
|
+
@Id
|
120
|
+
id: string
|
121
|
+
|
122
|
+
name: string
|
123
|
+
}`,
|
124
|
+
},
|
125
|
+
},
|
126
|
+
];
|
127
|
+
describe('event-parser', () => {
|
128
|
+
it('it can parse events into a plan and blocks with proper layout', () => {
|
129
|
+
const parser = new event_parser_1.StormEventParser(parserOptions);
|
130
|
+
events.forEach((event) => parser.addEvent('kapeta', event));
|
131
|
+
const result = parser.toResult('kapeta');
|
132
|
+
expect(result.plan.metadata.name).toBe('kapeta/my-plan');
|
133
|
+
expect(result.plan.metadata.description).toBe('my plan description');
|
134
|
+
expect(result.blocks.length).toBe(2);
|
135
|
+
expect(result.blocks[0].content.metadata.name).toBe('kapeta/service');
|
136
|
+
expect(result.blocks[1].content.metadata.name).toBe('kapeta/ui');
|
137
|
+
const dbResource = result.blocks[0].content.spec.consumers?.[0];
|
138
|
+
const apiResource = result.blocks[0].content.spec.providers?.[0];
|
139
|
+
const clientResource = result.blocks[1].content.spec.consumers?.[0];
|
140
|
+
const pageResource = result.blocks[1].content.spec.providers?.[0];
|
141
|
+
expect(apiResource).toBeDefined();
|
142
|
+
expect(clientResource).toBeDefined();
|
143
|
+
expect(dbResource).toBeDefined();
|
144
|
+
expect(pageResource).toBeDefined();
|
145
|
+
expect(apiResource?.kind).toBe(parserOptions.apiKind);
|
146
|
+
expect(clientResource?.kind).toBe(parserOptions.clientKind);
|
147
|
+
expect(dbResource?.kind).toBe(parserOptions.databaseKind);
|
148
|
+
expect(pageResource?.kind).toBe(parserOptions.webPageKind);
|
149
|
+
expect(apiResource?.spec).toEqual(clientResource?.spec);
|
150
|
+
expect(dbResource?.spec.source.value).toContain('type Entity');
|
151
|
+
const serviceBlockInstance = result.plan.spec.blocks[0];
|
152
|
+
expect(serviceBlockInstance.name).toBe('service');
|
153
|
+
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
154
|
+
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
155
|
+
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
156
|
+
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
157
|
+
const uiBlockInstance = result.plan.spec.blocks[1];
|
158
|
+
expect(uiBlockInstance.name).toBe('ui');
|
159
|
+
expect(uiBlockInstance.dimensions.width).toBe(150);
|
160
|
+
expect(uiBlockInstance.dimensions.height).toBe(200);
|
161
|
+
expect(uiBlockInstance.dimensions.top).toBe(107);
|
162
|
+
expect(uiBlockInstance.dimensions.left).toBe(112);
|
163
|
+
expect(result.plan.spec.connections.length).toBe(1);
|
164
|
+
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|
165
|
+
expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
|
166
|
+
expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
|
167
|
+
expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
|
168
|
+
});
|
169
|
+
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.47.0",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -74,6 +74,8 @@
|
|
74
74
|
"gunzip-maybe": "^1.4.2",
|
75
75
|
"lodash": "^4.17.15",
|
76
76
|
"md5": "2.2.1",
|
77
|
+
"ngraph.forcelayout": "^3.3.1",
|
78
|
+
"ngraph.graph": "^20.0.1",
|
77
79
|
"node-cache": "^5.1.2",
|
78
80
|
"node-uuid": "^1.4.8",
|
79
81
|
"parse-data-uri": "^0.2.0",
|
@@ -3,14 +3,7 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
|
6
|
-
import {
|
7
|
-
ScreenTemplate,
|
8
|
-
StormBlockInfoFilled,
|
9
|
-
StormBlockType,
|
10
|
-
StormConnection,
|
11
|
-
StormEvent,
|
12
|
-
StormResourceType,
|
13
|
-
} from './events';
|
6
|
+
import { StormBlockInfoFilled, StormBlockType, StormConnection, StormEvent, StormResourceType } from './events';
|
14
7
|
import {
|
15
8
|
BlockDefinition,
|
16
9
|
BlockInstance,
|
@@ -24,6 +17,8 @@ import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-ut
|
|
24
17
|
import { KAPLANG_ID, KAPLANG_VERSION, RESTMethod } from '@kapeta/kaplang-core';
|
25
18
|
import uuid from 'node-uuid';
|
26
19
|
import { definitionsManager } from '../definitionsManager';
|
20
|
+
import createGraph from 'ngraph.graph';
|
21
|
+
import createLayout from 'ngraph.forcelayout';
|
27
22
|
|
28
23
|
export interface BlockDefinitionInfo {
|
29
24
|
uri: KapetaURI;
|
@@ -31,7 +26,7 @@ export interface BlockDefinitionInfo {
|
|
31
26
|
aiName: string;
|
32
27
|
}
|
33
28
|
|
34
|
-
export interface
|
29
|
+
export interface StormDefinitions {
|
35
30
|
plan: Plan;
|
36
31
|
blocks: BlockDefinitionInfo[];
|
37
32
|
}
|
@@ -175,6 +170,8 @@ export async function resolveOptions(): Promise<StormOptions> {
|
|
175
170
|
};
|
176
171
|
}
|
177
172
|
|
173
|
+
const LAYOUT_MARGIN = 50;
|
174
|
+
|
178
175
|
export class StormEventParser {
|
179
176
|
private events: StormEvent[] = [];
|
180
177
|
private planName: string = '';
|
@@ -196,7 +193,7 @@ export class StormEventParser {
|
|
196
193
|
this.connections = [];
|
197
194
|
}
|
198
195
|
|
199
|
-
public addEvent(evt: StormEvent):
|
196
|
+
public addEvent(handle:string, evt: StormEvent): StormDefinitions {
|
200
197
|
this.events.push(evt);
|
201
198
|
switch (evt.type) {
|
202
199
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -236,6 +233,8 @@ export class StormEventParser {
|
|
236
233
|
case 'FILE':
|
237
234
|
break;
|
238
235
|
}
|
236
|
+
|
237
|
+
return this.toResult(handle);
|
239
238
|
}
|
240
239
|
|
241
240
|
public getEvents(): StormEvent[] {
|
@@ -250,15 +249,57 @@ export class StormEventParser {
|
|
250
249
|
return this.error;
|
251
250
|
}
|
252
251
|
|
253
|
-
private applyLayoutToBlocks(result:
|
252
|
+
private applyLayoutToBlocks(result: StormDefinitions): StormDefinitions {
|
253
|
+
const graph = createGraph();
|
254
|
+
const blockInstances: { [key: string]: BlockInstance } = {};
|
255
|
+
|
256
|
+
result.plan.spec.blocks.forEach((block, index) => {
|
257
|
+
graph.addNode(block.id, block);
|
258
|
+
blockInstances[block.id] = block;
|
259
|
+
});
|
260
|
+
|
261
|
+
result.plan.spec.connections.forEach((connection) => {
|
262
|
+
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
263
|
+
});
|
264
|
+
|
265
|
+
const layout = createLayout(graph, {
|
266
|
+
springLength: 150,
|
267
|
+
debug: true,
|
268
|
+
dimensions: 2,
|
269
|
+
gravity: 2,
|
270
|
+
springCoefficient: 0.0008,
|
271
|
+
});
|
272
|
+
|
273
|
+
for (let i = 0; i < 100; ++i) {
|
274
|
+
layout.step();
|
275
|
+
}
|
276
|
+
|
277
|
+
// Layout might place things in negative space. We move everything to positive space
|
278
|
+
const graphBox = layout.getGraphRect();
|
279
|
+
let yAdjust = 0;
|
280
|
+
let xAdjust = 0;
|
281
|
+
if (graphBox.y1 < 0) {
|
282
|
+
yAdjust = -graphBox.y1;
|
283
|
+
}
|
284
|
+
if (graphBox.x1 < 0) {
|
285
|
+
xAdjust = -graphBox.x1;
|
286
|
+
}
|
287
|
+
|
288
|
+
graph.forEachNode((node) => {
|
289
|
+
const position = layout.getNodePosition(node.id);
|
290
|
+
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
291
|
+
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
292
|
+
});
|
293
|
+
|
294
|
+
layout.dispose();
|
295
|
+
|
254
296
|
return result;
|
255
297
|
}
|
256
298
|
|
257
|
-
public toResult(handle: string):
|
299
|
+
public toResult(handle: string): StormDefinitions {
|
258
300
|
const planRef = this.toRef(handle, this.planName);
|
259
301
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
260
302
|
const refIdMap: { [key: string]: string } = {};
|
261
|
-
const screens: { [key: string]: ScreenTemplate[] } = {};
|
262
303
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
263
304
|
const id = uuid.v4();
|
264
305
|
refIdMap[ref] = id;
|
@@ -271,7 +312,7 @@ export class StormEventParser {
|
|
271
312
|
dimensions: {
|
272
313
|
left: 0,
|
273
314
|
top: 0,
|
274
|
-
width:
|
315
|
+
width: 150,
|
275
316
|
height: 200,
|
276
317
|
},
|
277
318
|
} satisfies BlockInstance;
|
@@ -504,7 +545,7 @@ export class StormEventParser {
|
|
504
545
|
} satisfies SourceCode,
|
505
546
|
},
|
506
547
|
};
|
507
|
-
blockSpec.
|
548
|
+
blockSpec.consumers!.push(dbResource);
|
508
549
|
break;
|
509
550
|
case 'JWTCONSUMER':
|
510
551
|
case 'WEBFRAGMENT':
|
package/src/storm/events.ts
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
+
import {StormDefinitions} from "./event-parser";
|
5
6
|
|
6
7
|
export type StormResourceType =
|
7
8
|
| 'API'
|
@@ -154,6 +155,13 @@ export interface StormEventDone {
|
|
154
155
|
created: number;
|
155
156
|
}
|
156
157
|
|
158
|
+
export interface StormEventDefinitionChange {
|
159
|
+
type: 'DEFINITION_CHANGE';
|
160
|
+
reason: string;
|
161
|
+
created: number;
|
162
|
+
payload: StormDefinitions;
|
163
|
+
}
|
164
|
+
|
157
165
|
export type StormEvent =
|
158
166
|
| StormEventCreateBlock
|
159
167
|
| StormEventCreateConnection
|
@@ -165,4 +173,5 @@ export type StormEvent =
|
|
165
173
|
| StormEventScreen
|
166
174
|
| StormEventScreenCandidate
|
167
175
|
| StormEventFile
|
168
|
-
| StormEventDone
|
176
|
+
| StormEventDone
|
177
|
+
| StormEventDefinitionChange;
|
package/src/storm/routes.ts
CHANGED
@@ -4,15 +4,15 @@
|
|
4
4
|
*/
|
5
5
|
|
6
6
|
import Router from 'express-promise-router';
|
7
|
-
import {
|
8
|
-
import {
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import {
|
13
|
-
import {
|
14
|
-
import {
|
15
|
-
import {
|
7
|
+
import {Response} from 'express';
|
8
|
+
import {corsHandler} from '../middleware/cors';
|
9
|
+
import {stringBody} from '../middleware/stringBody';
|
10
|
+
import {KapetaBodyRequest} from '../types';
|
11
|
+
import {StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream} from './stream';
|
12
|
+
import {stormClient} from './stormClient';
|
13
|
+
import {StormEvent} from './events';
|
14
|
+
import {resolveOptions, StormDefinitions, StormEventParser} from './event-parser';
|
15
|
+
import {StormCodegen} from './codegen';
|
16
16
|
|
17
17
|
const router = Router();
|
18
18
|
|
@@ -33,24 +33,29 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
33
33
|
res.set('Content-Type', 'application/x-ndjson');
|
34
34
|
|
35
35
|
metaStream.on('data', (data: StormEvent) => {
|
36
|
-
eventParser.addEvent(data);
|
36
|
+
const result = eventParser.addEvent(req.params.handle, data);
|
37
|
+
|
38
|
+
sendDefinitions(res, result);
|
37
39
|
});
|
38
40
|
|
39
41
|
await streamStormPartialResponse(metaStream, res);
|
40
42
|
|
41
43
|
if (!eventParser.isValid()) {
|
42
44
|
// We can't continue if the meta stream is invalid
|
43
|
-
res
|
45
|
+
sendEvent(res, {
|
44
46
|
type: 'ERROR_INTERNAL',
|
45
|
-
payload: {
|
47
|
+
payload: {error: eventParser.getError()},
|
46
48
|
reason: 'Failed to generate system',
|
47
49
|
created: Date.now(),
|
48
|
-
}
|
50
|
+
});
|
49
51
|
res.end();
|
50
52
|
return;
|
51
53
|
}
|
54
|
+
|
52
55
|
const result = eventParser.toResult(handle);
|
53
56
|
|
57
|
+
sendDefinitions(res, result);
|
58
|
+
|
54
59
|
const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
55
60
|
|
56
61
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
@@ -65,13 +70,20 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
65
70
|
}
|
66
71
|
});
|
67
72
|
|
73
|
+
function sendDefinitions(res: Response, result: StormDefinitions) {
|
74
|
+
sendEvent(res, {
|
75
|
+
type: 'DEFINITION_CHANGE',
|
76
|
+
payload: result,
|
77
|
+
reason: 'Updates to definition',
|
78
|
+
created: Date.now(),
|
79
|
+
});
|
80
|
+
}
|
81
|
+
|
68
82
|
function sendDone(res: Response) {
|
69
|
-
res
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
} satisfies StormEvent) + '\n'
|
74
|
-
);
|
83
|
+
sendEvent(res, {
|
84
|
+
type: 'DONE',
|
85
|
+
created: Date.now(),
|
86
|
+
});
|
75
87
|
|
76
88
|
res.end();
|
77
89
|
}
|
@@ -79,23 +91,22 @@ function sendDone(res: Response) {
|
|
79
91
|
function sendError(err: Error, res: Response) {
|
80
92
|
console.error('Failed to send prompt', err);
|
81
93
|
if (res.headersSent) {
|
82
|
-
res
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
} satisfies StormEvent) + '\n'
|
89
|
-
);
|
94
|
+
sendEvent(res, {
|
95
|
+
type: 'ERROR_INTERNAL',
|
96
|
+
created: Date.now(),
|
97
|
+
payload: {error: err.message},
|
98
|
+
reason: 'Failed while sending prompt',
|
99
|
+
});
|
90
100
|
} else {
|
91
|
-
res.status(400).send({
|
101
|
+
res.status(400).send({error: err.message});
|
92
102
|
}
|
93
103
|
}
|
94
104
|
|
105
|
+
|
95
106
|
function streamStormPartialResponse(result: StormStream, res: Response) {
|
96
107
|
return new Promise<void>((resolve, reject) => {
|
97
108
|
result.on('data', (data) => {
|
98
|
-
res
|
109
|
+
sendEvent(res, data);
|
99
110
|
});
|
100
111
|
|
101
112
|
result.on('error', (err) => {
|
@@ -108,4 +119,8 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
|
|
108
119
|
});
|
109
120
|
}
|
110
121
|
|
122
|
+
function sendEvent(res: Response, evt: StormEvent) {
|
123
|
+
res.write(JSON.stringify(evt) + '\n');
|
124
|
+
}
|
125
|
+
|
111
126
|
export default router;
|
@@ -0,0 +1,190 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { StormEventParser } from '../../src/storm/event-parser';
|
7
|
+
import { StormEvent } from '../../src/storm/events';
|
8
|
+
|
9
|
+
const parserOptions = {
|
10
|
+
serviceKind: 'kapeta/block-service:local',
|
11
|
+
serviceLanguage: 'kapeta/language-target-nodejs-ts:local',
|
12
|
+
|
13
|
+
frontendKind: 'kapeta/block-type-frontend:local',
|
14
|
+
frontendLanguage: 'kapeta/language-target-react-ts:local',
|
15
|
+
|
16
|
+
cliKind: 'kapeta/block-type-cli:local',
|
17
|
+
cliLanguage: 'kapeta/language-target-nodejs-ts:local',
|
18
|
+
|
19
|
+
desktopKind: 'kapeta/block-type-desktop:local',
|
20
|
+
desktopLanguage: 'kapeta/language-target-electron-ts:local',
|
21
|
+
|
22
|
+
gatewayKind: 'kapeta/block-type-gateway:local',
|
23
|
+
|
24
|
+
mqKind: 'kapeta/block-type-mq:local',
|
25
|
+
exchangeKind: 'kapeta/resource-type-exchange:local',
|
26
|
+
queueKind: 'kapeta/resource-type-queue:local',
|
27
|
+
publisherKind: 'kapeta/resource-type-publisher:local',
|
28
|
+
subscriberKind: 'kapeta/resource-type-subscriber:local',
|
29
|
+
databaseKind: 'kapeta/block-type-database:local',
|
30
|
+
|
31
|
+
apiKind: 'kapeta/block-type-api:local',
|
32
|
+
clientKind: 'kapeta/block-type-client:local',
|
33
|
+
|
34
|
+
webPageKind: 'kapeta/block-type-web-page:local',
|
35
|
+
webFragmentKind: 'kapeta/block-type-web-fragment:local',
|
36
|
+
|
37
|
+
jwtProviderKind: 'kapeta/resource-type-jwt-provider:local',
|
38
|
+
jwtConsumerKind: 'kapeta/resource-type-jwt-consumer:local',
|
39
|
+
|
40
|
+
smtpKind: 'kapeta/resource-type-smtp:local',
|
41
|
+
externalApiKind: 'kapeta/resource-type-external-api:local',
|
42
|
+
};
|
43
|
+
|
44
|
+
const events: StormEvent[] = [
|
45
|
+
{
|
46
|
+
type: 'CREATE_PLAN_PROPERTIES',
|
47
|
+
created: Date.now(),
|
48
|
+
reason: 'create plan properties',
|
49
|
+
payload: {
|
50
|
+
name: 'my-plan',
|
51
|
+
description: 'my plan description',
|
52
|
+
},
|
53
|
+
},
|
54
|
+
{
|
55
|
+
type: 'CREATE_BLOCK',
|
56
|
+
reason: 'create backend',
|
57
|
+
created: Date.now(),
|
58
|
+
payload: {
|
59
|
+
name: 'service',
|
60
|
+
description: 'A service block',
|
61
|
+
type: 'BACKEND',
|
62
|
+
resources: [
|
63
|
+
{
|
64
|
+
name: 'entities',
|
65
|
+
type: 'DATABASE',
|
66
|
+
description: 'A database resource',
|
67
|
+
},
|
68
|
+
{
|
69
|
+
type: 'API',
|
70
|
+
name: 'entities',
|
71
|
+
description: 'An API resource',
|
72
|
+
},
|
73
|
+
],
|
74
|
+
},
|
75
|
+
},
|
76
|
+
{
|
77
|
+
type: 'CREATE_BLOCK',
|
78
|
+
reason: 'create frontend',
|
79
|
+
created: Date.now(),
|
80
|
+
payload: {
|
81
|
+
name: 'ui',
|
82
|
+
description: 'A frontend block',
|
83
|
+
type: 'FRONTEND',
|
84
|
+
resources: [
|
85
|
+
{
|
86
|
+
name: 'web',
|
87
|
+
type: 'WEBPAGE',
|
88
|
+
description: 'A web page',
|
89
|
+
},
|
90
|
+
{
|
91
|
+
type: 'CLIENT',
|
92
|
+
name: 'entities',
|
93
|
+
description: 'Client for backend',
|
94
|
+
},
|
95
|
+
],
|
96
|
+
},
|
97
|
+
},
|
98
|
+
{
|
99
|
+
type: 'CREATE_CONNECTION',
|
100
|
+
created: Date.now(),
|
101
|
+
reason: 'connect service to ui',
|
102
|
+
payload: {
|
103
|
+
fromComponent: 'service',
|
104
|
+
fromResource: 'entities',
|
105
|
+
fromResourceType: 'API',
|
106
|
+
toComponent: 'ui',
|
107
|
+
toResource: 'entities',
|
108
|
+
toResourceType: 'CLIENT',
|
109
|
+
},
|
110
|
+
},
|
111
|
+
{
|
112
|
+
type: 'CREATE_API',
|
113
|
+
reason: 'create api',
|
114
|
+
created: Date.now(),
|
115
|
+
payload: {
|
116
|
+
blockName: 'service',
|
117
|
+
content: `controller Entities('/entities') {
|
118
|
+
@GET('/')
|
119
|
+
list(): string[]
|
120
|
+
}`,
|
121
|
+
},
|
122
|
+
},
|
123
|
+
{
|
124
|
+
type: 'CREATE_MODEL',
|
125
|
+
created: Date.now(),
|
126
|
+
reason: 'create model',
|
127
|
+
payload: {
|
128
|
+
blockName: 'service',
|
129
|
+
content: `type Entity {
|
130
|
+
@Id
|
131
|
+
id: string
|
132
|
+
|
133
|
+
name: string
|
134
|
+
}`,
|
135
|
+
},
|
136
|
+
},
|
137
|
+
];
|
138
|
+
|
139
|
+
describe('event-parser', () => {
|
140
|
+
it('it can parse events into a plan and blocks with proper layout', () => {
|
141
|
+
const parser = new StormEventParser(parserOptions);
|
142
|
+
events.forEach((event) => parser.addEvent('kapeta', event));
|
143
|
+
|
144
|
+
const result = parser.toResult('kapeta');
|
145
|
+
|
146
|
+
expect(result.plan.metadata.name).toBe('kapeta/my-plan');
|
147
|
+
expect(result.plan.metadata.description).toBe('my plan description');
|
148
|
+
expect(result.blocks.length).toBe(2);
|
149
|
+
expect(result.blocks[0].content.metadata.name).toBe('kapeta/service');
|
150
|
+
expect(result.blocks[1].content.metadata.name).toBe('kapeta/ui');
|
151
|
+
|
152
|
+
const dbResource = result.blocks[0].content.spec.consumers?.[0];
|
153
|
+
const apiResource = result.blocks[0].content.spec.providers?.[0];
|
154
|
+
const clientResource = result.blocks[1].content.spec.consumers?.[0];
|
155
|
+
const pageResource = result.blocks[1].content.spec.providers?.[0];
|
156
|
+
|
157
|
+
expect(apiResource).toBeDefined();
|
158
|
+
expect(clientResource).toBeDefined();
|
159
|
+
expect(dbResource).toBeDefined();
|
160
|
+
expect(pageResource).toBeDefined();
|
161
|
+
|
162
|
+
expect(apiResource?.kind).toBe(parserOptions.apiKind);
|
163
|
+
expect(clientResource?.kind).toBe(parserOptions.clientKind);
|
164
|
+
expect(dbResource?.kind).toBe(parserOptions.databaseKind);
|
165
|
+
expect(pageResource?.kind).toBe(parserOptions.webPageKind);
|
166
|
+
|
167
|
+
expect(apiResource?.spec).toEqual(clientResource?.spec);
|
168
|
+
expect(dbResource?.spec.source.value).toContain('type Entity');
|
169
|
+
|
170
|
+
const serviceBlockInstance = result.plan.spec.blocks[0];
|
171
|
+
expect(serviceBlockInstance.name).toBe('service');
|
172
|
+
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
173
|
+
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
174
|
+
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
175
|
+
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
176
|
+
|
177
|
+
const uiBlockInstance = result.plan.spec.blocks[1];
|
178
|
+
expect(uiBlockInstance.name).toBe('ui');
|
179
|
+
expect(uiBlockInstance.dimensions.width).toBe(150);
|
180
|
+
expect(uiBlockInstance.dimensions.height).toBe(200);
|
181
|
+
expect(uiBlockInstance.dimensions.top).toBe(107);
|
182
|
+
expect(uiBlockInstance.dimensions.left).toBe(112);
|
183
|
+
|
184
|
+
expect(result.plan.spec.connections.length).toBe(1);
|
185
|
+
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|
186
|
+
expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
|
187
|
+
expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
|
188
|
+
expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
|
189
|
+
});
|
190
|
+
});
|