diff --git a/.changeset/big-poets-decide.md b/.changeset/big-poets-decide.md new file mode 100644 index 00000000..600c0d5d --- /dev/null +++ b/.changeset/big-poets-decide.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": minor +--- + +feat(core): added ability to specify dependencies for catalogs diff --git a/.gitignore b/.gitignore index 50a011f2..a5bf17bf 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ git-push.sh /playwright-report/ /blob-report/ /playwright/.cache/ + +src/__tests__/example-catalog-dependencies/dependencies \ No newline at end of file diff --git a/examples/default/package.json b/examples/default/package.json new file mode 100644 index 00000000..4bdf7e71 --- /dev/null +++ b/examples/default/package.json @@ -0,0 +1,6 @@ +{ + "name": "default-example-catalog", + "version": "0.1.0", + "private": true + } + \ No newline at end of file diff --git a/src/__tests__/example-catalog-dependencies/eventcatalog.config.js b/src/__tests__/example-catalog-dependencies/eventcatalog.config.js new file mode 100644 index 00000000..80155ef0 --- /dev/null +++ b/src/__tests__/example-catalog-dependencies/eventcatalog.config.js @@ -0,0 +1,79 @@ +import path from 'path'; +import url from 'url'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +/** @type {import('@eventcatalog/core/bin/eventcatalog.config').Config} */ +export default { + title: 'OurLogix', + tagline: 'A comprehensive logistics and shipping management company', + organizationName: 'OurLogix', + homepageLink: 'https://eventcatalog.dev/', + landingPage: '', + editUrl: 'https://github.com/boyney123/eventcatalog-demo/edit/master', + // By default set to false, add true to get urls ending in / + trailingSlash: false, + // Change to make the base url of the site different, by default https://{website}.com/docs, + // changing to /company would be https://{website}.com/company/docs, + base: '/company', + // Customize the logo, add your logo to public/ folder + logo: { + alt: 'EventCatalog Logo', + src: '/logo.png', + text: 'OurLogix', + }, + docs: { + sidebar: { + // Should the sub heading be rendered in the docs sidebar? + showPageHeadings: true, + }, + }, + dependencies: { + events: [ + { id: 'TestingEventOrder', version: '5.0.0' } + ], + services: [ + { id: 'TestingServiceOrder', version: '5.0.0' } + ], + domains: [ + { id: 'TestingDomainOrder', version: '5.0.0' } + ], + commands: [ + { id: 'TestingCommandOrder', version: '5.0.0' } + ], + queries: [ + { id: 'TestingQueryOrder', version: '5.0.0' } + ] + }, + generators: [ + [ + '@eventcatalog/generator-asyncapi', + { + services: [ + { path: path.join(__dirname, 'asyncapi-files', 'orders-service.yml') }, + { path: path.join(__dirname, 'asyncapi-files', 'order-fulfillment-service.yml') }, + { path: path.join(__dirname, 'asyncapi-files', 'inventory-service.yml') }, + ], + domain: { id: 'orders', name: 'Orders', version: '0.0.1' }, + }, + ], + [ + '@eventcatalog/generator-asyncapi', + { + services: [ + { path: path.join(__dirname, 'asyncapi-files', 'payment-service.yml') }, + { path: path.join(__dirname, 'asyncapi-files', 'fraud-detection-service.yml') }, + ], + domain: { id: 'payment', name: 'Payment', version: '0.0.1' }, + }, + ], + [ + '@eventcatalog/generator-asyncapi', + { + services: [{ path: path.join(__dirname, 'asyncapi-files', 'user-service.yml') }], + domain: { id: 'users', name: 'User', version: '0.0.1' }, + debug: true, + }, + ], + ], +}; diff --git a/src/__tests__/example-catalog-dependencies/package.json b/src/__tests__/example-catalog-dependencies/package.json new file mode 100644 index 00000000..128dd603 --- /dev/null +++ b/src/__tests__/example-catalog-dependencies/package.json @@ -0,0 +1,4 @@ +{ + "name": "my-catalog", + "version": "0.1.0" +} diff --git a/src/__tests__/resolve-catalog-dependencies.spec.ts b/src/__tests__/resolve-catalog-dependencies.spec.ts new file mode 100644 index 00000000..ac6d03bc --- /dev/null +++ b/src/__tests__/resolve-catalog-dependencies.spec.ts @@ -0,0 +1,94 @@ +import resolveCatalogDependencies from '../resolve-catalog-dependencies'; +import * as path from 'path'; +import fs from 'fs/promises'; +import { existsSync } from 'fs'; +// import * as fs from 'fs-extra'; + +const TMP_DIRECTORY = path.join(__dirname, 'tmp'); +const ASTRO_OUTPUT = path.join(TMP_DIRECTORY); +const ASTRO_CONTENT_DIRECTORY = path.join(TMP_DIRECTORY, 'src', 'content'); +// const OUTPUT_EXTERNAL_FILES = path.join(TMP_DIRECTORY, 'catalog-files'); +const CATALOG_DIR = path.join(__dirname, 'example-catalog-dependencies'); + +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; + +describe('resolve-catalog-dependencies', () => { + beforeAll(async () => { + await fs.mkdir(TMP_DIRECTORY, { recursive: true }); + await fs.mkdir(ASTRO_OUTPUT, { recursive: true }); + // create src/content inside astro directory + await fs.mkdir(ASTRO_CONTENT_DIRECTORY, { recursive: true }); + await fs.writeFile(path.join(ASTRO_CONTENT_DIRECTORY, 'config.ts'), 'export const config = {};'); + }); + + afterAll(async () => { + await fs.rm(TMP_DIRECTORY, { recursive: true }); + }); + + describe('dependencies', () => { + it('if the dependencies are set in the catalog config, it creates a dependencies directory', async () => { + await resolveCatalogDependencies(CATALOG_DIR, ASTRO_OUTPUT); + expect(existsSync(path.join(CATALOG_DIR, 'dependencies'))).toBe(true); + }); + + describe('events', () => { + it('creates a dependency file for each event', async () => { + const content = await fs.readFile( + path.join(CATALOG_DIR, 'dependencies', 'events', 'TestingEventOrder', 'index.md'), + 'utf8' + ); + expect(content).toContain('id: TestingEventOrder'); + expect(content).toContain('name: TestingEventOrder'); + expect(content).toContain('version: 5.0.0'); + }); + }); + + describe('services', () => { + it('creates a dependency file for each service', async () => { + const content = await fs.readFile( + path.join(CATALOG_DIR, 'dependencies', 'services', 'TestingServiceOrder', 'index.md'), + 'utf8' + ); + expect(content).toContain('id: TestingServiceOrder'); + expect(content).toContain('name: TestingServiceOrder'); + expect(content).toContain('version: 5.0.0'); + }); + }); + + describe('domains', () => { + it('creates a dependency file for each domain', async () => { + const content = await fs.readFile( + path.join(CATALOG_DIR, 'dependencies', 'domains', 'TestingDomainOrder', 'index.md'), + 'utf8' + ); + expect(content).toContain('id: TestingDomainOrder'); + expect(content).toContain('name: TestingDomainOrder'); + expect(content).toContain('version: 5.0.0'); + }); + }); + + describe('commands', () => { + it('creates a dependency file for each command', async () => { + const content = await fs.readFile( + path.join(CATALOG_DIR, 'dependencies', 'commands', 'TestingCommandOrder', 'index.md'), + 'utf8' + ); + expect(content).toContain('id: TestingCommandOrder'); + expect(content).toContain('name: TestingCommandOrder'); + expect(content).toContain('version: 5.0.0'); + }); + }); + + describe('queries', () => { + it('creates a dependency file for each query', async () => { + const content = await fs.readFile( + path.join(CATALOG_DIR, 'dependencies', 'queries', 'TestingQueryOrder', 'index.md'), + 'utf8' + ); + expect(content).toContain('id: TestingQueryOrder'); + expect(content).toContain('name: TestingQueryOrder'); + expect(content).toContain('version: 5.0.0'); + }); + }); + }); +}); diff --git a/src/eventcatalog.config.ts b/src/eventcatalog.config.ts index b7bea3f9..dfafa0c9 100644 --- a/src/eventcatalog.config.ts +++ b/src/eventcatalog.config.ts @@ -2,6 +2,11 @@ type SideBarConfig = { visible: boolean; }; +type ResourceDependency = { + id: string; + version?: string; +}; + export interface Config { title: string; tagline: false; @@ -31,4 +36,10 @@ export interface Config { users?: SideBarConfig; }; }; + dependencies?: { + commands?: ResourceDependency[]; + events?: ResourceDependency[]; + services?: ResourceDependency[]; + domains?: ResourceDependency[]; + }; } diff --git a/src/eventcatalog.ts b/src/eventcatalog.ts index 36e0e6ca..40d91267 100755 --- a/src/eventcatalog.ts +++ b/src/eventcatalog.ts @@ -10,6 +10,7 @@ import logBuild from './analytics/log-build'; import { VERSION } from './constants'; import { watch } from './watcher'; import { catalogToAstro } from './catalog-to-astro-content-directory'; +import resolveCatalogDependencies from './resolve-catalog-dependencies'; const currentDir = path.dirname(fileURLToPath(import.meta.url)); @@ -78,6 +79,7 @@ program console.log('EventCatalog is starting at http://localhost:3000/docs'); + await resolveCatalogDependencies(dir, core); await catalogToAstro(dir, core); let watchUnsub; @@ -114,6 +116,7 @@ program await logBuild(dir); + await resolveCatalogDependencies(dir, core); await catalogToAstro(dir, core); execSync(`cross-env PROJECT_DIR='${dir}' CATALOG_DIR='${core}' npx astro build ${command.args.join(' ').trim()}`, { diff --git a/src/map-catalog-to-astro.js b/src/map-catalog-to-astro.js index 3c6ea00f..7860d37a 100644 --- a/src/map-catalog-to-astro.js +++ b/src/map-catalog-to-astro.js @@ -13,6 +13,7 @@ const COLLECTION_KEYS = [ 'queries', 'channels', 'ubiquitousLanguages', + 'dependencies', ]; /** diff --git a/src/resolve-catalog-dependencies.js b/src/resolve-catalog-dependencies.js new file mode 100644 index 00000000..2043dbc7 --- /dev/null +++ b/src/resolve-catalog-dependencies.js @@ -0,0 +1,58 @@ +import { getEventCatalogConfigFile } from './eventcatalog-config-file-utils'; +import path from 'node:path'; +import fs from 'node:fs'; + +// Create a fake file for this dependency in the project directory () + +export default async (catalogDir, core) => { + const catalogConfig = await getEventCatalogConfigFile(catalogDir); + + const dependencies = catalogConfig?.dependencies ?? null; + + if (!dependencies) { + // No dependencies found in catalog config just skip + return; + } + + // empty the dependencies directory if it exists + const dependenciesDir = path.join(catalogDir, 'dependencies'); + + if (fs.existsSync(dependenciesDir)) { + fs.rmSync(dependenciesDir, { recursive: true, force: true }); + } + + // Create a "dependencies" directory in the catalogDir + fs.mkdirSync(dependenciesDir, { recursive: true }); + + const resourceTypes = Object.keys(dependencies); + for (const resourceType of resourceTypes) { + for (const dependency of dependencies[resourceType]) { + const resource = { + id: dependency.id, + version: dependency.version || '1.0.0', + }; + + const markdown = `--- +id: ${resource.id} +name: ${resource.id} +version: ${resource.version} +--- + +:::warning + +You are running EventCatalog with dependencies enabled. + +This resource is mocked and is a dependency. This means that the resource is managed and owned by another catalog. +::: + +`; + + const resourceFile = path.join(dependenciesDir, resourceType, resource.id, `index.md`); + // ensure the directory exists + fs.mkdirSync(path.dirname(resourceFile), { recursive: true }); + fs.writeFileSync(resourceFile, markdown); + } + } + + return; +};