From 321b5eac3a79c1093017317bd9d500d66e047d96 Mon Sep 17 00:00:00 2001 From: glc-omid Date: Fri, 27 Sep 2024 21:59:41 +0200 Subject: [PATCH 1/3] fix(plugin): throw an explicit error message if service id is missing --- src/index.ts | 13 +++++++++++++ src/test/plugin.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/index.ts b/src/index.ts index cd0d0b6..e4b5a8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,18 @@ type Props = { debug?: boolean; }; +const validateOptions = (options: Props) => { + if (!options.services) { + throw new Error('Please provide correct services configuration'); + } + if (options.services.some((service) => !service.id)) { + throw new Error('The service id is required. please provide a service id'); + } + if (options.services.some((service) => !service.path)) { + throw new Error('The service path is required. please provide the path to specification file'); + } +}; + export default async (_: any, options: Props) => { if (!process.env.PROJECT_DIR) { throw new Error('Please provide catalog url (env variable PROJECT_DIR)'); @@ -37,6 +49,7 @@ export default async (_: any, options: Props) => { } = utils(process.env.PROJECT_DIR); const services = options.services ?? []; + validateOptions(options); for (const serviceSpec of services) { console.log(chalk.green(`Processing ${serviceSpec.path}`)); diff --git a/src/test/plugin.test.ts b/src/test/plugin.test.ts index 780b5b1..dfeeb81 100644 --- a/src/test/plugin.test.ts +++ b/src/test/plugin.test.ts @@ -510,6 +510,33 @@ describe('OpenAPI EventCatalog Plugin', () => { expect(service).toBeDefined(); }); + + it('[id] if the `id` not provided in the service config options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { + services: [ + { + path: join(openAPIExamples, 'petstore.yml'), + } as any, + ], + }) + ).rejects.toThrow('The service id is required'); + }); + it('[services] if the `services` not provided in options, The generator throw an explicit error', async () => { + await expect(plugin(config, {} as any)).rejects.toThrow('Please provide correct services configuration'); + }); + it('[path] if the `path` not provided in service config options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { + services: [ + { + name: 'Awesome account service', + id: 'awsome-service', + } as any, + ], + }) + ).rejects.toThrow('The service path is required. please provide the path to specification file'); + }); }); }); }); From 8a037330321708c806ae3a0ba1cd0f66a150ea9d Mon Sep 17 00:00:00 2001 From: glc-omid Date: Tue, 1 Oct 2024 00:48:22 +0200 Subject: [PATCH 2/3] feat([openapi]): add flag to let persisting parsed spec version or original file --- package.json | 2 ++ pnpm-lock.yaml | 34 ++++++++++++++++++++++++++++----- src/index.ts | 27 +++++++++++++++++++++++--- src/test/plugin.test.ts | 42 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 59528ad..3f1fc26 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "license": "ISC", "devDependencies": { "@types/fs-extra": "^11.0.4", + "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.17.7", "@types/node": "^20.16.1", "prettier": "^3.3.3", @@ -37,6 +38,7 @@ "@changesets/cli": "^2.27.7", "@eventcatalog/sdk": "^0.1.4", "chalk": "^4", + "js-yaml": "^4.1.0", "openapi-types": "^12.1.3", "slugify": "^1.6.6" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bc6ed2..8943522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,14 @@ importers: specifier: ^2.27.7 version: 2.27.7 '@eventcatalog/sdk': - specifier: ^0.1.4 - version: 0.1.4 + specifier: ^0.1.3 + version: 0.1.3 chalk: specifier: ^4 version: 4.1.2 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -30,6 +33,9 @@ importers: '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/lodash': specifier: ^4.17.7 version: 4.17.7 @@ -411,8 +417,8 @@ packages: cpu: [x64] os: [win32] - '@eventcatalog/sdk@0.1.4': - resolution: {integrity: sha512-f+M2xM7sUBof/o1/QsnD0TkpzquMsLFQUXItZhCoFllQqWYTuwtGaMuJPWeSxi1nXA4eAY8PSK4kozzpcRfSeQ==} + '@eventcatalog/sdk@0.1.3': + resolution: {integrity: sha512-eZ6ehiIrEuROMpJfD++Hea8l6NgNwtadi1BjNhDPx1CTgJBJNlNRNkJKwFyWlUqiyBxgMmR32AofpYCB9nCIsg==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -547,6 +553,9 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} @@ -625,6 +634,9 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -963,6 +975,10 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1878,7 +1894,7 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@eventcatalog/sdk@0.1.4': + '@eventcatalog/sdk@0.1.3': dependencies: '@changesets/cli': 2.27.7 fs-extra: 11.2.0 @@ -2000,6 +2016,8 @@ snapshots: '@types/jsonfile': 6.1.4 '@types/node': 20.16.3 + '@types/js-yaml@4.0.9': {} + '@types/jsonfile@6.1.4': dependencies: '@types/node': 20.16.3 @@ -2085,6 +2103,8 @@ snapshots: dependencies: sprintf-js: 1.0.3 + argparse@2.0.1: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -2469,6 +2489,10 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + json-schema-traverse@1.0.0: {} jsonfile@4.0.0: diff --git a/src/index.ts b/src/index.ts index f025018..bc3e698 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,11 +11,13 @@ import { Domain, Service } from './types'; import { getMessageTypeUtils } from './utils/catalog-shorthand'; import { OpenAPI } from 'openapi-types'; import checkLicense from './utils/checkLicense'; +import yaml from 'js-yaml'; type Props = { services: Service[]; domain?: Domain; debug?: boolean; + saveParsedSpecFile?: boolean; }; const validateOptions = (options: Props) => { @@ -48,7 +50,7 @@ export default async (_: any, options: Props) => { getSpecificationFilesForService, } = utils(process.env.PROJECT_DIR); - const services = options.services ?? []; + const { services = [], saveParsedSpecFile = false } = options; validateOptions(options); for (const serviceSpec of services) { console.log(chalk.green(`Processing ${serviceSpec.path}`)); @@ -61,7 +63,6 @@ export default async (_: any, options: Props) => { continue; } - const openAPIFile = await readFile(serviceSpec.path, 'utf-8'); const document = await SwaggerParser.parse(serviceSpec.path); const version = document.info.version; @@ -151,7 +152,7 @@ export default async (_: any, options: Props) => { // add any previous spec files to the list ...serviceSpecificationsFiles, { - content: openAPIFile, + content: saveParsedSpecFile ? getParsedSpecFile(serviceSpec, document) : await getRawSpecFile(serviceSpec), fileName: service.schemaPath, }, ]; @@ -248,3 +249,23 @@ const processMessagesForOpenAPISpec = async (pathToSpec: string, document: OpenA } return { receives, sends: [] }; }; + +const getParsedSpecFile = (service: Service, document: OpenAPI.Document) => { + const isSpecFileJSON = service.path.endsWith('.json'); + return isSpecFileJSON ? JSON.stringify(document, null, 4) : yaml.dump(document, { noRefs: true }); +}; + +const getRawSpecFile = async (service: Service) => await readFile(service.path, 'utf8'); + +const uniqueMessages = (messages: { id: string; version: string }[]): { id: string; version: string }[] => { + const uniqueSet = new Set(); + + return messages.filter((message) => { + const key = `${message.id}-${message.version}`; + if (!uniqueSet.has(key)) { + uniqueSet.add(key); + return true; + } + return false; + }); +}; diff --git a/src/test/plugin.test.ts b/src/test/plugin.test.ts index dfeeb81..5ffe29a 100644 --- a/src/test/plugin.test.ts +++ b/src/test/plugin.test.ts @@ -276,6 +276,48 @@ describe('OpenAPI EventCatalog Plugin', () => { expect(schema).toBeDefined(); }); + it('the original openapi file is added to the service by default instead of parsed version', async () => { + const { getService } = utils(catalogDir); + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const service = await getService('swagger-petstore', '1.0.0'); + + expect(service.schemaPath).toEqual('petstore.yml'); + + const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8'); + expect(schema).toBeDefined(); + }); + + it('the original openapi file is added to the service instead of parsed version', async () => { + const { getService } = utils(catalogDir); + await plugin(config, { + services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }], + saveParsedSpecFile: false, + }); + + const service = await getService('swagger-petstore', '1.0.0'); + + expect(service.schemaPath).toEqual('petstore.yml'); + + const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8'); + expect(schema).toBeDefined(); + }); + + it('the original openapi file is not added but the parsed version', async () => { + const { getService } = utils(catalogDir); + await plugin(config, { + services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }], + saveParsedSpecFile: true, + }); + + const service = await getService('swagger-petstore', '1.0.0'); + + expect(service.schemaPath).toEqual('petstore.yml'); + + const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8'); + expect(schema).toBeDefined(); + }); + it('the openapi file is added to the specifications list in eventcatalog', async () => { const { getService, writeService } = utils(catalogDir); From a33ae17b43a30711e24f7f97f2bfa75912d20e0c Mon Sep 17 00:00:00 2001 From: glc-omid Date: Fri, 4 Oct 2024 22:32:11 +0200 Subject: [PATCH 3/3] fix([config-validation]): generator config validation using zod --- package.json | 3 ++- pnpm-lock.yaml | 18 +++++++++---- src/index.ts | 58 ++++++++++++++++++++--------------------- src/test/plugin.test.ts | 44 +++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 3f1fc26..f8378b8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "chalk": "^4", "js-yaml": "^4.1.0", "openapi-types": "^12.1.3", - "slugify": "^1.6.6" + "slugify": "^1.6.6", + "zod": "^3.23.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8943522..13d0d46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^2.27.7 version: 2.27.7 '@eventcatalog/sdk': - specifier: ^0.1.3 - version: 0.1.3 + specifier: ^0.1.4 + version: 0.1.4 chalk: specifier: ^4 version: 4.1.2 @@ -29,6 +29,9 @@ importers: slugify: specifier: ^1.6.6 version: 1.6.6 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@types/fs-extra': specifier: ^11.0.4 @@ -417,8 +420,8 @@ packages: cpu: [x64] os: [win32] - '@eventcatalog/sdk@0.1.3': - resolution: {integrity: sha512-eZ6ehiIrEuROMpJfD++Hea8l6NgNwtadi1BjNhDPx1CTgJBJNlNRNkJKwFyWlUqiyBxgMmR32AofpYCB9nCIsg==} + '@eventcatalog/sdk@0.1.4': + resolution: {integrity: sha512-f+M2xM7sUBof/o1/QsnD0TkpzquMsLFQUXItZhCoFllQqWYTuwtGaMuJPWeSxi1nXA4eAY8PSK4kozzpcRfSeQ==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -1565,6 +1568,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + snapshots: '@ampproject/remapping@2.3.0': @@ -1894,7 +1900,7 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@eventcatalog/sdk@0.1.3': + '@eventcatalog/sdk@0.1.4': dependencies: '@changesets/cli': 2.27.7 fs-extra: 11.2.0 @@ -3008,3 +3014,5 @@ snapshots: yallist@2.1.2: {} yocto-queue@0.1.0: {} + + zod@3.23.8: {} diff --git a/src/index.ts b/src/index.ts index bc3e698..766baf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,31 +7,42 @@ import { defaultMarkdown as generateMarkdownForDomain } from './utils/domains'; import { buildService } from './utils/services'; import { buildMessage } from './utils/messages'; import { getOperationsByType } from './utils/openapi'; -import { Domain, Service } from './types'; +import { Service } from './types'; import { getMessageTypeUtils } from './utils/catalog-shorthand'; import { OpenAPI } from 'openapi-types'; import checkLicense from './utils/checkLicense'; import yaml from 'js-yaml'; - -type Props = { - services: Service[]; - domain?: Domain; - debug?: boolean; - saveParsedSpecFile?: boolean; -}; +import { z } from 'zod'; + +const optionsSchema = z.object({ + services: z.array( + z.object({ + id: z.string({ required_error: 'The service id is required. please provide the service id' }), + path: z.string({ required_error: 'The service path is required. please provide the path to specification file' }), + name: z.string().optional(), + }), + { message: 'Please provide correct services configuration' } + ), + domain: z + .object({ + id: z.string({ required_error: 'The domain id is required. please provide a domain id' }), + name: z.string({ required_error: 'The domain name is required. please provide a domain name' }), + version: z.string({ required_error: 'The domain version is required. please provide a domain version' }), + }) + .optional(), + debug: z.boolean().optional(), + saveParsedSpecFile: z.boolean({ invalid_type_error: 'The saveParsedSpecFile is not a boolean in options' }).optional(), +}); + +type Props = z.infer; const validateOptions = (options: Props) => { - if (!options.services) { - throw new Error('Please provide correct services configuration'); - } - if (options.services.some((service) => !service.id)) { - throw new Error('The service id is required. please provide a service id'); - } - if (options.services.some((service) => !service.path)) { - throw new Error('The service path is required. please provide the path to specification file'); + try { + optionsSchema.parse(options); + } catch (error: any) { + if (error instanceof z.ZodError) throw new Error(JSON.stringify(error.issues, null, 2)); } }; - export default async (_: any, options: Props) => { if (!process.env.PROJECT_DIR) { throw new Error('Please provide catalog url (env variable PROJECT_DIR)'); @@ -256,16 +267,3 @@ const getParsedSpecFile = (service: Service, document: OpenAPI.Document) => { }; const getRawSpecFile = async (service: Service) => await readFile(service.path, 'utf8'); - -const uniqueMessages = (messages: { id: string; version: string }[]): { id: string; version: string }[] => { - const uniqueSet = new Set(); - - return messages.filter((message) => { - const key = `${message.id}-${message.version}`; - if (!uniqueSet.has(key)) { - uniqueSet.add(key); - return true; - } - return false; - }); -}; diff --git a/src/test/plugin.test.ts b/src/test/plugin.test.ts index 5ffe29a..719e845 100644 --- a/src/test/plugin.test.ts +++ b/src/test/plugin.test.ts @@ -567,6 +567,21 @@ describe('OpenAPI EventCatalog Plugin', () => { it('[services] if the `services` not provided in options, The generator throw an explicit error', async () => { await expect(plugin(config, {} as any)).rejects.toThrow('Please provide correct services configuration'); }); + it('[services] if the `services` is undefiend in options, The generator throw an explicit error', async () => { + await expect(plugin(config, { services: undefined } as any)).rejects.toThrow( + 'Please provide correct services configuration' + ); + }); + it('[services::path] if the `services::path` not provided in options, The generator throw an explicit error', async () => { + await expect(plugin(config, { services: [{ id: 'service_id' }] } as any)).rejects.toThrow( + 'The service path is required. please provide the path to specification file' + ); + }); + it('[services::id] if the `services::id` not provided in options, The generator throw an explicit error', async () => { + await expect(plugin(config, { services: [{ path: 'path/to/spec' }] } as any)).rejects.toThrow( + 'The service id is required. please provide the service id' + ); + }); it('[path] if the `path` not provided in service config options, The generator throw an explicit error', async () => { await expect( plugin(config, { @@ -579,6 +594,35 @@ describe('OpenAPI EventCatalog Plugin', () => { }) ).rejects.toThrow('The service path is required. please provide the path to specification file'); }); + it('[services::saveParsedSpecFile] if the `services::saveParsedSpecFile` not a boolean in options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { services: [{ path: 'path/to/spec', id: 'sevice_id' }], saveParsedSpecFile: 'true' } as any) + ).rejects.toThrow('The saveParsedSpecFile is not a boolean in options'); + }); + it('[domain::id] if the `domain::id` not provided in options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { + domain: { name: 'domain_name', version: '1.0.0' }, + services: [{ path: 'path/to/spec', id: 'sevice_id' }], + } as any) + ).rejects.toThrow('The domain id is required. please provide a domain id'); + }); + it('[domain::name] if the `domain::name` not provided in options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { + domain: { id: 'domain_name', version: '1.0.0' }, + services: [{ path: 'path/to/spec', id: 'sevice_id' }], + } as any) + ).rejects.toThrow('The domain name is required. please provide a domain name'); + }); + it('[domain::version] if the `domain::version` not provided in options, The generator throw an explicit error', async () => { + await expect( + plugin(config, { + domain: { id: 'domain_name', name: 'domain_name' }, + services: [{ path: 'path/to/spec', id: 'sevice_id' }], + } as any) + ).rejects.toThrow('The domain version is required. please provide a domain version'); + }); }); }); });