@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/storm/codegen.d.ts +5 -1
  3. package/dist/cjs/src/storm/codegen.js +68 -18
  4. package/dist/cjs/src/storm/event-parser.d.ts +5 -5
  5. package/dist/cjs/src/storm/event-parser.js +50 -33
  6. package/dist/cjs/src/storm/events.d.ts +8 -2
  7. package/dist/cjs/src/storm/events.js +0 -4
  8. package/dist/cjs/src/storm/routes.js +20 -29
  9. package/dist/cjs/src/storm/stormClient.d.ts +2 -2
  10. package/dist/cjs/src/storm/stormClient.js +0 -7
  11. package/dist/cjs/src/storm/stream.d.ts +7 -0
  12. package/dist/cjs/test/storm/event-parser.test.d.ts +5 -0
  13. package/dist/cjs/test/storm/event-parser.test.js +169 -0
  14. package/dist/esm/src/storm/codegen.d.ts +5 -1
  15. package/dist/esm/src/storm/codegen.js +68 -18
  16. package/dist/esm/src/storm/event-parser.d.ts +5 -5
  17. package/dist/esm/src/storm/event-parser.js +50 -33
  18. package/dist/esm/src/storm/events.d.ts +8 -2
  19. package/dist/esm/src/storm/events.js +0 -4
  20. package/dist/esm/src/storm/routes.js +20 -29
  21. package/dist/esm/src/storm/stormClient.d.ts +2 -2
  22. package/dist/esm/src/storm/stormClient.js +0 -7
  23. package/dist/esm/src/storm/stream.d.ts +7 -0
  24. package/dist/esm/test/storm/event-parser.test.d.ts +5 -0
  25. package/dist/esm/test/storm/event-parser.test.js +169 -0
  26. package/package.json +3 -1
  27. package/src/storm/codegen.ts +83 -27
  28. package/src/storm/event-parser.ts +68 -47
  29. package/src/storm/events.ts +10 -2
  30. package/src/storm/routes.ts +42 -59
  31. package/src/storm/stormClient.ts +3 -11
  32. package/src/storm/stream.ts +8 -0
  33. 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
- screens: ScreenTemplate[];
26
+ aiName: string;
32
27
  }
33
28
 
34
- export interface ParsedResult {
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): void {
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: ParsedResult): ParsedResult {
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): ParsedResult {
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: 200,
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
- return;
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.providers!.push(dbResource);
548
+ blockSpec.consumers!.push(dbResource);
528
549
  break;
529
550
  case 'JWTCONSUMER':
530
551
  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
 
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;
@@ -4,15 +4,15 @@
4
4
  */
5
5
 
6
6
  import Router from 'express-promise-router';
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, StormEventParser } from './event-parser';
15
- import { StormCodegen } from './codegen';
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.write({
45
+ sendEvent(res, {
45
46
  type: 'ERROR_INTERNAL',
46
- payload: { error: eventParser.getError() },
47
+ payload: {error: eventParser.getError()},
47
48
  reason: 'Failed to generate system',
48
49
  created: Date.now(),
49
- } satisfies StormEvent);
50
+ });
50
51
  res.end();
51
52
  return;
52
53
  }
54
+
53
55
  const result = eventParser.toResult(handle);
54
56
 
55
- console.log('RESULT\n', JSON.stringify(result, null, 2));
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
- router.post('/metadata', async (req: KapetaBodyRequest, res: Response) => {
72
- const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
73
- const result = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
74
-
75
- await streamStormResponse(result, res);
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.write(
102
- JSON.stringify({
103
- type: 'DONE',
104
- created: Date.now(),
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.write(
115
- JSON.stringify({
116
- type: 'ERROR_INTERNAL',
117
- created: Date.now(),
118
- payload: { error: err.message },
119
- reason: 'Failed while sending prompt',
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({ error: err.message });
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.write(JSON.stringify(data) + '\n');
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;
@@ -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: StormFileImplementationPrompt, history?: ConversationItem[]) {
99
- return this.send<StormFileImplementationPrompt>('/v2/ui/merge', {
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,
@@ -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
+ });