domaindrivenjs
Version:
Composition-based Domain-Driven Design toolkit for JavaScript/TypeScript
1,593 lines (1,582 loc) • 79.7 kB
JavaScript
import { z } from 'zod';
import { randomUUID } from '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 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: 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: 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: 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: 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 randomUUID();
};
var String2 = valueObject({
name: "String",
schema: 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: 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 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 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 z.ZodObject ? schema.extend({
timestamp: 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 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(
ids.map((id) => adapter.findById(id))
);
for (let i =