@kapeta/local-cluster-service 0.47.0 → 0.47.2

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.
@@ -14,11 +14,19 @@ 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
+ DSLParser,
24
+ KAPLANG_ID,
25
+ KAPLANG_VERSION,
26
+ KaplangWriter,
27
+ } from '@kapeta/kaplang-core';
28
+ import { v5 as uuid } from 'uuid';
19
29
  import { definitionsManager } from '../definitionsManager';
20
- import createGraph from 'ngraph.graph';
21
- import createLayout from 'ngraph.forcelayout';
22
30
 
23
31
  export interface BlockDefinitionInfo {
24
32
  uri: KapetaURI;
@@ -57,6 +65,28 @@ export interface StormOptions {
57
65
  gatewayKind: string;
58
66
  }
59
67
 
68
+ function prettifyKaplang(source: string) {
69
+ if (!source || !source.trim()) {
70
+ return '';
71
+ }
72
+
73
+ try {
74
+ const ast = DSLParser.parse(source, {
75
+ ignoreSemantics: true,
76
+ types: true,
77
+ methods: true,
78
+ rest: true,
79
+ extends: true,
80
+ generics: true,
81
+ });
82
+
83
+ return KaplangWriter.write(ast.entities ?? []);
84
+ } catch (e) {
85
+ console.warn('Failed to prettify source:\n%s', source);
86
+ return source;
87
+ }
88
+ }
89
+
60
90
  export async function resolveOptions(): Promise<StormOptions> {
61
91
  // Predefined types for now - TODO: Allow user to select / change
62
92
 
@@ -170,8 +200,6 @@ export async function resolveOptions(): Promise<StormOptions> {
170
200
  };
171
201
  }
172
202
 
173
- const LAYOUT_MARGIN = 50;
174
-
175
203
  export class StormEventParser {
176
204
  private events: StormEvent[] = [];
177
205
  private planName: string = '';
@@ -193,8 +221,9 @@ export class StormEventParser {
193
221
  this.connections = [];
194
222
  }
195
223
 
196
- public addEvent(handle:string, evt: StormEvent): StormDefinitions {
224
+ public addEvent(handle: string, evt: StormEvent): StormDefinitions {
197
225
  this.events.push(evt);
226
+ console.log('evt', evt);
198
227
  switch (evt.type) {
199
228
  case 'CREATE_PLAN_PROPERTIES':
200
229
  this.planName = evt.payload.name;
@@ -216,13 +245,13 @@ export class StormEventParser {
216
245
  this.error = evt.payload.error;
217
246
  break;
218
247
  case 'CREATE_API':
219
- this.blocks[evt.payload.blockName].apis.push(evt.payload.content);
248
+ this.blocks[evt.payload.blockName].apis.push(prettifyKaplang(evt.payload.content));
220
249
  break;
221
250
  case 'CREATE_TYPE':
222
- this.blocks[evt.payload.blockName].types.push(evt.payload.content);
251
+ this.blocks[evt.payload.blockName].types.push(prettifyKaplang(evt.payload.content));
223
252
  break;
224
253
  case 'CREATE_MODEL':
225
- this.blocks[evt.payload.blockName].models.push(evt.payload.content);
254
+ this.blocks[evt.payload.blockName].models.push(prettifyKaplang(evt.payload.content));
226
255
  break;
227
256
  case 'CREATE_CONNECTION':
228
257
  this.connections.push(evt.payload);
@@ -242,6 +271,9 @@ export class StormEventParser {
242
271
  }
243
272
 
244
273
  public isValid(): boolean {
274
+ if (!this.planName) {
275
+ return false;
276
+ }
245
277
  return !this.failed;
246
278
  }
247
279
 
@@ -249,59 +281,13 @@ export class StormEventParser {
249
281
  return this.error;
250
282
  }
251
283
 
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
284
  public toResult(handle: string): StormDefinitions {
300
- const planRef = this.toRef(handle, this.planName);
285
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
301
286
  const blockDefinitions = this.toBlockDefinitions(handle);
302
287
  const refIdMap: { [key: string]: string } = {};
303
288
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
304
- const id = uuid.v4();
289
+ // Create a deterministic uuid
290
+ const id = uuid(ref, uuid.URL);
305
291
  refIdMap[ref] = id;
306
292
  return {
307
293
  id,
@@ -368,13 +354,41 @@ export class StormEventParser {
368
354
  return;
369
355
  }
370
356
 
357
+ if (apiProviderBlock.content.spec.entities?.source?.value) {
358
+ if (!clientConsumerBlock.content.spec.entities) {
359
+ clientConsumerBlock.content.spec.entities = {
360
+ types: [],
361
+ source: {
362
+ type: KAPLANG_ID,
363
+ version: KAPLANG_VERSION,
364
+ value: '',
365
+ },
366
+ };
367
+ }
368
+
369
+ const clientTypes = DSLDataTypeParser.parse(
370
+ clientConsumerBlock.content.spec.entities.source!.value
371
+ );
372
+ const apiTypes = DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
373
+
374
+ apiTypes.forEach((apiType) => {
375
+ if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
376
+ // Already exists
377
+ return;
378
+ }
379
+ clientTypes.push(apiType);
380
+ });
381
+
382
+ clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write(clientTypes);
383
+ }
371
384
  clientResource.spec.methods = apiResource.spec.methods;
372
385
  clientResource.spec.source = apiResource.spec.source;
373
386
  });
374
387
 
375
- const connections = this.connections.map((connection) => {
388
+ const connections: Connection[] = this.connections.map((connection) => {
376
389
  const fromRef = this.toRef(handle, connection.fromComponent);
377
390
  const toRef = this.toRef(handle, connection.toComponent);
391
+
378
392
  return {
379
393
  port: {
380
394
  type: this.toPortType(connection.fromResourceType),
@@ -387,9 +401,7 @@ export class StormEventParser {
387
401
  blockId: refIdMap[fromRef.toNormalizedString()],
388
402
  resourceName: connection.fromResource,
389
403
  },
390
- mapping: {
391
- //TODO: Add mapping
392
- },
404
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
393
405
  } satisfies Connection;
394
406
  });
395
407
 
@@ -406,10 +418,10 @@ export class StormEventParser {
406
418
  },
407
419
  };
408
420
 
409
- return this.applyLayoutToBlocks({
421
+ return {
410
422
  plan,
411
423
  blocks: Object.values(blockDefinitions),
412
- });
424
+ };
413
425
  }
414
426
 
415
427
  private toSafeName(name: string): string {
@@ -637,6 +649,55 @@ export class StormEventParser {
637
649
  return '';
638
650
  }
639
651
 
652
+ private toConnectionMapping(
653
+ handle: string,
654
+ connection: StormConnection,
655
+ blockDefinitions: { [key: string]: BlockDefinitionInfo }
656
+ ): any {
657
+ if (connection.fromResourceType !== 'API') {
658
+ return;
659
+ }
660
+
661
+ const fromRef = this.toRef(handle, connection.fromComponent);
662
+
663
+ const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
664
+ if (!apiProviderBlock) {
665
+ console.warn('Provider block not found: %s', connection.fromComponent, connection);
666
+ return;
667
+ }
668
+
669
+ const apiResource = apiProviderBlock.content.spec.providers?.find(
670
+ (p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource
671
+ );
672
+
673
+ if (!apiResource) {
674
+ console.warn(
675
+ 'API resource not found: %s on %s',
676
+ connection.fromResource,
677
+ fromRef.toNormalizedString(),
678
+ connection
679
+ );
680
+ return;
681
+ }
682
+
683
+ const apiMethods = DSLConverters.toSchemaMethods(
684
+ DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
685
+ ignoreSemantics: true,
686
+ }) as (DSLMethod | DSLController)[]
687
+ );
688
+
689
+ const mapping: any = {};
690
+
691
+ Object.entries(apiMethods).forEach(([methodId, method]) => {
692
+ mapping[methodId] = {
693
+ targetId: methodId,
694
+ type: 'EXACT',
695
+ };
696
+ });
697
+
698
+ return mapping;
699
+ }
700
+
640
701
  private toPortType(type: StormResourceType) {
641
702
  switch (type) {
642
703
  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);