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