diff --git a/packages/core/lib/events/DomainEventEmitter.types.spec.ts b/packages/core/lib/events/DomainEventEmitter.types.spec.ts new file mode 100644 index 000000000..c62f71473 --- /dev/null +++ b/packages/core/lib/events/DomainEventEmitter.types.spec.ts @@ -0,0 +1,50 @@ +import { + type CommonEventDefinition, + enrichMessageSchemaWithBase, +} from '@message-queue-toolkit/schemas' +import { describe, expectTypeOf, it } from 'vitest' +import { z } from 'zod/v4' + +import type { DomainEventEmitter } from './DomainEventEmitter.ts' + +const myEvents = { + transformingEvent: { + ...enrichMessageSchemaWithBase( + 'entity.updated', + z.object({ + mode: z.preprocess( + (value) => (value === 'live' ? value : undefined), + z.literal('live').optional(), + ), + }), + ), + }, +} as const satisfies Record + +type Emitter = DomainEventEmitter<[typeof myEvents.transformingEvent]> + +describe('DomainEventEmitter types', () => { + it('on() handlers receive the parsed event, with transformed fields as their output type', () => { + type OnHandler = Parameters[1] + type HandledEvent = Parameters[0] + + expectTypeOf().toEqualTypeOf<'live' | undefined>() + }) + + it('onAny() handlers receive the parsed event, with transformed fields as their output type', () => { + type AnyHandler = Parameters[0] + type HandledEvent = Parameters[0] + + expectTypeOf().toEqualTypeOf<'live' | undefined>() + }) + + it('emit() takes the raw event as input and resolves to the parsed event', () => { + type EmitInput = Parameters[1] + type EmittedEvent = Awaited> + + // The caller passes the raw payload, which emit() parses with the schema + expectTypeOf().toBeUnknown() + // What comes back is the validated event returned by the parse + expectTypeOf().toEqualTypeOf<'live' | undefined>() + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index dd62f1d42..ed561e26d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,7 +27,7 @@ "dependencies": { "@lokalise/node-core": "^14.2.0", "@lokalise/universal-ts-utils": "^4.5.1", - "@message-queue-toolkit/schemas": "^7.0.0", + "@message-queue-toolkit/schemas": "^7.2.0", "dot-prop": "^10.1.0", "fast-equals": "^6.0.0", "json-stream-stringify": "^3.1.6", diff --git a/packages/core/test/queues/HandlerContainer.types.spec.ts b/packages/core/test/queues/HandlerContainer.types.spec.ts index fddaa10e3..8111e7582 100644 --- a/packages/core/test/queues/HandlerContainer.types.spec.ts +++ b/packages/core/test/queues/HandlerContainer.types.spec.ts @@ -72,6 +72,27 @@ describe('HandlerContainer Types', () => { }) }) + it('should infer the schema output type for transforming schemas', () => { + const JOB_MESSAGE_SCHEMA = z.object({ + type: z.literal('job.scheduled'), + // Forward-compatible field: unknown values are dropped instead of failing validation + mode: z.preprocess( + (value) => (value === 'fast' ? value : undefined), + z.literal('fast').optional(), + ), + }) + type JobMessage = z.output + + const builder = new MessageHandlerConfigBuilder() + + // Consumers (e.g. SNS/SQS) parse messages with the schema before invoking the + // handler, so the handler message type is the schema output, not its raw input + builder.addConfig(JOB_MESSAGE_SCHEMA, (message, _context) => { + expectTypeOf(message.mode).toEqualTypeOf<'fast' | undefined>() + return Promise.resolve({ result: 'success' as const }) + }) + }) + it('should accept messageType in options', () => { const builder = new MessageHandlerConfigBuilder() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3a9c5e7c..e03d5bc6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: specifier: ^4.5.1 version: 4.10.0 '@message-queue-toolkit/schemas': - specifier: ^7.0.0 - version: 7.1.0(zod@4.4.3) + specifier: ^7.2.0 + version: 7.2.0(zod@4.4.3) dot-prop: specifier: ^10.1.0 version: 10.1.0 @@ -1221,6 +1221,11 @@ packages: peerDependencies: zod: '>=3.25.76 <5.0.0' + '@message-queue-toolkit/schemas@7.2.0': + resolution: {integrity: sha512-iuPbSeO1wm26tHPd5jIAmtllxVPRoMHHQbrqhsF5TpOfvDegXQ14lw/L5W2U7dWIYE+ErOvMWKTQAxdFZmza3A==} + peerDependencies: + zod: '>=3.25.67' + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] @@ -4201,6 +4206,10 @@ snapshots: dependencies: zod: 4.4.3 + '@message-queue-toolkit/schemas@7.2.0(zod@4.4.3)': + dependencies: + zod: 4.4.3 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true