/** * Copyright 2023 Kapeta Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { StormEventParser } from '../../src/storm/event-parser'; import { StormEvent } from '../../src/storm/events'; import simpleBlogEvents from './simple-blog-events.json'; import predefinedUserEvents from './predefined-user-events.json'; import testEvents from './blog-events.json'; import duplicateEntitiesEvents from './duplicate-entities-events.json'; import { DSLAPIParser, DSLDataType, DSLDataTypeParser, DSLMethod } from '@kapeta/kaplang-core'; export const parserOptions = { serviceKind: 'kapeta/block-service:local', serviceLanguage: 'kapeta/language-target-java-spring-boot:local', frontendKind: 'kapeta/block-type-frontend:local', frontendLanguage: 'kapeta/language-target-react-ts:local', htmlLanguage: 'kapeta/language-target-html:local', cliKind: 'kapeta/block-type-cli:local', cliLanguage: 'kapeta/language-target-nodejs-ts:local', desktopKind: 'kapeta/block-type-desktop:local', desktopLanguage: 'kapeta/language-target-electron-ts:local', gatewayKind: 'kapeta/block-type-gateway:local', mqKind: 'kapeta/block-type-mq:local', exchangeKind: 'kapeta/resource-type-exchange:local', queueKind: 'kapeta/resource-type-queue:local', publisherKind: 'kapeta/resource-type-publisher:local', subscriberKind: 'kapeta/resource-type-subscriber:local', databaseKind: 'kapeta/block-type-database:local', apiKind: 'kapeta/resource-type-rest-api:local', clientKind: 'kapeta/block-type-client:local', webPageKind: 'kapeta/block-type-web-page:local', webFragmentKind: 'kapeta/block-type-web-fragment:local', jwtProviderKind: 'kapeta/resource-type-jwt-provider:local', jwtConsumerKind: 'kapeta/resource-type-jwt-consumer:local', smtpKind: 'kapeta/resource-type-smtp:local', externalApiKind: 'kapeta/resource-type-external-api:local', }; const events: StormEvent[] = [ { type: 'CREATE_PLAN_PROPERTIES', created: Date.now(), reason: 'create plan properties', payload: { name: 'my-plan', description: 'my plan description', }, }, { type: 'CREATE_BLOCK', reason: 'create backend', created: Date.now(), payload: { name: 'service', description: 'A service block', type: 'BACKEND', resources: [ { name: 'entities', type: 'DATABASE', description: 'A database resource', }, { type: 'API', name: 'entities', description: 'An API resource', }, ], }, }, { type: 'CREATE_BLOCK', reason: 'create frontend', created: Date.now(), payload: { name: 'ui', description: 'A frontend block', type: 'FRONTEND', resources: [ { name: 'web', type: 'WEBPAGE', description: 'A web page', }, { type: 'CLIENT', name: 'entities', description: 'Client for backend', }, ], }, }, { type: 'CREATE_CONNECTION', created: Date.now(), reason: 'connect service to ui', payload: { fromComponent: 'service', fromResource: 'entities', fromResourceType: 'API', toComponent: 'ui', toResource: 'entities', toResourceType: 'CLIENT', }, }, { type: 'CREATE_API', reason: 'create api', created: Date.now(), payload: { blockName: 'service', content: `controller Entities('/entities') { @GET('/') list(): string[] }`, }, }, { type: 'CREATE_MODEL', created: Date.now(), reason: 'create model', payload: { blockName: 'service', content: `type Entity { @Id id: string name: string }`, }, }, ]; describe('event-parser', () => { it('it can parse events into a plan and blocks with proper layout', async () => { const parser = new StormEventParser(parserOptions); for (const event of events) { await parser.processEvent('kapeta', event); } const result = await parser.toResult('kapeta'); expect(result.plan.metadata.name).toBe('kapeta/my-plan'); expect(result.plan.metadata.description).toBe('my plan description'); expect(result.blocks.length).toBe(2); expect(result.blocks[0].content.metadata.name).toBe('kapeta/service'); expect(result.blocks[1].content.metadata.name).toBe('kapeta/ui'); const dbResource = result.blocks[0].content.spec.consumers?.[0]; const apiResource = result.blocks[0].content.spec.providers?.[0]; const clientResource = result.blocks[1].content.spec.consumers?.[0]; const pageResource = result.blocks[1].content.spec.providers?.[0]; expect(apiResource).toBeDefined(); expect(clientResource).toBeDefined(); expect(dbResource).toBeDefined(); expect(pageResource).toBeDefined(); expect(apiResource?.kind).toBe(parserOptions.apiKind); expect(clientResource?.kind).toBe(parserOptions.clientKind); expect(dbResource?.kind).toBe(parserOptions.databaseKind); expect(pageResource?.kind).toBe(parserOptions.webPageKind); expect(apiResource?.spec).toEqual(clientResource?.spec); expect(dbResource?.spec.source.value).toContain('type Entity'); const serviceBlockInstance = result.plan.spec.blocks[0]; expect(serviceBlockInstance.name).toBe('service'); const uiBlockInstance = result.plan.spec.blocks[1]; expect(uiBlockInstance.name).toBe('ui'); expect(result.plan.spec.connections.length).toBe(1); expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id); expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name); expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id); expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name); }); it('it will split api into correct provider', async () => { const events = simpleBlogEvents as StormEvent[]; const parser = new StormEventParser(parserOptions); for (const event of events) { await parser.processEvent('kapeta', event); } const result = await parser.toResult('kapeta'); const blogService = result.blocks.find((block) => block.aiName === 'blog-service'); expect(blogService).toBeDefined(); expect(blogService?.content).toBeDefined(); const apiProviders = blogService?.content?.spec?.providers?.filter( (provider) => provider.kind === 'kapeta/resource-type-rest-api:local' ); expect(apiProviders).toBeDefined(); expect(apiProviders!.length).toBe(2); expect(apiProviders!['0'].spec.source.value).not.toBe(''); expect(apiProviders!['1'].spec.source.value).not.toBe(''); }); it('predefined components', async () => { const events = predefinedUserEvents as StormEvent[]; const parser = new StormEventParser(parserOptions); for (const event of events) { await parser.processEvent('kapeta', event); } const result = await parser.toResult('kapeta'); expect(result.blocks.length).toBe(1); const block = result.blocks[0]; expect(block.content.metadata.title).toBe('user-service'); expect(block.content.metadata.title).toBe('user-service'); expect(block.content.metadata.name).toBe('kapeta/user-service'); expect(block.archetype).toBeDefined(); expect(block.content.spec.target?.options).toBeDefined(); expect(block.content.spec.target?.options!.groupId).toBe('ai.kapeta'); expect(block.content.spec.target?.options!.artifactId).toBe('blogplatform'); expect(block.content.spec.target?.options!.basePackage).toBe('ai.kapeta.blogplatform'); }); it('ensure post-service has api', async () => { const events = testEvents as StormEvent[]; const parser = new StormEventParser(parserOptions); for (const event of events) { await parser.processEvent('kapeta', event); } const result = await parser.toResult('kapeta', true); const postService = result.blocks.find((block) => block.aiName === 'post-service'); expect(postService).toBeDefined(); expect(postService?.content?.spec?.providers?.length).toBe(1); expect(postService?.content?.spec?.providers![0].spec.source.value.startsWith('controller')).toBeTruthy(); }); it('toSafeName produces valid artifact names', async () => { const safeName = StormEventParser.toSafeArtifactName('Browser-based CRM Application'); expect(safeName).toBe('browserbasedcrmapplication'); }); it('rename duplicate entity names', async () => { const events = duplicateEntitiesEvents as StormEvent[]; const parser = new StormEventParser(parserOptions); for (const event of events) { await parser.processEvent('kapeta', event); } const result = await parser.toResult('kapeta'); const apiGateway = result.blocks.find((block) => block.aiName === 'api-gateway'); expect(apiGateway).toBeDefined(); const dataTypes = DSLDataTypeParser.parse(apiGateway!.content!.spec!.entities!.source!.value, { ignoreSemantics: true, }); expect(dataTypes.map((type) => type.name)).toStrictEqual(['Status', 'Result', 'User', 'Status_1', 'Result_1']); const conflictingType = dataTypes.find((type) => type.name === 'Result_1'); expect(conflictingType).toBeDefined(); const dataType = conflictingType as DSLDataType; expect(dataType.properties?.length).toBe(1); const dslDataTypeProperty = dataType.properties![0]; expect(dslDataTypeProperty.type).toBe('Status_1'); const commentsClient = apiGateway?.content!.spec!.consumers?.find( (resource) => resource.metadata.name === 'comments' ); expect(commentsClient).toBeDefined(); const methods = DSLAPIParser.parse(commentsClient!.spec!.source!.value, { ignoreSemantics: true, }); expect(methods).toBeDefined(); expect(methods.length).toBe(1); const method = methods[0] as DSLMethod; expect(method.returnType).toBe('Result_1'); }); });