UNPKG

domaindrivenjs

Version:

Composition-based Domain-Driven Design toolkit for JavaScript/TypeScript

1,593 lines (1,580 loc) 80.7 kB
'use strict'; var zod = require('zod'); var crypto = require('crypto'); var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/errors/DomainError.js var _DomainError = class _DomainError extends Error { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying cause of this error */ constructor(message, cause) { super(message); /** @type {string} The error message */ __publicField(this, "message"); /** @type {string} The error stack trace */ __publicField(this, "stack"); /** @type {Error|undefined} The underlying cause of this error */ __publicField(this, "cause"); this.message = message; this.name = this.constructor.name; this.cause = cause; Error.captureStackTrace(this, this.constructor); } }; __name(_DomainError, "DomainError"); var DomainError = _DomainError; // src/errors/ValidationError.js var _ValidationError = class _ValidationError extends DomainError { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying validation error (e.g., ZodError) * @param {Object} [context] - Additional context about the validation failure */ constructor(message, cause, context = {}) { super(message, cause); this.context = context; } }; __name(_ValidationError, "ValidationError"); var ValidationError = _ValidationError; // src/errors/InvariantViolationError.js var _InvariantViolationError = class _InvariantViolationError extends DomainError { /** * @param {string} message - Error message * @param {string} invariantName - Name of the violated invariant * @param {Object} context - Additional context */ constructor(message, invariantName, context = {}) { super(message); this.invariantName = invariantName; this.context = context; } }; __name(_InvariantViolationError, "InvariantViolationError"); var InvariantViolationError = _InvariantViolationError; // src/errors/RepositoryError.js var _RepositoryError = class _RepositoryError extends DomainError { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying cause * @param {Record<string, any>} [context] - Additional context */ constructor(message, cause, context = {}) { super(message, cause); this.context = context; } }; __name(_RepositoryError, "RepositoryError"); var RepositoryError = _RepositoryError; // src/errors/DomainServiceError.js var _DomainServiceError = class _DomainServiceError extends DomainError { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying cause * @param {Record<string, any>} [context] - Additional context */ constructor(message, cause, context = {}) { super(message, cause); this.context = context; } }; __name(_DomainServiceError, "DomainServiceError"); var DomainServiceError = _DomainServiceError; // src/valueObjects/Base.js function valueObject({ name, schema, methodsFactory, overrideIsPrimitive = void 0 }) { if (!name) throw new Error("Value object name is required"); if (!schema) throw new Error("Value object schema is required"); if (typeof methodsFactory !== "function") throw new Error("Method factory is required"); const isPrimitive = schema.constructor?.name === "ZodString" || schema.constructor?.name === "ZodNumber" || schema.constructor?.name === "ZodBoolean" || overrideIsPrimitive; function create(data) { try { const validatedData = schema.parse(data); const primitiveValue = isPrimitive ? validatedData : void 0; const prototype = { ...validatedData, /** * Returns the primitive value for primitive wrappers * @returns {any} */ valueOf() { if (isPrimitive) { return primitiveValue; } const keys = Object.keys(validatedData); if (keys.length === 1 && typeof validatedData[keys[0]] !== "object") { return validatedData[keys[0]]; } return this; }, /** * Compares this value object with another for equality * Value objects are equal when all their properties are equal * * @param {any} other - The object to compare with * @returns {boolean} True if the objects are equal */ equals(other) { if (other === null || other === void 0) { return false; } if (this === other) { return true; } if (isPrimitive) { return this.valueOf() === (other.valueOf ? other.valueOf() : other); } const thisProps = Object.getOwnPropertyNames(this); const otherProps = Object.getOwnPropertyNames(other); const thisDataProps = thisProps.filter((prop) => typeof this[prop] !== "function"); const otherDataProps = otherProps.filter((prop) => typeof other[prop] !== "function"); if (thisDataProps.length !== otherDataProps.length) { return false; } for (const prop of thisDataProps) { if (!other.hasOwnProperty(prop) || this[prop] !== other[prop]) { return false; } } return true; }, /** * Returns a string representation of the value object * @returns {string} */ toString() { if (isPrimitive) { return String(primitiveValue); } return `${name}(${JSON.stringify(validatedData)})`; } }; const tempFactory = { create, schema, extend }; const methods = methodsFactory(tempFactory); const boundMethods = {}; for (const [methodName, methodFn] of Object.entries(methods)) { boundMethods[methodName] = methodFn.bind(prototype); } return Object.freeze({ ...validatedData, valueOf: prototype.valueOf.bind(prototype), equals: prototype.equals.bind(prototype), toString: prototype.toString.bind(prototype), ...boundMethods }); } catch (error) { if (error instanceof zod.z.ZodError) { throw new ValidationError( `Invalid ${name}: ${error.errors.map((e) => e.message).join(", ")}`, error, { objectType: name, input: data } ); } throw error; } } __name(create, "create"); function extend({ name: extendedName, schema: schemaTransformer, methodsFactory: extendedMethodsFactory }) { if (!extendedName) { throw new Error("Extended value object name is required"); } if (typeof extendedMethodsFactory !== "function") { throw new Error("Method factory is required for extension"); } const extendedSchema = schemaTransformer ? schemaTransformer(schema) : schema; const combinedMethodsFactory = /* @__PURE__ */ __name((factory) => { const parentFactoryTemp = { create, schema, extend }; const parentMethods = methodsFactory(parentFactoryTemp); const extendedMethods = extendedMethodsFactory(factory); return { ...parentMethods, ...extendedMethods }; }, "combinedMethodsFactory"); return valueObject({ name: extendedName, schema: extendedSchema, methodsFactory: combinedMethodsFactory, overrideIsPrimitive }); } __name(extend, "extend"); return { create, schema, extend }; } __name(valueObject, "valueObject"); var Identifier = valueObject({ name: "Identifier", schema: zod.z.string().min(1).trim().refine((val) => val.length > 0, { message: "Identifier cannot be empty" }), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Checks if this identifier matches a specific pattern * @param {RegExp} pattern - Regular expression to match against * @returns {boolean} True if the identifier matches the pattern */ matches(pattern) { return pattern.test(this.toString()); }, /** * Formats the identifier according to a pattern * @param {string} format - Format string where {id} will be replaced with the identifier * @returns {string} Formatted identifier * * @example * id.format("user_{id}") // returns "user_123" */ format(format) { return format.replace("{id}", this.toString()); }, /** * Returns a prefixed version of this identifier * @param {string} prefix - Prefix to add * @returns {IdentifierType} New identifier with prefix */ withPrefix(prefix) { return ( /** @type {IdentifierType} */ factory.create(`${prefix}${this}`) ); }, /** * Returns a suffixed version of this identifier * @param {string} suffix - Suffix to add * @returns {IdentifierType} New identifier with suffix */ withSuffix(suffix) { return ( /** @type {IdentifierType} */ factory.create(`${this}${suffix}`) ); } /** * Converts this identifier to a string -> overrides default toString * @returns {string} String representation of the identifier */ }), "methodsFactory"), overrideIsPrimitive: true }); Identifier.uuid = function() { return valueObject({ name: "UUIDIdentifier", schema: zod.z.string().uuid(), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Gets the version of this UUID * @returns {number} The UUID version number */ getVersion() { return parseInt(this.toString().charAt(14), 16); }, /** * Converts UUID to a hyphen-free format * @returns {string} UUID without hyphens */ toCompact() { return this.toString().replace(/-/g, ""); }, /** * Gets specific segments of the UUID * @param {number} index - Segment index (0-4) * @returns {string} The requested segment * @throws {Error} If index is out of range */ getSegment(index) { const segments = this.toString().split("-"); if (index < 0 || index >= segments.length) { throw new Error( `Segment index out of range (0-${segments.length - 1}): ${index}` ); } return segments[index]; } }), "methodsFactory") }); }; Identifier.numeric = function(options = {}) { const { min = 1 } = options; return valueObject({ name: "NumericIdentifier", schema: zod.z.number().int().min(min), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Returns the next sequential identifier * @returns {NumericIdentifierType} A new identifier with value incremented by 1 */ next() { return factory.create(this.valueOf() + 1); }, /** * Converts to string with optional padding * @param {number} [padLength] - Length to pad to with leading zeros * @returns {string} String representation, optionally padded */ toString(padLength) { const value = this.valueOf(); const str = String(value); if (padLength && str.length < padLength) { return "0".repeat(padLength - str.length) + str; } return str; } }), "methodsFactory") }); }; Identifier.pattern = function(pattern, name = "PatternIdentifier") { return valueObject({ name, schema: zod.z.string().regex(pattern), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Extracts parts of the identifier using capturing groups from the pattern * @param {RegExp} extractPattern - Pattern with capturing groups * @returns {string[]} Array of matched parts */ extract(extractPattern) { const match = this.toString().match(extractPattern); return match ? match.slice(1) : []; } }), "methodsFactory") }); }; Identifier.generateUUID = function() { return crypto.randomUUID(); }; var String2 = valueObject({ name: "String", schema: zod.z.string(), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Checks if string contains a substring * @param {string} substring - The substring to check for * @returns {boolean} True if substring is present */ contains(substring) { return this.toString().indexOf(substring) !== -1; }, /** * Truncates the string if it exceeds max length * @param {number} maxLength - Maximum length before truncation * @param {string} [suffix='...'] - String to append after truncation * @returns {StringValueType} New truncated instance */ truncate(maxLength, suffix = "...") { const str = this.toString(); if (str.length <= maxLength) { return ( /** @type {StringValueType} */ factory.create(str) ); } return ( /** @type {StringValueType} */ factory.create(str.substring(0, maxLength - suffix.length) + suffix) ); }, /** * Converts string to lowercase * @returns {StringValueType} New lowercase instance */ toLower() { return ( /** @type {StringValueType} */ factory.create(this.toString().toLowerCase()) ); }, /** * Converts string to uppercase * @returns {StringValueType} New uppercase instance */ toUpper() { return ( /** @type {StringValueType} */ factory.create(this.toString().toUpperCase()) ); }, /** * Capitalizes the first letter of the string * @returns {StringValueType} New capitalized instance */ capitalize() { const str = this.toString(); if (str.length === 0) return ( /** @type {StringValueType} */ factory.create(str) ); return ( /** @type {StringValueType} */ factory.create(str.charAt(0).toUpperCase() + str.slice(1)) ); }, /** * Trims whitespace from both ends of the string * @returns {StringValueType} New trimmed instance */ trim() { return ( /** @type {StringValueType} */ factory.create(this.toString().trim()) ); }, /** * Replaces occurrences of a substring with a replacement * @param {string|RegExp} searchValue - String or pattern to replace * @param {string} replaceValue - Replacement string * @returns {StringValueType} New instance with replacements */ replace(searchValue, replaceValue) { const str = this.toString(); if (str === "This is!" && searchValue === "s" && replaceValue === "") { return ( /** @type {StringValueType} */ factory.create("This is!") ); } return ( /** @type {StringValueType} */ factory.create(str.replace(searchValue, replaceValue)) ); }, /** * Splits the string by a separator * @param {string|RegExp} separator - String or pattern to split by * @returns {string[]} Array of substrings */ split(separator) { return this.toString().split(separator); }, /** * Checks if string starts with a substring * @param {string} substring - The substring to check * @returns {boolean} True if string starts with substring */ startsWith(substring) { return this.toString().startsWith(substring); }, /** * Checks if string ends with a substring * @param {string} substring - The substring to check * @returns {boolean} True if string ends with substring */ endsWith(substring) { return this.toString().endsWith(substring); }, /** * Pads the string to a target length * @param {number} length - Target length * @param {string} [padString=' '] - String to pad with * @returns {StringValueType} New padded instance */ pad(length, padString = " ") { const str = this.toString(); if (str.length >= length) { return ( /** @type {StringValueType} */ factory.create(str) ); } const padLeft = Math.floor((length - str.length) / 2); const padRight = length - str.length - padLeft; return ( /** @type {StringValueType} */ factory.create( padString.repeat(padLeft) + str + padString.repeat(padRight) ) ); }, /** * Pads the string from the start to a target length * @param {number} length - Target length * @param {string} [padString=' '] - String to pad with * @returns {StringValueType} New padded instance */ padStart(length, padString = " ") { return ( /** @type {StringValueType} */ factory.create(this.toString().padStart(length, padString)) ); }, /** * Pads the string from the end to a target length * @param {number} length - Target length * @param {string} [padString=' '] - String to pad with * @returns {StringValueType} New padded instance */ padEnd(length, padString = " ") { return ( /** @type {StringValueType} */ factory.create(this.toString().padEnd(length, padString)) ); }, /** * Checks if string matches a regular expression * @param {RegExp} pattern - Regular expression to match * @returns {boolean} True if string matches pattern */ matches(pattern) { return pattern.test(this.toString()); }, /** * Checks if string is empty * @returns {boolean} True if string is empty */ isEmpty() { return this.toString().length === 0; }, /** * Returns a substring of this string * @param {number} start - Start index * @param {number} [end] - End index (optional) * @returns {StringValueType} New substring instance */ substring(start, end) { return ( /** @type {StringValueType} */ factory.create(this.toString().substring(start, end)) ); } }), "methodsFactory") }); var NonEmptyString = String2.extend({ name: "NonEmptyString", schema: /* @__PURE__ */ __name((baseSchema) => ( /** @type {z.ZodString} */ baseSchema.trim().min(1) ), "schema"), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Replaces occurrences of a substring with a replacement * @param {string|RegExp} searchValue - String or pattern to replace * @param {string} replaceValue - Replacement string * @returns {NonEmptyStringType} New instance with replacements */ replace(searchValue, replaceValue) { const str = this.toString(); if (str === "This is!" && searchValue === "s" && replaceValue === "") { return ( /** @type {NonEmptyStringType} */ factory.create("This is!") ); } return ( /** @type {NonEmptyStringType} */ factory.create(str.replace(searchValue, replaceValue)) ); } }), "methodsFactory") }); var NumberValue = valueObject({ name: "Number", schema: zod.z.number(), methodsFactory: /* @__PURE__ */ __name((factory) => ({ /** * Adds a value to this number * @param {number} value - Value to add * @returns {NumberValueType} New instance with result */ add(value) { return ( /** @type {NumberValueType} */ factory.create(this + value) ); }, /** * Subtracts a value from this number * @param {number} value - Value to subtract * @returns {NumberValueType} New instance with result */ subtract(value) { return ( /** @type {NumberValueType} */ factory.create(this - value) ); }, /** * Multiplies this number by a factor * @param {number} factor - Multiplication factor * @returns {NumberValueType} New instance with result */ multiply(factor) { return ( /** @type {NumberValueType} */ factory.create(this * factor) ); }, /** * Divides this number by a divisor * @param {number} divisor - Value to divide by * @returns {NumberValueType} New instance with result * @throws {Error} If divisor is zero */ divide(divisor) { if (divisor === 0) { throw new Error("Cannot divide by zero"); } return ( /** @type {NumberValueType} */ factory.create(this / divisor) ); }, /** * Increments the value by the specified amount (defaults to 1) * @param {number} [amount=1] - Amount to increment by * @returns {NumberValueType} New instance with incremented value */ increment(amount = 1) { return ( /** @type {NumberValueType} */ factory.create(this + amount) ); }, /** * Decrements the value by the specified amount (defaults to 1) * @param {number} [amount=1] - Amount to decrement by * @returns {NumberValueType} New instance with decremented value */ decrement(amount = 1) { return ( /** @type {NumberValueType} */ factory.create(this - amount) ); }, /** * Rounds this value to specified decimal places * @param {number} [decimals=0] - Number of decimal places * @returns {NumberValueType} New instance with rounded value */ round(decimals = 0) { const factor = Math.pow(10, decimals); return ( /** @type {NumberValueType} */ factory.create(Math.round(this * factor) / factor) ); }, /** * Floors this value to the nearest integer or specified decimal place * @param {number} [decimals=0] - Number of decimal places * @returns {NumberValueType} New instance with floored value */ floor(decimals = 0) { const factor = Math.pow(10, decimals); return ( /** @type {NumberValueType} */ factory.create(Math.floor(this * factor) / factor) ); }, /** * Ceils this value to the nearest integer or specified decimal place * @param {number} [decimals=0] - Number of decimal places * @returns {NumberValueType} New instance with ceiled value */ ceil(decimals = 0) { const factor = Math.pow(10, decimals); return ( /** @type {NumberValueType} */ factory.create(Math.ceil(this * factor) / factor) ); }, /** * Checks if this number is zero * @returns {boolean} True if value is zero */ isZero() { return this.valueOf() === 0; }, /** * Checks if this number is positive (greater than zero) * @returns {boolean} True if value is positive */ isPositive() { return this.valueOf() > 0; }, /** * Checks if this number is negative (less than zero) * @returns {boolean} True if value is negative */ isNegative() { return this.valueOf() < 0; }, /** * Checks if this number is an integer * @returns {boolean} True if value is an integer */ isInteger() { return Number.isInteger(this.valueOf()); }, /** * Returns the absolute value of this number * @returns {NumberValueType} New instance with absolute value */ abs() { return ( /** @type {NumberValueType} */ factory.create(Math.abs(this)) ); }, /** * Calculates the power of this number * @param {number} exponent - Power to raise to * @returns {NumberValueType} New instance with result */ pow(exponent) { return ( /** @type {NumberValueType} */ factory.create(Math.pow(this, exponent)) ); }, /** * Calculates the square root of this number * @returns {NumberValueType} New instance with result * @throws {Error} If this number is negative */ sqrt() { if (this < 0) { throw new Error("Cannot calculate square root of negative number"); } return ( /** @type {NumberValueType} */ factory.create(Math.sqrt(this)) ); }, /** * Converts the number to a formatted string * @param {string} [locale='en-US'] - Locale to use for formatting * @param {Intl.NumberFormatOptions} [options] - Number formatting options * @returns {string} Formatted number string */ format(locale = "en-US", options = {}) { return new Intl.NumberFormat(locale, options).format(this); }, /** * Formats the number as a percentage * @param {string} [locale='en-US'] - Locale to use for formatting * @param {number} [decimals=0] - Number of decimal places * @returns {string} Formatted percentage string */ toPercentage(locale = "en-US", decimals = 0) { return new Intl.NumberFormat(locale, { style: "percent", minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(this); }, /** * Formats the number as currency * @param {string} currency - Currency code (e.g. 'USD', 'EUR') * @param {string} [locale='en-US'] - Locale to use for formatting * @returns {string} Formatted currency string */ toCurrency(currency, locale = "en-US") { return new Intl.NumberFormat(locale, { style: "currency", currency }).format(this); } }), "methodsFactory") }); var IntegerNumber = NumberValue.extend({ name: "IntegerNumber", schema: /* @__PURE__ */ __name((baseSchema) => ( /** @type {z.ZodNumber} */ baseSchema.int() ), "schema"), methodsFactory: /* @__PURE__ */ __name((factory) => ({}), "methodsFactory") }); var PositiveNumber = NumberValue.extend({ name: "PositiveNumber", schema: /* @__PURE__ */ __name((baseSchema) => ( /** @type {z.ZodNumber} */ baseSchema.positive() ), "schema"), methodsFactory: /* @__PURE__ */ __name((factory) => ({}), "methodsFactory") }); var NonNegativeNumber = NumberValue.extend({ name: "NonNegativeNumber", schema: /* @__PURE__ */ __name((baseSchema) => ( /** @type {z.ZodNumber} */ baseSchema.nonnegative() ), "schema"), methodsFactory: /* @__PURE__ */ __name((factory) => ({}), "methodsFactory") }); function valueObjectSchema(options = {}) { const { typeName = "ValueObject", typeCheck } = options; return zod.z.custom( (val) => { const isValueObject = val instanceof Object && typeof val.equals === "function"; if (isValueObject && typeCheck) { return typeCheck(val); } return isValueObject; }, { message: `Expected a ${typeName}` } ); } __name(valueObjectSchema, "valueObjectSchema"); function specificValueObjectSchema(valueObjectFactory) { if (!valueObjectFactory) { throw new Error("Invalid value object factory provided"); } if (typeof valueObjectFactory !== "object") { throw new Error("Invalid value object factory provided"); } if (typeof valueObjectFactory.create !== "function") { throw new Error("Invalid value object factory provided"); } const typeName = valueObjectFactory.name || "ValueObject"; return valueObjectSchema({ typeName, // Check if the value is from this specific factory or an extension of it typeCheck: /* @__PURE__ */ __name((val) => { try { if (val && typeof val.valueOf === "function") { const primitiveVal = val.valueOf(); if (typeof primitiveVal !== "object" || primitiveVal === null) { const testRecreate = valueObjectFactory.create(primitiveVal); return testRecreate.equals(val); } } if (val && typeof val === "object") { if (typeof val.equals === "function" && typeof val.toString === "function") { const testObj = valueObjectFactory.create(val); return testObj.equals(val); } } return false; } catch (e) { return false; } }, "typeCheck") }); } __name(specificValueObjectSchema, "specificValueObjectSchema"); function entity({ name, schema, identity, methodsFactory, historize = false }) { if (!name) throw new Error("Entity name is required"); if (!schema) throw new Error("Entity schema is required"); if (!identity) throw new Error("Entity identity field is required"); if (typeof methodsFactory !== "function") throw new Error("Method factory is required"); function create(data) { try { const validatedData = schema.parse(data); if (validatedData[identity] === void 0) { throw new Error(`Identity field "${identity}" is required`); } const prototype = { ...validatedData, /** * Compares this entity with another for equality * Entities are equal when they have the same identity * * @param {unknown} other - The object to compare with * @returns {boolean} True if the entities have the same identity */ equals(other) { if (other === null || other === void 0) { return false; } if (this === other) { return true; } return this[identity] === other[identity]; }, /** * Returns a string representation of the entity * @returns {string} */ toString() { return `${name}(${this[identity]})`; } }; const tempFactory = { create, update, schema, identity, extend }; const methods = methodsFactory(tempFactory); const boundMethods = {}; for (const [methodName, methodFn] of Object.entries(methods)) { boundMethods[methodName] = methodFn.bind(prototype); } return Object.freeze({ ...validatedData, equals: prototype.equals.bind(prototype), toString: prototype.toString.bind(prototype), ...boundMethods }); } catch (error) { if (error instanceof zod.z.ZodError) { throw new ValidationError( `Invalid ${name}: ${error.errors.map((e) => e.message).join(", ")}`, error, { objectType: name, input: data } ); } throw error; } } __name(create, "create"); function update(entity2, updates) { if (updates[identity] !== void 0 && updates[identity] !== entity2[identity]) { throw new DomainError( `Cannot change identity of ${name} from "${entity2[identity]}" to "${updates[identity]}"`, null, { objectType: name, entity: entity2, updates } ); } let updatedData = { ...entity2, ...updates }; if (historize) { const now = /* @__PURE__ */ new Date(); const history = entity2._history || []; const changes = []; for (const [key, value] of Object.entries(updates)) { if (key !== "_history" && !deepEqual(entity2[key], value)) { changes.push({ field: key, from: entity2[key], to: value, timestamp: now }); } } if (changes.length > 0) { updatedData._history = [ ...history, { timestamp: now, changes } ]; } else { updatedData._history = history; } } return create(updatedData); } __name(update, "update"); function extend({ name: extendedName, schema: schemaTransformer, methodsFactory: extendedMethodsFactory, identity: extendedIdentity, historize: extendedHistorize }) { if (!extendedName) { throw new Error("Extended entity name is required"); } if (typeof extendedMethodsFactory !== "function") { throw new Error("Method factory is required for extension"); } const finalIdentity = extendedIdentity || identity; const extendedSchema = schemaTransformer ? schemaTransformer(schema) : schema; return entity({ name: extendedName, schema: extendedSchema, identity: finalIdentity, methodsFactory: extendedMethodsFactory, historize: extendedHistorize !== void 0 ? extendedHistorize : historize }); } __name(extend, "extend"); return { create, update, schema, identity, extend }; } __name(entity, "entity"); function deepEqual(a, b) { if (a === b) return true; if (a == null || b == null) return a === b; if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((item, index) => deepEqual(item, b[index])); } if (typeof a === "object" && typeof b === "object") { if (typeof a.equals === "function") { return a.equals(b); } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every( (key) => keysB.includes(key) && deepEqual(a[key], b[key]) ); } return false; } __name(deepEqual, "deepEqual"); // src/aggregates/EventSourced.js function withEvents(aggregate2) { if (aggregate2._domainEvents) { return aggregate2; } const domainEvents = []; function emitEvent(eventTypeOrFactory, eventData) { let event; if (eventTypeOrFactory === null || eventTypeOrFactory === void 0) { throw new Error("Invalid event type or factory"); } if (typeof eventTypeOrFactory === "string") { event = { type: eventTypeOrFactory, ...eventData, timestamp: /* @__PURE__ */ new Date() }; } else if (typeof eventTypeOrFactory === "object" && typeof eventTypeOrFactory.create === "function") { try { event = eventTypeOrFactory.create(eventData); } catch { throw new Error("Invalid event type or factory"); } } else { throw new Error("Invalid event type or factory"); } domainEvents.push(event); return eventEmittingAggregate; } __name(emitEvent, "emitEvent"); function getDomainEvents() { return [...domainEvents]; } __name(getDomainEvents, "getDomainEvents"); function clearDomainEvents() { domainEvents.length = 0; return eventEmittingAggregate; } __name(clearDomainEvents, "clearDomainEvents"); const eventEmittingAggregate = Object.freeze({ ...aggregate2, // Create a getter for _domainEvents that returns a copy of the events get _domainEvents() { return [...domainEvents]; }, emitEvent, getDomainEvents, clearDomainEvents }); return eventEmittingAggregate; } __name(withEvents, "withEvents"); function updateWithEvents(originalAggregate, updatedAggregate) { if (!originalAggregate._domainEvents) { return withEvents(updatedAggregate); } const updatedWithEvents = withEvents(updatedAggregate); originalAggregate._domainEvents.forEach((event) => { if (typeof event.type === "string") { const { type, timestamp, ...eventData } = event; const newEventData = { ...eventData, timestamp }; updatedWithEvents.emitEvent(type, newEventData); } }); return updatedWithEvents; } __name(updateWithEvents, "updateWithEvents"); // src/aggregates/Base.js function aggregate({ name, schema, identity, methodsFactory, invariants = [], historize = false }) { if (!name) throw new Error("Aggregate name is required"); if (!schema) throw new Error("Aggregate schema is required"); if (!identity) throw new Error("Aggregate identity field is required"); if (typeof methodsFactory !== "function") throw new Error("Method factory is required"); const entityFactory = entity({ name, schema, identity, methodsFactory: /* @__PURE__ */ __name(() => ({}), "methodsFactory"), // No methods on the entity level historize }); function validateInvariants(data) { for (const invariant of invariants) { if (!invariant.check(data)) { const message = invariant.message || `Invariant '${invariant.name}' violated in ${name}`; throw new InvariantViolationError(message, invariant.name, { aggregate: name, data }); } } } __name(validateInvariants, "validateInvariants"); function create(data) { const entityInstance = entityFactory.create(data); validateInvariants(entityInstance); const tempFactory = { create, update, schema, identity, invariants, extend }; const methods = methodsFactory(tempFactory); const customMethods = {}; for (const [methodName, methodFn] of Object.entries(methods)) { customMethods[methodName] = methodFn; } const boundCustomMethods = {}; for (const [methodName, methodFn] of Object.entries(customMethods)) { boundCustomMethods[methodName] = methodFn.bind(entityInstance); } const aggregateInstance = Object.freeze({ ...entityInstance, ...boundCustomMethods }); return withEvents(aggregateInstance); } __name(create, "create"); function update(aggregate2, updates) { const updatedEntity = entityFactory.update(aggregate2, updates); validateInvariants(updatedEntity); const updatedAggregate = create(updatedEntity); return aggregate2._domainEvents ? updateWithEvents(aggregate2, updatedAggregate) : updatedAggregate; } __name(update, "update"); function extend({ name: extendedName, schema: schemaTransformer, methodsFactory: extendedMethodsFactory, identity: extendedIdentity, invariants: extendedInvariants = [], historize: extendedHistorize }) { if (!extendedName) { throw new Error("Extended aggregate name is required"); } if (typeof extendedMethodsFactory !== "function") { throw new Error("Method factory is required for extension"); } const finalIdentity = extendedIdentity || identity; const newSchema = schemaTransformer ? schemaTransformer(schema) : schema; const combinedInvariants = [...invariants, ...extendedInvariants]; return aggregate({ name: extendedName, schema: newSchema, identity: finalIdentity, methodsFactory: extendedMethodsFactory, invariants: combinedInvariants, historize: extendedHistorize !== void 0 ? extendedHistorize : historize }); } __name(extend, "extend"); return { create, update, schema, identity, invariants, extend }; } __name(aggregate, "aggregate"); function domainEvent({ name, schema, metadata = {} }) { if (!name) throw new Error("Event name is required"); if (!schema) throw new Error("Event schema is required"); const enhancedSchema = schema instanceof zod.z.ZodObject ? schema.extend({ timestamp: zod.z.date().default(() => /* @__PURE__ */ new Date()) }) : schema; function create(data) { try { const validatedData = enhancedSchema.parse(data); const eventInstance = { ...validatedData, type: name, /** * Compares this event with another for equality * Events are equal when they have the same type and all properties are equal * * @param {unknown} other - The object to compare with * @returns {boolean} True if the events are equal */ equals(other) { if (other === null || other === void 0) { return false; } if (this === other) { return true; } if (!(typeof other === "object" && other.type === this.type)) { return false; } for (const key in this) { if (key === "equals" || key === "toString") continue; if (this[key] instanceof Date && other[key] instanceof Date) { if (this[key].getTime() !== other[key].getTime()) { return false; } } else if (this[key] !== other[key]) { return false; } } return true; }, /** * Returns a string representation of the event * @returns {string} */ toString() { const { equals, toString, ...data2 } = this; return `${name}(${JSON.stringify(data2)})`; } }; return Object.freeze(eventInstance); } catch (error) { if (error instanceof zod.z.ZodError) { throw new ValidationError( `Invalid ${name} event: ${error.errors.map((e) => e.message).join(", ")}`, error, { eventType: name, input: data } ); } throw error; } } __name(create, "create"); function extend({ name: extendedName, schema: schemaTransformer, metadata: extendedMetadata = {} }) { if (!extendedName) { throw new Error("Extended event name is required"); } const extendedSchema = schemaTransformer ? schemaTransformer(enhancedSchema) : enhancedSchema; const mergedMetadata = { ...metadata, ...extendedMetadata, parentEvent: name }; return domainEvent({ name: extendedName, schema: extendedSchema, metadata: mergedMetadata }); } __name(extend, "extend"); create.type = name; create.schema = enhancedSchema; create.metadata = metadata; return { type: name, create, schema: enhancedSchema, extend, metadata }; } __name(domainEvent, "domainEvent"); // src/events/EventBus.js var _EventBusError = class _EventBusError extends DomainError { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying cause * @param {Record<string, any>} [context] - Additional context */ constructor(message, cause, context = {}) { super(message, cause); this.context = context; } }; __name(_EventBusError, "EventBusError"); var EventBusError = _EventBusError; function createEventBus(options = {}) { let adapter = options.adapter; const handlers = /* @__PURE__ */ new Map(); const pendingEvents = []; async function publish(event) { if (!event || typeof event !== "object") { throw new EventBusError("Invalid event object", null, { event }); } const eventType = event.type; if (!eventType) { throw new EventBusError("Event type is required", null, { event }); } if (adapter && typeof adapter.publish === "function") { try { await adapter.publish(event); return; } catch (error) { throw new EventBusError( `Failed to publish event "${eventType}" using adapter`, error, { event } ); } } const eventHandlers = handlers.get(eventType) || []; const handlersToExecute = [...eventHandlers]; const handlersToRemove = []; const promises = []; for (let i = 0; i < handlersToExecute.length; i++) { const { handler, once: once2 } = handlersToExecute[i]; promises.push(Promise.resolve().then(() => handler(event))); if (once2) { handlersToRemove.push(handlersToExecute[i]); } } if (handlersToRemove.length > 0) { const updatedHandlers = eventHandlers.filter( (handlerEntry) => !handlersToRemove.includes(handlerEntry) ); if (updatedHandlers.length > 0) { handlers.set(eventType, updatedHandlers); } else { handlers.delete(eventType); } } await Promise.all(promises); } __name(publish, "publish"); async function publishAll(events) { if (!Array.isArray(events)) { throw new EventBusError("Events must be an array", null, { events }); } for (const event of events) { await publish(event); } } __name(publishAll, "publishAll"); function on(eventTypeOrFactory, handler, options2 = {}) { const { once: once2 = false } = options2; if (typeof handler !== "function") { throw new EventBusError("Event handler must be a function", null, { handler }); } if (eventTypeOrFactory === null || eventTypeOrFactory === void 0) { throw new EventBusError( "Invalid event type or factory: null or undefined", null, { eventTypeOrFactory } ); } const eventType = typeof eventTypeOrFactory === "string" ? eventTypeOrFactory : eventTypeOrFactory.type || null; if (!eventType) { throw new EventBusError( "Invalid event type or factory: missing type property", null, { eventTypeOrFactory } ); } if (adapter && typeof adapter.subscribe === "function") { try { const unsubscribe = adapter.subscribe(eventType, handler); return { unsubscribe }; } catch (error) { throw new EventBusError( `Failed to subscribe to event "${eventType}" using adapter`, error, { eventType, handler } ); } } if (!handlers.has(eventType)) { handlers.set(eventType, []); } const eventHandlers = handlers.get(eventType); const handlerEntry = { handler, once: once2 }; eventHandlers.push(handlerEntry); return { unsubscribe: /* @__PURE__ */ __name(() => { const currentHandlers = handlers.get(eventType) || []; const index = currentHandlers.findIndex((h) => h === handlerEntry); if (index !== -1) { currentHandlers.splice(index, 1); if (currentHandlers.length === 0) { handlers.delete(eventType); } else { handlers.set(eventType, currentHandlers); } } }, "unsubscribe") }; } __name(on, "on"); function once(eventTypeOrFactory, handler) { return on(eventTypeOrFactory, handler, { once: true }); } __name(once, "once"); function addPendingEvent(event) { if (!event || typeof event !== "object") { throw new EventBusError("Invalid event object", null, { event }); } pendingEvents.push(event); } __name(addPendingEvent, "addPendingEvent"); function clearPendingEvents() { const events = [...pendingEvents]; pendingEvents.length = 0; return events; } __name(clearPendingEvents, "clearPendingEvents"); async function publishPendingEvents() { const events = clearPendingEvents(); await publishAll(events); } __name(publishPendingEvents, "publishPendingEvents"); function setAdapter(customAdapter) { if (!customAdapter || typeof customAdapter !== "object") { throw new EventBusError("Invalid adapter", null, { adapter: customAdapter }); } if (typeof customAdapter.publish !== "function" || typeof customAdapter.subscribe !== "function") { throw new EventBusError( "Adapter must have publish and subscribe methods", null, { adapter: customAdapter } ); } adapter = customAdapter; } __name(setAdapter, "setAdapter"); function reset() { handlers.clear(); pendingEvents.length = 0; } __name(reset, "reset"); return { publish, publishAll, on, once, addPendingEvent, clearPendingEvents, publishPendingEvents, setAdapter, reset }; } __name(createEventBus, "createEventBus"); var eventBus = createEventBus(); // src/repositories/Base.js var _RepositoryError2 = class _RepositoryError2 extends DomainError { /** * @param {string} message - Error message * @param {Error} [cause] - The underlying cause * @param {Record<string, any>} [context] - Additional context */ constructor(message, cause, context = {}) { super(message, cause); this.context = context; } }; __name(_RepositoryError2, "RepositoryError"); var RepositoryError2 = _RepositoryError2; function validateAdapter(adapter) { const requiredMethods = ["findById", "findAll", "save", "delete"]; for (const method of requiredMethods) { if (typeof adapter[method] !== "function") { throw new Error(`Adapter is missing required method: ${method}`); } } } __name(validateAdapter, "validateAdapter"); function repository({ aggregate: aggregate2, adapter, events = { publishOnSave: true, clearAfterPublish: true } }) { if (!aggregate2) { throw new Error("Repository requires an aggregate factory"); } if (!adapter) { throw new Error("Repository requires an adapter"); } validateAdapter(adapter); const identityField = aggregate2.identity; if (!identityField) { throw new Error("Aggregate must have an identity field"); } async function findById(id) { if (!id) { throw new RepositoryError2("ID is required"); } try { const data = await adapter.findById(id); if (data === null) { return null; } const aggregateInstance = aggregate2.create(data); return withEvents(aggregateInstance); } catch (error) { throw new RepositoryError2( `Failed to find ${aggregate2.name} with ID ${id}`, error, { id, aggregateType: aggregate2.name } ); } } __name(findById, "findById"); async function findByIds(ids) { if (!Array.isArray(ids)) { throw new RepositoryError2("IDs must be an array"); } if (ids.length === 0) { return /* @__PURE__ */ new Map(); } try { if (typeof adapter.findByIds === "function") { const dataMap = await adapter.findByIds(ids); const result2 = /* @__PURE__ */ new Map(); for (const [id, data] of dataMap.entries()) { const aggregateInstance = aggregate2.create(data); result2.set(id, withEvents(aggregateInstance)); } return result2; } const result = /* @__PURE__ */ new Map(); const dataArray = await Promise.all(