@kapeta/local-cluster-service 0.60.1 → 0.60.3

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.60.3](https://github.com/kapetacom/local-cluster-service/compare/v0.60.2...v0.60.3) (2024-08-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * rename conflicting entities when creating connections ([a5125cf](https://github.com/kapetacom/local-cluster-service/commit/a5125cf652b808cd94cf3ebe0df853f0c7256566))
7
+
8
+ ## [0.60.2](https://github.com/kapetacom/local-cluster-service/compare/v0.60.1...v0.60.2) (2024-08-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * actually use the new toSafeArtifactName() function ([41f5033](https://github.com/kapetacom/local-cluster-service/commit/41f5033d0a2b2cd21ff6954d2ef3468644ee56c7))
14
+
1
15
  ## [0.60.1](https://github.com/kapetacom/local-cluster-service/compare/v0.60.0...v0.60.1) (2024-08-06)
2
16
 
3
17
 
@@ -313,6 +313,7 @@ class StormEventParser {
313
313
  }
314
314
  return;
315
315
  }
316
+ const renamedEntities = {};
316
317
  if (apiProviderBlock.content.spec.entities?.source?.value) {
317
318
  if (!clientConsumerBlock.content.spec.entities) {
318
319
  clientConsumerBlock.content.spec.entities = {
@@ -328,17 +329,79 @@ class StormEventParser {
328
329
  const apiTypes = kaplang_core_1.DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value, {
329
330
  ignoreSemantics: true,
330
331
  });
332
+ const newTypes = [];
333
+ const clientTypeExists = function (apiType) {
334
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
335
+ return clientType != undefined;
336
+ };
337
+ const clientTypeIsCompatible = function (apiType) {
338
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
339
+ return (clientType != undefined &&
340
+ kaplang_core_1.DSLCompatibilityHelper.isDataCompatible(apiType, clientType, apiTypes, clientTypes));
341
+ };
331
342
  apiTypes.forEach((apiType) => {
332
- if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
333
- // Already exists
343
+ if (!clientTypeExists(apiType)) {
344
+ newTypes.push(apiType);
345
+ return;
346
+ }
347
+ if (clientTypeIsCompatible(apiType)) {
334
348
  return;
335
349
  }
336
- clientTypes.push(apiType);
350
+ const originalName = apiType.name;
351
+ const toEntity = lodash_1.default.cloneDeep(apiType);
352
+ let conflictCount = 1;
353
+ while (clientTypeExists(toEntity) && !clientTypeIsCompatible(toEntity)) {
354
+ toEntity.name = `${originalName}_${conflictCount}`;
355
+ conflictCount++;
356
+ }
357
+ newTypes.push(toEntity);
358
+ renamedEntities[originalName] = toEntity.name;
359
+ });
360
+ Object.entries(renamedEntities).forEach(([from, to]) => {
361
+ newTypes.forEach((newType) => {
362
+ if (newType.type !== kaplang_core_1.DSLEntityType.DATATYPE) {
363
+ return;
364
+ }
365
+ if (!newType.properties) {
366
+ return;
367
+ }
368
+ newType.properties.forEach((property) => {
369
+ const type = kaplang_core_1.DSLTypeHelper.asType(property.type);
370
+ if (from !== type.name) {
371
+ return;
372
+ }
373
+ type.name = to;
374
+ property.type = type;
375
+ });
376
+ });
337
377
  });
338
- clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write(clientTypes);
378
+ clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write([
379
+ ...clientTypes,
380
+ ...newTypes,
381
+ ]);
339
382
  }
340
383
  clientResource.spec.methods = apiResource.spec.methods;
341
- clientResource.spec.source = apiResource.spec.source;
384
+ if (Object.keys(renamedEntities).length == 0) {
385
+ clientResource.spec.source = apiResource.spec.source;
386
+ }
387
+ else {
388
+ // entities were renamed - rename references as well
389
+ const targetSource = lodash_1.default.cloneDeep(apiResource.spec.source);
390
+ const methods = kaplang_core_1.DSLAPIParser.parse(targetSource.value, {
391
+ ignoreSemantics: true,
392
+ });
393
+ const resolver = new kaplang_core_1.DSLReferenceResolver();
394
+ resolver.visitReferences(methods, (name) => {
395
+ const type = kaplang_core_1.DSLTypeHelper.asType(name);
396
+ if (renamedEntities[type.name]) {
397
+ type.name = renamedEntities[type.name];
398
+ return type;
399
+ }
400
+ return name;
401
+ });
402
+ targetSource.value = kaplang_core_1.KaplangWriter.write(methods);
403
+ clientResource.spec.source = targetSource;
404
+ }
342
405
  });
343
406
  const connections = this.connections.map((connection) => {
344
407
  const fromRef = StormEventParser.toRef(handle, connection.fromComponent);
@@ -677,7 +740,7 @@ class StormEventParser {
677
740
  let options = {};
678
741
  if (kind.includes('java')) {
679
742
  const groupId = `ai.${StormEventParser.toSafeName(handle)}`;
680
- const artifactId = StormEventParser.toSafeName(this.planName);
743
+ const artifactId = StormEventParser.toSafeArtifactName(this.planName);
681
744
  options = {
682
745
  groupId,
683
746
  artifactId,
@@ -0,0 +1,143 @@
1
+ [
2
+ {
3
+ "type": "CREATE_BLOCK",
4
+ "reason": "Block updated",
5
+ "payload": {
6
+ "archetype": "",
7
+ "description": "Handles traffic from the backend services.",
8
+ "name": "api-gateway",
9
+ "resources": [
10
+ {
11
+ "description": "",
12
+ "name": "posts",
13
+ "to": "post-service",
14
+ "type": "CLIENT"
15
+ },
16
+ {
17
+ "description": "",
18
+ "name": "comments",
19
+ "to": "comment-service",
20
+ "type": "CLIENT"
21
+ }
22
+ ],
23
+ "type": "GATEWAY",
24
+ "blockRef": "kapeta://kapeta/api-gateway:local",
25
+ "instanceId": "6b247f30-dec4-5960-a8a0-f300caa95226"
26
+ },
27
+ "created": 1720784242064
28
+ },
29
+ {
30
+ "type": "CREATE_BLOCK",
31
+ "reason": "Block updated",
32
+ "payload": {
33
+ "archetype": "",
34
+ "description": "Manages the creation, editing, and deletion of blog posts.",
35
+ "name": "post-service",
36
+ "resources": [
37
+ {
38
+ "description": "The posts API provides endpoints for managing blog posts. It includes functionality for creating, editing, and deleting posts, as well as retrieving and searching for posts. The API is designed to be secure and follows best practices for authentication and authorization.",
39
+ "name": "posts",
40
+ "type": "API"
41
+ }
42
+ ],
43
+ "type": "BACKEND",
44
+ "blockRef": "kapeta://kapeta/post-service:local",
45
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
46
+ },
47
+ "created": 1720784243137
48
+ },
49
+ {
50
+ "type": "CREATE_BLOCK",
51
+ "reason": "Block updated",
52
+ "payload": {
53
+ "archetype": "",
54
+ "description": "Manages the creation, editing, and deletion of comments on blog posts.",
55
+ "name": "comment-service",
56
+ "resources": [
57
+ {
58
+ "description": "The comments API provides endpoints for managing comments on blog posts. It includes functionality for creating, editing, and deleting comments, as well as retrieving and searching for comments. The API is designed to be secure and follows best practices for authentication and authorization.",
59
+ "name": "comments",
60
+ "type": "API"
61
+ }
62
+ ],
63
+ "type": "BACKEND",
64
+ "blockRef": "kapeta://kapeta/comment-service:local",
65
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
66
+ },
67
+ "created": 1720784243141
68
+ },
69
+ {
70
+ "type": "CREATE_TYPE",
71
+ "reason": "Create type for post-service",
72
+ "payload": {
73
+ "blockName": "post-service",
74
+ "content": "enum Status {\n NEW,\n ARCHIVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
75
+ "blockRef": "kapeta://kapeta/post-service:local",
76
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
77
+ },
78
+ "created": 1720784233286
79
+ },
80
+ {
81
+ "type": "CREATE_API",
82
+ "reason": "Create API for post-service",
83
+ "payload": {
84
+ "blockName": "post-service",
85
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
86
+ "blockRef": "kapeta://kapeta/post-service:local",
87
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
88
+ },
89
+ "created": 1720784233290
90
+ },
91
+ {
92
+ "type": "CREATE_TYPE",
93
+ "reason": "Create type for comment-service",
94
+ "payload": {
95
+ "blockName": "comment-service",
96
+ "content": "enum Status {\n NEW,\n APPROVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
97
+ "blockRef": "kapeta://kapeta/comment-service:local",
98
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
99
+ },
100
+ "created": 1720784241000
101
+ },
102
+ {
103
+ "type": "CREATE_API",
104
+ "reason": "Create API for comment-service",
105
+ "payload": {
106
+ "blockName": "comment-service",
107
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
108
+ "blockRef": "kapeta://kapeta/comment-service:local",
109
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
110
+ },
111
+ "created": 1720784241002
112
+ },
113
+ {
114
+ "type": "CREATE_CONNECTION",
115
+ "reason": "api-gateway needs to be able to serve the posts API from the post-service",
116
+ "payload": {
117
+ "fromComponent": "post-service",
118
+ "fromResource": "posts",
119
+ "fromResourceType": "API",
120
+ "toComponent": "api-gateway",
121
+ "toResource": "posts",
122
+ "toResourceType": "CLIENT",
123
+ "fromBlockId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26",
124
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
125
+ },
126
+ "created": 1720784243173
127
+ },
128
+ {
129
+ "type": "CREATE_CONNECTION",
130
+ "reason": "api-gateway needs to be able to serve the comments API from the comment-service",
131
+ "payload": {
132
+ "fromComponent": "comment-service",
133
+ "fromResource": "comments",
134
+ "fromResourceType": "API",
135
+ "toComponent": "api-gateway",
136
+ "toResource": "comments",
137
+ "toResourceType": "CLIENT",
138
+ "fromBlockId": "d4215220-f552-5a6d-9429-bef1c40c4d7c",
139
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
140
+ },
141
+ "created": 1720784243174
142
+ }
143
+ ]
@@ -12,6 +12,8 @@ const event_parser_1 = require("../../src/storm/event-parser");
12
12
  const simple_blog_events_json_1 = __importDefault(require("./simple-blog-events.json"));
13
13
  const predefined_user_events_json_1 = __importDefault(require("./predefined-user-events.json"));
14
14
  const blog_events_json_1 = __importDefault(require("./blog-events.json"));
15
+ const duplicate_entities_events_json_1 = __importDefault(require("./duplicate-entities-events.json"));
16
+ const kaplang_core_1 = require("@kapeta/kaplang-core");
15
17
  exports.parserOptions = {
16
18
  serviceKind: 'kapeta/block-service:local',
17
19
  serviceLanguage: 'kapeta/language-target-java-spring-boot:local',
@@ -217,4 +219,33 @@ describe('event-parser', () => {
217
219
  const safeName = event_parser_1.StormEventParser.toSafeArtifactName('Browser-based CRM Application');
218
220
  expect(safeName).toBe('browserbasedcrmapplication');
219
221
  });
222
+ it('rename duplicate entity names', async () => {
223
+ const events = duplicate_entities_events_json_1.default;
224
+ const parser = new event_parser_1.StormEventParser(exports.parserOptions);
225
+ for (const event of events) {
226
+ await parser.processEvent('kapeta', event);
227
+ }
228
+ const result = await parser.toResult('kapeta');
229
+ const apiGateway = result.blocks.find((block) => block.aiName === 'api-gateway');
230
+ expect(apiGateway).toBeDefined();
231
+ const dataTypes = kaplang_core_1.DSLDataTypeParser.parse(apiGateway.content.spec.entities.source.value, {
232
+ ignoreSemantics: true,
233
+ });
234
+ expect(dataTypes.map((type) => type.name)).toStrictEqual(['Status', 'Result', 'User', 'Status_1', 'Result_1']);
235
+ const conflictingType = dataTypes.find((type) => type.name === 'Result_1');
236
+ expect(conflictingType).toBeDefined();
237
+ const dataType = conflictingType;
238
+ expect(dataType.properties?.length).toBe(1);
239
+ const dslDataTypeProperty = dataType.properties[0];
240
+ expect(dslDataTypeProperty.type).toBe('Status_1');
241
+ const commentsClient = apiGateway?.content.spec.consumers?.find((resource) => resource.metadata.name === 'comments');
242
+ expect(commentsClient).toBeDefined();
243
+ const methods = kaplang_core_1.DSLAPIParser.parse(commentsClient.spec.source.value, {
244
+ ignoreSemantics: true,
245
+ });
246
+ expect(methods).toBeDefined();
247
+ expect(methods.length).toBe(1);
248
+ const method = methods[0];
249
+ expect(method.returnType).toBe('Result_1');
250
+ });
220
251
  });
@@ -313,6 +313,7 @@ class StormEventParser {
313
313
  }
314
314
  return;
315
315
  }
316
+ const renamedEntities = {};
316
317
  if (apiProviderBlock.content.spec.entities?.source?.value) {
317
318
  if (!clientConsumerBlock.content.spec.entities) {
318
319
  clientConsumerBlock.content.spec.entities = {
@@ -328,17 +329,79 @@ class StormEventParser {
328
329
  const apiTypes = kaplang_core_1.DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value, {
329
330
  ignoreSemantics: true,
330
331
  });
332
+ const newTypes = [];
333
+ const clientTypeExists = function (apiType) {
334
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
335
+ return clientType != undefined;
336
+ };
337
+ const clientTypeIsCompatible = function (apiType) {
338
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
339
+ return (clientType != undefined &&
340
+ kaplang_core_1.DSLCompatibilityHelper.isDataCompatible(apiType, clientType, apiTypes, clientTypes));
341
+ };
331
342
  apiTypes.forEach((apiType) => {
332
- if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
333
- // Already exists
343
+ if (!clientTypeExists(apiType)) {
344
+ newTypes.push(apiType);
345
+ return;
346
+ }
347
+ if (clientTypeIsCompatible(apiType)) {
334
348
  return;
335
349
  }
336
- clientTypes.push(apiType);
350
+ const originalName = apiType.name;
351
+ const toEntity = lodash_1.default.cloneDeep(apiType);
352
+ let conflictCount = 1;
353
+ while (clientTypeExists(toEntity) && !clientTypeIsCompatible(toEntity)) {
354
+ toEntity.name = `${originalName}_${conflictCount}`;
355
+ conflictCount++;
356
+ }
357
+ newTypes.push(toEntity);
358
+ renamedEntities[originalName] = toEntity.name;
359
+ });
360
+ Object.entries(renamedEntities).forEach(([from, to]) => {
361
+ newTypes.forEach((newType) => {
362
+ if (newType.type !== kaplang_core_1.DSLEntityType.DATATYPE) {
363
+ return;
364
+ }
365
+ if (!newType.properties) {
366
+ return;
367
+ }
368
+ newType.properties.forEach((property) => {
369
+ const type = kaplang_core_1.DSLTypeHelper.asType(property.type);
370
+ if (from !== type.name) {
371
+ return;
372
+ }
373
+ type.name = to;
374
+ property.type = type;
375
+ });
376
+ });
337
377
  });
338
- clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write(clientTypes);
378
+ clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write([
379
+ ...clientTypes,
380
+ ...newTypes,
381
+ ]);
339
382
  }
340
383
  clientResource.spec.methods = apiResource.spec.methods;
341
- clientResource.spec.source = apiResource.spec.source;
384
+ if (Object.keys(renamedEntities).length == 0) {
385
+ clientResource.spec.source = apiResource.spec.source;
386
+ }
387
+ else {
388
+ // entities were renamed - rename references as well
389
+ const targetSource = lodash_1.default.cloneDeep(apiResource.spec.source);
390
+ const methods = kaplang_core_1.DSLAPIParser.parse(targetSource.value, {
391
+ ignoreSemantics: true,
392
+ });
393
+ const resolver = new kaplang_core_1.DSLReferenceResolver();
394
+ resolver.visitReferences(methods, (name) => {
395
+ const type = kaplang_core_1.DSLTypeHelper.asType(name);
396
+ if (renamedEntities[type.name]) {
397
+ type.name = renamedEntities[type.name];
398
+ return type;
399
+ }
400
+ return name;
401
+ });
402
+ targetSource.value = kaplang_core_1.KaplangWriter.write(methods);
403
+ clientResource.spec.source = targetSource;
404
+ }
342
405
  });
343
406
  const connections = this.connections.map((connection) => {
344
407
  const fromRef = StormEventParser.toRef(handle, connection.fromComponent);
@@ -677,7 +740,7 @@ class StormEventParser {
677
740
  let options = {};
678
741
  if (kind.includes('java')) {
679
742
  const groupId = `ai.${StormEventParser.toSafeName(handle)}`;
680
- const artifactId = StormEventParser.toSafeName(this.planName);
743
+ const artifactId = StormEventParser.toSafeArtifactName(this.planName);
681
744
  options = {
682
745
  groupId,
683
746
  artifactId,
@@ -0,0 +1,143 @@
1
+ [
2
+ {
3
+ "type": "CREATE_BLOCK",
4
+ "reason": "Block updated",
5
+ "payload": {
6
+ "archetype": "",
7
+ "description": "Handles traffic from the backend services.",
8
+ "name": "api-gateway",
9
+ "resources": [
10
+ {
11
+ "description": "",
12
+ "name": "posts",
13
+ "to": "post-service",
14
+ "type": "CLIENT"
15
+ },
16
+ {
17
+ "description": "",
18
+ "name": "comments",
19
+ "to": "comment-service",
20
+ "type": "CLIENT"
21
+ }
22
+ ],
23
+ "type": "GATEWAY",
24
+ "blockRef": "kapeta://kapeta/api-gateway:local",
25
+ "instanceId": "6b247f30-dec4-5960-a8a0-f300caa95226"
26
+ },
27
+ "created": 1720784242064
28
+ },
29
+ {
30
+ "type": "CREATE_BLOCK",
31
+ "reason": "Block updated",
32
+ "payload": {
33
+ "archetype": "",
34
+ "description": "Manages the creation, editing, and deletion of blog posts.",
35
+ "name": "post-service",
36
+ "resources": [
37
+ {
38
+ "description": "The posts API provides endpoints for managing blog posts. It includes functionality for creating, editing, and deleting posts, as well as retrieving and searching for posts. The API is designed to be secure and follows best practices for authentication and authorization.",
39
+ "name": "posts",
40
+ "type": "API"
41
+ }
42
+ ],
43
+ "type": "BACKEND",
44
+ "blockRef": "kapeta://kapeta/post-service:local",
45
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
46
+ },
47
+ "created": 1720784243137
48
+ },
49
+ {
50
+ "type": "CREATE_BLOCK",
51
+ "reason": "Block updated",
52
+ "payload": {
53
+ "archetype": "",
54
+ "description": "Manages the creation, editing, and deletion of comments on blog posts.",
55
+ "name": "comment-service",
56
+ "resources": [
57
+ {
58
+ "description": "The comments API provides endpoints for managing comments on blog posts. It includes functionality for creating, editing, and deleting comments, as well as retrieving and searching for comments. The API is designed to be secure and follows best practices for authentication and authorization.",
59
+ "name": "comments",
60
+ "type": "API"
61
+ }
62
+ ],
63
+ "type": "BACKEND",
64
+ "blockRef": "kapeta://kapeta/comment-service:local",
65
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
66
+ },
67
+ "created": 1720784243141
68
+ },
69
+ {
70
+ "type": "CREATE_TYPE",
71
+ "reason": "Create type for post-service",
72
+ "payload": {
73
+ "blockName": "post-service",
74
+ "content": "enum Status {\n NEW,\n ARCHIVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
75
+ "blockRef": "kapeta://kapeta/post-service:local",
76
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
77
+ },
78
+ "created": 1720784233286
79
+ },
80
+ {
81
+ "type": "CREATE_API",
82
+ "reason": "Create API for post-service",
83
+ "payload": {
84
+ "blockName": "post-service",
85
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
86
+ "blockRef": "kapeta://kapeta/post-service:local",
87
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
88
+ },
89
+ "created": 1720784233290
90
+ },
91
+ {
92
+ "type": "CREATE_TYPE",
93
+ "reason": "Create type for comment-service",
94
+ "payload": {
95
+ "blockName": "comment-service",
96
+ "content": "enum Status {\n NEW,\n APPROVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
97
+ "blockRef": "kapeta://kapeta/comment-service:local",
98
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
99
+ },
100
+ "created": 1720784241000
101
+ },
102
+ {
103
+ "type": "CREATE_API",
104
+ "reason": "Create API for comment-service",
105
+ "payload": {
106
+ "blockName": "comment-service",
107
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
108
+ "blockRef": "kapeta://kapeta/comment-service:local",
109
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
110
+ },
111
+ "created": 1720784241002
112
+ },
113
+ {
114
+ "type": "CREATE_CONNECTION",
115
+ "reason": "api-gateway needs to be able to serve the posts API from the post-service",
116
+ "payload": {
117
+ "fromComponent": "post-service",
118
+ "fromResource": "posts",
119
+ "fromResourceType": "API",
120
+ "toComponent": "api-gateway",
121
+ "toResource": "posts",
122
+ "toResourceType": "CLIENT",
123
+ "fromBlockId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26",
124
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
125
+ },
126
+ "created": 1720784243173
127
+ },
128
+ {
129
+ "type": "CREATE_CONNECTION",
130
+ "reason": "api-gateway needs to be able to serve the comments API from the comment-service",
131
+ "payload": {
132
+ "fromComponent": "comment-service",
133
+ "fromResource": "comments",
134
+ "fromResourceType": "API",
135
+ "toComponent": "api-gateway",
136
+ "toResource": "comments",
137
+ "toResourceType": "CLIENT",
138
+ "fromBlockId": "d4215220-f552-5a6d-9429-bef1c40c4d7c",
139
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
140
+ },
141
+ "created": 1720784243174
142
+ }
143
+ ]
@@ -12,6 +12,8 @@ const event_parser_1 = require("../../src/storm/event-parser");
12
12
  const simple_blog_events_json_1 = __importDefault(require("./simple-blog-events.json"));
13
13
  const predefined_user_events_json_1 = __importDefault(require("./predefined-user-events.json"));
14
14
  const blog_events_json_1 = __importDefault(require("./blog-events.json"));
15
+ const duplicate_entities_events_json_1 = __importDefault(require("./duplicate-entities-events.json"));
16
+ const kaplang_core_1 = require("@kapeta/kaplang-core");
15
17
  exports.parserOptions = {
16
18
  serviceKind: 'kapeta/block-service:local',
17
19
  serviceLanguage: 'kapeta/language-target-java-spring-boot:local',
@@ -217,4 +219,33 @@ describe('event-parser', () => {
217
219
  const safeName = event_parser_1.StormEventParser.toSafeArtifactName('Browser-based CRM Application');
218
220
  expect(safeName).toBe('browserbasedcrmapplication');
219
221
  });
222
+ it('rename duplicate entity names', async () => {
223
+ const events = duplicate_entities_events_json_1.default;
224
+ const parser = new event_parser_1.StormEventParser(exports.parserOptions);
225
+ for (const event of events) {
226
+ await parser.processEvent('kapeta', event);
227
+ }
228
+ const result = await parser.toResult('kapeta');
229
+ const apiGateway = result.blocks.find((block) => block.aiName === 'api-gateway');
230
+ expect(apiGateway).toBeDefined();
231
+ const dataTypes = kaplang_core_1.DSLDataTypeParser.parse(apiGateway.content.spec.entities.source.value, {
232
+ ignoreSemantics: true,
233
+ });
234
+ expect(dataTypes.map((type) => type.name)).toStrictEqual(['Status', 'Result', 'User', 'Status_1', 'Result_1']);
235
+ const conflictingType = dataTypes.find((type) => type.name === 'Result_1');
236
+ expect(conflictingType).toBeDefined();
237
+ const dataType = conflictingType;
238
+ expect(dataType.properties?.length).toBe(1);
239
+ const dslDataTypeProperty = dataType.properties[0];
240
+ expect(dslDataTypeProperty.type).toBe('Status_1');
241
+ const commentsClient = apiGateway?.content.spec.consumers?.find((resource) => resource.metadata.name === 'comments');
242
+ expect(commentsClient).toBeDefined();
243
+ const methods = kaplang_core_1.DSLAPIParser.parse(commentsClient.spec.source.value, {
244
+ ignoreSemantics: true,
245
+ });
246
+ expect(methods).toBeDefined();
247
+ expect(methods.length).toBe(1);
248
+ const method = methods[0];
249
+ expect(method.returnType).toBe('Result_1');
250
+ });
220
251
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.60.1",
3
+ "version": "0.60.3",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -24,12 +24,16 @@ import {
24
24
  import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
25
25
  import {
26
26
  DSLAPIParser,
27
+ DSLCompatibilityHelper,
27
28
  DSLController,
28
29
  DSLConverters,
30
+ DSLData,
29
31
  DSLDataTypeParser,
30
32
  DSLEntityType,
31
33
  DSLMethod,
32
34
  DSLParser,
35
+ DSLReferenceResolver,
36
+ DSLTypeHelper,
33
37
  KAPLANG_ID,
34
38
  KAPLANG_VERSION,
35
39
  KaplangWriter,
@@ -413,13 +417,11 @@ export class StormEventParser {
413
417
  return;
414
418
  }
415
419
 
416
- const apiResource = apiProviderBlock.content.spec.providers?.find(
417
- (p) => {
418
- const pKind = normalizeKapetaUri(p.kind);
419
- const apiKind = normalizeKapetaUri(this.options.apiKind);
420
- return pKind === apiKind && p.metadata.name === apiConnection.fromResource
421
- }
422
- );
420
+ const apiResource = apiProviderBlock.content.spec.providers?.find((p) => {
421
+ const pKind = normalizeKapetaUri(p.kind);
422
+ const apiKind = normalizeKapetaUri(this.options.apiKind);
423
+ return pKind === apiKind && p.metadata.name === apiConnection.fromResource;
424
+ });
423
425
 
424
426
  if (!apiResource) {
425
427
  if (warn) {
@@ -454,6 +456,8 @@ export class StormEventParser {
454
456
  return;
455
457
  }
456
458
 
459
+ const renamedEntities: { [from: string]: string } = {};
460
+
457
461
  if (apiProviderBlock.content.spec.entities?.source?.value) {
458
462
  if (!clientConsumerBlock.content.spec.entities) {
459
463
  clientConsumerBlock.content.spec.entities = {
@@ -473,19 +477,99 @@ export class StormEventParser {
473
477
  const apiTypes = DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value, {
474
478
  ignoreSemantics: true,
475
479
  });
480
+ const newTypes: DSLData[] = [];
481
+
482
+ const clientTypeExists = function (apiType: DSLData): boolean {
483
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
484
+ return clientType != undefined;
485
+ };
486
+
487
+ const clientTypeIsCompatible = function (apiType: DSLData): boolean {
488
+ const clientType = clientTypes.find((t) => t.name === apiType.name);
489
+ return (
490
+ clientType != undefined &&
491
+ DSLCompatibilityHelper.isDataCompatible(apiType, clientType, apiTypes, clientTypes)
492
+ );
493
+ };
476
494
 
477
495
  apiTypes.forEach((apiType) => {
478
- if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
479
- // Already exists
496
+ if (!clientTypeExists(apiType)) {
497
+ newTypes.push(apiType);
480
498
  return;
481
499
  }
482
- clientTypes.push(apiType);
500
+
501
+ if (clientTypeIsCompatible(apiType)) {
502
+ return;
503
+ }
504
+
505
+ const originalName = apiType.name;
506
+ const toEntity = _.cloneDeep(apiType);
507
+ let conflictCount = 1;
508
+
509
+ while (clientTypeExists(toEntity) && !clientTypeIsCompatible(toEntity)) {
510
+ toEntity.name = `${originalName}_${conflictCount}`;
511
+ conflictCount++;
512
+ }
513
+
514
+ newTypes.push(toEntity);
515
+ renamedEntities[originalName] = toEntity.name;
516
+ });
517
+
518
+ Object.entries(renamedEntities).forEach(([from, to]) => {
519
+ newTypes.forEach((newType) => {
520
+ if (newType.type !== DSLEntityType.DATATYPE) {
521
+ return;
522
+ }
523
+
524
+ if (!newType.properties) {
525
+ return;
526
+ }
527
+
528
+ newType.properties.forEach((property) => {
529
+ const type = DSLTypeHelper.asType(property.type);
530
+
531
+ if (from !== type.name) {
532
+ return;
533
+ }
534
+
535
+ type.name = to;
536
+ property.type = type;
537
+ });
538
+ });
483
539
  });
484
540
 
485
- clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write(clientTypes);
541
+ clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write([
542
+ ...clientTypes,
543
+ ...newTypes,
544
+ ]);
486
545
  }
546
+
487
547
  clientResource.spec.methods = apiResource.spec.methods;
488
- clientResource.spec.source = apiResource.spec.source;
548
+
549
+ if (Object.keys(renamedEntities).length == 0) {
550
+ clientResource.spec.source = apiResource.spec.source;
551
+ } else {
552
+ // entities were renamed - rename references as well
553
+ const targetSource = _.cloneDeep(apiResource.spec.source);
554
+ const methods = DSLAPIParser.parse(targetSource.value, {
555
+ ignoreSemantics: true,
556
+ });
557
+
558
+ const resolver = new DSLReferenceResolver();
559
+ resolver.visitReferences(methods, (name) => {
560
+ const type = DSLTypeHelper.asType(name);
561
+
562
+ if (renamedEntities[type.name]) {
563
+ type.name = renamedEntities[type.name];
564
+ return type;
565
+ }
566
+
567
+ return name;
568
+ });
569
+
570
+ targetSource.value = KaplangWriter.write(methods);
571
+ clientResource.spec.source = targetSource;
572
+ }
489
573
  });
490
574
 
491
575
  const connections: Connection[] = this.connections.map((connection) => {
@@ -802,13 +886,11 @@ export class StormEventParser {
802
886
  return;
803
887
  }
804
888
 
805
- const apiResource = apiProviderBlock.content.spec.providers?.find(
806
- (p) => {
807
- const pKind = normalizeKapetaUri(p.kind);
808
- const apiKind = normalizeKapetaUri(this.options.apiKind);
809
- return pKind === apiKind && p.metadata.name === connection.fromResource
810
- }
811
- );
889
+ const apiResource = apiProviderBlock.content.spec.providers?.find((p) => {
890
+ const pKind = normalizeKapetaUri(p.kind);
891
+ const apiKind = normalizeKapetaUri(this.options.apiKind);
892
+ return pKind === apiKind && p.metadata.name === connection.fromResource;
893
+ });
812
894
 
813
895
  if (!apiResource) {
814
896
  if (warn) {
@@ -882,7 +964,7 @@ export class StormEventParser {
882
964
 
883
965
  if (kind.includes('java')) {
884
966
  const groupId = `ai.${StormEventParser.toSafeName(handle)}`;
885
- const artifactId = StormEventParser.toSafeName(this.planName);
967
+ const artifactId = StormEventParser.toSafeArtifactName(this.planName);
886
968
  options = {
887
969
  groupId,
888
970
  artifactId,
@@ -0,0 +1,143 @@
1
+ [
2
+ {
3
+ "type": "CREATE_BLOCK",
4
+ "reason": "Block updated",
5
+ "payload": {
6
+ "archetype": "",
7
+ "description": "Handles traffic from the backend services.",
8
+ "name": "api-gateway",
9
+ "resources": [
10
+ {
11
+ "description": "",
12
+ "name": "posts",
13
+ "to": "post-service",
14
+ "type": "CLIENT"
15
+ },
16
+ {
17
+ "description": "",
18
+ "name": "comments",
19
+ "to": "comment-service",
20
+ "type": "CLIENT"
21
+ }
22
+ ],
23
+ "type": "GATEWAY",
24
+ "blockRef": "kapeta://kapeta/api-gateway:local",
25
+ "instanceId": "6b247f30-dec4-5960-a8a0-f300caa95226"
26
+ },
27
+ "created": 1720784242064
28
+ },
29
+ {
30
+ "type": "CREATE_BLOCK",
31
+ "reason": "Block updated",
32
+ "payload": {
33
+ "archetype": "",
34
+ "description": "Manages the creation, editing, and deletion of blog posts.",
35
+ "name": "post-service",
36
+ "resources": [
37
+ {
38
+ "description": "The posts API provides endpoints for managing blog posts. It includes functionality for creating, editing, and deleting posts, as well as retrieving and searching for posts. The API is designed to be secure and follows best practices for authentication and authorization.",
39
+ "name": "posts",
40
+ "type": "API"
41
+ }
42
+ ],
43
+ "type": "BACKEND",
44
+ "blockRef": "kapeta://kapeta/post-service:local",
45
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
46
+ },
47
+ "created": 1720784243137
48
+ },
49
+ {
50
+ "type": "CREATE_BLOCK",
51
+ "reason": "Block updated",
52
+ "payload": {
53
+ "archetype": "",
54
+ "description": "Manages the creation, editing, and deletion of comments on blog posts.",
55
+ "name": "comment-service",
56
+ "resources": [
57
+ {
58
+ "description": "The comments API provides endpoints for managing comments on blog posts. It includes functionality for creating, editing, and deleting comments, as well as retrieving and searching for comments. The API is designed to be secure and follows best practices for authentication and authorization.",
59
+ "name": "comments",
60
+ "type": "API"
61
+ }
62
+ ],
63
+ "type": "BACKEND",
64
+ "blockRef": "kapeta://kapeta/comment-service:local",
65
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
66
+ },
67
+ "created": 1720784243141
68
+ },
69
+ {
70
+ "type": "CREATE_TYPE",
71
+ "reason": "Create type for post-service",
72
+ "payload": {
73
+ "blockName": "post-service",
74
+ "content": "enum Status {\n NEW,\n ARCHIVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
75
+ "blockRef": "kapeta://kapeta/post-service:local",
76
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
77
+ },
78
+ "created": 1720784233286
79
+ },
80
+ {
81
+ "type": "CREATE_API",
82
+ "reason": "Create API for post-service",
83
+ "payload": {
84
+ "blockName": "post-service",
85
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
86
+ "blockRef": "kapeta://kapeta/post-service:local",
87
+ "instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
88
+ },
89
+ "created": 1720784233290
90
+ },
91
+ {
92
+ "type": "CREATE_TYPE",
93
+ "reason": "Create type for comment-service",
94
+ "payload": {
95
+ "blockName": "comment-service",
96
+ "content": "enum Status {\n NEW,\n APPROVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
97
+ "blockRef": "kapeta://kapeta/comment-service:local",
98
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
99
+ },
100
+ "created": 1720784241000
101
+ },
102
+ {
103
+ "type": "CREATE_API",
104
+ "reason": "Create API for comment-service",
105
+ "payload": {
106
+ "blockName": "comment-service",
107
+ "content": "@GET(\"/status\")\ngetStatus(): Result",
108
+ "blockRef": "kapeta://kapeta/comment-service:local",
109
+ "instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
110
+ },
111
+ "created": 1720784241002
112
+ },
113
+ {
114
+ "type": "CREATE_CONNECTION",
115
+ "reason": "api-gateway needs to be able to serve the posts API from the post-service",
116
+ "payload": {
117
+ "fromComponent": "post-service",
118
+ "fromResource": "posts",
119
+ "fromResourceType": "API",
120
+ "toComponent": "api-gateway",
121
+ "toResource": "posts",
122
+ "toResourceType": "CLIENT",
123
+ "fromBlockId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26",
124
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
125
+ },
126
+ "created": 1720784243173
127
+ },
128
+ {
129
+ "type": "CREATE_CONNECTION",
130
+ "reason": "api-gateway needs to be able to serve the comments API from the comment-service",
131
+ "payload": {
132
+ "fromComponent": "comment-service",
133
+ "fromResource": "comments",
134
+ "fromResourceType": "API",
135
+ "toComponent": "api-gateway",
136
+ "toResource": "comments",
137
+ "toResourceType": "CLIENT",
138
+ "fromBlockId": "d4215220-f552-5a6d-9429-bef1c40c4d7c",
139
+ "toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
140
+ },
141
+ "created": 1720784243174
142
+ }
143
+ ]
@@ -8,6 +8,8 @@ import { StormEvent } from '../../src/storm/events';
8
8
  import simpleBlogEvents from './simple-blog-events.json';
9
9
  import predefinedUserEvents from './predefined-user-events.json';
10
10
  import testEvents from './blog-events.json';
11
+ import duplicateEntitiesEvents from './duplicate-entities-events.json';
12
+ import { DSLAPIParser, DSLDataType, DSLDataTypeParser, DSLMethod } from '@kapeta/kaplang-core';
11
13
 
12
14
  export const parserOptions = {
13
15
  serviceKind: 'kapeta/block-service:local',
@@ -245,4 +247,41 @@ describe('event-parser', () => {
245
247
  const safeName = StormEventParser.toSafeArtifactName('Browser-based CRM Application');
246
248
  expect(safeName).toBe('browserbasedcrmapplication');
247
249
  });
250
+
251
+ it('rename duplicate entity names', async () => {
252
+ const events = duplicateEntitiesEvents as StormEvent[];
253
+ const parser = new StormEventParser(parserOptions);
254
+ for (const event of events) {
255
+ await parser.processEvent('kapeta', event);
256
+ }
257
+
258
+ const result = await parser.toResult('kapeta');
259
+ const apiGateway = result.blocks.find((block) => block.aiName === 'api-gateway');
260
+ expect(apiGateway).toBeDefined();
261
+
262
+ const dataTypes = DSLDataTypeParser.parse(apiGateway!.content!.spec!.entities!.source!.value, {
263
+ ignoreSemantics: true,
264
+ });
265
+ expect(dataTypes.map((type) => type.name)).toStrictEqual(['Status', 'Result', 'User', 'Status_1', 'Result_1']);
266
+
267
+ const conflictingType = dataTypes.find((type) => type.name === 'Result_1');
268
+ expect(conflictingType).toBeDefined();
269
+ const dataType = conflictingType as DSLDataType;
270
+ expect(dataType.properties?.length).toBe(1);
271
+ const dslDataTypeProperty = dataType.properties![0];
272
+ expect(dslDataTypeProperty.type).toBe('Status_1');
273
+
274
+ const commentsClient = apiGateway?.content!.spec!.consumers?.find(
275
+ (resource) => resource.metadata.name === 'comments'
276
+ );
277
+ expect(commentsClient).toBeDefined();
278
+
279
+ const methods = DSLAPIParser.parse(commentsClient!.spec!.source!.value, {
280
+ ignoreSemantics: true,
281
+ });
282
+ expect(methods).toBeDefined();
283
+ expect(methods.length).toBe(1);
284
+ const method = methods[0] as DSLMethod;
285
+ expect(method.returnType).toBe('Result_1');
286
+ });
248
287
  });