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

151 lines 6.68 kB
import { extractMessageTypeFromSchema, isMessageTypeLiteralConfig, isMessageTypePathConfig, isMessageTypeResolverFnConfig, resolveMessageType, } from "./MessageTypeResolver.js"; const DEFAULT_SCHEMA_KEY = Symbol('NO_MESSAGE_TYPE'); export class MessageSchemaContainer { messageDefinitions; messageSchemas; messageTypeResolver; constructor(options) { this.messageTypeResolver = options.messageTypeResolver; this.messageSchemas = this.resolveSchemaMap(options.messageSchemas); this.messageDefinitions = this.resolveDefinitionMap(options.messageDefinitions ?? []); } /** * Resolves the schema for a message based on its type. * * @param message - The parsed message data * @param attributes - Optional message-level attributes (e.g., PubSub attributes) * @returns Either an error or the resolved schema */ resolveSchema( // biome-ignore lint/suspicious/noExplicitAny: This is expected message, attributes) { // If no resolver configured, use the single default schema if (!this.messageTypeResolver) { const schema = this.messageSchemas[DEFAULT_SCHEMA_KEY]; if (!schema) { return { error: new Error('No messageTypeResolver configured and no default schema available'), }; } return { result: schema }; } let messageType; try { messageType = this.resolveMessageTypeFromData(message, attributes); } catch (e) { return { error: e instanceof Error ? e : new Error(String(e)) }; } const schema = this.messageSchemas[messageType]; if (!schema) { return { error: new Error(`Unsupported message type: ${messageType}`), }; } return { result: schema }; } /** * Resolves message type from message data and optional attributes. * Only called when messageTypeResolver is configured. */ resolveMessageTypeFromData(messageData, messageAttributes) { // This method is only called after checking messageTypeResolver exists in resolveSchema const resolver = this.messageTypeResolver; const context = { messageData, messageAttributes }; return resolveMessageType(resolver, context); } /** * 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; } /** * Validates that multiple schemas can be properly mapped at registration time. */ validateMultipleSchemas(schemaCount) { if (schemaCount <= 1) return; if (!this.messageTypeResolver) { throw new Error('Multiple schemas require messageTypeResolver to be configured. ' + 'Use messageTypePath config (to extract types from schema literals) or literal config.'); } // Custom resolver function cannot be used with multiple schemas because // we can't know what types it will return until runtime. if (isMessageTypeResolverFnConfig(this.messageTypeResolver)) { throw new Error('Custom resolver function cannot be used with multiple schemas. ' + 'The resolver works for runtime type resolution, but at registration time ' + 'we cannot determine which schema corresponds to which type. ' + 'Use messageTypePath config (to extract types from schema literals) or register only a single schema.'); } } resolveSchemaMap(entries) { const result = {}; this.validateMultipleSchemas(entries.length); const literalType = this.getLiteralMessageType(); const messageTypePath = this.getMessageTypePathForSchema(); for (const entry of entries) { let type; // Priority 1: Explicit messageType on the entry if (entry.messageType) { type = entry.messageType; } // Priority 2: Literal type from resolver config (same for all schemas) else if (literalType) { type = literalType; } // Priority 3: Extract type from schema shape using the field path else if (messageTypePath) { // @ts-expect-error - ZodSchema has shape property at runtime type = extractMessageTypeFromSchema(entry.schema, messageTypePath); } // If no type extracted, use DEFAULT_SCHEMA_KEY (single schema fallback) const key = type ?? DEFAULT_SCHEMA_KEY; if (result[key]) throw new Error(`Duplicate schema for type: ${key.toString()}`); result[key] = entry.schema; } return result; } resolveDefinitionMap(entries) { const result = {}; const literalType = this.getLiteralMessageType(); const messageTypePath = this.getMessageTypePathForSchema(); for (const entry of entries) { let type; // Priority 1: Explicit messageType on the entry if (entry.messageType) { type = entry.messageType; } // Priority 2: Literal type from resolver config (same for all definitions) else if (literalType) { type = literalType; } // Priority 3: Extract type from definition's publisherSchema using the field path else if (messageTypePath) { type = extractMessageTypeFromSchema(entry.definition.publisherSchema, messageTypePath); } // If no type extracted, use DEFAULT_SCHEMA_KEY (single definition fallback) const key = type ?? DEFAULT_SCHEMA_KEY; if (result[key]) throw new Error(`Duplicate definition for type: ${key.toString()}`); result[key] = entry.definition; } return result; } } //# sourceMappingURL=MessageSchemaContainer.js.map