@kapeta/local-cluster-service 0.46.0 → 0.47.1

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.47.1](https://github.com/kapetacom/local-cluster-service/compare/v0.47.0...v0.47.1) (2024-05-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Make UUIDs deterministic to allow cleaner updates ([#150](https://github.com/kapetacom/local-cluster-service/issues/150)) ([0e3bbec](https://github.com/kapetacom/local-cluster-service/commit/0e3bbecb00d52e2cc0f5e0d8adb9953f40e6e253))
7
+
8
+ # [0.47.0](https://github.com/kapetacom/local-cluster-service/compare/v0.46.0...v0.47.0) (2024-05-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * 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))
14
+
1
15
  # [0.46.0](https://github.com/kapetacom/local-cluster-service/compare/v0.45.0...v0.46.0) (2024-05-28)
2
16
 
3
17
 
@@ -28,15 +28,15 @@ class StormCodegen {
28
28
  getStream() {
29
29
  return this.out;
30
30
  }
31
- handleTemplateFileOutput(blockUri, template, data) {
31
+ handleTemplateFileOutput(blockUri, aiName, template, data) {
32
32
  switch (data.type) {
33
33
  case 'FILE':
34
34
  template.filename = data.payload.filename;
35
35
  template.content = data.payload.content;
36
- return this.handleFileOutput(blockUri, data);
36
+ return this.handleFileOutput(blockUri, aiName, data);
37
37
  }
38
38
  }
39
- handleUiOutput(blockUri, data) {
39
+ handleUiOutput(blockUri, aiName, data) {
40
40
  switch (data.type) {
41
41
  case 'SCREEN':
42
42
  this.out.emit('data', {
@@ -45,17 +45,17 @@ class StormCodegen {
45
45
  created: Date.now(),
46
46
  payload: {
47
47
  ...data.payload,
48
- blockName: blockUri.toNormalizedString(),
48
+ blockName: aiName,
49
49
  },
50
50
  });
51
51
  case 'FILE':
52
- return this.handleFileOutput(blockUri, data);
52
+ return this.handleFileOutput(blockUri, aiName, data);
53
53
  }
54
54
  }
55
- handleFileOutput(blockUri, data) {
55
+ handleFileOutput(blockUri, aiName, data) {
56
56
  switch (data.type) {
57
57
  case 'FILE':
58
- this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
58
+ this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
59
59
  return {
60
60
  type: 'FILE',
61
61
  created: Date.now(),
@@ -77,7 +77,7 @@ class StormCodegen {
77
77
  }
78
78
  const allFiles = this.toStormFiles(generatedResult);
79
79
  // Send all the non-ai files to the stream
80
- this.emitFiles(block.uri, allFiles);
80
+ this.emitFiles(block.uri, block.aiName, allFiles);
81
81
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
82
82
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
83
83
  if (uiTemplates.length > 0) {
@@ -89,7 +89,7 @@ class StormCodegen {
89
89
  prompt: this.userPrompt,
90
90
  });
91
91
  uiStream.on('data', (evt) => {
92
- this.handleUiOutput(block.uri, evt);
92
+ this.handleUiOutput(block.uri, block.aiName, evt);
93
93
  });
94
94
  await uiStream.waitForDone();
95
95
  }
@@ -98,13 +98,13 @@ class StormCodegen {
98
98
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
99
99
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
100
100
  if (serviceFiles.length > 0) {
101
- await this.processTemplates(block.uri, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
101
+ await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
102
102
  }
103
103
  }
104
104
  /**
105
105
  * Emits the text-based files to the stream
106
106
  */
107
- emitFiles(uri, files) {
107
+ emitFiles(uri, aiName, files) {
108
108
  files.forEach((file) => {
109
109
  if (!file.content || typeof file.content !== 'string') {
110
110
  return;
@@ -119,10 +119,10 @@ class StormCodegen {
119
119
  // They will need to be implemented by the AI
120
120
  return;
121
121
  }
122
- this.emitFile(uri, file.filename, file.content);
122
+ this.emitFile(uri, aiName, file.filename, file.content);
123
123
  });
124
124
  }
125
- emitFile(uri, filename, content, reason = 'File generated') {
125
+ emitFile(uri, blockName, filename, content, reason = 'File generated') {
126
126
  this.out.emit('data', {
127
127
  type: 'FILE',
128
128
  reason,
@@ -130,6 +130,7 @@ class StormCodegen {
130
130
  payload: {
131
131
  filename: filename,
132
132
  content: content,
133
+ blockName,
133
134
  blockRef: uri.toNormalizedString(),
134
135
  },
135
136
  });
@@ -137,7 +138,7 @@ class StormCodegen {
137
138
  /**
138
139
  * Sends the template to the AI and processes the response
139
140
  */
140
- async processTemplates(blockUri, generator, templates, contextFiles) {
141
+ async processTemplates(blockUri, aiName, generator, templates, contextFiles) {
141
142
  const promises = templates.map(async (templateFile) => {
142
143
  const stream = await generator({
143
144
  context: contextFiles,
@@ -146,7 +147,7 @@ class StormCodegen {
146
147
  });
147
148
  const files = [];
148
149
  stream.on('data', (evt) => {
149
- const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
150
+ const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
150
151
  if (file) {
151
152
  files.push(file);
152
153
  }
@@ -10,7 +10,7 @@ export interface BlockDefinitionInfo {
10
10
  content: BlockDefinition;
11
11
  aiName: string;
12
12
  }
13
- export interface ParsedResult {
13
+ export interface StormDefinitions {
14
14
  plan: Plan;
15
15
  blocks: BlockDefinitionInfo[];
16
16
  }
@@ -51,12 +51,11 @@ export declare class StormEventParser {
51
51
  private options;
52
52
  constructor(options: StormOptions);
53
53
  private reset;
54
- addEvent(evt: StormEvent): void;
54
+ addEvent(handle: string, evt: StormEvent): StormDefinitions;
55
55
  getEvents(): StormEvent[];
56
56
  isValid(): boolean;
57
57
  getError(): string;
58
- private applyLayoutToBlocks;
59
- toResult(handle: string): ParsedResult;
58
+ toResult(handle: string): StormDefinitions;
60
59
  private toSafeName;
61
60
  private toRef;
62
61
  toBlockDefinitions(handle: string): {
@@ -64,6 +63,7 @@ export declare class StormEventParser {
64
63
  };
65
64
  private toResourceKind;
66
65
  private toBlockKind;
66
+ private toConnectionMapping;
67
67
  private toPortType;
68
68
  private toBlockTarget;
69
69
  private toBlockTargetKind;
@@ -3,14 +3,11 @@
3
3
  * Copyright 2023 Kapeta Inc.
4
4
  * SPDX-License-Identifier: BUSL-1.1
5
5
  */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
6
  Object.defineProperty(exports, "__esModule", { value: true });
10
7
  exports.StormEventParser = exports.resolveOptions = void 0;
11
8
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
12
9
  const kaplang_core_1 = require("@kapeta/kaplang-core");
13
- const node_uuid_1 = __importDefault(require("node-uuid"));
10
+ const uuid_1 = require("uuid");
14
11
  const definitionsManager_1 = require("../definitionsManager");
15
12
  async function resolveOptions() {
16
13
  // Predefined types for now - TODO: Allow user to select / change
@@ -105,8 +102,9 @@ class StormEventParser {
105
102
  this.blocks = {};
106
103
  this.connections = [];
107
104
  }
108
- addEvent(evt) {
105
+ addEvent(handle, evt) {
109
106
  this.events.push(evt);
107
+ console.log('evt', evt);
110
108
  switch (evt.type) {
111
109
  case 'CREATE_PLAN_PROPERTIES':
112
110
  this.planName = evt.payload.name;
@@ -144,26 +142,27 @@ class StormEventParser {
144
142
  case 'FILE':
145
143
  break;
146
144
  }
145
+ return this.toResult(handle);
147
146
  }
148
147
  getEvents() {
149
148
  return this.events;
150
149
  }
151
150
  isValid() {
151
+ if (!this.planName) {
152
+ return false;
153
+ }
152
154
  return !this.failed;
153
155
  }
154
156
  getError() {
155
157
  return this.error;
156
158
  }
157
- applyLayoutToBlocks(result) {
158
- return result;
159
- }
160
159
  toResult(handle) {
161
- const planRef = this.toRef(handle, this.planName);
160
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
162
161
  const blockDefinitions = this.toBlockDefinitions(handle);
163
162
  const refIdMap = {};
164
- const screens = {};
165
163
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
166
- const id = node_uuid_1.default.v4();
164
+ // Create a deterministic uuid
165
+ const id = (0, uuid_1.v5)(ref, uuid_1.v5.URL);
167
166
  refIdMap[ref] = id;
168
167
  return {
169
168
  id,
@@ -174,7 +173,7 @@ class StormEventParser {
174
173
  dimensions: {
175
174
  left: 0,
176
175
  top: 0,
177
- width: 200,
176
+ width: 150,
178
177
  height: 200,
179
178
  },
180
179
  };
@@ -211,6 +210,28 @@ class StormEventParser {
211
210
  console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
212
211
  return;
213
212
  }
213
+ if (apiProviderBlock.content.spec.entities?.source?.value) {
214
+ if (!clientConsumerBlock.content.spec.entities) {
215
+ clientConsumerBlock.content.spec.entities = {
216
+ types: [],
217
+ source: {
218
+ type: kaplang_core_1.KAPLANG_ID,
219
+ version: kaplang_core_1.KAPLANG_VERSION,
220
+ value: '',
221
+ },
222
+ };
223
+ }
224
+ const clientTypes = kaplang_core_1.DSLDataTypeParser.parse(clientConsumerBlock.content.spec.entities.source.value);
225
+ const apiTypes = kaplang_core_1.DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
226
+ apiTypes.forEach((apiType) => {
227
+ if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
228
+ // Already exists
229
+ return;
230
+ }
231
+ clientTypes.push(apiType);
232
+ });
233
+ clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write(clientTypes);
234
+ }
214
235
  clientResource.spec.methods = apiResource.spec.methods;
215
236
  clientResource.spec.source = apiResource.spec.source;
216
237
  });
@@ -229,9 +250,7 @@ class StormEventParser {
229
250
  blockId: refIdMap[fromRef.toNormalizedString()],
230
251
  resourceName: connection.fromResource,
231
252
  },
232
- mapping: {
233
- //TODO: Add mapping
234
- },
253
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
235
254
  };
236
255
  });
237
256
  const plan = {
@@ -246,10 +265,10 @@ class StormEventParser {
246
265
  connections,
247
266
  },
248
267
  };
249
- return this.applyLayoutToBlocks({
268
+ return {
250
269
  plan,
251
270
  blocks: Object.values(blockDefinitions),
252
- });
271
+ };
253
272
  }
254
273
  toSafeName(name) {
255
274
  return name.toLowerCase().replace(/[^0-9a-z-]/gi, '');
@@ -377,7 +396,7 @@ class StormEventParser {
377
396
  },
378
397
  },
379
398
  };
380
- blockSpec.providers.push(dbResource);
399
+ blockSpec.consumers.push(dbResource);
381
400
  break;
382
401
  case 'JWTCONSUMER':
383
402
  case 'WEBFRAGMENT':
@@ -460,6 +479,33 @@ class StormEventParser {
460
479
  }
461
480
  return '';
462
481
  }
482
+ toConnectionMapping(handle, connection, blockDefinitions) {
483
+ if (connection.fromResourceType !== 'API') {
484
+ return;
485
+ }
486
+ const fromRef = this.toRef(handle, connection.fromComponent);
487
+ const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
488
+ if (!apiProviderBlock) {
489
+ console.warn('Provider block not found: %s', connection.fromComponent, connection);
490
+ return;
491
+ }
492
+ const apiResource = apiProviderBlock.content.spec.providers?.find((p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource);
493
+ if (!apiResource) {
494
+ console.warn('API resource not found: %s on %s', connection.fromResource, fromRef.toNormalizedString(), connection);
495
+ return;
496
+ }
497
+ const apiMethods = kaplang_core_1.DSLConverters.toSchemaMethods(kaplang_core_1.DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
498
+ ignoreSemantics: true,
499
+ }));
500
+ const mapping = {};
501
+ Object.entries(apiMethods).forEach(([methodId, method]) => {
502
+ mapping[methodId] = {
503
+ targetId: methodId,
504
+ type: 'EXACT',
505
+ };
506
+ });
507
+ return mapping;
508
+ }
463
509
  toPortType(type) {
464
510
  switch (type) {
465
511
  case 'API':
@@ -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 {
@@ -116,6 +117,7 @@ export interface StormEventFile {
116
117
  payload: {
117
118
  filename: string;
118
119
  content: string;
120
+ blockName: string;
119
121
  blockRef: string;
120
122
  };
121
123
  }
@@ -123,4 +125,10 @@ export interface StormEventDone {
123
125
  type: 'DONE';
124
126
  created: number;
125
127
  }
126
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone;
128
+ export interface StormEventDefinitionChange {
129
+ type: 'DEFINITION_CHANGE';
130
+ reason: string;
131
+ created: number;
132
+ payload: StormDefinitions;
133
+ }
134
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange;
@@ -1,6 +1,2 @@
1
1
  "use strict";
2
- /**
3
- * Copyright 2023 Kapeta Inc.
4
- * SPDX-License-Identifier: BUSL-1.1
5
- */
6
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.write({
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.write(JSON.stringify({
64
+ sendEvent(res, {
55
65
  type: 'DONE',
56
66
  created: Date.now(),
57
- }) + '\n');
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.write(JSON.stringify({
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
- }) + '\n');
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.write(JSON.stringify(data) + '\n');
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;
@@ -28,6 +28,9 @@ class StormClient {
28
28
  if (api.hasToken()) {
29
29
  //headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
30
30
  }
31
+ if (body.conversationId) {
32
+ headers['conversationId'] = body.conversationId;
33
+ }
31
34
  return {
32
35
  url,
33
36
  method: method,
@@ -26,6 +26,7 @@ export interface ConversationItem {
26
26
  }
27
27
  export interface StormContextRequest<T = string> {
28
28
  history?: ConversationItem[];
29
+ conversationId?: string;
29
30
  prompt: T;
30
31
  }
31
32
  export interface StormFileInfo extends GeneratedFile {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ export {};
@@ -0,0 +1,161 @@
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
+ const uiBlockInstance = result.plan.spec.blocks[1];
154
+ expect(uiBlockInstance.name).toBe('ui');
155
+ expect(result.plan.spec.connections.length).toBe(1);
156
+ expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
157
+ expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
158
+ expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
159
+ expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
160
+ });
161
+ });