@kapeta/local-cluster-service 0.47.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,10 @@
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
+
1
8
  # [0.47.0](https://github.com/kapetacom/local-cluster-service/compare/v0.46.0...v0.47.0) (2024-05-29)
2
9
 
3
10
 
@@ -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
  }
@@ -55,7 +55,6 @@ export declare class StormEventParser {
55
55
  getEvents(): StormEvent[];
56
56
  isValid(): boolean;
57
57
  getError(): string;
58
- private applyLayoutToBlocks;
59
58
  toResult(handle: string): StormDefinitions;
60
59
  private toSafeName;
61
60
  private toRef;
@@ -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,17 +3,12 @@
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
- const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
16
- const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
17
12
  async function resolveOptions() {
18
13
  // Predefined types for now - TODO: Allow user to select / change
19
14
  const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
@@ -89,7 +84,6 @@ async function resolveOptions() {
89
84
  };
90
85
  }
91
86
  exports.resolveOptions = resolveOptions;
92
- const LAYOUT_MARGIN = 50;
93
87
  class StormEventParser {
94
88
  events = [];
95
89
  planName = '';
@@ -110,6 +104,7 @@ class StormEventParser {
110
104
  }
111
105
  addEvent(handle, evt) {
112
106
  this.events.push(evt);
107
+ console.log('evt', evt);
113
108
  switch (evt.type) {
114
109
  case 'CREATE_PLAN_PROPERTIES':
115
110
  this.planName = evt.payload.name;
@@ -153,55 +148,21 @@ class StormEventParser {
153
148
  return this.events;
154
149
  }
155
150
  isValid() {
151
+ if (!this.planName) {
152
+ return false;
153
+ }
156
154
  return !this.failed;
157
155
  }
158
156
  getError() {
159
157
  return this.error;
160
158
  }
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();
197
- return result;
198
- }
199
159
  toResult(handle) {
200
- const planRef = this.toRef(handle, this.planName);
160
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
201
161
  const blockDefinitions = this.toBlockDefinitions(handle);
202
162
  const refIdMap = {};
203
163
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
204
- const id = node_uuid_1.default.v4();
164
+ // Create a deterministic uuid
165
+ const id = (0, uuid_1.v5)(ref, uuid_1.v5.URL);
205
166
  refIdMap[ref] = id;
206
167
  return {
207
168
  id,
@@ -249,6 +210,28 @@ class StormEventParser {
249
210
  console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
250
211
  return;
251
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
+ }
252
235
  clientResource.spec.methods = apiResource.spec.methods;
253
236
  clientResource.spec.source = apiResource.spec.source;
254
237
  });
@@ -267,9 +250,7 @@ class StormEventParser {
267
250
  blockId: refIdMap[fromRef.toNormalizedString()],
268
251
  resourceName: connection.fromResource,
269
252
  },
270
- mapping: {
271
- //TODO: Add mapping
272
- },
253
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
273
254
  };
274
255
  });
275
256
  const plan = {
@@ -284,10 +265,10 @@ class StormEventParser {
284
265
  connections,
285
266
  },
286
267
  };
287
- return this.applyLayoutToBlocks({
268
+ return {
288
269
  plan,
289
270
  blocks: Object.values(blockDefinitions),
290
- });
271
+ };
291
272
  }
292
273
  toSafeName(name) {
293
274
  return name.toLowerCase().replace(/[^0-9a-z-]/gi, '');
@@ -498,6 +479,33 @@ class StormEventParser {
498
479
  }
499
480
  return '';
500
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
+ }
501
509
  toPortType(type) {
502
510
  switch (type) {
503
511
  case 'API':
@@ -2,7 +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
+ import { StormDefinitions } from './event-parser';
6
6
  export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
7
7
  export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
8
8
  export interface StormBlockInfo {
@@ -117,6 +117,7 @@ export interface StormEventFile {
117
117
  payload: {
118
118
  filename: string;
119
119
  content: string;
120
+ blockName: string;
120
121
  blockRef: string;
121
122
  };
122
123
  }
@@ -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 {
@@ -150,16 +150,8 @@ describe('event-parser', () => {
150
150
  expect(dbResource?.spec.source.value).toContain('type Entity');
151
151
  const serviceBlockInstance = result.plan.spec.blocks[0];
152
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
153
  const uiBlockInstance = result.plan.spec.blocks[1];
158
154
  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
155
  expect(result.plan.spec.connections.length).toBe(1);
164
156
  expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
165
157
  expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
@@ -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
  }
@@ -55,7 +55,6 @@ export declare class StormEventParser {
55
55
  getEvents(): StormEvent[];
56
56
  isValid(): boolean;
57
57
  getError(): string;
58
- private applyLayoutToBlocks;
59
58
  toResult(handle: string): StormDefinitions;
60
59
  private toSafeName;
61
60
  private toRef;
@@ -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,17 +3,12 @@
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
- const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
16
- const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
17
12
  async function resolveOptions() {
18
13
  // Predefined types for now - TODO: Allow user to select / change
19
14
  const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
@@ -89,7 +84,6 @@ async function resolveOptions() {
89
84
  };
90
85
  }
91
86
  exports.resolveOptions = resolveOptions;
92
- const LAYOUT_MARGIN = 50;
93
87
  class StormEventParser {
94
88
  events = [];
95
89
  planName = '';
@@ -110,6 +104,7 @@ class StormEventParser {
110
104
  }
111
105
  addEvent(handle, evt) {
112
106
  this.events.push(evt);
107
+ console.log('evt', evt);
113
108
  switch (evt.type) {
114
109
  case 'CREATE_PLAN_PROPERTIES':
115
110
  this.planName = evt.payload.name;
@@ -153,55 +148,21 @@ class StormEventParser {
153
148
  return this.events;
154
149
  }
155
150
  isValid() {
151
+ if (!this.planName) {
152
+ return false;
153
+ }
156
154
  return !this.failed;
157
155
  }
158
156
  getError() {
159
157
  return this.error;
160
158
  }
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();
197
- return result;
198
- }
199
159
  toResult(handle) {
200
- const planRef = this.toRef(handle, this.planName);
160
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
201
161
  const blockDefinitions = this.toBlockDefinitions(handle);
202
162
  const refIdMap = {};
203
163
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
204
- const id = node_uuid_1.default.v4();
164
+ // Create a deterministic uuid
165
+ const id = (0, uuid_1.v5)(ref, uuid_1.v5.URL);
205
166
  refIdMap[ref] = id;
206
167
  return {
207
168
  id,
@@ -249,6 +210,28 @@ class StormEventParser {
249
210
  console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
250
211
  return;
251
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
+ }
252
235
  clientResource.spec.methods = apiResource.spec.methods;
253
236
  clientResource.spec.source = apiResource.spec.source;
254
237
  });
@@ -267,9 +250,7 @@ class StormEventParser {
267
250
  blockId: refIdMap[fromRef.toNormalizedString()],
268
251
  resourceName: connection.fromResource,
269
252
  },
270
- mapping: {
271
- //TODO: Add mapping
272
- },
253
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
273
254
  };
274
255
  });
275
256
  const plan = {
@@ -284,10 +265,10 @@ class StormEventParser {
284
265
  connections,
285
266
  },
286
267
  };
287
- return this.applyLayoutToBlocks({
268
+ return {
288
269
  plan,
289
270
  blocks: Object.values(blockDefinitions),
290
- });
271
+ };
291
272
  }
292
273
  toSafeName(name) {
293
274
  return name.toLowerCase().replace(/[^0-9a-z-]/gi, '');
@@ -498,6 +479,33 @@ class StormEventParser {
498
479
  }
499
480
  return '';
500
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
+ }
501
509
  toPortType(type) {
502
510
  switch (type) {
503
511
  case 'API':
@@ -2,7 +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
+ import { StormDefinitions } from './event-parser';
6
6
  export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
7
7
  export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
8
8
  export interface StormBlockInfo {
@@ -117,6 +117,7 @@ export interface StormEventFile {
117
117
  payload: {
118
118
  filename: string;
119
119
  content: string;
120
+ blockName: string;
120
121
  blockRef: string;
121
122
  };
122
123
  }
@@ -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 {
@@ -150,16 +150,8 @@ describe('event-parser', () => {
150
150
  expect(dbResource?.spec.source.value).toContain('type Entity');
151
151
  const serviceBlockInstance = result.plan.spec.blocks[0];
152
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
153
  const uiBlockInstance = result.plan.spec.blocks[1];
158
154
  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
155
  expect(result.plan.spec.connections.length).toBe(1);
164
156
  expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
165
157
  expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.47.0",
3
+ "version": "0.47.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -74,8 +74,6 @@
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",
79
77
  "node-cache": "^5.1.2",
80
78
  "node-uuid": "^1.4.8",
81
79
  "parse-data-uri": "^0.2.0",
@@ -85,6 +83,7 @@
85
83
  "stream-json": "^1.8.0",
86
84
  "tar-stream": "^3.1.6",
87
85
  "typescript": "^5.1.6",
86
+ "uuid": "^9.0.1",
88
87
  "yaml": "^1.6.0"
89
88
  },
90
89
  "devDependencies": {
@@ -103,6 +102,7 @@
103
102
  "@types/node-uuid": "^0.0.29",
104
103
  "@types/request": "^2.48.8",
105
104
  "@types/tar-stream": "^2.2.2",
105
+ "@types/uuid": "^9.0.8",
106
106
  "eslint": "^8.42.0",
107
107
  "eslint-config-prettier": "^8.8.0",
108
108
  "jest": "^29.6.4",
@@ -8,7 +8,7 @@ import { AIFileTypes, BlockCodeGenerator, GeneratedFile, GeneratedResult } from
8
8
  import { BlockDefinition } from '@kapeta/schemas';
9
9
  import { codeGeneratorManager } from '../codeGeneratorManager';
10
10
  import { STORM_ID, stormClient } from './stormClient';
11
- import { ScreenTemplate, StormEvent, StormEventFile, StormEventScreen } from './events';
11
+ import { StormEvent, StormEventFile } from './events';
12
12
  import { BlockDefinitionInfo } from './event-parser';
13
13
  import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
14
14
  import { KapetaURI } from '@kapeta/nodejs-utils';
@@ -42,16 +42,16 @@ export class StormCodegen {
42
42
  return this.out;
43
43
  }
44
44
 
45
- private handleTemplateFileOutput(blockUri: KapetaURI, template: StormFileInfo, data: StormEvent) {
45
+ private handleTemplateFileOutput(blockUri: KapetaURI, aiName: string, template: StormFileInfo, data: StormEvent) {
46
46
  switch (data.type) {
47
47
  case 'FILE':
48
48
  template.filename = data.payload.filename;
49
49
  template.content = data.payload.content;
50
- return this.handleFileOutput(blockUri, data);
50
+ return this.handleFileOutput(blockUri, aiName, data);
51
51
  }
52
52
  }
53
53
 
54
- private handleUiOutput(blockUri: KapetaURI, data: StormEvent) {
54
+ private handleUiOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
55
55
  switch (data.type) {
56
56
  case 'SCREEN':
57
57
  this.out.emit('data', {
@@ -60,18 +60,18 @@ export class StormCodegen {
60
60
  created: Date.now(),
61
61
  payload: {
62
62
  ...data.payload,
63
- blockName: blockUri.toNormalizedString(),
63
+ blockName: aiName,
64
64
  },
65
65
  });
66
66
  case 'FILE':
67
- return this.handleFileOutput(blockUri, data);
67
+ return this.handleFileOutput(blockUri, aiName, data);
68
68
  }
69
69
  }
70
70
 
71
- private handleFileOutput(blockUri: KapetaURI, data: StormEvent) {
71
+ private handleFileOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
72
72
  switch (data.type) {
73
73
  case 'FILE':
74
- this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
74
+ this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
75
75
  return {
76
76
  type: 'FILE',
77
77
  created: Date.now(),
@@ -96,7 +96,7 @@ export class StormCodegen {
96
96
  const allFiles = this.toStormFiles(generatedResult);
97
97
 
98
98
  // Send all the non-ai files to the stream
99
- this.emitFiles(block.uri, allFiles);
99
+ this.emitFiles(block.uri, block.aiName, allFiles);
100
100
 
101
101
  const relevantFiles: StormFileInfo[] = allFiles.filter(
102
102
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -112,7 +112,7 @@ export class StormCodegen {
112
112
  });
113
113
 
114
114
  uiStream.on('data', (evt) => {
115
- this.handleUiOutput(block.uri, evt);
115
+ this.handleUiOutput(block.uri, block.aiName, evt);
116
116
  });
117
117
 
118
118
  await uiStream.waitForDone();
@@ -128,6 +128,7 @@ export class StormCodegen {
128
128
  if (serviceFiles.length > 0) {
129
129
  await this.processTemplates(
130
130
  block.uri,
131
+ block.aiName,
131
132
  stormClient.createServiceImplementation.bind(stormClient),
132
133
  serviceFiles,
133
134
  contextFiles
@@ -138,7 +139,7 @@ export class StormCodegen {
138
139
  /**
139
140
  * Emits the text-based files to the stream
140
141
  */
141
- private emitFiles(uri: KapetaURI, files: StormFileInfo[]) {
142
+ private emitFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
142
143
  files.forEach((file) => {
143
144
  if (!file.content || typeof file.content !== 'string') {
144
145
  return;
@@ -156,11 +157,17 @@ export class StormCodegen {
156
157
  return;
157
158
  }
158
159
 
159
- this.emitFile(uri, file.filename, file.content);
160
+ this.emitFile(uri, aiName, file.filename, file.content);
160
161
  });
161
162
  }
162
163
 
163
- private emitFile(uri: KapetaURI, filename: string, content: string, reason: string = 'File generated') {
164
+ private emitFile(
165
+ uri: KapetaURI,
166
+ blockName: string,
167
+ filename: string,
168
+ content: string,
169
+ reason: string = 'File generated'
170
+ ) {
164
171
  this.out.emit('data', {
165
172
  type: 'FILE',
166
173
  reason,
@@ -168,6 +175,7 @@ export class StormCodegen {
168
175
  payload: {
169
176
  filename: filename,
170
177
  content: content,
178
+ blockName,
171
179
  blockRef: uri.toNormalizedString(),
172
180
  },
173
181
  } satisfies StormEventFile);
@@ -178,6 +186,7 @@ export class StormCodegen {
178
186
  */
179
187
  private async processTemplates(
180
188
  blockUri: KapetaURI,
189
+ aiName: string,
181
190
  generator: ImplementationGenerator,
182
191
  templates: StormFileInfo[],
183
192
  contextFiles: StormFileInfo[]
@@ -192,7 +201,7 @@ export class StormCodegen {
192
201
  const files: StormEventFile[] = [];
193
202
 
194
203
  stream.on('data', (evt) => {
195
- const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
204
+ const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
196
205
  if (file) {
197
206
  files.push(file);
198
207
  }
@@ -14,11 +14,18 @@ import {
14
14
  SourceCode,
15
15
  } from '@kapeta/schemas';
16
16
  import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
17
- import { KAPLANG_ID, KAPLANG_VERSION, RESTMethod } from '@kapeta/kaplang-core';
18
- import uuid from 'node-uuid';
17
+ import {
18
+ DSLAPIParser,
19
+ DSLController,
20
+ DSLConverters,
21
+ DSLDataTypeParser,
22
+ DSLMethod,
23
+ KAPLANG_ID,
24
+ KAPLANG_VERSION,
25
+ KaplangWriter,
26
+ } from '@kapeta/kaplang-core';
27
+ import { v5 as uuid } from 'uuid';
19
28
  import { definitionsManager } from '../definitionsManager';
20
- import createGraph from 'ngraph.graph';
21
- import createLayout from 'ngraph.forcelayout';
22
29
 
23
30
  export interface BlockDefinitionInfo {
24
31
  uri: KapetaURI;
@@ -170,8 +177,6 @@ export async function resolveOptions(): Promise<StormOptions> {
170
177
  };
171
178
  }
172
179
 
173
- const LAYOUT_MARGIN = 50;
174
-
175
180
  export class StormEventParser {
176
181
  private events: StormEvent[] = [];
177
182
  private planName: string = '';
@@ -193,8 +198,9 @@ export class StormEventParser {
193
198
  this.connections = [];
194
199
  }
195
200
 
196
- public addEvent(handle:string, evt: StormEvent): StormDefinitions {
201
+ public addEvent(handle: string, evt: StormEvent): StormDefinitions {
197
202
  this.events.push(evt);
203
+ console.log('evt', evt);
198
204
  switch (evt.type) {
199
205
  case 'CREATE_PLAN_PROPERTIES':
200
206
  this.planName = evt.payload.name;
@@ -242,6 +248,9 @@ export class StormEventParser {
242
248
  }
243
249
 
244
250
  public isValid(): boolean {
251
+ if (!this.planName) {
252
+ return false;
253
+ }
245
254
  return !this.failed;
246
255
  }
247
256
 
@@ -249,59 +258,13 @@ export class StormEventParser {
249
258
  return this.error;
250
259
  }
251
260
 
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
-
296
- return result;
297
- }
298
-
299
261
  public toResult(handle: string): StormDefinitions {
300
- const planRef = this.toRef(handle, this.planName);
262
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
301
263
  const blockDefinitions = this.toBlockDefinitions(handle);
302
264
  const refIdMap: { [key: string]: string } = {};
303
265
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
304
- const id = uuid.v4();
266
+ // Create a deterministic uuid
267
+ const id = uuid(ref, uuid.URL);
305
268
  refIdMap[ref] = id;
306
269
  return {
307
270
  id,
@@ -368,13 +331,41 @@ export class StormEventParser {
368
331
  return;
369
332
  }
370
333
 
334
+ if (apiProviderBlock.content.spec.entities?.source?.value) {
335
+ if (!clientConsumerBlock.content.spec.entities) {
336
+ clientConsumerBlock.content.spec.entities = {
337
+ types: [],
338
+ source: {
339
+ type: KAPLANG_ID,
340
+ version: KAPLANG_VERSION,
341
+ value: '',
342
+ },
343
+ };
344
+ }
345
+
346
+ const clientTypes = DSLDataTypeParser.parse(
347
+ clientConsumerBlock.content.spec.entities.source!.value
348
+ );
349
+ const apiTypes = DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
350
+
351
+ apiTypes.forEach((apiType) => {
352
+ if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
353
+ // Already exists
354
+ return;
355
+ }
356
+ clientTypes.push(apiType);
357
+ });
358
+
359
+ clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write(clientTypes);
360
+ }
371
361
  clientResource.spec.methods = apiResource.spec.methods;
372
362
  clientResource.spec.source = apiResource.spec.source;
373
363
  });
374
364
 
375
- const connections = this.connections.map((connection) => {
365
+ const connections: Connection[] = this.connections.map((connection) => {
376
366
  const fromRef = this.toRef(handle, connection.fromComponent);
377
367
  const toRef = this.toRef(handle, connection.toComponent);
368
+
378
369
  return {
379
370
  port: {
380
371
  type: this.toPortType(connection.fromResourceType),
@@ -387,9 +378,7 @@ export class StormEventParser {
387
378
  blockId: refIdMap[fromRef.toNormalizedString()],
388
379
  resourceName: connection.fromResource,
389
380
  },
390
- mapping: {
391
- //TODO: Add mapping
392
- },
381
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
393
382
  } satisfies Connection;
394
383
  });
395
384
 
@@ -406,10 +395,10 @@ export class StormEventParser {
406
395
  },
407
396
  };
408
397
 
409
- return this.applyLayoutToBlocks({
398
+ return {
410
399
  plan,
411
400
  blocks: Object.values(blockDefinitions),
412
- });
401
+ };
413
402
  }
414
403
 
415
404
  private toSafeName(name: string): string {
@@ -637,6 +626,55 @@ export class StormEventParser {
637
626
  return '';
638
627
  }
639
628
 
629
+ private toConnectionMapping(
630
+ handle: string,
631
+ connection: StormConnection,
632
+ blockDefinitions: { [key: string]: BlockDefinitionInfo }
633
+ ): any {
634
+ if (connection.fromResourceType !== 'API') {
635
+ return;
636
+ }
637
+
638
+ const fromRef = this.toRef(handle, connection.fromComponent);
639
+
640
+ const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
641
+ if (!apiProviderBlock) {
642
+ console.warn('Provider block not found: %s', connection.fromComponent, connection);
643
+ return;
644
+ }
645
+
646
+ const apiResource = apiProviderBlock.content.spec.providers?.find(
647
+ (p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource
648
+ );
649
+
650
+ if (!apiResource) {
651
+ console.warn(
652
+ 'API resource not found: %s on %s',
653
+ connection.fromResource,
654
+ fromRef.toNormalizedString(),
655
+ connection
656
+ );
657
+ return;
658
+ }
659
+
660
+ const apiMethods = DSLConverters.toSchemaMethods(
661
+ DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
662
+ ignoreSemantics: true,
663
+ }) as (DSLMethod | DSLController)[]
664
+ );
665
+
666
+ const mapping: any = {};
667
+
668
+ Object.entries(apiMethods).forEach(([methodId, method]) => {
669
+ mapping[methodId] = {
670
+ targetId: methodId,
671
+ type: 'EXACT',
672
+ };
673
+ });
674
+
675
+ return mapping;
676
+ }
677
+
640
678
  private toPortType(type: StormResourceType) {
641
679
  switch (type) {
642
680
  case 'API':
@@ -2,7 +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
+ import { StormDefinitions } from './event-parser';
6
6
 
7
7
  export type StormResourceType =
8
8
  | 'API'
@@ -146,6 +146,7 @@ export interface StormEventFile {
146
146
  payload: {
147
147
  filename: string;
148
148
  content: string;
149
+ blockName: string;
149
150
  blockRef: string;
150
151
  };
151
152
  }
@@ -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, StormDefinitions, 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
 
@@ -44,7 +44,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
44
44
  // We can't continue if the meta stream is invalid
45
45
  sendEvent(res, {
46
46
  type: 'ERROR_INTERNAL',
47
- payload: {error: eventParser.getError()},
47
+ payload: { error: eventParser.getError() },
48
48
  reason: 'Failed to generate system',
49
49
  created: Date.now(),
50
50
  });
@@ -94,15 +94,14 @@ function sendError(err: Error, res: Response) {
94
94
  sendEvent(res, {
95
95
  type: 'ERROR_INTERNAL',
96
96
  created: Date.now(),
97
- payload: {error: err.message},
97
+ payload: { error: err.message },
98
98
  reason: 'Failed while sending prompt',
99
99
  });
100
100
  } else {
101
- res.status(400).send({error: err.message});
101
+ res.status(400).send({ error: err.message });
102
102
  }
103
103
  }
104
104
 
105
-
106
105
  function streamStormPartialResponse(result: StormStream, res: Response) {
107
106
  return new Promise<void>((resolve, reject) => {
108
107
  result.on('data', (data) => {
@@ -34,6 +34,10 @@ class StormClient {
34
34
  //headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
35
35
  }
36
36
 
37
+ if (body.conversationId) {
38
+ headers['conversationId'] = body.conversationId;
39
+ }
40
+
37
41
  return {
38
42
  url,
39
43
  method: method,
@@ -74,6 +74,7 @@ export interface ConversationItem {
74
74
 
75
75
  export interface StormContextRequest<T = string> {
76
76
  history?: ConversationItem[];
77
+ conversationId?: string;
77
78
  prompt: T;
78
79
  }
79
80
 
@@ -169,17 +169,9 @@ describe('event-parser', () => {
169
169
 
170
170
  const serviceBlockInstance = result.plan.spec.blocks[0];
171
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
172
 
177
173
  const uiBlockInstance = result.plan.spec.blocks[1];
178
174
  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
175
 
184
176
  expect(result.plan.spec.connections.length).toBe(1);
185
177
  expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);