Skip to content

Commit a8b3fca

Browse files
CarlosGameroclaude
andauthored
MQT-schemas bump + tests (#473)
* fix: resolve consumer message schemas to the schema output type Consumers receive messages that have already been parsed by the consumer schema, so ConsumerMessageSchema and AllConsumerMessageSchemas should resolve to z.output rather than z.input. For schemas without transforms both types are identical, so this changes nothing. They diverge once a schema uses transforms or preprocess (e.g. a field that tolerantly drops unknown enum values): there z.input degrades to unknown and breaks typing in message handlers, while the handler actually receives the parsed output. Publisher-side types intentionally stay z.input, since publishers pass the raw payload that the schema parses on emit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Adding type check tests to message type utils * Fixing one more typing issue * Adding type tests * Covering schema changes with tests on core * Remove override * schemas version bump --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c1853e4 commit a8b3fca

4 files changed

Lines changed: 83 additions & 3 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
type CommonEventDefinition,
3+
enrichMessageSchemaWithBase,
4+
} from '@message-queue-toolkit/schemas'
5+
import { describe, expectTypeOf, it } from 'vitest'
6+
import { z } from 'zod/v4'
7+
8+
import type { DomainEventEmitter } from './DomainEventEmitter.ts'
9+
10+
const myEvents = {
11+
transformingEvent: {
12+
...enrichMessageSchemaWithBase(
13+
'entity.updated',
14+
z.object({
15+
mode: z.preprocess(
16+
(value) => (value === 'live' ? value : undefined),
17+
z.literal('live').optional(),
18+
),
19+
}),
20+
),
21+
},
22+
} as const satisfies Record<string, CommonEventDefinition>
23+
24+
type Emitter = DomainEventEmitter<[typeof myEvents.transformingEvent]>
25+
26+
describe('DomainEventEmitter types', () => {
27+
it('on() handlers receive the parsed event, with transformed fields as their output type', () => {
28+
type OnHandler = Parameters<Emitter['on']>[1]
29+
type HandledEvent = Parameters<OnHandler['handleEvent']>[0]
30+
31+
expectTypeOf<HandledEvent['payload']['mode']>().toEqualTypeOf<'live' | undefined>()
32+
})
33+
34+
it('onAny() handlers receive the parsed event, with transformed fields as their output type', () => {
35+
type AnyHandler = Parameters<Emitter['onAny']>[0]
36+
type HandledEvent = Parameters<AnyHandler['handleEvent']>[0]
37+
38+
expectTypeOf<HandledEvent['payload']['mode']>().toEqualTypeOf<'live' | undefined>()
39+
})
40+
41+
it('emit() takes the raw event as input and resolves to the parsed event', () => {
42+
type EmitInput = Parameters<Emitter['emit']>[1]
43+
type EmittedEvent = Awaited<ReturnType<Emitter['emit']>>
44+
45+
// The caller passes the raw payload, which emit() parses with the schema
46+
expectTypeOf<EmitInput['payload']['mode']>().toBeUnknown()
47+
// What comes back is the validated event returned by the parse
48+
expectTypeOf<EmittedEvent['payload']['mode']>().toEqualTypeOf<'live' | undefined>()
49+
})
50+
})

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"dependencies": {
2828
"@lokalise/node-core": "^14.2.0",
2929
"@lokalise/universal-ts-utils": "^4.5.1",
30-
"@message-queue-toolkit/schemas": "^7.0.0",
30+
"@message-queue-toolkit/schemas": "^7.2.0",
3131
"dot-prop": "^10.1.0",
3232
"fast-equals": "^6.0.0",
3333
"json-stream-stringify": "^3.1.6",

packages/core/test/queues/HandlerContainer.types.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ describe('HandlerContainer Types', () => {
7272
})
7373
})
7474

75+
it('should infer the schema output type for transforming schemas', () => {
76+
const JOB_MESSAGE_SCHEMA = z.object({
77+
type: z.literal('job.scheduled'),
78+
// Forward-compatible field: unknown values are dropped instead of failing validation
79+
mode: z.preprocess(
80+
(value) => (value === 'fast' ? value : undefined),
81+
z.literal('fast').optional(),
82+
),
83+
})
84+
type JobMessage = z.output<typeof JOB_MESSAGE_SCHEMA>
85+
86+
const builder = new MessageHandlerConfigBuilder<SupportedMessages | JobMessage, TestContext>()
87+
88+
// Consumers (e.g. SNS/SQS) parse messages with the schema before invoking the
89+
// handler, so the handler message type is the schema output, not its raw input
90+
builder.addConfig(JOB_MESSAGE_SCHEMA, (message, _context) => {
91+
expectTypeOf(message.mode).toEqualTypeOf<'fast' | undefined>()
92+
return Promise.resolve({ result: 'success' as const })
93+
})
94+
})
95+
7596
it('should accept messageType in options', () => {
7697
const builder = new MessageHandlerConfigBuilder<SupportedMessages, TestContext>()
7798

pnpm-lock.yaml

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)