From eae41e67f2eba956854dc173f5ae01975265b030 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Thu, 18 Jul 2024 15:29:44 +0100 Subject: [PATCH 1/3] feat(sdk): added commands to sdk --- src/docs.ts | 1 + src/events.ts | 6 ++--- src/index.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ src/services.ts | 4 +-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/docs.ts b/src/docs.ts index 8e3a5bd..dadf61a 100644 --- a/src/docs.ts +++ b/src/docs.ts @@ -27,3 +27,4 @@ */ export * from './events'; export * from './services'; +export * from './commands'; diff --git a/src/events.ts b/src/events.ts index 12a32fd..5a48f1b 100644 --- a/src/events.ts +++ b/src/events.ts @@ -18,10 +18,10 @@ import { addFileToResource, getResource, rmResourceById, versionResource, writeR * const { getEvent } = utils('/path/to/eventcatalog'); * * // Gets the latest version of the event - * cont event = await getEvent('InventoryAdjusted'); + * const event = await getEvent('InventoryAdjusted'); * * // Gets a version of the event - * cont event = await getEvent('InventoryAdjusted', '0.0.1'); + * const event = await getEvent('InventoryAdjusted', '0.0.1'); * ``` */ export const getEvent = @@ -151,7 +151,7 @@ export const addFileToEvent = /** * Add a schema to an event by it's id. * - * Optionally specify a version to add a schame to a specific version of the event. + * Optionally specify a version to add a schema to a specific version of the event. * * @example * ```ts diff --git a/src/index.ts b/src/index.ts index 0d2a09a..3577ccd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,14 @@ import { join } from 'node:path'; import { rmEvent, rmEventById, writeEvent, versionEvent, getEvent, addFileToEvent, addSchemaToEvent } from './events'; +import { + rmCommand, + rmCommandById, + writeCommand, + versionCommand, + getCommand, + addFileToCommand, + addSchemaToCommand, +} from './commands'; import { writeService, getService, versionService, rmService, rmServiceById, addFileToService } from './services'; /** @@ -61,6 +70,63 @@ export default (path: string) => { */ addSchemaToEvent: addSchemaToEvent(join(path, 'events')), + /** + * ================================ + * Commands + * ================================ + */ + + /** + * Returns a command from EventCatalog + * @param id - The id of the command to retrieve + * @param version - Optional id of the version to get + * @returns + */ + getCommand: getCommand(join(path, 'commands')), + /** + * Adds an command to EventCatalog + * + * @param command - The command to write + * @param options - Optional options to write the command + * + */ + writeCommand: writeCommand(join(path, 'commands')), + /** + * Remove an command to EventCatalog (modeled on the standard POSIX rm utility) + * + * @param path - The path to your command, e.g. `/Inventory/InventoryAdjusted` + * + */ + rmCommand: rmCommand(join(path, 'commands')), + /** + * Remove an command by an Event id + * + * @param id - The id of the command you want to remove + * + */ + rmCommandById: rmCommandById(join(path, 'commands')), + /** + * Moves a given command id to the version directory + * @param directory + */ + versionCommand: versionCommand(join(path, 'commands')), + /** + * Adds a file to the given command + * @param id - The id of the command to add the file to + * @param file - File contents to add including the content and the file name + * @param version - Optional version of the command to add the file to + * @returns + */ + addFileToCommand: addFileToCommand(join(path, 'commands')), + /** + * Adds a schema to the given command + * @param id - The id of the command to add the schema to + * @param schema - Schema contents to add including the content and the file name + * @param version - Optional version of the command to add the schema to + * @returns + */ + addSchemaToCommand: addSchemaToCommand(join(path, 'commands')), + /** * ================================ * SERVICES diff --git a/src/services.ts b/src/services.ts index 373501f..d191ffd 100644 --- a/src/services.ts +++ b/src/services.ts @@ -17,10 +17,10 @@ import { addFileToResource, getResource, rmResourceById, versionResource, writeR * const { getService } = utils('/path/to/eventcatalog'); * * // Gets the latest version of the event - * cont event = await getService('InventoryService'); + * const service = await getService('InventoryService'); * * // Gets a version of the event - * cont event = await getService('InventoryService', '0.0.1'); + * const service = await getService('InventoryService', '0.0.1'); * ``` */ export const getService = From e92520012c982d07f80acf50cb4faf31432505b7 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Thu, 18 Jul 2024 15:30:04 +0100 Subject: [PATCH 2/3] Create chilled-trainers-obey.md --- .changeset/chilled-trainers-obey.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-trainers-obey.md diff --git a/.changeset/chilled-trainers-obey.md b/.changeset/chilled-trainers-obey.md new file mode 100644 index 0000000..96b84ca --- /dev/null +++ b/.changeset/chilled-trainers-obey.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/sdk": patch +--- + +feat(sdk): added commands to sdk From b0673e3fcfa80d362e202f9bb399dad03c9af5e0 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Thu, 18 Jul 2024 15:34:12 +0100 Subject: [PATCH 3/3] feat(sdk): added commands to sdk --- src/commands.ts | 185 ++++++++++++++++++ src/test/commands.test.ts | 387 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 572 insertions(+) create mode 100644 src/commands.ts create mode 100644 src/test/commands.test.ts diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..ab8d503 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,185 @@ +import fs from 'node:fs/promises'; +import { join } from 'node:path'; +import type { Command } from './types'; +import { addFileToResource, getResource, rmResourceById, versionResource, writeResource } from './internal/resources'; + +/** + * Returns a command from EventCatalog. + * + * You can optionally specify a version to get a specific version of the command + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { getCommand } = utils('/path/to/eventcatalog'); + * + * // Gets the latest version of the command + * const command = await getCommand('UpdateInventory'); + * + * // Gets a version of the command + * const command = await getCommand('UpdateInventory', '0.0.1'); + * ``` + */ +export const getCommand = + (directory: string) => + async (id: string, version?: string): Promise => + getResource(directory, id, version, { type: 'command' }) as Promise; + +/** + * Write a command to EventCatalog. + * + * You can optionally override the path of the command. + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { writeCommand } = utils('/path/to/eventcatalog'); + * + * // Write a command to the catalog + * // Command would be written to commands/UpdateInventory + * await writeCommand({ + * id: 'UpdateInventory', + * name: 'Update Inventory', + * version: '0.0.1', + * summary: 'This is a summary', + * markdown: '# Hello world', + * }); + * + * // Write a command to the catalog but override the path + * // Command would be written to commands/Inventory/UpdateInventory + * await writeCommand({ + * id: 'UpdateInventory', + * name: 'Update Inventory', + * version: '0.0.1', + * summary: 'This is a summary', + * markdown: '# Hello world', + * }, { path: "/Inventory/UpdateInventory"}); + * ``` + */ +export const writeCommand = + (directory: string) => + async (command: Command, options: { path: string } = { path: '' }) => + writeResource(directory, { ...command }, { ...options, type: 'command' }); + +/** + * Delete a command at it's given path. + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { rmCommand } = utils('/path/to/eventcatalog'); + * + * // removes a command at the given path (commands dir is appended to the given path) + * // Removes the command at commands/UpdateInventory + * await rmCommand('/UpdateInventory'); + * ``` + */ +export const rmCommand = (directory: string) => async (path: string) => { + await fs.rm(join(directory, path), { recursive: true }); +}; + +/** + * Delete a command by it's id. + * + * Optionally specify a version to delete a specific version of the command. + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { rmCommandById } = utils('/path/to/eventcatalog'); + * + * // deletes the latest UpdateInventory command + * await rmCommandById('UpdateInventory'); + * + * // deletes a specific version of the UpdateInventory command + * await rmCommandById('UpdateInventory', '0.0.1'); + * ``` + */ +export const rmCommandById = (directory: string) => async (id: string, version?: string) => + rmResourceById(directory, id, version, { type: 'command' }); + +/** + * Version a command by it's id. + * + * Takes the latest command and moves it to a versioned directory. + * All files with this command are also versioned (e.g /commands/UpdateInventory/schema.json) + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { versionCommand } = utils('/path/to/eventcatalog'); + * + * // moves the latest UpdateInventory command to a versioned directory + * // the version within that command is used as the version number. + * await versionCommand('UpdateInventory'); + * + * ``` + */ +export const versionCommand = (directory: string) => async (id: string) => versionResource(directory, id); + +/** + * Add a file to a command by it's id. + * + * Optionally specify a version to add a file to a specific version of the command. + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { addFileToCommand } = utils('/path/to/eventcatalog'); + * + * // adds a file to the latest UpdateInventory command + * await addFileToCommand('UpdateInventory', { content: 'Hello world', fileName: 'hello.txt' }); + * + * // adds a file to a specific version of the UpdateInventory command + * await addFileToCommand('UpdateInventory', { content: 'Hello world', fileName: 'hello.txt' }, '0.0.1'); + * + * ``` + */ +export const addFileToCommand = + (directory: string) => async (id: string, file: { content: string; fileName: string }, version?: string) => + addFileToResource(directory, id, file, version); + +/** + * Add a schema to a command by it's id. + * + * Optionally specify a version to add a schema to a specific version of the command. + * + * @example + * ```ts + * import utils from '@eventcatalog/utils'; + * + * const { addSchemaToCommand } = utils('/path/to/eventcatalog'); + * + * // JSON schema example + * const schema = { + * "$schema": "http://json-schema.org/draft-07/schema#", + * "type": "object", + * "properties": { + * "name": { + * "type": "string" + * }, + * "age": { + * "type": "number" + * } + * }, + * "required": ["name", "age"] + * }; + * + * // adds a schema to the latest UpdateInventory command + * await addSchemaToCommand('UpdateInventory', { schema, fileName: 'schema.json' }); + * + * // adds a file to a specific version of the UpdateInventory command + * await addSchemaToCommand('UpdateInventory', { schema, fileName: 'schema.json' }, '0.0.1'); + * + * ``` + */ +export const addSchemaToCommand = + (directory: string) => async (id: string, schema: { schema: string; fileName: string }, version?: string) => { + await addFileToCommand(directory)(id, { content: schema.schema, fileName: schema.fileName }, version); + }; diff --git a/src/test/commands.test.ts b/src/test/commands.test.ts new file mode 100644 index 0000000..b9b778f --- /dev/null +++ b/src/test/commands.test.ts @@ -0,0 +1,387 @@ +// sum.test.js +import { expect, it, describe, beforeEach, afterEach } from 'vitest'; +import utils from '../index'; +import path from 'node:path'; +import fs from 'node:fs'; + +const CATALOG_PATH = path.join(__dirname, 'catalog-commands'); + +const { writeCommand, getCommand, rmCommand, rmCommandById, versionCommand, addFileToCommand, addSchemaToCommand } = + utils(CATALOG_PATH); + +// clean the catalog before each test +beforeEach(() => { + fs.rmSync(CATALOG_PATH, { recursive: true, force: true }); + fs.mkdirSync(CATALOG_PATH, { recursive: true }); +}); + +afterEach(() => { + fs.rmSync(CATALOG_PATH, { recursive: true, force: true }); +}); + +describe('Commands SDK', () => { + describe('getCommand', () => { + it('returns the given command id from EventCatalog and the latest version when no version is given,', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + const test = await getCommand('UpdateInventory'); + + expect(test).toEqual({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + }); + + it('returns the given command id from EventCatalog and the requested version when a version is given,', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await versionCommand('UpdateInventory'); + + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '1.0.0', + summary: 'This is version 1.0.0', + markdown: '# Hello world', + }); + + const test = await getCommand('UpdateInventory', '0.0.1'); + + expect(test).toEqual({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + }); + + it('throws an error if the command is not found', async () => { + await expect(getCommand('UpdateInventory')).rejects.toThrowError('No command found for the given id: UpdateInventory'); + }); + + it('throws an error if the command is found but not the version', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await expect(getCommand('UpdateInventory', '1.0.0')).rejects.toThrowError( + 'No command found for the given id: UpdateInventory and version 1.0.0' + ); + }); + }); + + describe('writeCommand', () => { + it('writes the given command to EventCatalog and assumes the path if one if not given', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + const command = await getCommand('UpdateInventory'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + + expect(command).toEqual({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + }); + + it('writes the given command to EventCatalog under the correct path when a path is given', async () => { + await writeCommand( + { + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }, + { path: '/Inventory/UpdateInventory' } + ); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/Inventory/UpdateInventory', 'index.md'))).toBe(true); + }); + + it('throws an error when trying to write an command that already exists', async () => { + const createEvent = async () => + writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await createEvent(); + + await expect( + writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }) + ).rejects.toThrowError('Failed to write command as the version 0.0.1 already exists'); + }); + }); + + describe('rmCommand', () => { + it('removes a command from eventcatalog', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + + await rmCommand('/UpdateInventory'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(false); + }); + }); + + describe('rmCommandById', () => { + it('removes an command from eventcatalog by id', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + + await rmCommandById('UpdateInventory'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(false); + }); + + it('removes an command from eventcatalog by id and version', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + + await rmCommandById('UpdateInventory', '0.0.1'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(false); + }); + + it('if version is given, only removes that version and not any other versions of the command', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await versionCommand('UpdateInventory'); + + // Write the versioned command + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.2', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.1', 'index.md'))).toBe(true); + + await rmCommandById('UpdateInventory', '0.0.1'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(true); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.2', 'index.md'))).toBe(false); + }); + }); + + describe('versionCommand', () => { + it('adds the given command to the versioned directory and removes itself from the root', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.2', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + // // Add random file in there + // await fs.writeFileSync(path.join(CATALOG_PATH, 'commands/Inventory/UpdateInventory', 'schema.json'), 'SCHEMA!'); + + await versionCommand('UpdateInventory'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.2', 'index.md'))).toBe(true); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(false); + }); + it('adds the given command to the versioned directory and all files that are associated to it', async () => { + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.2', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + // Add random file in there + await fs.writeFileSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'schema.json'), 'SCHEMA!'); + + await versionCommand('UpdateInventory'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.2', 'index.md'))).toBe(true); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.2', 'schema.json'))).toBe(true); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'index.md'))).toBe(false); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'schema.json'))).toBe(false); + }); + }); + + describe('addFileToCommand', () => { + it('takes a given file and writes it to the location of the given command', async () => { + const file = { content: 'hello', fileName: 'test.txt' }; + + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await addFileToCommand('UpdateInventory', file); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'test.txt'))).toBe(true); + }); + + it('takes a given file and version and writes the file to the correct location', async () => { + const file = { content: 'hello', fileName: 'test.txt' }; + + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + // version the command + await versionCommand('UpdateInventory'); + + await addFileToCommand('UpdateInventory', file, '0.0.1'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.1', 'test.txt'))).toBe(true); + }); + + it('throws an error when trying to write to a command that does not exist', () => { + const file = { content: 'hello', fileName: 'test.txt' }; + + expect(addFileToCommand('UpdateInventory', file)).rejects.toThrowError('Cannot find directory to write file to'); + }); + }); + + describe('addSchemaToCommand', () => { + it('takes a given file and writes it to the location of the given command', async () => { + const schema = `{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "number" + } + } + }`; + const file = { schema, fileName: 'schema.json' }; + + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + await addSchemaToCommand('UpdateInventory', file); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory', 'schema.json'))).toBe(true); + }); + + it('takes a given file and version and writes the file to the correct location', async () => { + const schema = `{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "number" + } + } + }`; + const file = { schema, fileName: 'schema.json' }; + + await writeCommand({ + id: 'UpdateInventory', + name: 'Update Inventory', + version: '0.0.1', + summary: 'This is a summary', + markdown: '# Hello world', + }); + + // version the command + await versionCommand('UpdateInventory'); + + await addSchemaToCommand('UpdateInventory', file, '0.0.1'); + + expect(fs.existsSync(path.join(CATALOG_PATH, 'commands/UpdateInventory/versioned/0.0.1', 'schema.json'))).toBe(true); + }); + + it('throws an error when trying to write to a command that does not exist', () => { + const file = { schema: 'hello', fileName: 'test.txt' }; + + expect(addSchemaToCommand('UpdateInventory', file)).rejects.toThrowError('Cannot find directory to write file to'); + }); + }); +});