/** * Copyright 2023 Kapeta Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { StormBlockInfoFilled, StormBlockType, StormConnection, StormEvent, StormEventPhases, StormEventPhaseType, StormResourceType, } from './events'; import { BlockDefinition, BlockInstance, Connection, LanguageTargetReference, Plan, Resource, SourceCode, } from '@kapeta/schemas'; import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils'; import { DSLAPIParser, DSLCompatibilityHelper, DSLController, DSLConverters, DSLData, DSLDataTypeParser, DSLEntityType, DSLMethod, DSLParser, DSLReferenceResolver, DSLTypeHelper, KAPLANG_ID, KAPLANG_VERSION, KaplangWriter, } from '@kapeta/kaplang-core'; import { v5 as uuid } from 'uuid'; import { definitionsManager } from '../definitionsManager'; import { PREDEFINED_BLOCKS } from './predefined'; import _ from 'lodash'; export interface BlockDefinitionInfo { uri: string; content: BlockDefinition; aiName: string; archetype?: string; } export interface StormDefinitions { plan: Plan; blocks: BlockDefinitionInfo[]; } export interface StormOptions { databaseKind: string; apiKind: string; clientKind: string; webPageKind: string; webFragmentKind: string; mqKind: string; serviceKind: string; serviceLanguage: string; frontendKind: string; frontendLanguage: string; htmlLanguage: string; exchangeKind: string; queueKind: string; publisherKind: string; subscriberKind: string; jwtProviderKind: string; jwtConsumerKind: string; smtpKind: string; externalApiKind: string; cliKind: string; cliLanguage: string; desktopKind: string; desktopLanguage: string; gatewayKind: string; systemId?: string; [key: string]: string | undefined; } function prettifyKaplang(source: string) { if (!source || !source.trim()) { return ''; } try { const ast = DSLParser.parse(source, { ignoreSemantics: true, types: true, methods: true, rest: true, extends: true, generics: true, }); return KaplangWriter.write(ast.entities ?? []); } catch (e) { console.warn('Failed to prettify source:\n%s', source); return source; } } export function createPhaseStartEvent(type: StormEventPhaseType): StormEventPhases { return createPhaseEvent(true, type); } export function createPhaseEndEvent(type: StormEventPhaseType): StormEventPhases { return createPhaseEvent(false, type); } export function createPhaseEvent(start: boolean, type: StormEventPhaseType): StormEventPhases { return { type: start ? 'PHASE_START' : 'PHASE_END', created: Date.now(), payload: { phaseType: type, }, }; } export async function resolveOptions(): Promise { // Predefined types for now - TODO: Allow user to select / change const blockTypeService = await definitionsManager.getLatestDefinition('kapeta/block-type-service'); const blockTypeFrontend = await definitionsManager.getLatestDefinition('kapeta/block-type-frontend'); const blockTypeDesktop = await definitionsManager.getLatestDefinition('kapeta/block-type-desktop'); const blockTypeCli = await definitionsManager.getLatestDefinition('kapeta/block-type-cli'); const blockTypeGateway = await definitionsManager.getLatestDefinition('kapeta/block-type-gateway-http'); const postgresResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-postgresql'); const webPageResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-web-page'); const webFragmentResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-web-fragment'); const restApiResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-rest-api'); const restClientResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-rest-client'); const javaLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-java-spring-boot'); const reactLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-react-ts'); const nodejsLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-nodejs'); const htmlLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-html'); const blockTypePubsub = await definitionsManager.getLatestDefinition('kapeta/block-type-pubsub'); const resourceTypePubsubSubscriber = await definitionsManager.getLatestDefinition( 'kapeta/resource-type-pubsub-subscriber' ); const resourceTypePubsubSubscription = await definitionsManager.getLatestDefinition( 'kapeta/resource-type-pubsub-subscription' ); const resourceTypePubsubTopic = await definitionsManager.getLatestDefinition('kapeta/resource-type-pubsub-topic'); const resourceTypePubsubPublisher = await definitionsManager.getLatestDefinition( 'kapeta/resource-type-pubsub-publisher' ); const jwtProvider = await definitionsManager.getLatestDefinition('kapeta/resource-type-auth-jwt-provider'); const jwtConsumer = await definitionsManager.getLatestDefinition('kapeta/resource-type-auth-jwt-consumer'); const smtpClient = await definitionsManager.getLatestDefinition('kapeta/resource-type-smtp-client'); const externalService = await definitionsManager.getLatestDefinition('kapeta/resource-type-external-services'); if ( !blockTypeService || !blockTypeFrontend || !blockTypeDesktop || !blockTypeGateway || !postgresResource || !javaLanguage || !reactLanguage || !htmlLanguage || !webPageResource || !restApiResource || !restClientResource || !webFragmentResource || !blockTypePubsub || !resourceTypePubsubSubscriber || !resourceTypePubsubSubscription || !resourceTypePubsubTopic || !resourceTypePubsubPublisher || !jwtProvider || !jwtConsumer || !smtpClient || !externalService || !blockTypeCli || !nodejsLanguage ) { throw new Error('Missing definitions'); } return { serviceKind: normalizeKapetaUri(`${blockTypeService.definition.metadata.name}:${blockTypeService.version}`), serviceLanguage: normalizeKapetaUri(`${javaLanguage.definition.metadata.name}:${javaLanguage.version}`), frontendKind: normalizeKapetaUri(`${blockTypeFrontend.definition.metadata.name}:${blockTypeFrontend.version}`), frontendLanguage: normalizeKapetaUri(`${reactLanguage.definition.metadata.name}:${reactLanguage.version}`), htmlLanguage: normalizeKapetaUri(`${htmlLanguage.definition.metadata.name}:${htmlLanguage.version}`), cliKind: normalizeKapetaUri(`${blockTypeCli.definition.metadata.name}:${blockTypeCli.version}`), cliLanguage: normalizeKapetaUri(`${nodejsLanguage.definition.metadata.name}:${nodejsLanguage.version}`), desktopKind: normalizeKapetaUri(`${blockTypeDesktop.definition.metadata.name}:${blockTypeDesktop.version}`), desktopLanguage: normalizeKapetaUri(`${reactLanguage.definition.metadata.name}:${reactLanguage.version}`), gatewayKind: normalizeKapetaUri(`${blockTypeGateway.definition.metadata.name}:${blockTypeGateway.version}`), mqKind: normalizeKapetaUri(`${blockTypePubsub.definition.metadata.name}:${blockTypePubsub.version}`), exchangeKind: normalizeKapetaUri( `${resourceTypePubsubTopic.definition.metadata.name}:${resourceTypePubsubTopic.version}` ), queueKind: normalizeKapetaUri( `${resourceTypePubsubSubscription.definition.metadata.name}:${resourceTypePubsubSubscription.version}` ), publisherKind: normalizeKapetaUri( `${resourceTypePubsubPublisher.definition.metadata.name}:${resourceTypePubsubPublisher.version}` ), subscriberKind: normalizeKapetaUri( `${resourceTypePubsubSubscriber.definition.metadata.name}:${resourceTypePubsubSubscriber.version}` ), databaseKind: normalizeKapetaUri(`${postgresResource.definition.metadata.name}:${postgresResource.version}`), apiKind: normalizeKapetaUri(`${restApiResource.definition.metadata.name}:${restApiResource.version}`), clientKind: normalizeKapetaUri(`${restClientResource.definition.metadata.name}:${restClientResource.version}`), webPageKind: normalizeKapetaUri(`${webPageResource.definition.metadata.name}:${webPageResource.version}`), webFragmentKind: normalizeKapetaUri( `${webFragmentResource.definition.metadata.name}:${webFragmentResource.version}` ), jwtProviderKind: normalizeKapetaUri(`${jwtProvider.definition.metadata.name}:${jwtProvider.version}`), jwtConsumerKind: normalizeKapetaUri(`${jwtConsumer.definition.metadata.name}:${jwtConsumer.version}`), smtpKind: normalizeKapetaUri(`${smtpClient.definition.metadata.name}:${smtpClient.version}`), externalApiKind: normalizeKapetaUri(`${externalService.definition.metadata.name}:${externalService.version}`), }; } export class StormEventParser { public static toInstanceId(handle: string, blockName: string) { const ref = this.toRef(handle, blockName); return this.toInstanceIdFromRef(ref.toNormalizedString()); } public static toInstanceIdFromRef(ref: string) { return uuid(normalizeKapetaUri(ref), uuid.URL); } public static toSafeName(name: string): string { return name.toLowerCase().replace(/[^0-9a-z-]/gi, ''); } public static toSafeArtifactName(name: string): string { let safeName = name.toLowerCase().replace(/[^0-9a-z]/g, ''); if (/^[0-9]/.test(safeName)) { safeName = 'a' + safeName; // Prepend a letter if the string starts with a number } return safeName; } public static toRef(handle: string, name: string) { return parseKapetaUri(handle + '/' + this.toSafeName(name) + ':local'); } private events: StormEvent[] = []; private planName: string = ''; private planDescription: string = ''; private blocks: { [key: string]: StormBlockInfoFilled } = {}; private connections: StormConnection[] = []; private failed: boolean = false; private error: string = ''; private options: StormOptions; constructor(options: StormOptions) { this.options = options; } private reset() { this.planDescription = ''; this.planName = ''; this.blocks = {}; this.connections = []; } /** * Builds plan and block definitions - and enriches events with relevant refs and ids */ public async processEvent(handle: string, evt: StormEvent): Promise { let blockInfo; this.events.push(evt); switch (evt.type) { case 'CREATE_PLAN_PROPERTIES': this.planName = evt.payload.name; this.planDescription = evt.payload.description; break; case 'CREATE_BLOCK': const apis = this.blocks[evt.payload.name]?.apis; const models = this.blocks[evt.payload.name]?.models; const types = this.blocks[evt.payload.name]?.types; this.blocks[evt.payload.name] = { ...evt.payload, apis: apis ?? [], models: models ?? [], types: types ?? [], }; evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.name).toNormalizedString(); evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef); break; case 'PLAN_RETRY': this.reset(); break; case 'PLAN_RETRY_FAILED': this.failed = true; this.error = evt.payload.error; break; case 'CREATE_API': blockInfo = this.blocks[evt.payload.blockName]; evt.payload.content = prettifyKaplang(evt.payload.content); blockInfo.apis.push(evt.payload.content); evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString(); evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef); const api = blockInfo.resources.find((r) => r.type == 'API'); evt.payload.resourceName = api?.name; break; case 'CREATE_MODEL': blockInfo = this.blocks[evt.payload.blockName]; evt.payload.content = prettifyKaplang(evt.payload.content); blockInfo.models.push(evt.payload.content); evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString(); evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef); const database = blockInfo.resources.find((r) => r.type == 'DATABASE'); evt.payload.resourceName = database?.name; break; case 'CREATE_TYPE': evt.payload.content = prettifyKaplang(evt.payload.content); this.blocks[evt.payload.blockName].types.push(evt.payload.content); evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString(); evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef); break; case 'CREATE_CONNECTION': evt.payload.fromBlockId = StormEventParser.toInstanceId(handle, evt.payload.fromComponent); evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent); this.connections.push(evt.payload); break; case 'API_RETRY': Object.values(this.blocks).forEach((block) => { block.types = []; block.apis = []; }); break; case 'MODELS_RETRY': if ('blockName' in evt.payload) { this.blocks[evt.payload.blockName].models = []; } break; case 'API_STREAM_CHUNK': case 'API_STREAM_CHUNK_RESET': case 'API_STREAM_DONE': case 'API_STREAM_FAILED': case 'API_STREAM_STATE': case 'API_STREAM_START': if ('blockName' in evt.payload) { evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString(); evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef); } break; } return await this.toResult(handle); } public getEvents(): StormEvent[] { return this.events; } public isValid(): boolean { if (!this.planName) { return false; } return !this.failed; } public getError(): string { return this.error; } public async toResult(handle: string, warn: boolean = false): Promise { const planRef = StormEventParser.toRef(handle, this.planName || 'undefined'); const blockDefinitions = await this.toBlockDefinitions(handle); const refIdMap: { [key: string]: string } = {}; const blocks = Object.entries(blockDefinitions).map(([ref, block]) => { // Create a deterministic uuid const id = StormEventParser.toInstanceIdFromRef(ref); refIdMap[ref] = id; return { id, block: { ref, }, name: block.content.metadata.title || block.content.metadata.name, dimensions: { left: 0, top: 0, width: 150, height: 200, }, } satisfies BlockInstance; }); // Copy API methods from API provider to CLIENT consumer this.connections .filter((connection) => connection.fromResourceType === 'API' && connection.toResourceType === 'CLIENT') .forEach((apiConnection) => { const apiProviderRef = StormEventParser.toRef(handle, apiConnection.fromComponent); const clientConsumerRef = StormEventParser.toRef(handle, apiConnection.toComponent); const apiProviderBlock = blockDefinitions[apiProviderRef.toNormalizedString()]; if (!apiProviderBlock) { console.warn('API provider not found: %s', apiConnection.fromComponent, apiConnection); return; } const clientConsumerBlock = blockDefinitions[clientConsumerRef.toNormalizedString()]; if (!clientConsumerBlock) { console.warn('Client consumer not found: %s', apiConnection.toComponent, apiConnection); return; } const apiResource = apiProviderBlock.content.spec.providers?.find((p) => { const pKind = normalizeKapetaUri(p.kind); const apiKind = normalizeKapetaUri(this.options.apiKind); return pKind === apiKind && p.metadata.name === apiConnection.fromResource; }); if (!apiResource) { if (warn) { console.warn( 'API resource not found: %s on %s', apiConnection.fromResource, apiProviderRef.toNormalizedString(), apiConnection ); } return; } const clientResource = clientConsumerBlock.content.spec.consumers?.find((clientResource) => { if (clientResource.kind !== this.options.clientKind) { console.warn('Client resource kind mismatch: %s', clientResource.kind, this.options.clientKind); return false; } return clientResource.metadata.name === apiConnection.toResource; }); if (!clientResource) { if (warn) { console.warn( 'Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection ); } return; } const renamedEntities: { [from: string]: string } = {}; if (apiProviderBlock.content.spec.entities?.source?.value) { if (!clientConsumerBlock.content.spec.entities) { clientConsumerBlock.content.spec.entities = { types: [], source: { type: KAPLANG_ID, version: KAPLANG_VERSION, value: '', }, }; } const clientTypes = DSLDataTypeParser.parse( clientConsumerBlock.content.spec.entities.source!.value, { ignoreSemantics: true } ); const apiTypes = DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value, { ignoreSemantics: true, }); const newTypes: DSLData[] = []; const clientTypeExists = function (apiType: DSLData): boolean { const clientType = clientTypes.find((t) => t.name === apiType.name); return clientType != undefined; }; const clientTypeIsCompatible = function (apiType: DSLData): boolean { const clientType = clientTypes.find((t) => t.name === apiType.name); return ( clientType != undefined && DSLCompatibilityHelper.isDataCompatible(apiType, clientType, apiTypes, clientTypes) ); }; apiTypes.forEach((apiType) => { if (!clientTypeExists(apiType)) { newTypes.push(apiType); return; } if (clientTypeIsCompatible(apiType)) { return; } const originalName = apiType.name; const toEntity = _.cloneDeep(apiType); let conflictCount = 1; while (clientTypeExists(toEntity) && !clientTypeIsCompatible(toEntity)) { toEntity.name = `${originalName}_${conflictCount}`; conflictCount++; } newTypes.push(toEntity); renamedEntities[originalName] = toEntity.name; }); Object.entries(renamedEntities).forEach(([from, to]) => { newTypes.forEach((newType) => { if (newType.type !== DSLEntityType.DATATYPE) { return; } if (!newType.properties) { return; } newType.properties.forEach((property) => { const type = DSLTypeHelper.asType(property.type); if (from !== type.name) { return; } type.name = to; property.type = type; }); }); }); clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write([ ...clientTypes, ...newTypes, ]); } clientResource.spec.methods = apiResource.spec.methods; if (Object.keys(renamedEntities).length == 0) { clientResource.spec.source = apiResource.spec.source; } else { // entities were renamed - rename references as well const targetSource = _.cloneDeep(apiResource.spec.source); const methods = DSLAPIParser.parse(targetSource.value, { ignoreSemantics: true, }); const resolver = new DSLReferenceResolver(); resolver.visitReferences(methods, (name) => { const type = DSLTypeHelper.asType(name); if (renamedEntities[type.name]) { type.name = renamedEntities[type.name]; return type; } return name; }); targetSource.value = KaplangWriter.write(methods); clientResource.spec.source = targetSource; } }); const connections: Connection[] = this.connections.map((connection) => { const fromRef = StormEventParser.toRef(handle, connection.fromComponent); const toRef = StormEventParser.toRef(handle, connection.toComponent); return { port: { type: this.toPortType(connection.fromResourceType), }, consumer: { blockId: refIdMap[toRef.toNormalizedString()], resourceName: connection.toResource, }, provider: { blockId: refIdMap[fromRef.toNormalizedString()], resourceName: connection.fromResource, }, mapping: this.toConnectionMapping(handle, connection, blockDefinitions, warn), } satisfies Connection; }); const plan: Plan = { kind: 'core/plan', metadata: { name: planRef.fullName, title: this.planName, description: this.planDescription, structure: 'mono', visibility: 'private', }, spec: { blocks, connections, }, }; return { plan, blocks: Object.values(blockDefinitions), }; } public async toBlockDefinitions(handle: string): Promise<{ [key: string]: BlockDefinitionInfo }> { const result: { [key: string]: BlockDefinitionInfo } = {}; for (const [, blockInfo] of Object.entries(this.blocks)) { const blockRef = StormEventParser.toRef(handle, blockInfo.name); let blockDefinitionInfo: BlockDefinitionInfo; if (blockInfo.archetype) { blockDefinitionInfo = await this.resolveArchetypeBlockDefinition(blockRef, blockInfo, handle); } else { blockDefinitionInfo = this.createBlockDefinitionInfo(blockRef, blockInfo, handle); } result[blockRef.toNormalizedString()] = blockDefinitionInfo; } return result; } private createBlockDefinitionInfo( blockRef: KapetaURI, blockInfo: StormBlockInfoFilled, handle: string ): BlockDefinitionInfo { const blockDefinitionInfo: BlockDefinitionInfo = { uri: blockRef.toNormalizedString(), aiName: blockInfo.name, content: { kind: this.toBlockKind(blockInfo.type), metadata: { title: blockInfo.name, name: blockRef.fullName, description: blockInfo.description, }, spec: { entities: { types: [], source: { type: KAPLANG_ID, version: KAPLANG_VERSION, value: '', }, }, target: this.toBlockTarget(handle, blockInfo.type), providers: [], consumers: [], }, }, }; const blockSpec = blockDefinitionInfo.content.spec; const apiResources: { [key: string]: Resource | undefined } = {}; let dbResource: Resource | undefined = undefined; (blockInfo.resources || []).forEach((resource) => { const port = { type: this.toPortType(resource.type), }; switch (resource.type) { case 'API': { const apiResource = { kind: this.toResourceKind(resource.type), metadata: { name: resource.name, description: resource.description, }, spec: { port, methods: {}, source: { type: KAPLANG_ID, version: KAPLANG_VERSION, value: '', } satisfies SourceCode, }, }; apiResources[resource.name] = apiResource; blockSpec.providers!.push(apiResource); break; } case 'CLIENT': blockSpec.consumers!.push({ kind: this.toResourceKind(resource.type), metadata: { name: resource.name, description: resource.description, }, spec: { port, methods: {}, source: { type: KAPLANG_ID, version: KAPLANG_VERSION, value: '', } satisfies SourceCode, }, }); break; case 'EXTERNAL_API': break; case 'EXCHANGE': break; case 'PUBLISHER': break; case 'QUEUE': break; case 'SUBSCRIBER': break; case 'JWTPROVIDER': case 'WEBPAGE': blockSpec.providers!.push({ kind: this.toResourceKind(resource.type), metadata: { name: resource.name, description: resource.description, }, spec: { port, }, }); break; case 'DATABASE': if (dbResource) { break; } dbResource = { kind: this.toResourceKind(resource.type), metadata: { name: resource.name, description: resource.description, }, spec: { port, models: [], source: { type: KAPLANG_ID, version: KAPLANG_VERSION, value: '', } satisfies SourceCode, }, }; blockSpec.consumers!.push(dbResource); break; case 'JWTCONSUMER': case 'WEBFRAGMENT': case 'SMTPCLIENT': blockSpec.consumers!.push({ kind: this.toResourceKind(resource.type), metadata: { name: resource.name, description: resource.description, }, spec: { port, }, }); } }); blockInfo.apis.forEach((api) => { const dslApi = DSLAPIParser.parse(api, { ignoreSemantics: true, }) as (DSLMethod | DSLController)[]; let exactMatch = false; if (dslApi[0] && dslApi[0].type == DSLEntityType.CONTROLLER) { const name = dslApi[0].name.toLowerCase(); const apiResourceName = Object.keys(apiResources).find((key) => key.indexOf(name) > -1); if (apiResourceName) { const exactResource = apiResources[apiResourceName]; exactResource!.spec.source.value += api + '\n\n'; exactMatch = true; } } if (!exactMatch) { // if we couldn't place the given api on the exact resource we just park it on the first // available rest resource const firstKey = Object.keys(apiResources)[0]; const firstEntry = apiResources[firstKey]; if (firstEntry) { firstEntry.spec.source.value += api + '\n\n'; } else { // this might be ok as we might receive api and types before resources from the ai-service } } }); blockInfo.types.forEach((type) => { blockSpec.entities!.source!.value += type + '\n'; }); if (dbResource) { blockInfo.models.forEach((model) => { dbResource!.spec.source.value += model + '\n'; }); } return blockDefinitionInfo; } private toResourceKind(type: StormResourceType) { //TODO: Handle support for multiple resource types and versions switch (type) { case 'API': return this.options.apiKind; case 'CLIENT': return this.options.clientKind; case 'DATABASE': return this.options.databaseKind; case 'JWTPROVIDER': return this.options.jwtProviderKind; case 'JWTCONSUMER': return this.options.jwtConsumerKind; case 'WEBFRAGMENT': return this.options.webFragmentKind; case 'WEBPAGE': return this.options.webPageKind; case 'SMTPCLIENT': return this.options.smtpKind; case 'EXTERNAL_API': return this.options.externalApiKind; case 'SUBSCRIBER': return this.options.subscriberKind; case 'PUBLISHER': return this.options.publisherKind; case 'QUEUE': return this.options.queueKind; case 'EXCHANGE': return this.options.exchangeKind; } return ''; } private toBlockKind(type: StormBlockType) { switch (type) { case 'BACKEND': return this.options.serviceKind; case 'FRONTEND': return this.options.frontendKind; case 'CLI': return this.options.cliKind; case 'DESKTOP': return this.options.desktopKind; case 'GATEWAY': return this.options.gatewayKind; case 'MQ': return this.options.mqKind; } return ''; } private toConnectionMapping( handle: string, connection: StormConnection, blockDefinitions: { [key: string]: BlockDefinitionInfo }, warn: boolean ): any { if (connection.fromResourceType !== 'API') { return; } const fromRef = StormEventParser.toRef(handle, connection.fromComponent); const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()]; if (!apiProviderBlock) { console.warn('Provider block not found: %s', connection.fromComponent, connection); return; } const apiResource = apiProviderBlock.content.spec.providers?.find((p) => { const pKind = normalizeKapetaUri(p.kind); const apiKind = normalizeKapetaUri(this.options.apiKind); return pKind === apiKind && p.metadata.name === connection.fromResource; }); if (!apiResource) { if (warn) { console.warn( 'API resource not found: %s on %s', connection.fromResource, fromRef.toNormalizedString(), connection ); } return; } const apiMethods = DSLConverters.toSchemaMethods( DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', { ignoreSemantics: true, }) as (DSLMethod | DSLController)[] ); const mapping: any = {}; Object.entries(apiMethods).forEach(([methodId, method]) => { mapping[methodId] = { targetId: methodId, type: 'EXACT', }; }); return mapping; } private toPortType(type: StormResourceType) { switch (type) { case 'API': case 'CLIENT': return 'rest'; case 'JWTPROVIDER': case 'JWTCONSUMER': return 'http'; case 'WEBFRAGMENT': case 'WEBPAGE': return 'web'; case 'SUBSCRIBER': case 'PUBLISHER': case 'QUEUE': case 'EXCHANGE': // PubSub uses http. TODO: Handle support for multiple MQ types return 'http'; case 'SMTPCLIENT': return 'smtp'; case 'DATABASE': //TODO: Handle support for multiple DB types return 'postgres'; } return 'http'; } private toBlockTarget(handle: string, type: StormBlockType): LanguageTargetReference | undefined { const kind = this.toBlockTargetKind(type); if (!kind) { return undefined; } let options: { [key: string]: any } = {}; if (kind.includes('java')) { const groupId = `ai.${StormEventParser.toSafeName(handle)}`; const artifactId = StormEventParser.toSafeArtifactName(this.planName); options = { groupId, artifactId, basePackage: `${groupId}.${artifactId}`, }; } return { kind, options, }; } private toBlockTargetKind(type: StormBlockType): string | undefined { switch (type) { case 'BACKEND': return this.options.serviceLanguage; case 'CLI': return this.options.cliLanguage; case 'FRONTEND': return this.options.systemId ? this.options.htmlLanguage : this.options.frontendLanguage; case 'DESKTOP': return this.options.desktopLanguage; } return undefined; } private async resolveArchetypeBlockDefinition( blockRef: KapetaURI, blockInfo: StormBlockInfoFilled, handle: string ): Promise { const predefinedBlock = PREDEFINED_BLOCKS.get(blockInfo.archetype!); if (!predefinedBlock) { throw new Error('Predefined block not found for archetype [' + blockInfo.archetype + ']'); } const target = this.toBlockTarget(handle, blockInfo.type); const blockDefinition = await predefinedBlock.getBlockDefinition(); _.set(blockDefinition!, ['metadata', 'name'], blockRef.fullName); _.set(blockDefinition!, ['metadata', 'title'], blockRef.name); _.set(blockDefinition!, ['spec', 'target', 'options'], target?.options); const options: StormOptions = this.options; function getKind(kind: string): string | undefined { for (const prop in options) { if (options.hasOwnProperty(prop)) { const value = options[prop]; if (typeof value === 'string' && value.indexOf(kind) > 0) { return value; } } } } blockDefinition!.kind = getKind(parseKapetaUri(blockDefinition!.kind).fullName) ?? blockDefinition!.kind; for (const provider of blockDefinition!.spec.providers ?? []) { provider.kind = getKind(parseKapetaUri(provider.kind).fullName) ?? provider.kind; } for (const consumer of blockDefinition!.spec.consumers ?? []) { consumer.kind = getKind(parseKapetaUri(consumer.kind).fullName) ?? consumer.kind; } blockDefinition!.spec.target!.kind = getKind(parseKapetaUri(blockDefinition!.spec.target!.kind).fullName) ?? blockDefinition!.spec.target!.kind; return { uri: blockRef.toNormalizedString(), aiName: blockInfo.name, content: blockDefinition, archetype: blockInfo.archetype, } as BlockDefinitionInfo; } }