UNPKG

@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
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