@kapeta/local-cluster-service 0.54.11 → 0.55.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 (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/definitions.d.ts +11 -0
  3. package/dist/cjs/src/storm/archetype.d.ts +12 -0
  4. package/dist/cjs/src/storm/archetype.js +98 -0
  5. package/dist/cjs/src/storm/codegen.d.ts +2 -0
  6. package/dist/cjs/src/storm/codegen.js +28 -4
  7. package/dist/cjs/src/storm/event-parser.d.ts +7 -4
  8. package/dist/cjs/src/storm/event-parser.js +190 -160
  9. package/dist/cjs/src/storm/events.d.ts +1 -0
  10. package/dist/cjs/src/storm/predefined.d.ts +27 -0
  11. package/dist/cjs/src/storm/predefined.js +64 -0
  12. package/dist/cjs/src/storm/routes.js +23 -15
  13. package/dist/cjs/test/storm/codegen.test.d.ts +5 -0
  14. package/dist/cjs/test/storm/codegen.test.js +41 -0
  15. package/dist/cjs/test/storm/event-parser.test.d.ts +25 -1
  16. package/dist/cjs/test/storm/event-parser.test.js +34 -15
  17. package/dist/cjs/test/storm/predefined-user-events.json +13 -0
  18. package/dist/esm/src/storm/archetype.d.ts +12 -0
  19. package/dist/esm/src/storm/archetype.js +98 -0
  20. package/dist/esm/src/storm/codegen.d.ts +2 -0
  21. package/dist/esm/src/storm/codegen.js +28 -4
  22. package/dist/esm/src/storm/event-parser.d.ts +7 -4
  23. package/dist/esm/src/storm/event-parser.js +190 -160
  24. package/dist/esm/src/storm/events.d.ts +1 -0
  25. package/dist/esm/src/storm/predefined.d.ts +27 -0
  26. package/dist/esm/src/storm/predefined.js +64 -0
  27. package/dist/esm/src/storm/routes.js +23 -15
  28. package/dist/esm/test/storm/codegen.test.d.ts +5 -0
  29. package/dist/esm/test/storm/codegen.test.js +41 -0
  30. package/dist/esm/test/storm/event-parser.test.d.ts +25 -1
  31. package/dist/esm/test/storm/event-parser.test.js +34 -15
  32. package/dist/esm/test/storm/predefined-user-events.json +13 -0
  33. package/package.json +6 -1
  34. package/src/storm/archetype.ts +85 -0
  35. package/src/storm/codegen.ts +34 -4
  36. package/src/storm/event-parser.ts +200 -159
  37. package/src/storm/events.ts +1 -0
  38. package/src/storm/predefined.ts +52 -0
  39. package/src/storm/routes.ts +25 -18
  40. package/test/storm/codegen.test.ts +46 -0
  41. package/test/storm/event-parser.test.ts +32 -10
  42. package/test/storm/predefined-user-events.json +13 -0
@@ -36,11 +36,15 @@ import {
36
36
  } from '@kapeta/kaplang-core';
37
37
  import { v5 as uuid } from 'uuid';
38
38
  import { definitionsManager } from '../definitionsManager';
39
+ import { PREDEFINED_BLOCKS } from './predefined';
40
+
41
+ import _ from 'lodash';
39
42
 
40
43
  export interface BlockDefinitionInfo {
41
44
  uri: string;
42
45
  content: BlockDefinition;
43
46
  aiName: string;
47
+ archetype?: string;
44
48
  }
45
49
 
46
50
  export interface StormDefinitions {
@@ -268,7 +272,7 @@ export class StormEventParser {
268
272
  /**
269
273
  * Builds plan and block definitions - and enriches events with relevant refs and ids
270
274
  */
271
- public processEvent(handle: string, evt: StormEvent): StormDefinitions {
275
+ public async processEvent(handle: string, evt: StormEvent): Promise<StormDefinitions> {
272
276
  let blockInfo;
273
277
  this.events.push(evt);
274
278
  switch (evt.type) {
@@ -338,7 +342,7 @@ export class StormEventParser {
338
342
  break;
339
343
  }
340
344
 
341
- return this.toResult(handle);
345
+ return await this.toResult(handle);
342
346
  }
343
347
 
344
348
  public getEvents(): StormEvent[] {
@@ -356,9 +360,9 @@ export class StormEventParser {
356
360
  return this.error;
357
361
  }
358
362
 
359
- public toResult(handle: string): StormDefinitions {
363
+ public async toResult(handle: string): Promise<StormDefinitions> {
360
364
  const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
361
- const blockDefinitions = this.toBlockDefinitions(handle);
365
+ const blockDefinitions = await this.toBlockDefinitions(handle);
362
366
  const refIdMap: { [key: string]: string } = {};
363
367
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
364
368
  // Create a deterministic uuid
@@ -504,163 +508,182 @@ export class StormEventParser {
504
508
  };
505
509
  }
506
510
 
507
- public toBlockDefinitions(handle: string): { [key: string]: BlockDefinitionInfo } {
511
+ public async toBlockDefinitions(handle: string): Promise<{ [key: string]: BlockDefinitionInfo }> {
508
512
  const result: { [key: string]: BlockDefinitionInfo } = {};
509
513
 
510
- Object.entries(this.blocks).forEach(([, blockInfo]) => {
514
+ for (const [, blockInfo] of Object.entries(this.blocks)) {
511
515
  const blockRef = StormEventParser.toRef(handle, blockInfo.name);
512
516
 
513
- const blockDefinitionInfo: BlockDefinitionInfo = {
514
- uri: blockRef.toNormalizedString(),
515
- aiName: blockInfo.name,
516
- content: {
517
- kind: this.toBlockKind(blockInfo.type),
518
- metadata: {
519
- title: blockInfo.name,
520
- name: blockRef.fullName,
521
- description: blockInfo.description,
522
- },
523
- spec: {
524
- entities: {
525
- types: [],
526
- source: {
527
- type: KAPLANG_ID,
528
- version: KAPLANG_VERSION,
529
- value: '',
530
- },
517
+ let blockDefinitionInfo: BlockDefinitionInfo;
518
+
519
+ if (blockInfo.archetype) {
520
+ blockDefinitionInfo = await this.resolveArchetypeBlockDefinition(blockRef, blockInfo);
521
+ } else {
522
+ blockDefinitionInfo = this.createBlockDefinitionInfo(blockRef, blockInfo, handle);
523
+ }
524
+
525
+ result[blockRef.toNormalizedString()] = blockDefinitionInfo;
526
+ }
527
+
528
+ return result;
529
+ }
530
+
531
+ private createBlockDefinitionInfo(
532
+ blockRef: KapetaURI,
533
+ blockInfo: StormBlockInfoFilled,
534
+ handle: string
535
+ ): BlockDefinitionInfo {
536
+ const blockDefinitionInfo: BlockDefinitionInfo = {
537
+ uri: blockRef.toNormalizedString(),
538
+ aiName: blockInfo.name,
539
+ content: {
540
+ kind: this.toBlockKind(blockInfo.type),
541
+ metadata: {
542
+ title: blockInfo.name,
543
+ name: blockRef.fullName,
544
+ description: blockInfo.description,
545
+ },
546
+ spec: {
547
+ entities: {
548
+ types: [],
549
+ source: {
550
+ type: KAPLANG_ID,
551
+ version: KAPLANG_VERSION,
552
+ value: '',
531
553
  },
532
- target: this.toBlockTarget(handle, blockInfo.type),
533
- providers: [],
534
- consumers: [],
535
554
  },
555
+ target: this.toBlockTarget(handle, blockInfo.type),
556
+ providers: [],
557
+ consumers: [],
536
558
  },
537
- };
559
+ },
560
+ };
538
561
 
539
- const blockSpec = blockDefinitionInfo.content.spec;
562
+ const blockSpec = blockDefinitionInfo.content.spec;
540
563
 
541
- const apiResources: { [key: string]: Resource | undefined } = {};
542
- let dbResource: Resource | undefined = undefined;
564
+ const apiResources: { [key: string]: Resource | undefined } = {};
565
+ let dbResource: Resource | undefined = undefined;
543
566
 
544
- (blockInfo.resources || []).forEach((resource) => {
545
- const port = {
546
- type: this.toPortType(resource.type),
547
- };
548
- switch (resource.type) {
549
- case 'API': {
550
- const apiResource = {
551
- kind: this.toResourceKind(resource.type),
552
- metadata: {
553
- name: resource.name,
554
- description: resource.description,
555
- },
556
- spec: {
557
- port,
558
- methods: {},
559
- source: {
560
- type: KAPLANG_ID,
561
- version: KAPLANG_VERSION,
562
- value: '',
563
- } satisfies SourceCode,
564
- },
565
- };
566
- apiResources[resource.name] = apiResource;
567
- blockSpec.providers!.push(apiResource);
567
+ (blockInfo.resources || []).forEach((resource) => {
568
+ const port = {
569
+ type: this.toPortType(resource.type),
570
+ };
571
+ switch (resource.type) {
572
+ case 'API': {
573
+ const apiResource = {
574
+ kind: this.toResourceKind(resource.type),
575
+ metadata: {
576
+ name: resource.name,
577
+ description: resource.description,
578
+ },
579
+ spec: {
580
+ port,
581
+ methods: {},
582
+ source: {
583
+ type: KAPLANG_ID,
584
+ version: KAPLANG_VERSION,
585
+ value: '',
586
+ } satisfies SourceCode,
587
+ },
588
+ };
589
+ apiResources[resource.name] = apiResource;
590
+ blockSpec.providers!.push(apiResource);
591
+ break;
592
+ }
593
+ case 'CLIENT':
594
+ blockSpec.consumers!.push({
595
+ kind: this.toResourceKind(resource.type),
596
+ metadata: {
597
+ name: resource.name,
598
+ description: resource.description,
599
+ },
600
+ spec: {
601
+ port,
602
+ methods: {},
603
+ source: {
604
+ type: KAPLANG_ID,
605
+ version: KAPLANG_VERSION,
606
+ value: '',
607
+ } satisfies SourceCode,
608
+ },
609
+ });
610
+ break;
611
+ case 'EXTERNAL_API':
612
+ break;
613
+ case 'EXCHANGE':
614
+ break;
615
+ case 'PUBLISHER':
616
+ break;
617
+ case 'QUEUE':
618
+ break;
619
+ case 'SUBSCRIBER':
620
+ break;
621
+ case 'JWTPROVIDER':
622
+ case 'WEBPAGE':
623
+ blockSpec.providers!.push({
624
+ kind: this.toResourceKind(resource.type),
625
+ metadata: {
626
+ name: resource.name,
627
+ description: resource.description,
628
+ },
629
+ spec: {
630
+ port,
631
+ },
632
+ });
633
+ break;
634
+ case 'DATABASE':
635
+ if (dbResource) {
568
636
  break;
569
637
  }
570
- case 'CLIENT':
571
- blockSpec.consumers!.push({
572
- kind: this.toResourceKind(resource.type),
573
- metadata: {
574
- name: resource.name,
575
- description: resource.description,
576
- },
577
- spec: {
578
- port,
579
- methods: {},
580
- source: {
581
- type: KAPLANG_ID,
582
- version: KAPLANG_VERSION,
583
- value: '',
584
- } satisfies SourceCode,
585
- },
586
- });
587
- break;
588
- case 'EXTERNAL_API':
589
- break;
590
- case 'EXCHANGE':
591
- break;
592
- case 'PUBLISHER':
593
- break;
594
- case 'QUEUE':
595
- break;
596
- case 'SUBSCRIBER':
597
- break;
598
- case 'JWTPROVIDER':
599
- case 'WEBPAGE':
600
- blockSpec.providers!.push({
601
- kind: this.toResourceKind(resource.type),
602
- metadata: {
603
- name: resource.name,
604
- description: resource.description,
605
- },
606
- spec: {
607
- port,
608
- },
609
- });
610
- break;
611
- case 'DATABASE':
612
- if (dbResource) {
613
- break;
614
- }
615
- dbResource = {
616
- kind: this.toResourceKind(resource.type),
617
- metadata: {
618
- name: resource.name,
619
- description: resource.description,
620
- },
621
- spec: {
622
- port,
623
- models: [],
624
- source: {
625
- type: KAPLANG_ID,
626
- version: KAPLANG_VERSION,
627
- value: '',
628
- } satisfies SourceCode,
629
- },
630
- };
631
- blockSpec.consumers!.push(dbResource);
632
- break;
633
- case 'JWTCONSUMER':
634
- case 'WEBFRAGMENT':
635
- case 'SMTPCLIENT':
636
- blockSpec.consumers!.push({
637
- kind: this.toResourceKind(resource.type),
638
- metadata: {
639
- name: resource.name,
640
- description: resource.description,
641
- },
642
- spec: {
643
- port,
644
- },
645
- });
646
- }
647
- });
638
+ dbResource = {
639
+ kind: this.toResourceKind(resource.type),
640
+ metadata: {
641
+ name: resource.name,
642
+ description: resource.description,
643
+ },
644
+ spec: {
645
+ port,
646
+ models: [],
647
+ source: {
648
+ type: KAPLANG_ID,
649
+ version: KAPLANG_VERSION,
650
+ value: '',
651
+ } satisfies SourceCode,
652
+ },
653
+ };
654
+ blockSpec.consumers!.push(dbResource);
655
+ break;
656
+ case 'JWTCONSUMER':
657
+ case 'WEBFRAGMENT':
658
+ case 'SMTPCLIENT':
659
+ blockSpec.consumers!.push({
660
+ kind: this.toResourceKind(resource.type),
661
+ metadata: {
662
+ name: resource.name,
663
+ description: resource.description,
664
+ },
665
+ spec: {
666
+ port,
667
+ },
668
+ });
669
+ }
670
+ });
648
671
 
649
- blockInfo.apis.forEach((api) => {
650
- const dslApi = DSLAPIParser.parse(api, {
651
- ignoreSemantics: true,
652
- }) as (DSLMethod | DSLController)[];
653
-
654
- let exactMatch = false;
655
- if (dslApi[0] && dslApi[0].type == DSLEntityType.CONTROLLER) {
656
- const name = dslApi[0].name.toLowerCase();
657
- const apiResourceName = Object.keys(apiResources).find((key) => key.indexOf(name) > -1);
658
- if (apiResourceName) {
659
- const exactResource = apiResources[apiResourceName];
660
- exactResource!.spec.source.value += api + '\n\n';
661
- exactMatch = true;
662
- }
672
+ blockInfo.apis.forEach((api) => {
673
+ const dslApi = DSLAPIParser.parse(api, {
674
+ ignoreSemantics: true,
675
+ }) as (DSLMethod | DSLController)[];
676
+
677
+ let exactMatch = false;
678
+ if (dslApi[0] && dslApi[0].type == DSLEntityType.CONTROLLER) {
679
+ const name = dslApi[0].name.toLowerCase();
680
+ const apiResourceName = Object.keys(apiResources).find((key) => key.indexOf(name) > -1);
681
+ if (apiResourceName) {
682
+ const exactResource = apiResources[apiResourceName];
683
+ exactResource!.spec.source.value += api + '\n\n';
684
+ exactMatch = true;
663
685
  }
686
+ }
664
687
 
665
688
  if (!exactMatch) {
666
689
  // if we couldn't place the given api on the exact resource we just park it on the first
@@ -675,20 +698,17 @@ export class StormEventParser {
675
698
  }
676
699
  });
677
700
 
678
- blockInfo.types.forEach((type) => {
679
- blockSpec.entities!.source!.value += type + '\n';
680
- });
681
-
682
- if (dbResource) {
683
- blockInfo.models.forEach((model) => {
684
- dbResource!.spec.source.value += model + '\n';
685
- });
686
- }
687
-
688
- result[blockRef.toNormalizedString()] = blockDefinitionInfo;
701
+ blockInfo.types.forEach((type) => {
702
+ blockSpec.entities!.source!.value += type + '\n';
689
703
  });
690
704
 
691
- return result;
705
+ if (dbResource) {
706
+ blockInfo.models.forEach((model) => {
707
+ dbResource!.spec.source.value += model + '\n';
708
+ });
709
+ }
710
+
711
+ return blockDefinitionInfo;
692
712
  }
693
713
 
694
714
  private toResourceKind(type: StormResourceType) {
@@ -861,4 +881,25 @@ export class StormEventParser {
861
881
  }
862
882
  return undefined;
863
883
  }
884
+
885
+ private async resolveArchetypeBlockDefinition(
886
+ blockRef: KapetaURI,
887
+ blockInfo: StormBlockInfoFilled
888
+ ): Promise<BlockDefinitionInfo> {
889
+ const predefinedBlock = PREDEFINED_BLOCKS.get(blockInfo.archetype!);
890
+ if (!predefinedBlock) {
891
+ throw new Error('Predefined block not found for archetype [' + blockInfo.archetype + ']');
892
+ }
893
+
894
+ const blockDefinition = await predefinedBlock.getBlockDefinition();
895
+ _.set(blockDefinition!, ['metadata', 'name'], blockRef.fullName);
896
+ _.set(blockDefinition!, ['metadata', 'title'], blockRef.name);
897
+
898
+ return {
899
+ uri: blockRef.toNormalizedString(),
900
+ aiName: blockInfo.name,
901
+ content: blockDefinition,
902
+ archetype: blockInfo.archetype,
903
+ } as BlockDefinitionInfo;
904
+ }
864
905
  }
@@ -32,6 +32,7 @@ export interface StormBlockInfo {
32
32
  }[];
33
33
  blockRef?: string;
34
34
  instanceId?: string;
35
+ archetype?: string;
35
36
  }
36
37
 
37
38
  export interface StormBlockInfoFilled extends StormBlockInfo {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ import { BlockDefinition } from '@kapeta/schemas';
7
+ import * as yaml from 'js-yaml';
8
+
9
+ export class PredefinedBlock {
10
+ constructor(
11
+ public archetype: string,
12
+ public kapetaYml: string,
13
+ public gitRepo: {
14
+ owner: string;
15
+ repo: string;
16
+ path: string;
17
+ }
18
+ ) {}
19
+
20
+ public getGitRepo(): { owner: string; repo: string; path: string } {
21
+ return this.gitRepo;
22
+ }
23
+
24
+ private async getKapetaYML(): Promise<string> {
25
+ const response = await fetch(this.kapetaYml);
26
+ if (!response.ok) {
27
+ throw new Error(`HTTP error! Status: ${response.status}`);
28
+ }
29
+ return await response.text();
30
+ }
31
+
32
+ public async getBlockDefinition(): Promise<BlockDefinition> {
33
+ const kapetaYml = await this.getKapetaYML();
34
+ return yaml.load(kapetaYml) as BlockDefinition;
35
+ }
36
+ }
37
+
38
+ const predefinedBlocks: PredefinedBlock[] = [
39
+ new PredefinedBlock(
40
+ 'USER_SERVICE',
41
+ 'https://raw.githubusercontent.com/kapetacom/everything/master/blocks/everything-user/kapeta.yml',
42
+ {
43
+ owner: 'kapetacom',
44
+ repo: 'everything',
45
+ path: 'blocks/everything-user',
46
+ }
47
+ ),
48
+ ];
49
+
50
+ export const PREDEFINED_BLOCKS = new Map<string, PredefinedBlock>(
51
+ predefinedBlocks.map((block) => [block.archetype, block])
52
+ );
@@ -22,6 +22,7 @@ import {
22
22
  import { StormCodegen } from './codegen';
23
23
  import { assetManager } from '../assetManager';
24
24
  import Path from 'path';
25
+ import _ from 'lodash';
25
26
 
26
27
  const router = Router();
27
28
 
@@ -29,7 +30,7 @@ router.use('/', corsHandler);
29
30
  router.use('/', stringBody);
30
31
 
31
32
  router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
32
- const handle = req.params.handle;
33
+ const handle = req.params.handle as string;
33
34
 
34
35
  try {
35
36
  const stormOptions = await resolveOptions();
@@ -51,23 +52,29 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
51
52
 
52
53
  let currentPhase = StormEventPhaseType.META;
53
54
 
54
- metaStream.on('data', (data: StormEvent) => {
55
- const result = eventParser.processEvent(req.params.handle, data);
56
-
57
- switch (data.type) {
58
- case 'CREATE_API':
59
- case 'CREATE_MODEL':
60
- case 'CREATE_TYPE':
61
- if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
62
- sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
63
- currentPhase = StormEventPhaseType.DEFINITIONS;
64
- sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
65
- }
66
- break;
67
- }
55
+ // Helper to avoid sending the plan multiple times in a row
56
+ const sendUpdatedPlan = _.debounce(sendDefinitions, 50, { maxWait: 200 });
57
+ metaStream.on('data', async (data: StormEvent) => {
58
+ try {
59
+ const result = await eventParser.processEvent(handle, data);
60
+
61
+ switch (data.type) {
62
+ case 'CREATE_API':
63
+ case 'CREATE_MODEL':
64
+ case 'CREATE_TYPE':
65
+ if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
66
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
67
+ currentPhase = StormEventPhaseType.DEFINITIONS;
68
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
69
+ }
70
+ break;
71
+ }
68
72
 
69
- sendEvent(res, data);
70
- sendDefinitions(res, result);
73
+ sendEvent(res, data);
74
+ sendUpdatedPlan(res, result);
75
+ } catch (e) {
76
+ console.error('Failed to process event', e);
77
+ }
71
78
  });
72
79
 
73
80
  try {
@@ -96,7 +103,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
96
103
  return;
97
104
  }
98
105
 
99
- const result = eventParser.toResult(handle);
106
+ const result = await eventParser.toResult(handle);
100
107
 
101
108
  if (metaStream.isAborted()) {
102
109
  return;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ import predefinedUserEvents from './predefined-user-events.json';
7
+ import { StormEventParser } from '../../src/storm/event-parser';
8
+ import { parserOptions } from './event-parser.test';
9
+ import { StormEvent } from '../../src/storm/events';
10
+ import { StormCodegen } from '../../src/storm/codegen';
11
+ import uuid from 'node-uuid';
12
+ import { StormStream } from '../../src/storm/stream';
13
+
14
+ describe('codegen', () => {
15
+ it('predefined components', async () => {
16
+ const events = predefinedUserEvents as StormEvent[];
17
+ const parser = new StormEventParser(parserOptions);
18
+ for (const event of events) {
19
+ await parser.processEvent('kapeta', event);
20
+ }
21
+
22
+ const result = await parser.toResult('kapeta');
23
+
24
+ const conversationId = uuid.v4().toString();
25
+
26
+ let codegen = new StormCodegen(conversationId, '', result.blocks, parser.getEvents());
27
+ let codegenPromise = consumeStream(codegen.getStream());
28
+ await codegen.process();
29
+
30
+ const stormEvents = await codegenPromise;
31
+ expect(stormEvents[0].type).toBe('FILE_DONE');
32
+ expect(stormEvents[stormEvents.length - 1].type).toBe('BLOCK_READY');
33
+ });
34
+ });
35
+
36
+ async function consumeStream(stream: StormStream): Promise<StormEvent[]> {
37
+ const events: StormEvent[] = [];
38
+ return new Promise<StormEvent[]>((resolve) => {
39
+ stream.on('data', (data) => {
40
+ events.push(data);
41
+ });
42
+
43
+ stream.waitForDone();
44
+ resolve(events);
45
+ });
46
+ }