@kapeta/local-cluster-service 0.60.2 → 0.61.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.
- package/CHANGELOG.md +19 -0
- package/dist/cjs/src/storm/event-parser.js +68 -5
- package/dist/cjs/src/storm/events.d.ts +20 -1
- package/dist/cjs/src/storm/routes.js +100 -45
- package/dist/cjs/src/storm/stormClient.d.ts +12 -0
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/cjs/test/storm/duplicate-entities-events.json +143 -0
- package/dist/cjs/test/storm/event-parser.test.js +31 -0
- package/dist/esm/src/storm/event-parser.js +68 -5
- package/dist/esm/src/storm/events.d.ts +20 -1
- package/dist/esm/src/storm/routes.js +100 -45
- package/dist/esm/src/storm/stormClient.d.ts +12 -0
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/dist/esm/test/storm/duplicate-entities-events.json +143 -0
- package/dist/esm/test/storm/event-parser.test.js +31 -0
- package/package.json +1 -1
- package/src/storm/event-parser.ts +101 -19
- package/src/storm/events.ts +25 -1
- package/src/storm/routes.ts +133 -57
- package/src/storm/stormClient.ts +19 -0
- package/test/storm/duplicate-entities-events.json +143 -0
- package/test/storm/event-parser.test.ts +39 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
# [0.61.0](https://github.com/kapetacom/local-cluster-service/compare/v0.60.3...v0.61.0) (2024-08-09)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* Move UI server start to later ([fb75cb1](https://github.com/kapetacom/local-cluster-service/commit/fb75cb1b44e8047b9f451a12219d3c8295d9394e))
|
7
|
+
|
8
|
+
|
9
|
+
### Features
|
10
|
+
|
11
|
+
* Get UI shells and use them when creating pages ([de6dc6f](https://github.com/kapetacom/local-cluster-service/commit/de6dc6fcebeaad1522a9d280ee95cf163f73fb7f))
|
12
|
+
|
13
|
+
## [0.60.3](https://github.com/kapetacom/local-cluster-service/compare/v0.60.2...v0.60.3) (2024-08-07)
|
14
|
+
|
15
|
+
|
16
|
+
### Bug Fixes
|
17
|
+
|
18
|
+
* rename conflicting entities when creating connections ([a5125cf](https://github.com/kapetacom/local-cluster-service/commit/a5125cf652b808cd94cf3ebe0df853f0c7256566))
|
19
|
+
|
1
20
|
## [0.60.2](https://github.com/kapetacom/local-cluster-service/compare/v0.60.1...v0.60.2) (2024-08-06)
|
2
21
|
|
3
22
|
|
@@ -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 (
|
333
|
-
|
343
|
+
if (!clientTypeExists(apiType)) {
|
344
|
+
newTypes.push(apiType);
|
345
|
+
return;
|
346
|
+
}
|
347
|
+
if (clientTypeIsCompatible(apiType)) {
|
334
348
|
return;
|
335
349
|
}
|
336
|
-
|
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(
|
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
|
-
|
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);
|
@@ -280,6 +280,7 @@ export interface Page {
|
|
280
280
|
method: string;
|
281
281
|
conversationId: string;
|
282
282
|
prompt: string;
|
283
|
+
shellPage?: string;
|
283
284
|
}
|
284
285
|
export interface StormEventPage {
|
285
286
|
type: 'PAGE';
|
@@ -323,5 +324,23 @@ export interface StormEventUserJourney {
|
|
323
324
|
created: number;
|
324
325
|
payload: UserJourney;
|
325
326
|
}
|
326
|
-
export
|
327
|
+
export interface UIShell {
|
328
|
+
name: string;
|
329
|
+
content: string;
|
330
|
+
screens: string[];
|
331
|
+
}
|
332
|
+
export interface StormEventUIShell {
|
333
|
+
type: 'UI_SHELL';
|
334
|
+
reason: string;
|
335
|
+
created: number;
|
336
|
+
payload: UIShell;
|
337
|
+
}
|
338
|
+
export interface StormEventPromptImprove {
|
339
|
+
type: 'PROMPT_IMPROVE';
|
340
|
+
reason: string;
|
341
|
+
payload: {
|
342
|
+
prompt: string;
|
343
|
+
};
|
344
|
+
}
|
345
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove;
|
327
346
|
export {};
|
@@ -110,24 +110,18 @@ router.post('/:handle/ui', async (req, res) => {
|
|
110
110
|
try {
|
111
111
|
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
|
112
112
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
113
|
+
// Get user journeys
|
113
114
|
const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
|
115
|
+
const outerConversationId = userJourneysStream.getConversationId();
|
114
116
|
onRequestAborted(req, res, () => {
|
115
117
|
userJourneysStream.abort();
|
116
118
|
});
|
117
119
|
res.set('Content-Type', 'application/x-ndjson');
|
118
120
|
res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
|
119
|
-
res.set(stormClient_1.ConversationIdHeader,
|
120
|
-
const
|
121
|
-
|
122
|
-
onRequestAborted(req, res, () => {
|
123
|
-
queue.cancel();
|
124
|
-
});
|
125
|
-
const systemId = userJourneysStream.getConversationId();
|
126
|
-
UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
|
127
|
-
await UI_SERVERS[systemId].start();
|
128
|
-
userJourneysStream.on('data', async (data) => {
|
121
|
+
res.set(stormClient_1.ConversationIdHeader, outerConversationId);
|
122
|
+
const uniqueUserJourneyScreens = {};
|
123
|
+
userJourneysStream.on('data', (data) => {
|
129
124
|
try {
|
130
|
-
console.log('Processing user journey event', data);
|
131
125
|
sendEvent(res, data);
|
132
126
|
if (data.type !== 'USER_JOURNEY') {
|
133
127
|
return;
|
@@ -136,48 +130,109 @@ router.post('/:handle/ui', async (req, res) => {
|
|
136
130
|
return;
|
137
131
|
}
|
138
132
|
data.payload.screens.forEach((screen) => {
|
139
|
-
if (screen.name
|
140
|
-
|
133
|
+
if (!uniqueUserJourneyScreens[screen.name]) {
|
134
|
+
uniqueUserJourneyScreens[screen.name] = screen;
|
141
135
|
}
|
142
|
-
promises[screen.name] = queue.add(() => new Promise(async (resolve, reject) => {
|
143
|
-
try {
|
144
|
-
const innerConversationId = node_uuid_1.default.v4();
|
145
|
-
const screenStream = await stormClient_1.stormClient.createUIPage({
|
146
|
-
prompt: screen.requirements,
|
147
|
-
method: screen.method,
|
148
|
-
path: screen.path,
|
149
|
-
description: screen.requirements,
|
150
|
-
name: screen.name,
|
151
|
-
title: screen.title,
|
152
|
-
filename: screen.filename,
|
153
|
-
storage_prefix: userJourneysStream.getConversationId() + '_',
|
154
|
-
}, innerConversationId);
|
155
|
-
const promises = [];
|
156
|
-
screenStream.on('data', (screenData) => {
|
157
|
-
if (screenData.type === 'PAGE') {
|
158
|
-
screenData.payload.conversationId = innerConversationId;
|
159
|
-
promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
|
160
|
-
}
|
161
|
-
else {
|
162
|
-
sendEvent(res, screenData);
|
163
|
-
}
|
164
|
-
});
|
165
|
-
screenStream.on('end', () => {
|
166
|
-
Promise.allSettled(promises).finally(resolve);
|
167
|
-
});
|
168
|
-
}
|
169
|
-
catch (e) {
|
170
|
-
console.error('Failed to process screen', e);
|
171
|
-
reject(e);
|
172
|
-
}
|
173
|
-
}));
|
174
136
|
});
|
175
137
|
}
|
176
138
|
catch (e) {
|
177
139
|
console.error('Failed to process event', e);
|
178
140
|
}
|
179
141
|
});
|
142
|
+
userJourneysStream.on('error', (error) => {
|
143
|
+
console.error('Error on userJourneysStream', error);
|
144
|
+
userJourneysStream.abort();
|
145
|
+
sendError(error, res);
|
146
|
+
});
|
180
147
|
await waitForStormStream(userJourneysStream);
|
148
|
+
// Get the UI shells
|
149
|
+
const shellsStream = await stormClient_1.stormClient.createUIShells({
|
150
|
+
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
151
|
+
name: screen.name,
|
152
|
+
title: screen.title,
|
153
|
+
filename: screen.filename,
|
154
|
+
path: screen.path,
|
155
|
+
method: screen.method,
|
156
|
+
requirements: screen.requirements,
|
157
|
+
})),
|
158
|
+
}, conversationId);
|
159
|
+
onRequestAborted(req, res, () => {
|
160
|
+
shellsStream.abort();
|
161
|
+
});
|
162
|
+
const uiShells = [];
|
163
|
+
shellsStream.on('data', (data) => {
|
164
|
+
console.log('Processing shell event', data);
|
165
|
+
sendEvent(res, data);
|
166
|
+
if (data.type !== 'UI_SHELL') {
|
167
|
+
return;
|
168
|
+
}
|
169
|
+
if (shellsStream.isAborted()) {
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
uiShells.push(data.payload);
|
173
|
+
});
|
174
|
+
shellsStream.on('error', (error) => {
|
175
|
+
console.error('Error on shellsStream', error);
|
176
|
+
shellsStream.abort();
|
177
|
+
sendError(error, res);
|
178
|
+
});
|
179
|
+
await waitForStormStream(shellsStream);
|
180
|
+
UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
|
181
|
+
await UI_SERVERS[outerConversationId].start();
|
182
|
+
// Get the pages (5 at a time)
|
183
|
+
const queue = new PromiseQueue_1.PromiseQueue(5);
|
184
|
+
onRequestAborted(req, res, () => {
|
185
|
+
queue.cancel();
|
186
|
+
});
|
187
|
+
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
188
|
+
await queue.add(() => new Promise(async (resolve, reject) => {
|
189
|
+
try {
|
190
|
+
const innerConversationId = node_uuid_1.default.v4();
|
191
|
+
const screenStream = await stormClient_1.stormClient.createUIPage({
|
192
|
+
prompt: screen.requirements,
|
193
|
+
method: screen.method,
|
194
|
+
path: screen.path,
|
195
|
+
description: screen.requirements,
|
196
|
+
name: screen.name,
|
197
|
+
title: screen.title,
|
198
|
+
filename: screen.filename,
|
199
|
+
storage_prefix: outerConversationId + '_',
|
200
|
+
shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
|
201
|
+
}, innerConversationId);
|
202
|
+
const promiseList = [];
|
203
|
+
screenStream.on('data', (screenData) => {
|
204
|
+
if (screenData.type === 'PAGE') {
|
205
|
+
promiseList.push(sendPageEvent(outerConversationId, {
|
206
|
+
...screenData,
|
207
|
+
payload: {
|
208
|
+
...screenData.payload,
|
209
|
+
conversationId: innerConversationId,
|
210
|
+
},
|
211
|
+
}, res));
|
212
|
+
}
|
213
|
+
else {
|
214
|
+
sendEvent(res, screenData);
|
215
|
+
}
|
216
|
+
});
|
217
|
+
screenStream.on('end', async () => {
|
218
|
+
try {
|
219
|
+
await Promise.allSettled(promiseList).finally(() => resolve(true));
|
220
|
+
}
|
221
|
+
catch (error) {
|
222
|
+
console.error('Failed to process screen', error);
|
223
|
+
}
|
224
|
+
});
|
225
|
+
screenStream.on('error', (error) => {
|
226
|
+
console.error('Error on screenStream', error);
|
227
|
+
screenStream.abort();
|
228
|
+
});
|
229
|
+
}
|
230
|
+
catch (e) {
|
231
|
+
console.error('Failed to process screen', e);
|
232
|
+
reject(e);
|
233
|
+
}
|
234
|
+
}));
|
235
|
+
}
|
181
236
|
await queue.wait();
|
182
237
|
if (userJourneysStream.isAborted()) {
|
183
238
|
return;
|
@@ -2,6 +2,16 @@ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIIm
|
|
2
2
|
import { Page, StormEventPageUrl } from './events';
|
3
3
|
export declare const STORM_ID = "storm";
|
4
4
|
export declare const ConversationIdHeader = "Conversation-Id";
|
5
|
+
export interface UIShellsPrompt {
|
6
|
+
pages: {
|
7
|
+
name: string;
|
8
|
+
title: string;
|
9
|
+
filename: string;
|
10
|
+
path: string;
|
11
|
+
method: string;
|
12
|
+
requirements: string;
|
13
|
+
}[];
|
14
|
+
}
|
5
15
|
export interface UIPagePrompt {
|
6
16
|
name: string;
|
7
17
|
title: string;
|
@@ -11,6 +21,7 @@ export interface UIPagePrompt {
|
|
11
21
|
method: string;
|
12
22
|
description: string;
|
13
23
|
storage_prefix: string;
|
24
|
+
shell_page?: string;
|
14
25
|
}
|
15
26
|
export interface UIPageEditPrompt {
|
16
27
|
planDescription: string;
|
@@ -32,6 +43,7 @@ declare class StormClient {
|
|
32
43
|
createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
|
33
44
|
createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
|
34
45
|
createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
|
46
|
+
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
35
47
|
createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
|
36
48
|
editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
|
37
49
|
listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
|
@@ -92,6 +92,12 @@ class StormClient {
|
|
92
92
|
conversationId,
|
93
93
|
});
|
94
94
|
}
|
95
|
+
createUIShells(prompt, conversationId) {
|
96
|
+
return this.send('/v2/ui/shells', {
|
97
|
+
prompt: JSON.stringify(prompt),
|
98
|
+
conversationId,
|
99
|
+
});
|
100
|
+
}
|
95
101
|
createUIPage(prompt, conversationId) {
|
96
102
|
return this.send('/v2/ui/page', {
|
97
103
|
prompt: prompt,
|
@@ -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 (
|
333
|
-
|
343
|
+
if (!clientTypeExists(apiType)) {
|
344
|
+
newTypes.push(apiType);
|
345
|
+
return;
|
346
|
+
}
|
347
|
+
if (clientTypeIsCompatible(apiType)) {
|
334
348
|
return;
|
335
349
|
}
|
336
|
-
|
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(
|
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
|
-
|
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);
|