diff --git a/.github/workflows/release-to-npm.yml b/.github/workflows/release-to-npm.yml index e9d1729c..68bea9db 100644 --- a/.github/workflows/release-to-npm.yml +++ b/.github/workflows/release-to-npm.yml @@ -54,6 +54,10 @@ jobs: with: token: ${{ secrets.NPM_TOKEN }} package: ./packages/command-json-schema/package.json + - uses: JS-DevTools/npm-publish@v2 + with: + token: ${{ secrets.NPM_TOKEN }} + package: ./packages/command-zod/package.json - uses: JS-DevTools/npm-publish@v2 with: token: ${{ secrets.NPM_TOKEN }} diff --git a/castore.code-workspace b/castore.code-workspace index 1f7c6aee..146553e6 100644 --- a/castore.code-workspace +++ b/castore.code-workspace @@ -61,6 +61,10 @@ "path": "packages/command-json-schema", "name": "👮‍♀️ Json Schema Command" }, + { + "path": "packages/command-zod", + "name": "👮‍♀️ Zod Command" + }, { "path": "packages/message-bus-adapter-event-bridge", "name": "🚌 EventBridge" diff --git a/docs/docs/4-packages.md b/docs/docs/4-packages.md index ead08fd2..82af759c 100644 --- a/docs/docs/4-packages.md +++ b/docs/docs/4-packages.md @@ -78,6 +78,7 @@ To add run-time validation to your event types: To add run-time validation to your commands: - [JSON Schema Command](https://www.npmjs.com/package/@castore/command-json-schema): DRY `Command` definition using [JSON Schemas](http://json-schema.org/understanding-json-schema/reference/index.html) and [`json-schema-to-ts`](https://github.com/ThomasAribart/json-schema-to-ts) +- [Zod Command](https://www.npmjs.com/package/@castore/command-zod): DRY `Command` definition using [`zod`](https://github.com/colinhacks/zod) ## 📨 Message Queue Adapters diff --git a/packages/command-json-schema/README.md b/packages/command-json-schema/README.md index 935e8aea..6a30f521 100644 --- a/packages/command-json-schema/README.md +++ b/packages/command-json-schema/README.md @@ -55,7 +55,11 @@ const pokemonAppearCommand = new JSONSchemaCommand({ inputSchema: pokemonAppearedInputSchema, outputSchema: pokemonAppearedOutputSchema, // 👇 handler input/output types are correctly inferred - handler: async (commandInput, [pokemonsEventStore]) => { + handler: async ( + commandInput, + [pokemonsEventStore], + { generateUuid }: { generateUuid: () => string }, + ) => { const { name, level } = commandInput; const pokemonId = generateUuid(); @@ -63,7 +67,6 @@ const pokemonAppearCommand = new JSONSchemaCommand({ aggregateId: pokemonId, version: 1, type: 'POKEMON_APPEARED', - timestamp: new Date().toISOString(), payload: { name, level }, }); @@ -89,7 +92,11 @@ const pokemonAppearCommand = new Command< >({ commandId: 'POKEMON_APPEAR', requiredEventStores: [pokemonsEventStore], - handler: async (commandInput, [pokemonsEventStore]) => { + handler: async ( + commandInput, + [pokemonsEventStore], + { generateUuid }: { generateUuid: () => string }, + ) => { // ...same code }, }); diff --git a/packages/command-json-schema/package.json b/packages/command-json-schema/package.json index 777287d2..67f3b077 100644 --- a/packages/command-json-schema/package.json +++ b/packages/command-json-schema/package.json @@ -37,9 +37,6 @@ "transpile": "babel src --extensions .ts --quiet", "watch": "rm -rf dist && concurrently 'yarn:package-* --watch'" }, - "dependencies": { - "ts-toolbelt": "^9.6.0" - }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.17.9", @@ -55,6 +52,7 @@ "json-schema-to-ts": "^2.5.4", "prettier": "^2.6.2", "ts-node": "^10.7.0", + "ts-toolbelt": "^9.6.0", "tsc-alias": "^1.8.7", "typescript": "^4.6.3", "vitest": "^0.26.2" diff --git a/packages/command-json-schema/src/command.ts b/packages/command-json-schema/src/command.ts index 0627c026..8e920c6b 100644 --- a/packages/command-json-schema/src/command.ts +++ b/packages/command-json-schema/src/command.ts @@ -2,16 +2,11 @@ import type { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { Command, - EventAlreadyExistsError, EventStore, $Contravariant, + OnEventAlreadyExistsCallback, } from '@castore/core'; -export type OnEventAlreadyExistsCallback = ( - error: EventAlreadyExistsError, - context: { attemptNumber: number; retriesLeft: number }, -) => Promise; - export class JSONSchemaCommand< COMMAND_ID extends string = string, EVENT_STORES extends EventStore[] = EventStore[], diff --git a/packages/command-json-schema/src/index.ts b/packages/command-json-schema/src/index.ts index 8bcd935e..57b89db2 100644 --- a/packages/command-json-schema/src/index.ts +++ b/packages/command-json-schema/src/index.ts @@ -1,2 +1 @@ export { JSONSchemaCommand } from './command'; -export type { OnEventAlreadyExistsCallback } from './command'; diff --git a/packages/command-zod/.eslintignore b/packages/command-zod/.eslintignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/command-zod/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/packages/command-zod/.lintstagedrc.js b/packages/command-zod/.lintstagedrc.js new file mode 100644 index 00000000..08c7ed8b --- /dev/null +++ b/packages/command-zod/.lintstagedrc.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../.lintstagedrc'); +module.exports = baseConfig; diff --git a/packages/command-zod/README.md b/packages/command-zod/README.md new file mode 100644 index 00000000..184a6243 --- /dev/null +++ b/packages/command-zod/README.md @@ -0,0 +1,108 @@ +# Zod Command + +DRY Castore [`Command`](https://github.com/castore-dev/castore/#--command) definition using [`zod`](https://github.com/colinhacks/zod). + +## 📥 Installation + +```bash +# npm +npm install @castore/command-zod + +# yarn +yarn add @castore/command-zod +``` + +This package has `@castore/core` and `zod` (above v3) as peer dependencies, so you will have to install them as well: + +```bash +# npm +npm install @castore/core zod + +# yarn +yarn add @castore/core zod +``` + +## 👩‍💻 Usage + +```ts +import z from 'zod'; + +import { ZodCommand } from '@castore/command-zod'; +import { tuple } from '@castore/core'; + +const pokemonAppearedInputSchema = z.object({ + name: z.string(), + level: z.number(), +}); + +const pokemonAppearedOutputSchema = z.object({ + pokemonId: z.string().uuid(), +}); + +// 👇 generics are correctly inferred +const pokemonAppearCommand = new ZodCommand({ + commandId: 'POKEMON_APPEAR', + requiredEventStores: tuple(pokemonsEventStore), + inputSchema: pokemonAppearedInputSchema, + outputSchema: pokemonAppearedOutputSchema, + // 👇 handler input/output types are correctly inferred + handler: async ( + commandInput, + [pokemonsEventStore], + { generateUuid }: { generateUuid: () => string }, + ) => { + const { name, level } = commandInput; + const pokemonId = generateUuid(); + + await pokemonsEventStore.pushEvent({ + aggregateId: pokemonId, + version: 1, + type: 'POKEMON_APPEARED', + payload: { name, level }, + }); + + return { pokemonId }; + }, +}); +``` + +👇 Equivalent to: + +```ts +import { Command } from '@castore/core'; + +type RequiredEventStores = [typeof pokemonsEventStore]; +type CommandInput = { name: string; level: number }; +type CommandOutput = { pokemonId: string }; + +const pokemonAppearCommand = new Command< + RequiredEventStores, + RequiredEventStores, + CommandInput, + CommandOutput +>({ + commandId: 'POKEMON_APPEAR', + requiredEventStores: [pokemonsEventStore], + handler: async (commandInput, [pokemonsEventStore]) => { + // ...same code + }, +}); +``` + +## ⚙️ Properties & Methods + +`ZodCommand` implements the [`Command`](https://github.com/castore-dev/castore/#--command) class and adds the following properties to it: + +- inputSchema (?object): The command input zod schema + +```ts +const inputSchema = pokemonAppearCommand.inputSchema; +// => pokemonAppearedInputSchema +``` + +- outputSchema (?object): The command output zod schema + +```ts +const outputSchema = pokemonAppearCommand.outputSchema; +// => pokemonAppearedOutputSchema +``` diff --git a/packages/command-zod/babel.config.js b/packages/command-zod/babel.config.js new file mode 100644 index 00000000..31b1233b --- /dev/null +++ b/packages/command-zod/babel.config.js @@ -0,0 +1,3 @@ +const commonBabelConfig = require('../../commonConfiguration/babel.config'); + +module.exports = commonBabelConfig(); diff --git a/packages/command-zod/dependency-cruiser.js b/packages/command-zod/dependency-cruiser.js new file mode 100644 index 00000000..fd07843d --- /dev/null +++ b/packages/command-zod/dependency-cruiser.js @@ -0,0 +1,3 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +const baseConfig = require('../../dependency-cruiser'); +module.exports = baseConfig; diff --git a/packages/command-zod/package.json b/packages/command-zod/package.json new file mode 100644 index 00000000..6b44d289 --- /dev/null +++ b/packages/command-zod/package.json @@ -0,0 +1,80 @@ +{ + "name": "@castore/command-zod", + "description": "DRY Castore Command definition using Zod", + "license": "MIT", + "homepage": "https://github.com/theodo/castore#readme", + "bugs": "https://github.com/theodo/castore/issues", + "repository": "theodo/castore.git", + "keywords": [ + "event", + "source", + "store", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "files": [ + "dist" + ], + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "scripts": { + "lint-fix": "yarn linter-base-config --fix", + "lint-fix-all": "yarn lint-fix .", + "linter-base-config": "eslint --ext=js,ts", + "package": "rm -rf dist && yarn package-cjs && yarn package-esm && yarn package-types", + "package-cjs": "NODE_ENV=cjs yarn transpile --out-dir dist/cjs --source-maps", + "package-esm": "NODE_ENV=esm yarn transpile --out-dir dist/esm --source-maps", + "package-types": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "test": "yarn test-type && yarn test-unit && yarn test-circular && yarn test-linter", + "test-circular": "yarn depcruise --validate dependency-cruiser.js .", + "test-linter": "yarn linter-base-config .", + "test-type": "tsc --noEmit --emitDeclarationOnly false", + "test-unit": "yarn vitest run --passWithNoTests", + "transpile": "babel src --extensions .ts --quiet", + "watch": "rm -rf dist && concurrently 'yarn:package-* --watch'" + }, + "devDependencies": { + "@babel/cli": "^7.17.6", + "@babel/core": "^7.17.9", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-typescript": "^7.16.7", + "@castore/core": "workspace:", + "@types/node": "^17.0.29", + "babel-plugin-module-resolver": "^4.1.0", + "concurrently": "^7.1.0", + "dependency-cruiser": "^11.7.0", + "eslint": "^8.14.0", + "prettier": "^2.6.2", + "ts-node": "^10.7.0", + "ts-toolbelt": "^9.6.0", + "tsc-alias": "^1.8.7", + "typescript": "^4.6.3", + "vitest": "^0.26.2", + "zod": "^3.15.1" + }, + "maintainers": [ + "Thomas Aribart", + "Charles Géry", + "Juliette Fournier", + "Valentin Beggi", + "Stanislas Hannebelle" + ], + "nx": { + "targets": { + "package": { + "outputs": [ + "packages/command-zod/dist" + ] + } + } + }, + "peerDependencies": { + "@castore/core": "*", + "zod": "^3.0.0" + } +} diff --git a/packages/command-zod/project.json b/packages/command-zod/project.json new file mode 100644 index 00000000..dda444ae --- /dev/null +++ b/packages/command-zod/project.json @@ -0,0 +1,6 @@ +{ + "root": "packages/command-zod", + "projectType": "library", + "tags": [], + "implicitDependencies": ["core"] +} diff --git a/packages/command-zod/src/command.fixtures.test.ts b/packages/command-zod/src/command.fixtures.test.ts new file mode 100644 index 00000000..b6eab063 --- /dev/null +++ b/packages/command-zod/src/command.fixtures.test.ts @@ -0,0 +1,209 @@ +/* eslint-disable max-lines */ +import { vi } from 'vitest'; +import { z } from 'zod'; + +import { + EventStore, + EventType, + EventTypeDetail, + EventStorageAdapter, + tuple, +} from '@castore/core'; + +import { ZodCommand } from './command'; + +export const pushEventMock = vi.fn(); +export const pushEventGroupMock = vi.fn(); +export const groupEvent = vi.fn(); +export const getEventsMock = vi.fn(); +export const listAggregateIdsMock = vi.fn(); + +export const eventStorageAdapterMock: EventStorageAdapter = { + pushEvent: pushEventMock, + pushEventGroup: pushEventGroupMock, + groupEvent: groupEvent, + getEvents: getEventsMock, + listAggregateIds: listAggregateIdsMock, +}; + +export const counterCreatedEvent = new EventType<'COUNTER_CREATED'>({ + type: 'COUNTER_CREATED', +}); +export const counterIncrementedEvent = new EventType<'COUNTER_INCREMENTED'>({ + type: 'COUNTER_INCREMENTED', +}); +export const counterDeletedEvent = new EventType<'COUNTER_DELETED'>({ + type: 'COUNTER_DELETED', +}); +export type CounterEventsDetails = + | EventTypeDetail + | EventTypeDetail + | EventTypeDetail; + +export type CounterAggregate = { + aggregateId: string; + version: number; + count: number; + status: string; +}; + +export const counterIdMock = 'counterId'; +export const counterEventsMocks: [CounterEventsDetails, CounterEventsDetails] = + [ + { + aggregateId: counterIdMock, + version: 1, + type: 'COUNTER_CREATED', + timestamp: '2022', + }, + { + aggregateId: counterIdMock, + version: 2, + type: 'COUNTER_INCREMENTED', + timestamp: '2023', + }, + ]; + +export const countersReducer = ( + counterAggregate: CounterAggregate, + event: CounterEventsDetails, +): CounterAggregate => { + const { version, aggregateId } = event; + switch (event.type) { + case 'COUNTER_CREATED': + return { aggregateId, version: event.version, count: 0, status: 'LIVE' }; + case 'COUNTER_INCREMENTED': + return { + ...counterAggregate, + version, + count: counterAggregate.count + 1, + }; + case 'COUNTER_DELETED': + return { ...counterAggregate, version, status: 'DELETED' }; + default: { + return { ...counterAggregate, version }; + } + } +}; + +export const counterEventStore = new EventStore({ + eventStoreId: 'Counters', + eventTypes: [ + counterCreatedEvent, + counterIncrementedEvent, + counterDeletedEvent, + ], + reducer: countersReducer, + eventStorageAdapter: eventStorageAdapterMock, +}); + +export const inputSchema = z.object({ + counterId: z.string(), +}); + +export const outputSchema = z.object({ + nextCount: z.number(), +}); + +export const requiredEventStores = tuple(counterEventStore); + +export const createCounter = new ZodCommand({ + commandId: 'CREATE_COUNTER', + requiredEventStores: tuple(counterEventStore), + outputSchema: inputSchema, + handler: async ( + _, + [countersStore], + { generateUuid }: { generateUuid: () => string }, + ) => { + const counterId = generateUuid(); + + await countersStore.pushEvent({ + aggregateId: counterId, + type: 'COUNTER_CREATED', + version: 1, + }); + + return { counterId }; + }, +}); + +export const incrementCounter = new ZodCommand({ + commandId: 'INCREMENT_COUNTER', + requiredEventStores, + inputSchema, + outputSchema, + handler: async (input, eventStores) => { + const { counterId } = input; + const [countersStore] = eventStores; + + const { aggregate } = await countersStore.getExistingAggregate(counterId); + const { count, version } = aggregate; + + await countersStore.pushEvent({ + aggregateId: counterId, + version: version + 1, + type: 'COUNTER_INCREMENTED', + }); + + return { nextCount: count + 1 }; + }, +}); + +export const incrementCounterNoOutput = new ZodCommand({ + commandId: 'INCREMENT_COUNTER_NO_OUTPUT', + requiredEventStores: tuple(counterEventStore), + inputSchema, + handler: async (input, eventStores) => { + const { counterId } = input; + const [countersStore] = eventStores; + + const { aggregate } = await countersStore.getExistingAggregate(counterId); + const { version } = aggregate; + + await countersStore.pushEvent({ + aggregateId: counterId, + type: 'COUNTER_INCREMENTED', + version: version + 1, + }); + }, +}); + +export const incrementCounterA = new ZodCommand({ + commandId: 'INCREMENT_COUNTER_A', + requiredEventStores: tuple(counterEventStore), + outputSchema, + handler: async (_, eventStores) => { + const counterId = 'A'; + const [countersStore] = eventStores; + + const { aggregate } = await countersStore.getExistingAggregate(counterId); + const { count, version } = aggregate; + + await countersStore.pushEvent({ + aggregateId: counterId, + type: 'COUNTER_INCREMENTED', + version: version + 1, + }); + + return { nextCount: count + 1 }; + }, +}); + +export const incrementCounterANoOutput = new ZodCommand({ + commandId: 'INCREMENT_COUNTER_A_NO_OUTPUT', + requiredEventStores: tuple(counterEventStore), + handler: async (_, eventStores) => { + const counterId = 'A'; + const [countersStore] = eventStores; + + const { aggregate } = await countersStore.getExistingAggregate(counterId); + const { version } = aggregate; + + await countersStore.pushEvent({ + aggregateId: counterId, + type: 'COUNTER_INCREMENTED', + version: version + 1, + }); + }, +}); diff --git a/packages/command-zod/src/command.ts b/packages/command-zod/src/command.ts new file mode 100644 index 00000000..df5b305a --- /dev/null +++ b/packages/command-zod/src/command.ts @@ -0,0 +1,79 @@ +import { z, ZodType } from 'zod'; + +import { + Command, + EventStore, + $Contravariant, + OnEventAlreadyExistsCallback, +} from '@castore/core'; + +export class ZodCommand< + COMMAND_ID extends string = string, + EVENT_STORES extends EventStore[] = EventStore[], + $EVENT_STORES extends EventStore[] = $Contravariant< + EVENT_STORES, + EventStore[] + >, + INPUT_SCHEMA extends ZodType | undefined = ZodType | undefined, + INPUT = $Contravariant< + INPUT_SCHEMA, + ZodType, + INPUT_SCHEMA extends ZodType ? z.infer : never + >, + OUTPUT_SCHEMA extends ZodType | undefined = ZodType | undefined, + OUTPUT = $Contravariant< + OUTPUT_SCHEMA, + ZodType, + OUTPUT_SCHEMA extends ZodType ? z.infer : never + >, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + CONTEXT extends any[] = any[], +> extends Command< + COMMAND_ID, + EVENT_STORES, + $EVENT_STORES, + INPUT, + OUTPUT, + CONTEXT +> { + inputSchema?: INPUT_SCHEMA; + outputSchema?: OUTPUT_SCHEMA; + + constructor({ + commandId, + requiredEventStores, + eventAlreadyExistsRetries, + onEventAlreadyExists, + handler, + inputSchema, + outputSchema, + }: { + commandId: COMMAND_ID; + requiredEventStores: EVENT_STORES; + eventAlreadyExistsRetries?: number; + onEventAlreadyExists?: OnEventAlreadyExistsCallback; + handler: ( + input: INPUT, + eventStores: $EVENT_STORES, + ...context: CONTEXT + ) => Promise; + inputSchema?: INPUT_SCHEMA; + outputSchema?: OUTPUT_SCHEMA; + }) { + super({ + commandId, + requiredEventStores, + eventAlreadyExistsRetries, + onEventAlreadyExists, + handler, + }); + + if (inputSchema !== undefined) { + this.inputSchema = inputSchema; + } + + if (outputSchema !== undefined) { + this.outputSchema = outputSchema; + } + } +} diff --git a/packages/command-zod/src/command.type.test.ts b/packages/command-zod/src/command.type.test.ts new file mode 100644 index 00000000..47f67093 --- /dev/null +++ b/packages/command-zod/src/command.type.test.ts @@ -0,0 +1,179 @@ +import { A } from 'ts-toolbelt'; +import { z } from 'zod'; + +import { Command } from '@castore/core'; + +import { ZodCommand } from './command'; +import { + counterEventStore, + createCounter, + incrementCounter, + incrementCounterA, + incrementCounterANoOutput, + incrementCounterNoOutput, + inputSchema, + outputSchema, +} from './command.fixtures.test'; + +type Input = z.infer; +type Output = z.infer; + +// --- CLASS --- + +const assertZodCommandExtendsCommand: A.Extends = 1; +assertZodCommandExtendsCommand; + +const assertCreateCounterExtendsZodCommand: A.Extends< + typeof createCounter, + ZodCommand +> = 1; +assertCreateCounterExtendsZodCommand; + +const assertCreateCounterExtendsCommand: A.Extends< + typeof createCounter, + Command +> = 1; +assertCreateCounterExtendsCommand; + +const assertIncrementCounterExtendsZodCommand: A.Extends< + typeof incrementCounter, + ZodCommand +> = 1; +assertIncrementCounterExtendsZodCommand; + +const assertIncrementCounterExtendsCommand: A.Extends< + typeof incrementCounter, + Command +> = 1; +assertIncrementCounterExtendsCommand; + +const assertIncrementCounterNoOutputExtendsZodCommand: A.Extends< + typeof incrementCounterNoOutput, + ZodCommand +> = 1; +assertIncrementCounterNoOutputExtendsZodCommand; + +const assertIncrementCounterNoOutputExtendsCommand: A.Extends< + typeof incrementCounterNoOutput, + Command +> = 1; +assertIncrementCounterNoOutputExtendsCommand; + +const assertIncrementCounterAExtendsZodCommand: A.Extends< + typeof incrementCounterA, + ZodCommand +> = 1; +assertIncrementCounterAExtendsZodCommand; + +const assertIncrementCounterAExtendsCommand: A.Extends< + typeof incrementCounterA, + Command +> = 1; +assertIncrementCounterAExtendsCommand; + +const assertIncrementCounterANoOutputExtendsZodCommand: A.Extends< + typeof incrementCounterANoOutput, + ZodCommand +> = 1; +assertIncrementCounterANoOutputExtendsZodCommand; + +const assertIncrementCounterANoOutputExtendsCommand: A.Extends< + typeof incrementCounterANoOutput, + Command +> = 1; +assertIncrementCounterANoOutputExtendsCommand; + +// --- SCHEMAS --- + +const assertIncrementCounterInputSchema: A.Equals< + typeof incrementCounter.inputSchema, + /** + * @debt type "Find a way to remove undefined" + */ + typeof inputSchema | undefined +> = 1; +assertIncrementCounterInputSchema; + +const assertIncrementCounterOutputSchema: A.Equals< + typeof incrementCounter.outputSchema, + /** + * @debt type "Find a way to remove undefined" + */ + typeof outputSchema | undefined +> = 1; +assertIncrementCounterOutputSchema; + +const assertIncrementCounterNoOutputInputSchema: A.Equals< + typeof incrementCounterNoOutput.inputSchema, + /** + * @debt type "Find a way to remove undefined" + */ + typeof inputSchema | undefined +> = 1; +assertIncrementCounterNoOutputInputSchema; + +const assertIncrementCounterASchemaOutputSchema: A.Equals< + typeof incrementCounterA.outputSchema, + /** + * @debt type "Find a way to remove undefined" + */ + typeof outputSchema | undefined +> = 1; +assertIncrementCounterASchemaOutputSchema; + +// --- HANDLER --- + +const assertCreateCounterHandler: A.Equals< + typeof createCounter.handler, + ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: any, + requiredEventStores: [typeof counterEventStore], + context: { generateUuid: () => string }, + ) => Promise +> = 1; +assertCreateCounterHandler; + +const assertIncrementCounterHandler: A.Equals< + typeof incrementCounter.handler, + ( + input: Input, + requiredEventStores: [typeof counterEventStore], + ) => Promise +> = 1; +assertIncrementCounterHandler; + +const assertIncrementCounterNoOutputHandler: A.Equals< + typeof incrementCounterNoOutput.handler, + ( + input: Input, + requiredEventStores: [typeof counterEventStore], + ) => Promise +> = 1; +assertIncrementCounterNoOutputHandler; + +const assertIncrementCounterAHandler: A.Equals< + typeof incrementCounterA.handler, + ( + /** + * @debt type "input should be typed as unknown" + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: any, + requiredEventStores: [typeof counterEventStore], + ) => Promise +> = 1; +assertIncrementCounterAHandler; + +const assertIncrementCounterANoOutputHandler: A.Equals< + typeof incrementCounterANoOutput.handler, + ( + /** + * @debt type "input should be typed as unknown" + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: any, + requiredEventStores: [typeof counterEventStore], + ) => Promise +> = 1; +assertIncrementCounterANoOutputHandler; diff --git a/packages/command-zod/src/command.unit.test.ts b/packages/command-zod/src/command.unit.test.ts new file mode 100644 index 00000000..3fc1bc4b --- /dev/null +++ b/packages/command-zod/src/command.unit.test.ts @@ -0,0 +1,64 @@ +import { + counterEventsMocks, + getEventsMock, + incrementCounter, + incrementCounterA, + incrementCounterANoOutput, + incrementCounterNoOutput, + inputSchema, + outputSchema, + requiredEventStores, +} from './command.fixtures.test'; + +getEventsMock.mockResolvedValue({ events: counterEventsMocks }); + +describe('zodCommand implementation', () => { + const expectedProperties = new Set([ + // applying super(...) apparently adds { _types: undefined } to the class + '_types', + 'commandId', + 'requiredEventStores', + 'inputSchema', + 'outputSchema', + 'eventAlreadyExistsRetries', + 'onEventAlreadyExists', + 'handler', + ]); + + it('has correct properties', () => { + expect(new Set(Object.keys(incrementCounter))).toStrictEqual( + expectedProperties, + ); + + expect( + incrementCounter.requiredEventStores.map( + ({ eventStoreId }) => eventStoreId, + ), + ).toStrictEqual( + requiredEventStores.map(({ eventStoreId }) => eventStoreId), + ); + + expect(incrementCounter.inputSchema).toStrictEqual(inputSchema); + expect(incrementCounter.outputSchema).toStrictEqual(outputSchema); + }); + + it('has correct properties (no output)', () => { + expect(Object.keys(incrementCounterNoOutput)).toHaveLength( + expectedProperties.size - 1, + ); + expect(incrementCounterNoOutput.inputSchema).toStrictEqual(inputSchema); + }); + + it('has correct properties (no input)', () => { + expect(Object.keys(incrementCounterA)).toHaveLength( + expectedProperties.size - 1, + ); + expect(incrementCounterA.outputSchema).toStrictEqual(outputSchema); + }); + + it('has correct properties (no input, no output)', () => { + expect(Object.keys(incrementCounterANoOutput)).toHaveLength( + expectedProperties.size - 2, + ); + }); +}); diff --git a/packages/command-zod/src/index.ts b/packages/command-zod/src/index.ts new file mode 100644 index 00000000..c415f48d --- /dev/null +++ b/packages/command-zod/src/index.ts @@ -0,0 +1 @@ +export { ZodCommand } from './command'; diff --git a/packages/command-zod/tsconfig.build.json b/packages/command-zod/tsconfig.build.json new file mode 100644 index 00000000..a60a7fbf --- /dev/null +++ b/packages/command-zod/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src" + }, + "exclude": ["./dist"] +} diff --git a/packages/command-zod/tsconfig.json b/packages/command-zod/tsconfig.json new file mode 100644 index 00000000..f46441e8 --- /dev/null +++ b/packages/command-zod/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "src", + "composite": true, + "emitDeclarationOnly": true, + "outDir": "./dist/types", + "esModuleInterop": true + }, + "exclude": ["./dist"], + "include": ["./**/*.ts"] +} diff --git a/packages/command-zod/vite.config.js b/packages/command-zod/vite.config.js new file mode 100644 index 00000000..ad07ed3a --- /dev/null +++ b/packages/command-zod/vite.config.js @@ -0,0 +1,7 @@ +const { defineConfig } = require('vitest/config'); + +const { testConfig } = require('../../commonConfiguration/vite.config'); + +export default defineConfig({ + test: testConfig, +}); diff --git a/workspace.json b/workspace.json index f21834c6..6ff8b84d 100644 --- a/workspace.json +++ b/workspace.json @@ -8,6 +8,7 @@ "event-type-json-schema": "packages/event-type-json-schema", "event-type-zod": "packages/event-type-zod", "command-json-schema": "packages/command-json-schema", + "command-zod": "packages/command-zod", "message-bus-adapter-event-bridge": "packages/message-bus-adapter-event-bridge", "message-bus-adapter-event-bridge-s3": "packages/message-bus-adapter-event-bridge-s3", "message-bus-adapter-in-memory": "packages/message-bus-adapter-in-memory", diff --git a/yarn.lock b/yarn.lock index a8e8d92a..40273f47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6029,6 +6029,34 @@ __metadata: languageName: unknown linkType: soft +"@castore/command-zod@workspace:packages/command-zod": + version: 0.0.0-use.local + resolution: "@castore/command-zod@workspace:packages/command-zod" + dependencies: + "@babel/cli": ^7.17.6 + "@babel/core": ^7.17.9 + "@babel/plugin-transform-runtime": ^7.17.0 + "@babel/preset-env": ^7.16.11 + "@babel/preset-typescript": ^7.16.7 + "@castore/core": "workspace:" + "@types/node": ^17.0.29 + babel-plugin-module-resolver: ^4.1.0 + concurrently: ^7.1.0 + dependency-cruiser: ^11.7.0 + eslint: ^8.14.0 + prettier: ^2.6.2 + ts-node: ^10.7.0 + ts-toolbelt: ^9.6.0 + tsc-alias: ^1.8.7 + typescript: ^4.6.3 + vitest: ^0.26.2 + zod: ^3.15.1 + peerDependencies: + "@castore/core": "*" + zod: ^3.0.0 + languageName: unknown + linkType: soft + "@castore/core@workspace:, @castore/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@castore/core@workspace:packages/core"