@kapeta/local-cluster-service 0.45.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 +14 -0
- package/dist/cjs/src/storm/codegen.d.ts +5 -1
- package/dist/cjs/src/storm/codegen.js +68 -18
- package/dist/cjs/src/storm/event-parser.d.ts +5 -5
- package/dist/cjs/src/storm/event-parser.js +50 -33
- package/dist/cjs/src/storm/events.d.ts +8 -2
- package/dist/cjs/src/storm/events.js +0 -4
- package/dist/cjs/src/storm/routes.js +20 -29
- package/dist/cjs/src/storm/stormClient.d.ts +2 -2
- package/dist/cjs/src/storm/stormClient.js +0 -7
- package/dist/cjs/src/storm/stream.d.ts +7 -0
- 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/codegen.d.ts +5 -1
- package/dist/esm/src/storm/codegen.js +68 -18
- package/dist/esm/src/storm/event-parser.d.ts +5 -5
- package/dist/esm/src/storm/event-parser.js +50 -33
- package/dist/esm/src/storm/events.d.ts +8 -2
- package/dist/esm/src/storm/events.js +0 -4
- package/dist/esm/src/storm/routes.js +20 -29
- package/dist/esm/src/storm/stormClient.d.ts +2 -2
- package/dist/esm/src/storm/stormClient.js +0 -7
- package/dist/esm/src/storm/stream.d.ts +7 -0
- 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/codegen.ts +83 -27
- package/src/storm/event-parser.ts +68 -47
- package/src/storm/events.ts +10 -2
- package/src/storm/routes.ts +42 -59
- package/src/storm/stormClient.ts +3 -11
- package/src/storm/stream.ts +8 -0
- package/test/storm/event-parser.test.ts +190 -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,14 +17,16 @@ 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;
|
30
25
|
content: BlockDefinition;
|
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,8 +193,7 @@ export class StormEventParser {
|
|
196
193
|
this.connections = [];
|
197
194
|
}
|
198
195
|
|
199
|
-
public addEvent(evt: StormEvent):
|
200
|
-
console.log('Processing storm event', evt);
|
196
|
+
public addEvent(handle:string, evt: StormEvent): StormDefinitions {
|
201
197
|
this.events.push(evt);
|
202
198
|
switch (evt.type) {
|
203
199
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -210,7 +206,6 @@ export class StormEventParser {
|
|
210
206
|
apis: [],
|
211
207
|
models: [],
|
212
208
|
types: [],
|
213
|
-
screens: [],
|
214
209
|
};
|
215
210
|
break;
|
216
211
|
case 'PLAN_RETRY':
|
@@ -232,21 +227,14 @@ export class StormEventParser {
|
|
232
227
|
case 'CREATE_CONNECTION':
|
233
228
|
this.connections.push(evt.payload);
|
234
229
|
break;
|
235
|
-
case 'SCREEN':
|
236
|
-
this.blocks[evt.payload.blockName].screens.push({
|
237
|
-
name: evt.payload.name,
|
238
|
-
description: evt.payload.description,
|
239
|
-
url: evt.payload.url,
|
240
|
-
template: evt.payload.template,
|
241
|
-
});
|
242
|
-
break;
|
243
230
|
|
244
231
|
default:
|
245
232
|
case 'SCREEN_CANDIDATE':
|
246
233
|
case 'FILE':
|
247
|
-
console.warn('Unhandled event: %s', evt.type, evt);
|
248
234
|
break;
|
249
235
|
}
|
236
|
+
|
237
|
+
return this.toResult(handle);
|
250
238
|
}
|
251
239
|
|
252
240
|
public getEvents(): StormEvent[] {
|
@@ -261,15 +249,57 @@ export class StormEventParser {
|
|
261
249
|
return this.error;
|
262
250
|
}
|
263
251
|
|
264
|
-
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
|
+
|
265
296
|
return result;
|
266
297
|
}
|
267
298
|
|
268
|
-
public toResult(handle: string):
|
299
|
+
public toResult(handle: string): StormDefinitions {
|
269
300
|
const planRef = this.toRef(handle, this.planName);
|
270
301
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
271
302
|
const refIdMap: { [key: string]: string } = {};
|
272
|
-
const screens: { [key: string]: ScreenTemplate[] } = {};
|
273
303
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
274
304
|
const id = uuid.v4();
|
275
305
|
refIdMap[ref] = id;
|
@@ -282,23 +312,12 @@ export class StormEventParser {
|
|
282
312
|
dimensions: {
|
283
313
|
left: 0,
|
284
314
|
top: 0,
|
285
|
-
width:
|
315
|
+
width: 150,
|
286
316
|
height: 200,
|
287
317
|
},
|
288
318
|
} satisfies BlockInstance;
|
289
319
|
});
|
290
320
|
|
291
|
-
Object.values(this.blocks).forEach((blockInfo) => {
|
292
|
-
const blockRef = this.toRef(handle, blockInfo.name);
|
293
|
-
const block = blockDefinitions[blockRef.toNormalizedString()];
|
294
|
-
if (!block) {
|
295
|
-
console.warn('Block not found: %s', blockInfo.name);
|
296
|
-
return;
|
297
|
-
}
|
298
|
-
|
299
|
-
screens[blockRef.fullName] = blockInfo.screens;
|
300
|
-
});
|
301
|
-
|
302
321
|
// Copy API methods from API provider to CLIENT consumer
|
303
322
|
this.connections
|
304
323
|
.filter((connection) => connection.fromResourceType === 'API' && connection.toResourceType === 'CLIENT')
|
@@ -307,12 +326,12 @@ export class StormEventParser {
|
|
307
326
|
const clientConsumerRef = this.toRef(handle, apiConnection.toComponent);
|
308
327
|
const apiProviderBlock = blockDefinitions[apiProviderRef.toNormalizedString()];
|
309
328
|
if (!apiProviderBlock) {
|
310
|
-
console.warn('API provider not found: %s', apiConnection.fromComponent);
|
329
|
+
console.warn('API provider not found: %s', apiConnection.fromComponent, apiConnection);
|
311
330
|
return;
|
312
331
|
}
|
313
332
|
const clientConsumerBlock = blockDefinitions[clientConsumerRef.toNormalizedString()];
|
314
333
|
if (!clientConsumerBlock) {
|
315
|
-
console.warn('Client consumer not found: %s', apiConnection.toComponent);
|
334
|
+
console.warn('Client consumer not found: %s', apiConnection.toComponent, apiConnection);
|
316
335
|
return;
|
317
336
|
}
|
318
337
|
|
@@ -324,25 +343,27 @@ export class StormEventParser {
|
|
324
343
|
console.warn(
|
325
344
|
'API resource not found: %s on %s',
|
326
345
|
apiConnection.fromResource,
|
327
|
-
apiProviderRef.toNormalizedString()
|
346
|
+
apiProviderRef.toNormalizedString(),
|
347
|
+
apiConnection
|
328
348
|
);
|
329
349
|
return;
|
330
350
|
}
|
331
351
|
|
332
352
|
const clientResource = clientConsumerBlock.content.spec.consumers?.find((clientResource) => {
|
333
353
|
if (clientResource.kind !== this.options.clientKind) {
|
334
|
-
|
335
|
-
|
336
|
-
if (clientResource.metadata.name !== apiConnection.toResource) {
|
337
|
-
return;
|
354
|
+
console.warn('Client resource kind mismatch: %s', clientResource.kind, this.options.clientKind);
|
355
|
+
return false;
|
338
356
|
}
|
357
|
+
|
358
|
+
return clientResource.metadata.name === apiConnection.toResource;
|
339
359
|
});
|
340
360
|
|
341
361
|
if (!clientResource) {
|
342
362
|
console.warn(
|
343
363
|
'Client resource not found: %s on %s',
|
344
364
|
apiConnection.toResource,
|
345
|
-
clientConsumerRef.toNormalizedString()
|
365
|
+
clientConsumerRef.toNormalizedString(),
|
366
|
+
apiConnection
|
346
367
|
);
|
347
368
|
return;
|
348
369
|
}
|
@@ -407,6 +428,7 @@ export class StormEventParser {
|
|
407
428
|
|
408
429
|
const blockDefinitionInfo: BlockDefinitionInfo = {
|
409
430
|
uri: blockRef,
|
431
|
+
aiName: blockInfo.name,
|
410
432
|
content: {
|
411
433
|
kind: this.toBlockKind(blockInfo.type),
|
412
434
|
metadata: {
|
@@ -428,7 +450,6 @@ export class StormEventParser {
|
|
428
450
|
consumers: [],
|
429
451
|
},
|
430
452
|
},
|
431
|
-
screens: blockInfo.screens,
|
432
453
|
};
|
433
454
|
|
434
455
|
const blockSpec = blockDefinitionInfo.content.spec;
|
@@ -524,7 +545,7 @@ export class StormEventParser {
|
|
524
545
|
} satisfies SourceCode,
|
525
546
|
},
|
526
547
|
};
|
527
|
-
blockSpec.
|
548
|
+
blockSpec.consumers!.push(dbResource);
|
528
549
|
break;
|
529
550
|
case 'JWTCONSUMER':
|
530
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'
|
@@ -35,7 +36,6 @@ export interface StormBlockInfoFilled extends StormBlockInfo {
|
|
35
36
|
apis: string[];
|
36
37
|
types: string[];
|
37
38
|
models: string[];
|
38
|
-
screens: ScreenTemplate[];
|
39
39
|
}
|
40
40
|
|
41
41
|
export interface StormEventCreateBlock {
|
@@ -155,6 +155,13 @@ export interface StormEventDone {
|
|
155
155
|
created: number;
|
156
156
|
}
|
157
157
|
|
158
|
+
export interface StormEventDefinitionChange {
|
159
|
+
type: 'DEFINITION_CHANGE';
|
160
|
+
reason: string;
|
161
|
+
created: number;
|
162
|
+
payload: StormDefinitions;
|
163
|
+
}
|
164
|
+
|
158
165
|
export type StormEvent =
|
159
166
|
| StormEventCreateBlock
|
160
167
|
| StormEventCreateConnection
|
@@ -166,4 +173,5 @@ export type StormEvent =
|
|
166
173
|
| StormEventScreen
|
167
174
|
| StormEventScreenCandidate
|
168
175
|
| StormEventFile
|
169
|
-
| 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
|
|
@@ -27,34 +27,36 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
27
27
|
|
28
28
|
const eventParser = new StormEventParser(stormOptions);
|
29
29
|
|
30
|
-
console.log('Got prompt', req.stringBody);
|
31
30
|
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
32
31
|
const metaStream = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
33
32
|
|
34
33
|
res.set('Content-Type', 'application/x-ndjson');
|
35
34
|
|
36
35
|
metaStream.on('data', (data: StormEvent) => {
|
37
|
-
eventParser.addEvent(data);
|
36
|
+
const result = eventParser.addEvent(req.params.handle, data);
|
37
|
+
|
38
|
+
sendDefinitions(res, result);
|
38
39
|
});
|
39
40
|
|
40
41
|
await streamStormPartialResponse(metaStream, res);
|
41
42
|
|
42
43
|
if (!eventParser.isValid()) {
|
43
44
|
// We can't continue if the meta stream is invalid
|
44
|
-
res
|
45
|
+
sendEvent(res, {
|
45
46
|
type: 'ERROR_INTERNAL',
|
46
|
-
payload: {
|
47
|
+
payload: {error: eventParser.getError()},
|
47
48
|
reason: 'Failed to generate system',
|
48
49
|
created: Date.now(),
|
49
|
-
}
|
50
|
+
});
|
50
51
|
res.end();
|
51
52
|
return;
|
52
53
|
}
|
54
|
+
|
53
55
|
const result = eventParser.toResult(handle);
|
54
56
|
|
55
|
-
|
57
|
+
sendDefinitions(res, result);
|
56
58
|
|
57
|
-
const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks);
|
59
|
+
const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
58
60
|
|
59
61
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
60
62
|
|
@@ -68,42 +70,20 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
68
70
|
}
|
69
71
|
});
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
router.post('/services/implement', async (req: KapetaBodyRequest, res: Response) => {
|
79
|
-
const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
|
80
|
-
const result = await stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
|
81
|
-
|
82
|
-
await streamStormResponse(result, res);
|
83
|
-
});
|
84
|
-
|
85
|
-
router.post('/ui/implement', async (req: KapetaBodyRequest, res: Response) => {
|
86
|
-
const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
|
87
|
-
const result = await stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
|
88
|
-
|
89
|
-
await streamStormResponse(result, res);
|
90
|
-
});
|
91
|
-
|
92
|
-
async function streamStormResponse(result: StormStream, res: Response) {
|
93
|
-
res.set('Content-Type', 'application/x-ndjson');
|
94
|
-
|
95
|
-
await streamStormPartialResponse(result, res);
|
96
|
-
|
97
|
-
sendDone(res);
|
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
|
+
});
|
98
80
|
}
|
99
81
|
|
100
82
|
function sendDone(res: Response) {
|
101
|
-
res
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
} satisfies StormEvent) + '\n'
|
106
|
-
);
|
83
|
+
sendEvent(res, {
|
84
|
+
type: 'DONE',
|
85
|
+
created: Date.now(),
|
86
|
+
});
|
107
87
|
|
108
88
|
res.end();
|
109
89
|
}
|
@@ -111,23 +91,22 @@ function sendDone(res: Response) {
|
|
111
91
|
function sendError(err: Error, res: Response) {
|
112
92
|
console.error('Failed to send prompt', err);
|
113
93
|
if (res.headersSent) {
|
114
|
-
res
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
} satisfies StormEvent) + '\n'
|
121
|
-
);
|
94
|
+
sendEvent(res, {
|
95
|
+
type: 'ERROR_INTERNAL',
|
96
|
+
created: Date.now(),
|
97
|
+
payload: {error: err.message},
|
98
|
+
reason: 'Failed while sending prompt',
|
99
|
+
});
|
122
100
|
} else {
|
123
|
-
res.status(400).send({
|
101
|
+
res.status(400).send({error: err.message});
|
124
102
|
}
|
125
103
|
}
|
126
104
|
|
105
|
+
|
127
106
|
function streamStormPartialResponse(result: StormStream, res: Response) {
|
128
107
|
return new Promise<void>((resolve, reject) => {
|
129
108
|
result.on('data', (data) => {
|
130
|
-
res
|
109
|
+
sendEvent(res, data);
|
131
110
|
});
|
132
111
|
|
133
112
|
result.on('error', (err) => {
|
@@ -140,4 +119,8 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
|
|
140
119
|
});
|
141
120
|
}
|
142
121
|
|
122
|
+
function sendEvent(res: Response, evt: StormEvent) {
|
123
|
+
res.write(JSON.stringify(evt) + '\n');
|
124
|
+
}
|
125
|
+
|
143
126
|
export default router;
|
package/src/storm/stormClient.ts
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
StormFileImplementationPrompt,
|
13
13
|
StormFileInfo,
|
14
14
|
StormStream,
|
15
|
+
StormUIImplementationPrompt,
|
15
16
|
} from './stream';
|
16
17
|
|
17
18
|
export const STORM_ID = 'storm';
|
@@ -73,14 +74,6 @@ class StormClient {
|
|
73
74
|
out.emit('error', error);
|
74
75
|
});
|
75
76
|
|
76
|
-
jsonLStream.on('pause', () => {
|
77
|
-
console.log('paused');
|
78
|
-
});
|
79
|
-
|
80
|
-
jsonLStream.on('resume', () => {
|
81
|
-
console.log('resumed');
|
82
|
-
});
|
83
|
-
|
84
77
|
jsonLStream.on('close', () => {
|
85
78
|
out.end();
|
86
79
|
});
|
@@ -95,15 +88,14 @@ class StormClient {
|
|
95
88
|
});
|
96
89
|
}
|
97
90
|
|
98
|
-
public createUIImplementation(prompt:
|
99
|
-
return this.send<
|
91
|
+
public createUIImplementation(prompt: StormUIImplementationPrompt, history?: ConversationItem[]) {
|
92
|
+
return this.send<StormUIImplementationPrompt>('/v2/ui/merge', {
|
100
93
|
history: history ?? [],
|
101
94
|
prompt,
|
102
95
|
});
|
103
96
|
}
|
104
97
|
|
105
98
|
public createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]) {
|
106
|
-
console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
|
107
99
|
return this.send<StormFileImplementationPrompt>('/v2/services/merge', {
|
108
100
|
history: history ?? [],
|
109
101
|
prompt,
|
package/src/storm/stream.ts
CHANGED
@@ -86,3 +86,11 @@ export interface StormFileImplementationPrompt {
|
|
86
86
|
template: StormFileInfo;
|
87
87
|
prompt: string;
|
88
88
|
}
|
89
|
+
|
90
|
+
export interface StormUIImplementationPrompt {
|
91
|
+
events: StormEvent[];
|
92
|
+
templates: StormFileInfo[];
|
93
|
+
context: StormFileInfo[];
|
94
|
+
blockName: string;
|
95
|
+
prompt: string;
|
96
|
+
}
|
@@ -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
|
+
});
|