diff --git a/package.json b/package.json index 2e478c8..f82a862 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/lodash": "^4.17.7", "@types/minimist": "^1.2.5", "@types/node": "^20.16.1", + "@types/semver": "^7.5.8", "prettier": "^3.3.3", "tsup": "^8.1.0", "typescript": "^5.5.3", @@ -46,6 +47,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", + "semver": "^7.6.3", "slugify": "^1.6.6", "zod": "^3.23.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dc0b01..9ba042c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: minimist: specifier: ^1.2.8 version: 1.2.8 + semver: + specifier: ^7.6.3 + version: 7.6.3 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -63,6 +66,9 @@ importers: '@types/node': specifier: ^20.16.1 version: 20.16.9 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -676,6 +682,9 @@ packages: '@types/node@20.16.9': resolution: {integrity: sha512-rkvIVJxsOfBejxK7I0FO5sa2WxFmJCzoDwcd88+fq/CUfynNywTo/1/T6hyFz22CyztsnLS9nVlHOnTI36RH5w==} + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/urijs@1.19.25': resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==} @@ -2614,6 +2623,8 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/semver@7.5.8': {} + '@types/urijs@1.19.25': {} '@vitest/expect@2.1.1': diff --git a/src/index.ts b/src/index.ts index c1a9d19..77517fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { z } from 'zod'; import { AvroSchemaParser } from '@asyncapi/avro-schema-parser'; import path from 'path'; import { EventType, MessageOperations } from './types'; +import * as semver from 'semver'; const parser = new Parser(); @@ -67,12 +68,13 @@ export default async (config: any, options: Props) => { const { writeService, + writeVersionedService, writeEvent, writeCommand, writeQuery, getService, versionService, - rmService, + rmServiceById, getDomain, writeDomain, addServiceToDomain, @@ -151,10 +153,10 @@ export default async (config: any, options: Props) => { // What messages does this service send and receive let sends = []; let receives = []; - - let serviceSpecifications = {}; - let serviceSpecificationsFiles = []; - let serviceMarkdown = generateMarkdownForService(document); + let specifications = {}; + let specFiles = []; + let markdown = generateMarkdownForService(document); + let isOldVersion = false; // Manage domain if (options.domain) { @@ -273,63 +275,73 @@ export default async (config: any, options: Props) => { // Check if service is already defined... if the versions do not match then create service. const latestServiceInCatalog = await getService(serviceId, 'latest'); + const existingVersionInCatalog = await getService(serviceId, version); console.log(chalk.blue(`Processing service: ${serviceId} (v${version})`)); + // Found a service, and versions do not match, we need to version the one already there if (latestServiceInCatalog) { - serviceMarkdown = latestServiceInCatalog.markdown; - // Found a service, and versions do not match, we need to version the one already there - if (latestServiceInCatalog.version !== version) { - await versionService(serviceId); + if (isHigherVersion(version, latestServiceInCatalog.version)) { + await versionService(service.id); console.log(chalk.cyan(` - Versioned previous service (v${latestServiceInCatalog.version})`)); + } else { + isOldVersion = true; + console.log( + chalk.yellow(` - Previous Service (v${version}) detected over newer version ${latestServiceInCatalog.version}...`) + ); } + } + + if (existingVersionInCatalog) { + markdown = existingVersionInCatalog.markdown; + specFiles = await getSpecificationFilesForService(service.id, version); + sends = [...(existingVersionInCatalog.sends ?? []), ...sends]; + receives = [...(existingVersionInCatalog.receives ?? []), ...receives]; + + // persist any specifications that are already in the catalog + specifications = { + ...specifications, + ...existingVersionInCatalog.specifications, + }; // Match found, override it - if (latestServiceInCatalog.version === version) { - // we want to preserve the markdown any any spec files that are already there - serviceMarkdown = latestServiceInCatalog.markdown; - serviceSpecifications = latestServiceInCatalog.specifications ?? {}; - sends = latestServiceInCatalog.sends ? [...latestServiceInCatalog.sends, ...sends] : sends; - receives = latestServiceInCatalog.receives ? [...latestServiceInCatalog.receives, ...receives] : receives; - serviceSpecificationsFiles = await getSpecificationFilesForService(serviceId, version); - await rmService(serviceId); - } + await rmServiceById(serviceId, version); } const fileName = path.basename(service.path); - - await writeService({ + const choosenWriteServiceActtion = isOldVersion ? writeVersionedService : writeService; + await choosenWriteServiceActtion({ id: serviceId, name: serviceName, version: version, summary: getServiceSummary(document), badges: documentTags.map((tag) => ({ content: tag.name(), textColor: 'blue', backgroundColor: 'blue' })), - markdown: serviceMarkdown, + markdown: markdown, sends, receives, schemaPath: fileName || 'asyncapi.yml', specifications: { - ...serviceSpecifications, + ...specifications, asyncapiPath: fileName || 'asyncapi.yml', }, }); // What files need added to the service (speficiation files) - const specFiles = [ + const existingSpecFiles = [ // add any previous spec files to the list - ...serviceSpecificationsFiles, + ...specFiles, { content: saveParsedSpecFile ? getParsedSpecFile(service, document) : await getRawSpecFile(service), fileName: path.basename(service.path) || 'asyncapi.yml', }, ]; - for (const specFile of specFiles) { + for (const spec of existingSpecFiles) { await addFileToService( serviceId, { - fileName: specFile.fileName, - content: specFile.content, + fileName: spec.fileName, + content: spec.content, }, version ); @@ -363,3 +375,7 @@ const getRawSpecFile = async (service: Service) => { return await readFile(service.path, 'utf8'); } }; + +function isHigherVersion(sourceVersion: string, targetVersion: string) { + return semver.gt(sourceVersion, targetVersion); +} diff --git a/src/test/plugin.test.ts b/src/test/plugin.test.ts index 2907dda..1265839 100644 --- a/src/test/plugin.test.ts +++ b/src/test/plugin.test.ts @@ -516,6 +516,68 @@ describe('AsyncAPI EventCatalog Plugin', () => { asyncapiPath: 'simple.asyncapi.yml', }); }); + + it('if the service already has higher version the new one with lowert version will be created as versioned', async () => { + const { getService, writeService } = utils(catalogDir); + + await writeService( + { + id: 'account-service', + version: '2.0.0', + name: 'account-service', + markdown: 'My content', + }, + { path: 'account-service' } + ); + + await plugin(config, { services: [{ path: join(asyncAPIExamplesDir, 'simple.asyncapi.yml'), id: 'account-service' }] }); + + const latestservice = await getService('account-service', '2.0.0'); + const serviceMarkdown = ( + await fs.readFile(join(catalogDir, 'services', 'account-service', 'versioned', '1.0.0', 'index.md'), 'utf8') + ).toString(); + expect(latestservice).toBeDefined(); + expect(serviceMarkdown).toBeDefined(); + expect(serviceMarkdown).toContain('version: 1.0.0'); + }); + + it('if the service version already exists enrich the service with the new one new service', async () => { + const { getService, writeService } = utils(catalogDir); + + await writeService( + { + id: 'account-service', + version: '2.0.0', + name: 'account-service', + markdown: 'My content', + sends: [{ id: 'strange-event', version: '2.0.0' }], + }, + { path: 'account-service' } + ); + + await writeService( + { + id: 'account-service', + version: '1.0.0', + name: 'account-service', + markdown: 'My content', + sends: [{ id: 'strange-event', version: '1.0.0' }], + }, + { path: 'account-service/versioned/1.0.0' } + ); + + await plugin(config, { services: [{ path: join(asyncAPIExamplesDir, 'simple.asyncapi.yml'), id: 'account-service' }] }); + + const service = await getService('account-service', '1.0.0'); + const latestservice = await getService('account-service', '2.0.0'); + + expect(service).toBeDefined(); + expect(latestservice).toBeDefined(); + + expect(service.sends?.length).toBe(3); + expect(service.receives?.length).toBe(3); + expect(service.markdown).toBe('My content'); + }); }); describe('generator options', () => {