@message-queue-toolkit/core
Version:
Useful utilities, interfaces and base classes for message queue handling. Supports AMQP and SQS with a common abstraction on top currently
166 lines • 6.41 kB
JavaScript
import { isCommonEventDefinition } from '@message-queue-toolkit/schemas';
import { extractMessageTypeFromSchema, isMessageTypeLiteralConfig, isMessageTypePathConfig, resolveMessageType, } from "./MessageTypeResolver.js";
export class MessageHandlerConfig {
schema;
definition;
/**
* Explicit message type for this handler, if provided.
* Used for routing when type cannot be extracted from schema.
*/
messageType;
handler;
messageLogFormatter;
preHandlerBarrier;
preHandlers;
constructor(schema, handler, options, eventDefinition) {
this.schema = schema;
this.definition = eventDefinition;
this.messageType = options?.messageType;
this.handler = handler;
this.messageLogFormatter = options?.messageLogFormatter;
this.preHandlerBarrier = options?.preHandlerBarrier;
this.preHandlers = options?.preHandlers ?? [];
}
}
export class MessageHandlerConfigBuilder {
configs;
constructor() {
this.configs = [];
}
/**
* Add a handler configuration for a specific message type.
* The schema is used for both routing (to match the message type) and validation (for the handler).
*
* The message type field (e.g., 'type' or 'detail-type') must be at the root level of the message
* and must be a literal value in the schema for routing to work.
*
* Example:
* ```typescript
* const USER_CREATED_SCHEMA = z.object({
* type: z.literal('user.created'),
* userId: z.string(),
* email: z.string()
* })
*
* builder.addConfig(USER_CREATED_SCHEMA, async (message) => {
* // message has type 'user.created', userId, and email
* })
* ```
*
* EventBridge example:
* ```typescript
* const USER_PRESENCE_SCHEMA = z.object({
* 'detail-type': z.literal('v2.users.{id}.presence'),
* time: z.string(),
* detail: z.object({
* userId: z.string(),
* presenceStatus: z.string()
* })
* })
*
* builder.addConfig(USER_PRESENCE_SCHEMA, async (message) => {
* // message is the full EventBridge envelope
* const detail = message.detail // Access nested payload directly
* })
* ```
*/
addConfig(schema, handler, options) {
const payloadSchema = isCommonEventDefinition(schema)
? // @ts-ignore
schema.consumerSchema
: schema;
const definition = isCommonEventDefinition(schema) ? schema : undefined;
this.configs.push(new MessageHandlerConfig(payloadSchema,
// @ts-expect-error
handler, options, definition));
return this;
}
build() {
return this.configs;
}
}
export class HandlerContainer {
messageHandlers;
messageTypeResolver;
constructor(options) {
this.messageTypeResolver = options.messageTypeResolver;
this.messageHandlers = this.resolveHandlerMap(options.messageHandlers);
}
/**
* Resolves a handler for the given message type.
*/
resolveHandler(messageType) {
const handler = this.messageHandlers[messageType];
if (!handler) {
throw new Error(`Unsupported message type: ${messageType}`);
}
// @ts-expect-error
return handler;
}
/**
* Resolves message type from message data and optional attributes using the configured resolver.
*
* @param messageData - The parsed message data
* @param messageAttributes - Optional message-level attributes (e.g., PubSub attributes)
* @returns The resolved message type
* @throws Error if message type cannot be resolved
*/
resolveMessageType(messageData, messageAttributes) {
if (this.messageTypeResolver) {
const context = { messageData, messageAttributes };
return resolveMessageType(this.messageTypeResolver, context);
}
throw new Error('Unable to resolve message type: messageTypeResolver is not configured');
}
/**
* Gets the field path used for extracting message type from schemas during registration.
* Returns undefined for literal or custom resolver modes.
*/
getMessageTypePathForSchema() {
if (this.messageTypeResolver && isMessageTypePathConfig(this.messageTypeResolver)) {
return this.messageTypeResolver.messageTypePath;
}
// For literal or custom resolver, we don't extract type from schema
return undefined;
}
/**
* Gets the literal message type if configured.
*/
getLiteralMessageType() {
if (this.messageTypeResolver && isMessageTypeLiteralConfig(this.messageTypeResolver)) {
return this.messageTypeResolver.literal;
}
return undefined;
}
resolveHandlerMap(supportedHandlers) {
const literalType = this.getLiteralMessageType();
const messageTypePath = this.getMessageTypePathForSchema();
return supportedHandlers.reduce((acc, entry) => {
let messageType;
// Priority 1: Explicit messageType on the handler config
if (entry.messageType) {
messageType = entry.messageType;
}
// Priority 2: Literal type from resolver config (same for all handlers)
else if (literalType) {
messageType = literalType;
}
// Priority 3: Extract type from schema shape using the field path
else if (messageTypePath) {
// @ts-expect-error - ZodSchema has shape property at runtime
messageType = extractMessageTypeFromSchema(entry.schema, messageTypePath);
}
if (!messageType) {
throw new Error('Unable to determine message type for handler at registration time. ' +
'Either provide explicit messageType in handler options (required for custom resolver functions), ' +
'use a literal resolver, or ensure the schema has a literal type field matching messageTypePath.');
}
if (acc[messageType]) {
throw new Error(`Duplicate handler for message type: ${messageType}`);
}
acc[messageType] = entry;
return acc;
}, {});
}
}
//# sourceMappingURL=HandlerContainer.js.map