🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@cerios/cerios-builder

Package Overview
Dependencies
Maintainers
2
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cerios/cerios-builder - npm Package Compare versions

Comparing version
1.6.0
to
1.7.0
+1396
dist/index.cjs
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const RESERVED_SET = /* @__PURE__ */ new Set([
"build",
"buildWithoutRuntimeValidation",
"buildWithoutCompileTimeValidation",
"buildUnsafe",
"buildPartial",
"buildFrozen",
"buildDeepFrozen",
"buildSealed",
"buildDeepSealed",
"clone",
"addValidator",
"setRequiredFields",
"removeOptionalProperty",
"clearOptionalProperties",
"setProperty",
"setProperties",
"setNestedProperty",
"addToArrayProperty",
"createBuilder",
"instantiateBuilder",
"getClassConstructor",
"getRequiredTemplate",
"validateRequiredFields",
"runValidators",
"assertValid",
"instantiate",
"snapshot",
"_actual",
"_requiredFields",
"_validators",
"_classConstructor",
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"then",
"toJSON"
]);
/**
* Resolves an auto-setter method name back to the property key it sets.
* `buildProp` -> `build` (reserved); everything else is identity.
* @internal
*/
function setterNameToKey(name) {
if (name.endsWith("Prop")) {
const base = name.slice(0, -4);
if (RESERVED_SET.has(base)) return base;
}
return name;
}
/**
* Deep clone helper for seeding builders from existing objects/instances, and for `clone()`.
*
* The single implementation for both base builders and both auto builders. It is
* cycle-safe, and preserves the built-in types a plain key-walk destroys: `new Date(0)`
* used to clone to `{}`, and self-referencing data threw a `RangeError`.
*
* The prototype is preserved, so a nested class instance stays an instance of its class.
* Flattening it to a plain object loses every method on it, which matters as soon as a
* nested builder contributes a real instance to the parent's state.
*
* @param obj - The value to clone
* @param seen - Cycle-tracking map; callers do not pass this
* @internal
*/
function deepClone(obj, seen = /* @__PURE__ */ new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
const existing = seen.get(obj);
if (existing !== void 0) return existing;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (Array.isArray(obj)) {
const cloned = [];
seen.set(obj, cloned);
for (const item of obj) cloned.push(deepClone(item, seen));
return cloned;
}
if (obj instanceof Map) {
const cloned = /* @__PURE__ */ new Map();
seen.set(obj, cloned);
for (const [key, value] of obj) cloned.set(deepClone(key, seen), deepClone(value, seen));
return cloned;
}
if (obj instanceof Set) {
const cloned = /* @__PURE__ */ new Set();
seen.set(obj, cloned);
for (const value of obj) cloned.add(deepClone(value, seen));
return cloned;
}
const cloned = Object.create(Object.getPrototypeOf(obj));
seen.set(obj, cloned);
for (const key of Object.keys(obj)) Object.defineProperty(cloned, key, {
value: deepClone(obj[key], seen),
writable: true,
enumerable: true,
configurable: true
});
return cloned;
}
/**
* Recursively freezes or seals a value and everything reachable from it.
*
* One implementation for both operations and both builders. The `seen` set makes it
* cycle-safe - `buildDeepFrozen()` on self-referencing data previously threw a
* `RangeError`. Functions are not traversed: walking into a function would freeze its
* `prototype`, which may be an object the caller does not own.
*
* @internal
*/
function deepHarden(obj, mode, seen = /* @__PURE__ */ new WeakSet()) {
if (obj === null || typeof obj !== "object" || seen.has(obj)) return obj;
seen.add(obj);
for (const key of Object.getOwnPropertyNames(obj)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor && "value" in descriptor) deepHarden(descriptor.value, mode, seen);
}
return mode === "freeze" ? Object.freeze(obj) : Object.seal(obj);
}
/**
* Builds the copy-on-write successor of an auto builder without invoking any constructor.
*
* Copy-on-write cannot call the subclass constructor: users write `constructor(data?)`,
* which has no way to receive the base class's internal argument shape. But creating a bare
* object off the prototype and assigning only the internal fields silently discards every
* other field the subclass declared - and because the discarded name is then absent from
* the target, the proxy hands back an auto-setter function for it, so `this.cache` reads as
* a function rather than `undefined`. Carrying the source's own properties over first fixes
* both halves of that.
*
* Subclass fields are carried **shallowly**: forks share the same field references, so a
* `Map` held in a field is shared between every builder forked from it. That matches how
* copy-on-write already treats nested data.
*
* @param source - The builder being forked. This is the Proxy; `Reflect.*` forwards to the
* target, and copying descriptors preserves getters and non-enumerable fields.
* @param internals - The internal fields to overwrite after the carry-over
* @internal
*/
function createBuilderCopy(source, internals) {
const copy = Object.create(Object.getPrototypeOf(source));
for (const key of Reflect.ownKeys(source)) {
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (descriptor) Object.defineProperty(copy, key, descriptor);
}
Object.assign(copy, internals);
return copy;
}
/**
* Proxy handler shared by both auto builders. Real members always win; any other
* accessed name becomes a bare property setter that delegates to the inherited
* (hidden) `setProperty`, so copy-on-write re-proxies through `this.constructor`.
* @internal
*/
const autoSetterHandler = { get(target, prop, receiver) {
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
if (RESERVED_SET.has(prop)) return Reflect.get(target, prop, receiver);
if (prop in target) return Reflect.get(target, prop, receiver);
const key = setterNameToKey(prop);
return (value) => receiver.setProperty(key, value);
} };
/**
* Path segments that must never be written through, because assigning to them mutates a
* prototype rather than the object itself.
* @internal
*/
const UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
"__proto__",
"constructor",
"prototype"
]);
/**
* Rejects dot-notation paths that would write through a prototype.
*
* `setNestedProperty("__proto__.x", v)` used to walk onto the clone's prototype and assign
* there, so the write silently vanished from the built object. It is harmless today only
* because the clone is always a fresh literal; refusing the path keeps it that way.
*
* @throws {Error} If any segment of the path is `__proto__`, `constructor`, or `prototype`
* @internal
*/
function assertSafePath(path) {
for (const segment of path.split(".")) if (UNSAFE_PATH_SEGMENTS.has(segment)) throw new Error(`Unsafe property path "${path}": the segment "${segment}" would write through a prototype rather than the object.`);
}
/**
* Rejects a single property key that would write through a prototype.
*
* Guarding only dot-notation paths was not enough. `__proto__` was reserved, so
* `SetterName` generated a `__proto__Prop` setter, and `setterNameToKey` mapped it straight
* back to `__proto__`. On the class builder the resulting `Object.assign` then re-parented
* the built instance, so it silently stopped being an instance of its own class and lost
* every method.
*
* @throws {Error} If the key is `__proto__`, `constructor`, or `prototype`
* @internal
*/
function assertSafeKey(key) {
if (key === "__proto__") throw new Error(`Unsafe property key "__proto__": writing it would re-parent the object.`);
}
/**
* Guards the auto-builder constructors' `init` argument.
*
* `setRequiredFields` accepts a bare {@link RequiredFieldsRecord}, so passing that same
* record straight to the constructor - `new B({}, { name: true })` - is an easy mistake.
* It structurally matches `BuilderInit`, so it used to be accepted silently and produce a
* builder with *zero* required fields: validation then passed for every incomplete object.
* Failing loudly is the only safe response.
*
* @throws {Error} If the object has keys but none of them are `BuilderInit` keys
* @internal
*/
function assertBuilderInit(init) {
const keys = Object.keys(init);
if (keys.length === 0 || keys.some((key) => key === "requiredFields" || key === "validators")) return;
throw new Error(`Invalid builder options: expected { requiredFields?, validators? } but received keys ${keys.map((key) => `"${key}"`).join(", ")}. If you meant to declare required fields, wrap them: { requiredFields: { ... } }.`);
}
/**
* Normalises the two accepted required-field shapes - an array of dot-notation paths, or
* an exhaustive {@link RequiredFieldsRecord} - to the array of paths used internally.
*
* @internal
*/
function toRequiredPaths(fields) {
return Array.isArray(fields) ? fields : Object.keys(fields);
}
/**
* Picks the properties to keep when clearing optional properties.
*
* A required path may be nested (`"address.city"`), in which case the whole root property
* (`address`) is preserved - the required leaf cannot survive without it. Shared by both
* base builders so the two cannot drift; `CeriosClassBuilder` previously treated
* `"address.city"` as a single literal key, found no match, and dropped `address` entirely.
*
* @internal
*/
function pickRequiredRoots(actual, requiredPaths) {
const kept = {};
for (const path of requiredPaths) {
const rootKey = path.split(".")[0];
if (rootKey in actual && !(rootKey in kept)) kept[rootKey] = actual[rootKey];
}
return kept;
}
//#endregion
//#region src/builder-error.ts
/**
* The error thrown by every validating build variant.
*
* The message text is unchanged from when these were plain `Error`s, so existing
* string-matching callers keep working. The structured fields are the reason this type
* exists: they let callers branch on *what* failed without parsing the message.
*
* @example
* ```typescript
* try {
* builder.build();
* } catch (error) {
* if (error instanceof CeriosBuilderError && error.missingFields.length > 0) {
* promptFor(error.missingFields);
* }
* }
* ```
*/
var CeriosBuilderError = class CeriosBuilderError extends Error {
constructor(message, missingFields, validationErrors) {
super(message);
this.name = "CeriosBuilderError";
this.missingFields = missingFields;
this.validationErrors = validationErrors;
Object.setPrototypeOf(this, CeriosBuilderError.prototype);
}
};
/**
* Describes a validator for error messages: its index, plus its name when it has one.
* Arrow functions assigned to a `const` are named; truly anonymous ones are not.
*/
function describeValidator(validator, index) {
const name = validator.name;
return name ? `Validator #${index} (${name})` : `Validator #${index}`;
}
/**
* Runs every validator and collects the failures.
*
* Shared by both base builders, which previously held byte-identical private copies.
*
* Two behaviours differ from those copies, both of which were silent failures:
* - A validator returning `false` used to push the bare string `"Validation failed"`, so
* three failing validators produced `"Validation failed: Validation failed; Validation
* failed; Validation failed"` and you could not tell which one failed. It now names the
* validator.
* - A validator returning anything other than `true`, `false`, or a non-empty string - most
* easily `undefined`, from a function that forgot to return - used to count as a *pass*.
* It is now an error, because silently accepting invalid data is the worse outcome.
*
* @param validators - The validators to run
* @param actual - The current builder state to validate
* @returns One message per failure, in validator order
* @internal
*/
function runValidatorsAgainst(validators, actual) {
const errors = [];
for (const [index, validator] of validators.entries()) {
const result = validator(actual);
if (result === true) continue;
if (result === false) {
errors.push(`${describeValidator(validator, index)} returned false`);
continue;
}
if (typeof result === "string" && result.length > 0) {
errors.push(result);
continue;
}
errors.push(`${describeValidator(validator, index)} returned ${result === "" ? "an empty string" : String(result)}; expected true, false, or a non-empty error message`);
}
return errors;
}
//#endregion
//#region src/cerios-builder.ts
/**
* @deprecated Will be removed in the next major version. Migrate to `CeriosAutoBuilder<T>()`,
* which generates every setter automatically with the same compile-time safety — see
* MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`).
*/
var CeriosBuilder = class {
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .setRequiredFields(['path.to.field1', 'path.to.field2'])
* .setField1('value1')
* .setField2('value2')
* .buildSafe();
* ```
*/
setRequiredFields(fields) {
return this.instantiateBuilder(this._actual, toRequiredPaths(fields), this._validators);
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setAge(20)
* .setEmail('user@example.com')
* .build();
* ```
*/
addValidator(validator) {
return this.instantiateBuilder(this._actual, this._requiredFields, [...this._validators, validator]);
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* @private
*/
getRequiredTemplate() {
const staticFields = this.constructor.requiredTemplate ?? [];
if (this._requiredFields.size === 0) return staticFields;
if (staticFields.length === 0) return [...this._requiredFields];
return [.../* @__PURE__ */ new Set([...staticFields, ...this._requiredFields])];
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
const seen = /* @__PURE__ */ new Set();
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
seen.add(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!seen.has(path)) {
missing.push(path);
seen.add(path);
}
}
}
return missing;
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
return runValidatorsAgainst(this._validators, this._actual);
}
/**
* Removes an optional property from the builder.
* Only works with optional properties (those that can be undefined).
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John')
* .setEmail('john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.instantiateBuilder(newData, this._requiredFields, this._validators);
}
/**
* Clears all optional properties from the builder, keeping only required ones.
* Properties in the required template and those marked as required are preserved.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John') // required
* .setAge(30) // required
* .setEmail('john@example.com') // optional
* .setPhone('555-1234') // optional
* .clearOptionalProperties();
* // Only name and age remain
* ```
*/
clearOptionalProperties() {
const newData = pickRequiredRoots(this._actual, this.getRequiredTemplate());
return this.instantiateBuilder(newData, this._requiredFields, this._validators);
}
/**
* Creates a new builder instance. Intended to be called by subclasses.
*
* @param _actual - The current partial state of the object being built
* @param _requiredFields - Optional array of required field paths to preserve across instances
* @param _validators - Optional array of validators to preserve across instances
* @protected
*/
constructor(_actual, _requiredFields, _validators) {
this._actual = _actual;
this._requiredFields = /* @__PURE__ */ new Set();
this._validators = [];
if (_requiredFields) this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]);
if (_validators) this._validators = [..._validators];
}
/**
* Sets a property value and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses.
*
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set and type state updated
* @protected
*/
setProperty(key, value) {
assertSafeKey(key);
return this.instantiateBuilder({
...this._actual,
[key]: value
}, this._requiredFields, this._validators);
}
/**
* Sets multiple property values at once and returns a new builder instance with updated type state.
* @param props - An object with one or more properties to set.
* @returns A new builder instance with the properties set and type state updated.
* @protected
*/
setProperties(props) {
Object.keys(props).forEach(assertSafeKey);
return this.instantiateBuilder({
...this._actual,
...props
}, this._requiredFields, this._validators);
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
*
* @template P - The property path (e.g., "parent.child.grandchild")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* builder.setNestedProperty('Order.Details.CustomerId', 'value')
* ```
*/
setNestedProperty(path, value) {
assertSafePath(path);
const keys = path.split(".");
const newActual = deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) current[key] = {};
current = current[key];
}
current[keys[keys.length - 1]] = value;
return this.instantiateBuilder(newActual, this._requiredFields, this._validators);
}
/**
* Adds a value to an array property and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses for array properties.
*
* @template K - The property key (must be an array property)
* @template V - The type of the array element
* @param key - The array property key to add to
* @param value - The value to add to the array
* @returns A new builder instance with the array property updated and type state updated
* @protected
*/
addToArrayProperty(key, value) {
assertSafeKey(key);
const currentArray = this._actual[key] ?? [];
return this.instantiateBuilder({
...this._actual,
[key]: [...currentArray, value]
}, this._requiredFields, this._validators);
}
/**
* Builds the final object with both compile-time and runtime validation.
* This is the recommended and safest way to build objects.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
build() {
this.assertValid("build");
return this.snapshot();
}
/**
* Builds the final object with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: No validation
*
* @returns The fully built object of type T
*/
buildWithoutRuntimeValidation() {
return this.snapshot();
}
/**
* Builds the final object with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildWithoutCompileTimeValidation() {
this.assertValid("buildWithoutCompileTimeValidation");
return this.snapshot();
}
/**
* Builds the final object without any validation (neither compile-time nor runtime).
* Use this only when you're certain the object is valid and need maximum performance.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: No validation
*
* @returns The object of type T (may be incomplete)
*/
buildUnsafe() {
return this.snapshot();
}
/**
* Builds a partial object, which may not have all required fields set.
* This is useful for scenarios where you want to inspect or validate the current state before finalizing.
*
* @returns The partially built object
*/
/**
* Runs both runtime checks, throwing on the first failure.
*
* The single copy of what used to be an identical nine-line block in all six validating
* build variants. Centralising it is what lets the thrown error name the method the
* caller actually invoked instead of always saying "build".
*
* @param methodName - The build variant being run, used in the error message
* @throws {CeriosBuilderError} If a required field is missing or a validator fails
*/
assertValid(methodName) {
const missing = this.validateRequiredFields();
if (missing.length > 0) throw new CeriosBuilderError(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, missing, []);
const validationErrors = this.runValidators();
if (validationErrors.length > 0) throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors);
}
/**
* Returns the built value as a deep copy.
*
* Every build variant goes through this. Returning `this._actual` directly - as the
* build methods used to - handed the caller the builder's live internal state, so
* mutating the result mutated the builder and every object built from it afterwards.
* `buildFrozen()` was worse: it froze the builder's own state in place.
*/
snapshot() {
return deepClone(this._actual);
}
/**
* The single construction seam used by every copy-on-write method.
*
* By default this calls the subclass constructor with the internal 3-argument shape,
* which requires the subclass to accept and forward it. Subclasses whose constructor
* has a different signature - such as the auto builders, where users write
* `constructor(data?)` - override this to build the copy without invoking a
* constructor at all, so the required fields and validators cannot be lost.
*
* @param data - The state for the new builder
* @param requiredFields - Required field paths to carry over
* @param validators - Validators to carry over
* @returns A new builder instance of the same concrete type
*/
instantiateBuilder(data, requiredFields, validators) {
const BuilderClass = this.constructor;
return new BuilderClass(data, requiredFields instanceof Set ? [...requiredFields] : requiredFields, validators);
}
buildPartial() {
return deepClone(this._actual);
}
static from(instance) {
const clonedData = deepClone(instance);
return new this(clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = new MyBuilder({}).setName('John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = deepClone(this._actual);
return this.instantiateBuilder(clonedData, this._requiredFields, this._validators);
}
/**
* Builds and freezes the final object with both compile-time and runtime validation.
* The returned object is shallowly frozen - top-level properties cannot be modified,
* but nested objects remain mutable.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.freeze()
*
* @returns The frozen object of type Readonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildFrozen() {
this.assertValid("buildFrozen");
return Object.freeze(this.snapshot());
}
/**
* Builds and deeply freezes the final object with both compile-time and runtime validation.
* The returned object is recursively frozen - all nested objects and arrays are also frozen.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.freeze()
*
* @returns The deeply frozen object of type DeepReadonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildDeepFrozen() {
this.assertValid("buildDeepFrozen");
return deepHarden(this.snapshot(), "freeze");
}
/**
* Builds and seals the final object with both compile-time and runtime validation.
* The returned object is shallowly sealed - properties cannot be added or removed,
* but existing properties can still be modified. Nested objects remain unsealed.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.seal()
*
* @returns The sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildSealed() {
this.assertValid("buildSealed");
return Object.seal(this.snapshot());
}
/**
* Builds and deeply seals the final object with both compile-time and runtime validation.
* The returned object is recursively sealed - properties cannot be added or removed at any level,
* but existing properties can still be modified.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.seal()
*
* @returns The deeply sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildDeepSealed() {
this.assertValid("buildDeepSealed");
return deepHarden(this.snapshot(), "seal");
}
};
//#endregion
//#region src/cerios-auto-builder.ts
/**
* Concrete runtime backing class for {@link CeriosAutoBuilder}. Extends the real
* builder so it inherits all build/validation/clone/from machinery, and returns a
* Proxy from its constructor so bare-name property setters work.
* @internal
*/
var AutoBuilderRuntime = class extends CeriosBuilder {
constructor(data = {}, initOrRequiredFields, validators) {
if (initOrRequiredFields !== void 0 && !Array.isArray(initOrRequiredFields)) {
const init = initOrRequiredFields;
assertBuilderInit(init);
const required = init.requiredFields === void 0 ? void 0 : toRequiredPaths(init.requiredFields);
super(data, required, init.validators);
} else super(data, initOrRequiredFields, validators);
return new Proxy(this, autoSetterHandler);
}
/**
* Copy-on-write must not go through the subclass constructor. Users write
* `constructor(data?) { super(data, { requiredFields: [...] }) }`, which discards the
* arguments the base class passes - so a later `setRequiredFields()` would be silently
* reverted to whatever the constructor hardcodes. {@link createBuilderCopy} sidesteps
* user constructors while still carrying the subclass's own fields across.
*/
instantiateBuilder(data, requiredFields, validators) {
return new Proxy(createBuilderCopy(this, {
_actual: data,
_requiredFields: new Set(requiredFields),
_validators: [...validators]
}), autoSetterHandler);
}
};
/**
* Creates a base class with automatic bare-name setters for every property of T.
* Extend the returned class; add ordinary methods for any custom logic (they use
* the auto setters on `this`, and their return types can be left to inference).
*
* @example
* ```typescript
* interface User { id: string; name: string; role: string; age?: number; }
*
* class UserBuilder extends CeriosAutoBuilder<User>() {
* static create(): UserBuilder { return new UserBuilder({}); }
* asAdmin() { return this.role("admin"); } // inferred BuilderStep<this, User, "role">
* }
*
* const user = UserBuilder.create().id("1").name("Alice").asAdmin().build();
* // UserBuilder.create().id("1").build(); // compile error: name and role not set
* ```
*
* Custom methods use a name of their own (`asAdmin`, `trimmedName`) and delegate to the
* generated setter — a custom method cannot reuse a property's exact name, because the
* method would shadow the setter it needs to call.
*
* @template T - The type to build
* @returns An abstract base class to extend
*/
function CeriosAutoBuilder() {
return AutoBuilderRuntime;
}
//#endregion
//#region src/cerios-class-builder.ts
/**
* Getter-only accessor names per target class, computed once per class.
* @internal
*/
const GETTER_ONLY_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
/**
* Collects the names on the class's prototype chain that resolve to an accessor with a
* getter but no setter. The nearest descriptor wins, so a derived class that redeclares an
* inherited getter-only accessor as a getter/setter pair makes the name writable again.
*
* `DataPropertiesOnly` cannot exclude these at the type level - a getter is structurally
* indistinguishable from a data property - so they must be caught at runtime instead.
*
* @internal
*/
function getterOnlyKeys(ctor) {
const cached = GETTER_ONLY_KEYS_CACHE.get(ctor);
if (cached) return cached;
const keys = /* @__PURE__ */ new Set();
const seen = /* @__PURE__ */ new Set();
let proto = ctor.prototype;
while (proto && proto !== Object.prototype) {
for (const name of Object.getOwnPropertyNames(proto)) {
if (seen.has(name)) continue;
seen.add(name);
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (descriptor?.get && !descriptor.set) keys.add(name);
}
proto = Object.getPrototypeOf(proto);
}
GETTER_ONLY_KEYS_CACHE.set(ctor, keys);
return keys;
}
/**
* Rejects writes to a getter-only accessor of the target class.
*
* Without this, the value sat in the builder state until build, where assigning it threw
* `TypeError: Cannot set property ... which has only a getter` - far from the misuse and
* without naming the builder as the culprit.
*
* @throws {CeriosBuilderError} If the key is a getter-only accessor on the target class
* @internal
*/
function assertNotGetterOnly(ctor, key) {
if (typeof key === "string" && getterOnlyKeys(ctor).has(key)) throw new CeriosBuilderError(`"${key}" is a getter-only accessor on ${ctor.name}; its value is computed by the class and cannot be set through the builder.`, [], []);
}
/**
* @deprecated Will be removed in the next major version. Migrate to `CeriosClassAutoBuilder(Class)`,
* which generates every setter automatically with the same compile-time safety — see
* MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`).
*/
var CeriosClassBuilder = class {
/**
* Creates a new class builder instance.
* @param classConstructor - The class constructor to use for building
* @param data - Optional initial data
* @param _validators - Optional array of validators to preserve across instances
* @param _requiredFields - Optional required fields to preserve across instances
*/
constructor(classConstructor, data = {}, _validators, _requiredFields) {
this._validators = [];
this._requiredFields = /* @__PURE__ */ new Set();
for (const key of Object.keys(data)) assertNotGetterOnly(classConstructor, key);
this._classConstructor = classConstructor;
this._actual = data;
if (_validators) this._validators = [..._validators];
if (_requiredFields) this._requiredFields = _requiredFields instanceof Set ? new Set(_requiredFields) : /* @__PURE__ */ new Set([..._requiredFields]);
}
/**
* Gets the class constructor for this builder.
*
* Protected rather than private so `instantiateBuilder` overrides can read it from the
* instance instead of capturing it, which is what lets the class auto builder's runtime
* class hold no per-factory-call state.
*/
getClassConstructor() {
return this._classConstructor;
}
/**
* Creates a new instance of the builder with updated data.
* Subclasses can override this to return the correct subclass type.
* @private
*/
createBuilder(data) {
return this.instantiateBuilder(data, this._validators, this._requiredFields);
}
/**
* Runs both runtime checks, throwing on the first failure.
*
* The single copy of what used to be an identical nine-line block in all six validating
* build variants. Centralising it is what lets the thrown error name the method the
* caller actually invoked instead of always saying "build".
*
* @param methodName - The build variant being run, used in the error message
* @throws {CeriosBuilderError} If a required field is missing or a validator fails
*/
assertValid(methodName) {
const missing = this.validateRequiredFields();
if (missing.length > 0) throw new CeriosBuilderError(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, missing, []);
const validationErrors = this.runValidators();
if (validationErrors.length > 0) throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors);
}
/**
* Instantiates the target class from the current state.
*
* The single copy of what used to be an identical eight-line block in all eight build
* variants. Classes that assign in their constructor need nothing further; those that
* do not get the data assigned afterwards, which is what the `needsAssign` check
* detects.
*/
instantiate() {
const ctor = this.getClassConstructor();
const data = deepClone(this._actual);
const instance = new ctor(data);
const getterOnly = getterOnlyKeys(ctor);
const dataKeys = Object.keys(data).filter((key) => !getterOnly.has(key));
if (dataKeys.some((key) => instance[key] === void 0 && data[key] !== void 0)) for (const key of dataKeys) instance[key] = data[key];
return instance;
}
/**
* The single construction seam used by every copy-on-write method.
*
* By default this calls the subclass constructor with the internal 4-argument shape,
* which requires the subclass to accept it. Subclasses whose constructor has a
* different signature - such as the class auto builders, where users write
* `constructor(data?)` - override this to build the copy without invoking a
* constructor at all.
*
* @param data - The state for the new builder
* @param validators - Validators to carry over
* @param requiredFields - Required fields to carry over
* @returns A new builder instance of the same concrete type
*/
instantiateBuilder(data, validators, requiredFields) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, data, validators, requiredFields);
}
setProperty(key, value) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
return this.createBuilder({
...this._actual,
[key]: value
});
}
/**
* Sets multiple properties at once.
* @template K - The property keys being set
* @param props - Object with properties to set
* @returns A new builder instance with the properties set
* @protected
*/
setProperties(props) {
for (const key of Object.keys(props)) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
}
return this.createBuilder({
...this._actual,
...props
});
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
* Only supports data properties (excludes methods).
*
* @template P - The property path (e.g., "address.city")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* class Address {
* street!: string;
* city!: string;
* }
* class Person {
* name!: string;
* address!: Address;
* }
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setNestedProperty('address.city', 'New York')
* .build();
* ```
*/
setNestedProperty(path, value) {
assertSafePath(path);
const keys = path.split(".");
assertNotGetterOnly(this.getClassConstructor(), keys[0]);
const newActual = deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) current[key] = {};
current = current[key];
}
current[keys[keys.length - 1]] = value;
return this.createBuilder(newActual);
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .setRequiredFields(['name', 'age'])
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .buildWithoutCompileTimeValidation();
* ```
*/
setRequiredFields(fields) {
return this.instantiateBuilder(this._actual, this._validators, new Set(toRequiredPaths(fields)));
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* If instance-level fields are set via setRequiredFields(), they are combined with static fields.
* @private
*/
getRequiredTemplate() {
const staticFields = this.constructor.requiredDataProperties ?? [];
const instanceFields = Array.from(this._requiredFields);
if (instanceFields.length > 0) return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
return staticFields;
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
const seen = /* @__PURE__ */ new Set();
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
seen.add(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!seen.has(path)) {
missing.push(path);
seen.add(path);
}
}
}
return missing;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setProperty('age', 20)
* .setProperty('email', 'user@example.com')
* .build();
* ```
*/
addValidator(validator) {
return this.instantiateBuilder(this._actual, [...this._validators, validator], this._requiredFields);
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
return runValidatorsAgainst(this._validators, this._actual);
}
/**
* Removes an optional property from the builder.
* Only works with optional data properties (those that can be undefined).
* Methods are automatically excluded.
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* class Person {
* name!: string;
* email?: string;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('email', 'john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.createBuilder(newData);
}
/**
* Clears all optional properties from the builder, keeping only required data properties.
* Uses the combined required-field template from static defaults and instance-level fields.
* If no required fields are configured, all properties are cleared.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* class Person {
* name!: string; // required
* age!: number; // required
* email?: string; // optional
* phone?: string; // optional
* }
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .setProperty('email', 'john@example.com')
* .setProperty('phone', '555-1234')
* .clearOptionalProperties();
* // Only name and age are preserved, email and phone are cleared
* ```
*/
clearOptionalProperties() {
return this.createBuilder(pickRequiredRoots(this._actual, this.getRequiredTemplate()));
}
/**
* Adds a value to an array property.
* @template K - The array property key
* @template V - The array element type
* @param key - The array property key
* @param value - The value to add to the array
* @returns A new builder instance with the value added
*/
addToArrayProperty(key, value) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
const currentArray = this._actual[key] ?? [];
return this.createBuilder({
...this._actual,
[key]: [...currentArray, value]
});
}
/**
* Builds the final class instance with compile-time and runtime validation.
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built and validated class instance
* @throws {Error} If required fields are missing or validation fails
*/
build() {
this.assertValid("build");
return this.instantiate();
}
/**
* Builds the final class instance with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: No validation
*
* @returns The fully built class instance
*/
buildWithoutRuntimeValidation() {
return this.instantiate();
}
/**
* Builds the final class instance with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildWithoutCompileTimeValidation() {
this.assertValid("buildWithoutCompileTimeValidation");
return this.instantiate();
}
/**
* Builds the class instance without any validation.
* Use only when you're certain the object is valid.
*
* @returns The built class instance (may be incomplete)
*/
buildUnsafe() {
return this.instantiate();
}
/**
* Builds a partial object (may not have all required fields).
*
* @returns The partially built object
*/
buildPartial() {
return deepClone(this._actual);
}
/**
* Builds and freezes the class instance (shallow freeze).
*
* @returns The frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildFrozen() {
this.assertValid("buildFrozen");
const instance = this.instantiate();
return Object.freeze(instance);
}
/**
* Builds and deeply freezes the class instance.
*
* @returns The deeply frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepFrozen() {
this.assertValid("buildDeepFrozen");
return deepHarden(this.instantiate(), "freeze");
}
/**
* Builds and seals the class instance (shallow freeze).
*
* @returns The sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildSealed() {
this.assertValid("buildSealed");
const instance = this.instantiate();
return Object.seal(instance);
}
/**
* Builds and deeply seals the class instance.
*
* @returns The deeply sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepSealed() {
this.assertValid("buildDeepSealed");
return deepHarden(this.instantiate(), "seal");
}
/**
* Creates a new builder instance from an existing class instance.
* This is useful for creating builders from existing instances to modify them.
*
* @param classConstructor - The class constructor to use
* @param instance - The existing class instance to create a builder from
* @returns A new builder instance initialized with the instance's data
*
* @example
* ```typescript
* const existingPerson = new Person({ name: 'John', age: 30 });
* const builder = PersonBuilder.from(Person, existingPerson);
* const updated = builder.setProperty('age', 31).build();
* ```
*/
static from(classConstructor, instance) {
const clonedData = deepClone(instance);
return new this(classConstructor, clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = PersonBuilder.create().setProperty('name', 'John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = deepClone(this._actual);
return this.createBuilder(clonedData);
}
};
//#endregion
//#region src/cerios-class-auto-builder.ts
/**
* One runtime class per target class, rather than per factory call.
*
* Without this, `CeriosClassAutoBuilder(Person) === CeriosClassAutoBuilder(Person)` is
* false, so `instanceof` silently fails whenever the factory is called twice for the same
* class, and every call adds a distinct object shape for the engine to track. Caching is
* only sound because the runtime class captures nothing per call beyond the cache key:
* `instantiateBuilder` reads `this.getClassConstructor()` rather than the closure.
*
* @internal
*/
const RUNTIME_CACHE = /* @__PURE__ */ new WeakMap();
/**
* Creates a base class with automatic bare-name setters for every **writable** data
* property of the class T. Building instantiates the real class, so methods, getters, and
* decorators are preserved. Methods, getter-only accessors, and `readonly` fields get no
* setter - a class's `readonly` fields are established through its constructor, so seed them
* with `new Builder({ ... })` or `Builder.from(instance)`. Extend the returned class and add
* ordinary methods for any custom logic.
*
* @example
* ```typescript
* class Person {
* name!: string;
* age!: number;
* constructor(data?: Partial<Person>) { if (data) Object.assign(this, data); }
* greet(): string { return `Hi, ${this.name}`; }
* }
*
* class PersonBuilder extends CeriosClassAutoBuilder(Person) {
* static create(): PersonBuilder { return new PersonBuilder(); }
* }
*
* const person = PersonBuilder.create().name("Alice").age(30).build();
* person.greet(); // "Hi, Alice"
* ```
*
* Custom methods use a name of their own (`asAdult`, `trimmedName`) and delegate to the
* generated setter — a custom method cannot reuse a data property's exact name, because
* the method would shadow the setter it needs to call.
*
* @param classConstructor - The class constructor to instantiate on build
* @template T - The class type to build
* @returns An abstract base class to extend
*/
function CeriosClassAutoBuilder(classConstructor) {
const cached = RUNTIME_CACHE.get(classConstructor);
if (cached) return cached;
class ClassAutoBuilderRuntime extends CeriosClassBuilder {
constructor(dataOrConstructor, initOrData, validators, requiredFields) {
if (typeof dataOrConstructor === "function") super(dataOrConstructor, initOrData, validators, requiredFields);
else {
const init = initOrData ?? {};
assertBuilderInit(init);
const required = init.requiredFields === void 0 ? void 0 : new Set(toRequiredPaths(init.requiredFields));
super(classConstructor, dataOrConstructor ?? {}, init.validators, required);
}
return new Proxy(this, autoSetterHandler);
}
/**
* Copy-on-write must not go through the subclass constructor. Users write
* `constructor(data?) { super(data, { requiredFields: [...] }) }`, which cannot
* receive the base class's internal 4-argument shape - forwarding it would land the
* class constructor in the data slot and drop the validators and required fields.
* {@link createBuilderCopy} sidesteps user constructors while still carrying the
* subclass's own fields across.
*/
instantiateBuilder(data, validators, requiredFields) {
return new Proxy(createBuilderCopy(this, {
_classConstructor: this.getClassConstructor(),
_actual: data,
_validators: [...validators],
_requiredFields: new Set(requiredFields)
}), autoSetterHandler);
}
}
const Runtime = ClassAutoBuilderRuntime;
Runtime.from = function(instance) {
return new this(deepClone(instance));
};
RUNTIME_CACHE.set(classConstructor, ClassAutoBuilderRuntime);
return ClassAutoBuilderRuntime;
}
//#endregion
exports.CeriosAutoBuilder = CeriosAutoBuilder;
exports.CeriosBuilder = CeriosBuilder;
exports.CeriosBuilderError = CeriosBuilderError;
exports.CeriosClassAutoBuilder = CeriosClassAutoBuilder;
exports.CeriosClassBuilder = CeriosClassBuilder;
//# sourceMappingURL=index.cjs.map

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

+1385
-1148

@@ -1,1154 +0,1391 @@

// src/cerios-builder.ts
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
deepFreeze(value);
}
});
return Object.freeze(obj);
const RESERVED_SET = /* @__PURE__ */ new Set([
"build",
"buildWithoutRuntimeValidation",
"buildWithoutCompileTimeValidation",
"buildUnsafe",
"buildPartial",
"buildFrozen",
"buildDeepFrozen",
"buildSealed",
"buildDeepSealed",
"clone",
"addValidator",
"setRequiredFields",
"removeOptionalProperty",
"clearOptionalProperties",
"setProperty",
"setProperties",
"setNestedProperty",
"addToArrayProperty",
"createBuilder",
"instantiateBuilder",
"getClassConstructor",
"getRequiredTemplate",
"validateRequiredFields",
"runValidators",
"assertValid",
"instantiate",
"snapshot",
"_actual",
"_requiredFields",
"_validators",
"_classConstructor",
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"then",
"toJSON"
]);
/**
* Resolves an auto-setter method name back to the property key it sets.
* `buildProp` -> `build` (reserved); everything else is identity.
* @internal
*/
function setterNameToKey(name) {
if (name.endsWith("Prop")) {
const base = name.slice(0, -4);
if (RESERVED_SET.has(base)) return base;
}
return name;
}
function deepSeal(obj) {
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
deepSeal(value);
}
});
return Object.seal(obj);
/**
* Deep clone helper for seeding builders from existing objects/instances, and for `clone()`.
*
* The single implementation for both base builders and both auto builders. It is
* cycle-safe, and preserves the built-in types a plain key-walk destroys: `new Date(0)`
* used to clone to `{}`, and self-referencing data threw a `RangeError`.
*
* The prototype is preserved, so a nested class instance stays an instance of its class.
* Flattening it to a plain object loses every method on it, which matters as soon as a
* nested builder contributes a real instance to the parent's state.
*
* @param obj - The value to clone
* @param seen - Cycle-tracking map; callers do not pass this
* @internal
*/
function deepClone(obj, seen = /* @__PURE__ */ new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
const existing = seen.get(obj);
if (existing !== void 0) return existing;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (Array.isArray(obj)) {
const cloned = [];
seen.set(obj, cloned);
for (const item of obj) cloned.push(deepClone(item, seen));
return cloned;
}
if (obj instanceof Map) {
const cloned = /* @__PURE__ */ new Map();
seen.set(obj, cloned);
for (const [key, value] of obj) cloned.set(deepClone(key, seen), deepClone(value, seen));
return cloned;
}
if (obj instanceof Set) {
const cloned = /* @__PURE__ */ new Set();
seen.set(obj, cloned);
for (const value of obj) cloned.add(deepClone(value, seen));
return cloned;
}
const cloned = Object.create(Object.getPrototypeOf(obj));
seen.set(obj, cloned);
for (const key of Object.keys(obj)) Object.defineProperty(cloned, key, {
value: deepClone(obj[key], seen),
writable: true,
enumerable: true,
configurable: true
});
return cloned;
}
var CeriosBuilder = class _CeriosBuilder {
/**
* Creates a new builder instance. Intended to be called by subclasses.
*
* @param _actual - The current partial state of the object being built
* @param _requiredFields - Optional array of required field paths to preserve across instances
* @param _validators - Optional array of validators to preserve across instances
* @protected
*/
constructor(_actual, _requiredFields, _validators) {
this._actual = _actual;
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
this._requiredFields = /* @__PURE__ */ new Set();
/**
* Custom validators that run during build.
* @private
*/
this._validators = [];
if (_requiredFields) {
this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]);
}
if (_validators) {
this._validators = [..._validators];
}
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .setRequiredFields(['path.to.field1', 'path.to.field2'])
* .setField1('value1')
* .setField2('value2')
* .buildSafe();
* ```
*/
setRequiredFields(fields) {
this._requiredFields = /* @__PURE__ */ new Set([...fields]);
return this;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setAge(20)
* .setEmail('user@example.com')
* .build();
* ```
*/
addValidator(validator) {
const BuilderClass = this.constructor;
return new BuilderClass(this._actual, Array.from(this._requiredFields), [
...this._validators,
validator
]);
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* @private
*/
getRequiredTemplate() {
var _a;
const ctor = this.constructor;
const staticFields = (_a = ctor.requiredTemplate) != null ? _a : [];
const instanceFields = Array.from(this._requiredFields);
return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!missing.includes(path)) {
missing.push(path);
}
}
}
return missing;
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
const errors = [];
for (const validator of this._validators) {
const result = validator(this._actual);
if (result === false) {
errors.push("Validation failed");
} else if (typeof result === "string") {
errors.push(result);
}
}
return errors;
}
/**
* Removes an optional property from the builder.
* Only works with optional properties (those that can be undefined).
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John')
* .setEmail('john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const BuilderClass = this.constructor;
const newData = { ...this._actual };
delete newData[key];
return new BuilderClass(
newData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Clears all optional properties from the builder, keeping only required ones.
* Properties in the required template and those marked as required are preserved.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John') // required
* .setAge(30) // required
* .setEmail('john@example.com') // optional
* .setPhone('555-1234') // optional
* .clearOptionalProperties();
* // Only name and age remain
* ```
*/
clearOptionalProperties() {
const BuilderClass = this.constructor;
const requiredPaths = this.getRequiredTemplate();
const newData = {};
for (const path of requiredPaths) {
const keys = path.split(".");
if (keys.length === 1) {
const key = keys[0];
if (key in this._actual) {
newData[key] = this._actual[key];
}
} else {
const rootKey = keys[0];
if (rootKey in this._actual && !(rootKey in newData)) {
newData[rootKey] = this._actual[rootKey];
}
}
}
return new BuilderClass(
newData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets a property value and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses.
*
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set and type state updated
* @protected
*/
setProperty(key, value) {
const BuilderClass = this.constructor;
return new BuilderClass(
{
...this._actual,
[key]: value
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets multiple property values at once and returns a new builder instance with updated type state.
* @param props - An object with one or more properties to set.
* @returns A new builder instance with the properties set and type state updated.
* @protected
*/
setProperties(props) {
const BuilderClass = this.constructor;
return new BuilderClass(
{
...this._actual,
...props
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
*
* @template P - The property path (e.g., "parent.child.grandchild")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* builder.setNestedProperty('Order.Details.CustomerId', 'value')
* ```
*/
setNestedProperty(path, value) {
const BuilderClass = this.constructor;
const keys = path.split(".");
const newActual = this.deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) {
current[key] = {};
} else {
current[key] = this.deepClone(existing);
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
return new BuilderClass(
newActual,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Deep clone helper for nested objects
* @private
*/
deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
/**
* Adds a value to an array property and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses for array properties.
*
* @template K - The property key (must be an array property)
* @template V - The type of the array element
* @param key - The array property key to add to
* @param value - The value to add to the array
* @returns A new builder instance with the array property updated and type state updated
* @protected
*/
addToArrayProperty(key, value) {
var _a;
const BuilderClass = this.constructor;
const currentArray = (_a = this._actual[key]) != null ? _a : [];
return new BuilderClass(
{
...this._actual,
[key]: [...currentArray, value]
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Builds the final object with both compile-time and runtime validation.
* This is the recommended and safest way to build objects.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
build() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return this._actual;
}
/**
* Builds the final object with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: No validation
*
* @returns The fully built object of type T
*/
buildWithoutRuntimeValidation() {
return this._actual;
}
/**
* Builds the final object with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildWithoutCompileTimeValidation() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return this._actual;
}
/**
* Builds the final object without any validation (neither compile-time nor runtime).
* Use this only when you're certain the object is valid and need maximum performance.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: No validation
*
* @returns The object of type T (may be incomplete)
*/
buildUnsafe() {
return this._actual;
}
/**
* Builds a partial object, which may not have all required fields set.
* This is useful for scenarios where you want to inspect or validate the current state before finalizing.
*
* @returns The partially built object
*/
buildPartial() {
return this._actual;
}
/**
* Creates a new builder instance from an existing object.
* This is useful for creating builders from existing instances to modify them.
*
* @param instance - The existing object to create a builder from
* @returns A new builder instance initialized with the object's data
*
* @example
* ```typescript
* const existingPerson = { name: 'John', age: 30 };
* const builder = MyBuilder.from(existingPerson);
* const updated = builder.setAge(31).build();
* ```
*/
static from(instance) {
const clonedData = _CeriosBuilder.deepCloneStatic(instance);
return new this(clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = new MyBuilder({}).setName('John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const BuilderClass = this.constructor;
const clonedData = this.deepClone(this._actual);
return new BuilderClass(
clonedData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Static deep clone helper for the from() method.
* @private
*/
static deepCloneStatic(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepCloneStatic(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepCloneStatic(obj[key]);
}
}
return cloned;
}
/**
* Builds and freezes the final object with both compile-time and runtime validation.
* The returned object is shallowly frozen - top-level properties cannot be modified,
* but nested objects remain mutable.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.freeze()
*
* @returns The frozen object of type Readonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return Object.freeze(this._actual);
}
/**
* Builds and deeply freezes the final object with both compile-time and runtime validation.
* The returned object is recursively frozen - all nested objects and arrays are also frozen.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.freeze()
*
* @returns The deeply frozen object of type DeepReadonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildDeepFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return deepFreeze(this._actual);
}
/**
* Builds and seals the final object with both compile-time and runtime validation.
* The returned object is shallowly sealed - properties cannot be added or removed,
* but existing properties can still be modified. Nested objects remain unsealed.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.seal()
*
* @returns The sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return Object.seal(this._actual);
}
/**
* Builds and deeply seals the final object with both compile-time and runtime validation.
* The returned object is recursively sealed - properties cannot be added or removed at any level,
* but existing properties can still be modified.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.seal()
*
* @returns The deeply sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildDeepSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return deepSeal(this._actual);
}
/**
* Recursively freezes or seals a value and everything reachable from it.
*
* One implementation for both operations and both builders. The `seen` set makes it
* cycle-safe - `buildDeepFrozen()` on self-referencing data previously threw a
* `RangeError`. Functions are not traversed: walking into a function would freeze its
* `prototype`, which may be an object the caller does not own.
*
* @internal
*/
function deepHarden(obj, mode, seen = /* @__PURE__ */ new WeakSet()) {
if (obj === null || typeof obj !== "object" || seen.has(obj)) return obj;
seen.add(obj);
for (const key of Object.getOwnPropertyNames(obj)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor && "value" in descriptor) deepHarden(descriptor.value, mode, seen);
}
return mode === "freeze" ? Object.freeze(obj) : Object.seal(obj);
}
/**
* Builds the copy-on-write successor of an auto builder without invoking any constructor.
*
* Copy-on-write cannot call the subclass constructor: users write `constructor(data?)`,
* which has no way to receive the base class's internal argument shape. But creating a bare
* object off the prototype and assigning only the internal fields silently discards every
* other field the subclass declared - and because the discarded name is then absent from
* the target, the proxy hands back an auto-setter function for it, so `this.cache` reads as
* a function rather than `undefined`. Carrying the source's own properties over first fixes
* both halves of that.
*
* Subclass fields are carried **shallowly**: forks share the same field references, so a
* `Map` held in a field is shared between every builder forked from it. That matches how
* copy-on-write already treats nested data.
*
* @param source - The builder being forked. This is the Proxy; `Reflect.*` forwards to the
* target, and copying descriptors preserves getters and non-enumerable fields.
* @param internals - The internal fields to overwrite after the carry-over
* @internal
*/
function createBuilderCopy(source, internals) {
const copy = Object.create(Object.getPrototypeOf(source));
for (const key of Reflect.ownKeys(source)) {
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (descriptor) Object.defineProperty(copy, key, descriptor);
}
Object.assign(copy, internals);
return copy;
}
/**
* Proxy handler shared by both auto builders. Real members always win; any other
* accessed name becomes a bare property setter that delegates to the inherited
* (hidden) `setProperty`, so copy-on-write re-proxies through `this.constructor`.
* @internal
*/
const autoSetterHandler = { get(target, prop, receiver) {
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
if (RESERVED_SET.has(prop)) return Reflect.get(target, prop, receiver);
if (prop in target) return Reflect.get(target, prop, receiver);
const key = setterNameToKey(prop);
return (value) => receiver.setProperty(key, value);
} };
/**
* Path segments that must never be written through, because assigning to them mutates a
* prototype rather than the object itself.
* @internal
*/
const UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
"__proto__",
"constructor",
"prototype"
]);
/**
* Rejects dot-notation paths that would write through a prototype.
*
* `setNestedProperty("__proto__.x", v)` used to walk onto the clone's prototype and assign
* there, so the write silently vanished from the built object. It is harmless today only
* because the clone is always a fresh literal; refusing the path keeps it that way.
*
* @throws {Error} If any segment of the path is `__proto__`, `constructor`, or `prototype`
* @internal
*/
function assertSafePath(path) {
for (const segment of path.split(".")) if (UNSAFE_PATH_SEGMENTS.has(segment)) throw new Error(`Unsafe property path "${path}": the segment "${segment}" would write through a prototype rather than the object.`);
}
/**
* Rejects a single property key that would write through a prototype.
*
* Guarding only dot-notation paths was not enough. `__proto__` was reserved, so
* `SetterName` generated a `__proto__Prop` setter, and `setterNameToKey` mapped it straight
* back to `__proto__`. On the class builder the resulting `Object.assign` then re-parented
* the built instance, so it silently stopped being an instance of its own class and lost
* every method.
*
* @throws {Error} If the key is `__proto__`, `constructor`, or `prototype`
* @internal
*/
function assertSafeKey(key) {
if (key === "__proto__") throw new Error(`Unsafe property key "__proto__": writing it would re-parent the object.`);
}
/**
* Guards the auto-builder constructors' `init` argument.
*
* `setRequiredFields` accepts a bare {@link RequiredFieldsRecord}, so passing that same
* record straight to the constructor - `new B({}, { name: true })` - is an easy mistake.
* It structurally matches `BuilderInit`, so it used to be accepted silently and produce a
* builder with *zero* required fields: validation then passed for every incomplete object.
* Failing loudly is the only safe response.
*
* @throws {Error} If the object has keys but none of them are `BuilderInit` keys
* @internal
*/
function assertBuilderInit(init) {
const keys = Object.keys(init);
if (keys.length === 0 || keys.some((key) => key === "requiredFields" || key === "validators")) return;
throw new Error(`Invalid builder options: expected { requiredFields?, validators? } but received keys ${keys.map((key) => `"${key}"`).join(", ")}. If you meant to declare required fields, wrap them: { requiredFields: { ... } }.`);
}
/**
* Normalises the two accepted required-field shapes - an array of dot-notation paths, or
* an exhaustive {@link RequiredFieldsRecord} - to the array of paths used internally.
*
* @internal
*/
function toRequiredPaths(fields) {
return Array.isArray(fields) ? fields : Object.keys(fields);
}
/**
* Picks the properties to keep when clearing optional properties.
*
* A required path may be nested (`"address.city"`), in which case the whole root property
* (`address`) is preserved - the required leaf cannot survive without it. Shared by both
* base builders so the two cannot drift; `CeriosClassBuilder` previously treated
* `"address.city"` as a single literal key, found no match, and dropped `address` entirely.
*
* @internal
*/
function pickRequiredRoots(actual, requiredPaths) {
const kept = {};
for (const path of requiredPaths) {
const rootKey = path.split(".")[0];
if (rootKey in actual && !(rootKey in kept)) kept[rootKey] = actual[rootKey];
}
return kept;
}
//#endregion
//#region src/builder-error.ts
/**
* The error thrown by every validating build variant.
*
* The message text is unchanged from when these were plain `Error`s, so existing
* string-matching callers keep working. The structured fields are the reason this type
* exists: they let callers branch on *what* failed without parsing the message.
*
* @example
* ```typescript
* try {
* builder.build();
* } catch (error) {
* if (error instanceof CeriosBuilderError && error.missingFields.length > 0) {
* promptFor(error.missingFields);
* }
* }
* ```
*/
var CeriosBuilderError = class CeriosBuilderError extends Error {
constructor(message, missingFields, validationErrors) {
super(message);
this.name = "CeriosBuilderError";
this.missingFields = missingFields;
this.validationErrors = validationErrors;
Object.setPrototypeOf(this, CeriosBuilderError.prototype);
}
};
// src/cerios-class-builder.ts
var CeriosClassBuilder = class _CeriosClassBuilder {
/**
* Creates a new class builder instance.
* @param classConstructor - The class constructor to use for building
* @param data - Optional initial data
* @param _validators - Optional array of validators to preserve across instances
* @param _requiredFields - Optional required fields to preserve across instances
*/
constructor(classConstructor, data = {}, _validators, _requiredFields) {
/**
* Custom validators that run during build.
* @private
*/
this._validators = [];
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
this._requiredFields = /* @__PURE__ */ new Set();
this._classConstructor = classConstructor;
this._actual = data;
if (_validators) {
this._validators = [..._validators];
}
if (_requiredFields) {
this._requiredFields = _requiredFields instanceof Set ? new Set(_requiredFields) : /* @__PURE__ */ new Set([..._requiredFields]);
}
}
/**
* Gets the class constructor for this builder.
* @private
*/
getClassConstructor() {
return this._classConstructor;
}
/**
* Creates a new instance of the builder with updated data.
* Subclasses can override this to return the correct subclass type.
* @private
*/
createBuilder(data) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, data, this._validators, this._requiredFields);
}
setProperty(key, value) {
const newBuilder = this.createBuilder({
...this._actual,
[key]: value
});
return newBuilder;
}
/**
* Sets multiple properties at once.
* @template K - The property keys being set
* @param props - Object with properties to set
* @returns A new builder instance with the properties set
* @protected
*/
setProperties(props) {
const newBuilder = this.createBuilder({
...this._actual,
...props
});
return newBuilder;
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
* Only supports data properties (excludes methods).
*
* @template P - The property path (e.g., "address.city")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* class Address {
* street!: string;
* city!: string;
* }
* class Person {
* name!: string;
* address!: Address;
* }
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setNestedProperty('address.city', 'New York')
* .build();
* ```
*/
setNestedProperty(path, value) {
const keys = path.split(".");
const newActual = this.deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) {
current[key] = {};
} else {
current[key] = this.deepClone(existing);
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
const newBuilder = this.createBuilder(newActual);
return newBuilder;
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .setRequiredFields(['name', 'age'])
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .buildWithoutCompileTimeValidation();
* ```
*/
setRequiredFields(fields) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, this._actual, this._validators, /* @__PURE__ */ new Set([...fields]));
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* If instance-level fields are set via setRequiredFields(), they are combined with static fields.
* @private
*/
getRequiredTemplate() {
var _a;
const ctor = this.constructor;
const staticFields = (_a = ctor.requiredDataProperties) != null ? _a : [];
const instanceFields = Array.from(this._requiredFields);
if (instanceFields.length > 0) {
return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
}
return staticFields;
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!missing.includes(path)) {
missing.push(path);
}
}
}
return missing;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setProperty('age', 20)
* .setProperty('email', 'user@example.com')
* .build();
* ```
*/
addValidator(validator) {
const BuilderClass = this.constructor;
return new BuilderClass(
this._classConstructor,
this._actual,
[...this._validators, validator],
this._requiredFields
);
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
const errors = [];
for (const validator of this._validators) {
const result = validator(this._actual);
if (result === false) {
errors.push("Validation failed");
} else if (typeof result === "string") {
errors.push(result);
}
}
return errors;
}
/**
* Removes an optional property from the builder.
* Only works with optional data properties (those that can be undefined).
* Methods are automatically excluded.
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* class Person {
* name!: string;
* email?: string;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('email', 'john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.createBuilder(newData);
}
/**
* Clears all optional properties from the builder, keeping only required data properties.
* Uses the combined required-field template from static defaults and instance-level fields.
* If no required fields are configured, all properties are cleared.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* class Person {
* name!: string; // required
* age!: number; // required
* email?: string; // optional
* phone?: string; // optional
* }
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .setProperty('email', 'john@example.com')
* .setProperty('phone', '555-1234')
* .clearOptionalProperties();
* // Only name and age are preserved, email and phone are cleared
* ```
*/
clearOptionalProperties() {
const requiredProps = this.getRequiredTemplate();
const newData = {};
for (const prop of requiredProps) {
const key = prop;
if (key in this._actual) {
newData[key] = this._actual[key];
}
}
return this.createBuilder(newData);
}
/**
* Adds a value to an array property.
* @template K - The array property key
* @template V - The array element type
* @param key - The array property key
* @param value - The value to add to the array
* @returns A new builder instance with the value added
*/
addToArrayProperty(key, value) {
var _a;
const currentArray = (_a = this._actual[key]) != null ? _a : [];
const newBuilder = this.createBuilder({
...this._actual,
[key]: [...currentArray, value]
});
return newBuilder;
}
/**
* Builds the final class instance with compile-time and runtime validation.
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built and validated class instance
* @throws {Error} If required fields are missing or validation fails
*/
build() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the final class instance with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: No validation
*
* @returns The fully built class instance
*/
buildWithoutRuntimeValidation() {
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the final class instance with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildWithoutCompileTimeValidation() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the class instance without any validation.
* Use only when you're certain the object is valid.
*
* @returns The built class instance (may be incomplete)
*/
buildUnsafe() {
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds a partial object (may not have all required fields).
*
* @returns The partially built object
*/
buildPartial() {
return { ...this._actual };
}
/**
* Builds and freezes the class instance (shallow freeze).
*
* @returns The frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return Object.freeze(instance);
}
/**
* Builds and deeply freezes the class instance.
*
* @returns The deeply frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return this.deepFreeze(instance);
}
/**
* Builds and seals the class instance (shallow freeze).
*
* @returns The sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return Object.seal(instance);
}
/**
* Builds and deeply seals the class instance.
*
* @returns The deeply sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return this.deepSeal(instance);
}
/**
* Deep freeze helper.
* @private
*/
deepFreeze(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
this.deepFreeze(value);
}
});
return Object.freeze(obj);
}
/**
* Deep seal helper.
* @private
*/
deepSeal(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
this.deepSeal(value);
}
});
return Object.seal(obj);
}
/**
* Deep clone helper for nested objects.
* @private
*/
deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepClone(item));
}
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = this.deepClone(obj[key]);
}
return cloned;
}
/**
* Creates a new builder instance from an existing class instance.
* This is useful for creating builders from existing instances to modify them.
*
* @param classConstructor - The class constructor to use
* @param instance - The existing class instance to create a builder from
* @returns A new builder instance initialized with the instance's data
*
* @example
* ```typescript
* const existingPerson = new Person({ name: 'John', age: 30 });
* const builder = PersonBuilder.from(Person, existingPerson);
* const updated = builder.setProperty('age', 31).build();
* ```
*/
static from(classConstructor, instance) {
const clonedData = _CeriosClassBuilder.deepCloneStatic(instance);
return new this(classConstructor, clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = PersonBuilder.create().setProperty('name', 'John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = this.deepClone(this._actual);
return this.createBuilder(clonedData);
}
/**
* Static deep clone helper for the from() method.
* @private
*/
static deepCloneStatic(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepCloneStatic(item));
}
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = this.deepCloneStatic(obj[key]);
}
return cloned;
}
/**
* Describes a validator for error messages: its index, plus its name when it has one.
* Arrow functions assigned to a `const` are named; truly anonymous ones are not.
*/
function describeValidator(validator, index) {
const name = validator.name;
return name ? `Validator #${index} (${name})` : `Validator #${index}`;
}
/**
* Runs every validator and collects the failures.
*
* Shared by both base builders, which previously held byte-identical private copies.
*
* Two behaviours differ from those copies, both of which were silent failures:
* - A validator returning `false` used to push the bare string `"Validation failed"`, so
* three failing validators produced `"Validation failed: Validation failed; Validation
* failed; Validation failed"` and you could not tell which one failed. It now names the
* validator.
* - A validator returning anything other than `true`, `false`, or a non-empty string - most
* easily `undefined`, from a function that forgot to return - used to count as a *pass*.
* It is now an error, because silently accepting invalid data is the worse outcome.
*
* @param validators - The validators to run
* @param actual - The current builder state to validate
* @returns One message per failure, in validator order
* @internal
*/
function runValidatorsAgainst(validators, actual) {
const errors = [];
for (const [index, validator] of validators.entries()) {
const result = validator(actual);
if (result === true) continue;
if (result === false) {
errors.push(`${describeValidator(validator, index)} returned false`);
continue;
}
if (typeof result === "string" && result.length > 0) {
errors.push(result);
continue;
}
errors.push(`${describeValidator(validator, index)} returned ${result === "" ? "an empty string" : String(result)}; expected true, false, or a non-empty error message`);
}
return errors;
}
//#endregion
//#region src/cerios-builder.ts
/**
* @deprecated Will be removed in the next major version. Migrate to `CeriosAutoBuilder<T>()`,
* which generates every setter automatically with the same compile-time safety — see
* MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`).
*/
var CeriosBuilder = class {
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .setRequiredFields(['path.to.field1', 'path.to.field2'])
* .setField1('value1')
* .setField2('value2')
* .buildSafe();
* ```
*/
setRequiredFields(fields) {
return this.instantiateBuilder(this._actual, toRequiredPaths(fields), this._validators);
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setAge(20)
* .setEmail('user@example.com')
* .build();
* ```
*/
addValidator(validator) {
return this.instantiateBuilder(this._actual, this._requiredFields, [...this._validators, validator]);
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* @private
*/
getRequiredTemplate() {
const staticFields = this.constructor.requiredTemplate ?? [];
if (this._requiredFields.size === 0) return staticFields;
if (staticFields.length === 0) return [...this._requiredFields];
return [.../* @__PURE__ */ new Set([...staticFields, ...this._requiredFields])];
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
const seen = /* @__PURE__ */ new Set();
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
seen.add(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!seen.has(path)) {
missing.push(path);
seen.add(path);
}
}
}
return missing;
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
return runValidatorsAgainst(this._validators, this._actual);
}
/**
* Removes an optional property from the builder.
* Only works with optional properties (those that can be undefined).
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John')
* .setEmail('john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.instantiateBuilder(newData, this._requiredFields, this._validators);
}
/**
* Clears all optional properties from the builder, keeping only required ones.
* Properties in the required template and those marked as required are preserved.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John') // required
* .setAge(30) // required
* .setEmail('john@example.com') // optional
* .setPhone('555-1234') // optional
* .clearOptionalProperties();
* // Only name and age remain
* ```
*/
clearOptionalProperties() {
const newData = pickRequiredRoots(this._actual, this.getRequiredTemplate());
return this.instantiateBuilder(newData, this._requiredFields, this._validators);
}
/**
* Creates a new builder instance. Intended to be called by subclasses.
*
* @param _actual - The current partial state of the object being built
* @param _requiredFields - Optional array of required field paths to preserve across instances
* @param _validators - Optional array of validators to preserve across instances
* @protected
*/
constructor(_actual, _requiredFields, _validators) {
this._actual = _actual;
this._requiredFields = /* @__PURE__ */ new Set();
this._validators = [];
if (_requiredFields) this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]);
if (_validators) this._validators = [..._validators];
}
/**
* Sets a property value and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses.
*
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set and type state updated
* @protected
*/
setProperty(key, value) {
assertSafeKey(key);
return this.instantiateBuilder({
...this._actual,
[key]: value
}, this._requiredFields, this._validators);
}
/**
* Sets multiple property values at once and returns a new builder instance with updated type state.
* @param props - An object with one or more properties to set.
* @returns A new builder instance with the properties set and type state updated.
* @protected
*/
setProperties(props) {
Object.keys(props).forEach(assertSafeKey);
return this.instantiateBuilder({
...this._actual,
...props
}, this._requiredFields, this._validators);
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
*
* @template P - The property path (e.g., "parent.child.grandchild")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* builder.setNestedProperty('Order.Details.CustomerId', 'value')
* ```
*/
setNestedProperty(path, value) {
assertSafePath(path);
const keys = path.split(".");
const newActual = deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) current[key] = {};
current = current[key];
}
current[keys[keys.length - 1]] = value;
return this.instantiateBuilder(newActual, this._requiredFields, this._validators);
}
/**
* Adds a value to an array property and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses for array properties.
*
* @template K - The property key (must be an array property)
* @template V - The type of the array element
* @param key - The array property key to add to
* @param value - The value to add to the array
* @returns A new builder instance with the array property updated and type state updated
* @protected
*/
addToArrayProperty(key, value) {
assertSafeKey(key);
const currentArray = this._actual[key] ?? [];
return this.instantiateBuilder({
...this._actual,
[key]: [...currentArray, value]
}, this._requiredFields, this._validators);
}
/**
* Builds the final object with both compile-time and runtime validation.
* This is the recommended and safest way to build objects.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
build() {
this.assertValid("build");
return this.snapshot();
}
/**
* Builds the final object with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: No validation
*
* @returns The fully built object of type T
*/
buildWithoutRuntimeValidation() {
return this.snapshot();
}
/**
* Builds the final object with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildWithoutCompileTimeValidation() {
this.assertValid("buildWithoutCompileTimeValidation");
return this.snapshot();
}
/**
* Builds the final object without any validation (neither compile-time nor runtime).
* Use this only when you're certain the object is valid and need maximum performance.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: No validation
*
* @returns The object of type T (may be incomplete)
*/
buildUnsafe() {
return this.snapshot();
}
/**
* Builds a partial object, which may not have all required fields set.
* This is useful for scenarios where you want to inspect or validate the current state before finalizing.
*
* @returns The partially built object
*/
/**
* Runs both runtime checks, throwing on the first failure.
*
* The single copy of what used to be an identical nine-line block in all six validating
* build variants. Centralising it is what lets the thrown error name the method the
* caller actually invoked instead of always saying "build".
*
* @param methodName - The build variant being run, used in the error message
* @throws {CeriosBuilderError} If a required field is missing or a validator fails
*/
assertValid(methodName) {
const missing = this.validateRequiredFields();
if (missing.length > 0) throw new CeriosBuilderError(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, missing, []);
const validationErrors = this.runValidators();
if (validationErrors.length > 0) throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors);
}
/**
* Returns the built value as a deep copy.
*
* Every build variant goes through this. Returning `this._actual` directly - as the
* build methods used to - handed the caller the builder's live internal state, so
* mutating the result mutated the builder and every object built from it afterwards.
* `buildFrozen()` was worse: it froze the builder's own state in place.
*/
snapshot() {
return deepClone(this._actual);
}
/**
* The single construction seam used by every copy-on-write method.
*
* By default this calls the subclass constructor with the internal 3-argument shape,
* which requires the subclass to accept and forward it. Subclasses whose constructor
* has a different signature - such as the auto builders, where users write
* `constructor(data?)` - override this to build the copy without invoking a
* constructor at all, so the required fields and validators cannot be lost.
*
* @param data - The state for the new builder
* @param requiredFields - Required field paths to carry over
* @param validators - Validators to carry over
* @returns A new builder instance of the same concrete type
*/
instantiateBuilder(data, requiredFields, validators) {
const BuilderClass = this.constructor;
return new BuilderClass(data, requiredFields instanceof Set ? [...requiredFields] : requiredFields, validators);
}
buildPartial() {
return deepClone(this._actual);
}
static from(instance) {
const clonedData = deepClone(instance);
return new this(clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = new MyBuilder({}).setName('John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = deepClone(this._actual);
return this.instantiateBuilder(clonedData, this._requiredFields, this._validators);
}
/**
* Builds and freezes the final object with both compile-time and runtime validation.
* The returned object is shallowly frozen - top-level properties cannot be modified,
* but nested objects remain mutable.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.freeze()
*
* @returns The frozen object of type Readonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildFrozen() {
this.assertValid("buildFrozen");
return Object.freeze(this.snapshot());
}
/**
* Builds and deeply freezes the final object with both compile-time and runtime validation.
* The returned object is recursively frozen - all nested objects and arrays are also frozen.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.freeze()
*
* @returns The deeply frozen object of type DeepReadonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildDeepFrozen() {
this.assertValid("buildDeepFrozen");
return deepHarden(this.snapshot(), "freeze");
}
/**
* Builds and seals the final object with both compile-time and runtime validation.
* The returned object is shallowly sealed - properties cannot be added or removed,
* but existing properties can still be modified. Nested objects remain unsealed.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.seal()
*
* @returns The sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildSealed() {
this.assertValid("buildSealed");
return Object.seal(this.snapshot());
}
/**
* Builds and deeply seals the final object with both compile-time and runtime validation.
* The returned object is recursively sealed - properties cannot be added or removed at any level,
* but existing properties can still be modified.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.seal()
*
* @returns The deeply sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildDeepSealed() {
this.assertValid("buildDeepSealed");
return deepHarden(this.snapshot(), "seal");
}
};
export {
CeriosBuilder,
CeriosClassBuilder
//#endregion
//#region src/cerios-auto-builder.ts
/**
* Concrete runtime backing class for {@link CeriosAutoBuilder}. Extends the real
* builder so it inherits all build/validation/clone/from machinery, and returns a
* Proxy from its constructor so bare-name property setters work.
* @internal
*/
var AutoBuilderRuntime = class extends CeriosBuilder {
constructor(data = {}, initOrRequiredFields, validators) {
if (initOrRequiredFields !== void 0 && !Array.isArray(initOrRequiredFields)) {
const init = initOrRequiredFields;
assertBuilderInit(init);
const required = init.requiredFields === void 0 ? void 0 : toRequiredPaths(init.requiredFields);
super(data, required, init.validators);
} else super(data, initOrRequiredFields, validators);
return new Proxy(this, autoSetterHandler);
}
/**
* Copy-on-write must not go through the subclass constructor. Users write
* `constructor(data?) { super(data, { requiredFields: [...] }) }`, which discards the
* arguments the base class passes - so a later `setRequiredFields()` would be silently
* reverted to whatever the constructor hardcodes. {@link createBuilderCopy} sidesteps
* user constructors while still carrying the subclass's own fields across.
*/
instantiateBuilder(data, requiredFields, validators) {
return new Proxy(createBuilderCopy(this, {
_actual: data,
_requiredFields: new Set(requiredFields),
_validators: [...validators]
}), autoSetterHandler);
}
};
/**
* Creates a base class with automatic bare-name setters for every property of T.
* Extend the returned class; add ordinary methods for any custom logic (they use
* the auto setters on `this`, and their return types can be left to inference).
*
* @example
* ```typescript
* interface User { id: string; name: string; role: string; age?: number; }
*
* class UserBuilder extends CeriosAutoBuilder<User>() {
* static create(): UserBuilder { return new UserBuilder({}); }
* asAdmin() { return this.role("admin"); } // inferred BuilderStep<this, User, "role">
* }
*
* const user = UserBuilder.create().id("1").name("Alice").asAdmin().build();
* // UserBuilder.create().id("1").build(); // compile error: name and role not set
* ```
*
* Custom methods use a name of their own (`asAdmin`, `trimmedName`) and delegate to the
* generated setter — a custom method cannot reuse a property's exact name, because the
* method would shadow the setter it needs to call.
*
* @template T - The type to build
* @returns An abstract base class to extend
*/
function CeriosAutoBuilder() {
return AutoBuilderRuntime;
}
//#endregion
//#region src/cerios-class-builder.ts
/**
* Getter-only accessor names per target class, computed once per class.
* @internal
*/
const GETTER_ONLY_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
/**
* Collects the names on the class's prototype chain that resolve to an accessor with a
* getter but no setter. The nearest descriptor wins, so a derived class that redeclares an
* inherited getter-only accessor as a getter/setter pair makes the name writable again.
*
* `DataPropertiesOnly` cannot exclude these at the type level - a getter is structurally
* indistinguishable from a data property - so they must be caught at runtime instead.
*
* @internal
*/
function getterOnlyKeys(ctor) {
const cached = GETTER_ONLY_KEYS_CACHE.get(ctor);
if (cached) return cached;
const keys = /* @__PURE__ */ new Set();
const seen = /* @__PURE__ */ new Set();
let proto = ctor.prototype;
while (proto && proto !== Object.prototype) {
for (const name of Object.getOwnPropertyNames(proto)) {
if (seen.has(name)) continue;
seen.add(name);
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (descriptor?.get && !descriptor.set) keys.add(name);
}
proto = Object.getPrototypeOf(proto);
}
GETTER_ONLY_KEYS_CACHE.set(ctor, keys);
return keys;
}
/**
* Rejects writes to a getter-only accessor of the target class.
*
* Without this, the value sat in the builder state until build, where assigning it threw
* `TypeError: Cannot set property ... which has only a getter` - far from the misuse and
* without naming the builder as the culprit.
*
* @throws {CeriosBuilderError} If the key is a getter-only accessor on the target class
* @internal
*/
function assertNotGetterOnly(ctor, key) {
if (typeof key === "string" && getterOnlyKeys(ctor).has(key)) throw new CeriosBuilderError(`"${key}" is a getter-only accessor on ${ctor.name}; its value is computed by the class and cannot be set through the builder.`, [], []);
}
/**
* @deprecated Will be removed in the next major version. Migrate to `CeriosClassAutoBuilder(Class)`,
* which generates every setter automatically with the same compile-time safety — see
* MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`).
*/
var CeriosClassBuilder = class {
/**
* Creates a new class builder instance.
* @param classConstructor - The class constructor to use for building
* @param data - Optional initial data
* @param _validators - Optional array of validators to preserve across instances
* @param _requiredFields - Optional required fields to preserve across instances
*/
constructor(classConstructor, data = {}, _validators, _requiredFields) {
this._validators = [];
this._requiredFields = /* @__PURE__ */ new Set();
for (const key of Object.keys(data)) assertNotGetterOnly(classConstructor, key);
this._classConstructor = classConstructor;
this._actual = data;
if (_validators) this._validators = [..._validators];
if (_requiredFields) this._requiredFields = _requiredFields instanceof Set ? new Set(_requiredFields) : /* @__PURE__ */ new Set([..._requiredFields]);
}
/**
* Gets the class constructor for this builder.
*
* Protected rather than private so `instantiateBuilder` overrides can read it from the
* instance instead of capturing it, which is what lets the class auto builder's runtime
* class hold no per-factory-call state.
*/
getClassConstructor() {
return this._classConstructor;
}
/**
* Creates a new instance of the builder with updated data.
* Subclasses can override this to return the correct subclass type.
* @private
*/
createBuilder(data) {
return this.instantiateBuilder(data, this._validators, this._requiredFields);
}
/**
* Runs both runtime checks, throwing on the first failure.
*
* The single copy of what used to be an identical nine-line block in all six validating
* build variants. Centralising it is what lets the thrown error name the method the
* caller actually invoked instead of always saying "build".
*
* @param methodName - The build variant being run, used in the error message
* @throws {CeriosBuilderError} If a required field is missing or a validator fails
*/
assertValid(methodName) {
const missing = this.validateRequiredFields();
if (missing.length > 0) throw new CeriosBuilderError(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, missing, []);
const validationErrors = this.runValidators();
if (validationErrors.length > 0) throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors);
}
/**
* Instantiates the target class from the current state.
*
* The single copy of what used to be an identical eight-line block in all eight build
* variants. Classes that assign in their constructor need nothing further; those that
* do not get the data assigned afterwards, which is what the `needsAssign` check
* detects.
*/
instantiate() {
const ctor = this.getClassConstructor();
const data = deepClone(this._actual);
const instance = new ctor(data);
const getterOnly = getterOnlyKeys(ctor);
const dataKeys = Object.keys(data).filter((key) => !getterOnly.has(key));
if (dataKeys.some((key) => instance[key] === void 0 && data[key] !== void 0)) for (const key of dataKeys) instance[key] = data[key];
return instance;
}
/**
* The single construction seam used by every copy-on-write method.
*
* By default this calls the subclass constructor with the internal 4-argument shape,
* which requires the subclass to accept it. Subclasses whose constructor has a
* different signature - such as the class auto builders, where users write
* `constructor(data?)` - override this to build the copy without invoking a
* constructor at all.
*
* @param data - The state for the new builder
* @param validators - Validators to carry over
* @param requiredFields - Required fields to carry over
* @returns A new builder instance of the same concrete type
*/
instantiateBuilder(data, validators, requiredFields) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, data, validators, requiredFields);
}
setProperty(key, value) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
return this.createBuilder({
...this._actual,
[key]: value
});
}
/**
* Sets multiple properties at once.
* @template K - The property keys being set
* @param props - Object with properties to set
* @returns A new builder instance with the properties set
* @protected
*/
setProperties(props) {
for (const key of Object.keys(props)) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
}
return this.createBuilder({
...this._actual,
...props
});
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
* Only supports data properties (excludes methods).
*
* @template P - The property path (e.g., "address.city")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* class Address {
* street!: string;
* city!: string;
* }
* class Person {
* name!: string;
* address!: Address;
* }
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setNestedProperty('address.city', 'New York')
* .build();
* ```
*/
setNestedProperty(path, value) {
assertSafePath(path);
const keys = path.split(".");
assertNotGetterOnly(this.getClassConstructor(), keys[0]);
const newActual = deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) current[key] = {};
current = current[key];
}
current[keys[keys.length - 1]] = value;
return this.createBuilder(newActual);
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .setRequiredFields(['name', 'age'])
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .buildWithoutCompileTimeValidation();
* ```
*/
setRequiredFields(fields) {
return this.instantiateBuilder(this._actual, this._validators, new Set(toRequiredPaths(fields)));
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* If instance-level fields are set via setRequiredFields(), they are combined with static fields.
* @private
*/
getRequiredTemplate() {
const staticFields = this.constructor.requiredDataProperties ?? [];
const instanceFields = Array.from(this._requiredFields);
if (instanceFields.length > 0) return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
return staticFields;
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
const seen = /* @__PURE__ */ new Set();
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
seen.add(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!seen.has(path)) {
missing.push(path);
seen.add(path);
}
}
}
return missing;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setProperty('age', 20)
* .setProperty('email', 'user@example.com')
* .build();
* ```
*/
addValidator(validator) {
return this.instantiateBuilder(this._actual, [...this._validators, validator], this._requiredFields);
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
return runValidatorsAgainst(this._validators, this._actual);
}
/**
* Removes an optional property from the builder.
* Only works with optional data properties (those that can be undefined).
* Methods are automatically excluded.
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* class Person {
* name!: string;
* email?: string;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('email', 'john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.createBuilder(newData);
}
/**
* Clears all optional properties from the builder, keeping only required data properties.
* Uses the combined required-field template from static defaults and instance-level fields.
* If no required fields are configured, all properties are cleared.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* class Person {
* name!: string; // required
* age!: number; // required
* email?: string; // optional
* phone?: string; // optional
* }
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .setProperty('email', 'john@example.com')
* .setProperty('phone', '555-1234')
* .clearOptionalProperties();
* // Only name and age are preserved, email and phone are cleared
* ```
*/
clearOptionalProperties() {
return this.createBuilder(pickRequiredRoots(this._actual, this.getRequiredTemplate()));
}
/**
* Adds a value to an array property.
* @template K - The array property key
* @template V - The array element type
* @param key - The array property key
* @param value - The value to add to the array
* @returns A new builder instance with the value added
*/
addToArrayProperty(key, value) {
assertSafeKey(key);
assertNotGetterOnly(this.getClassConstructor(), key);
const currentArray = this._actual[key] ?? [];
return this.createBuilder({
...this._actual,
[key]: [...currentArray, value]
});
}
/**
* Builds the final class instance with compile-time and runtime validation.
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built and validated class instance
* @throws {Error} If required fields are missing or validation fails
*/
build() {
this.assertValid("build");
return this.instantiate();
}
/**
* Builds the final class instance with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: No validation
*
* @returns The fully built class instance
*/
buildWithoutRuntimeValidation() {
return this.instantiate();
}
/**
* Builds the final class instance with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildWithoutCompileTimeValidation() {
this.assertValid("buildWithoutCompileTimeValidation");
return this.instantiate();
}
/**
* Builds the class instance without any validation.
* Use only when you're certain the object is valid.
*
* @returns The built class instance (may be incomplete)
*/
buildUnsafe() {
return this.instantiate();
}
/**
* Builds a partial object (may not have all required fields).
*
* @returns The partially built object
*/
buildPartial() {
return deepClone(this._actual);
}
/**
* Builds and freezes the class instance (shallow freeze).
*
* @returns The frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildFrozen() {
this.assertValid("buildFrozen");
const instance = this.instantiate();
return Object.freeze(instance);
}
/**
* Builds and deeply freezes the class instance.
*
* @returns The deeply frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepFrozen() {
this.assertValid("buildDeepFrozen");
return deepHarden(this.instantiate(), "freeze");
}
/**
* Builds and seals the class instance (shallow freeze).
*
* @returns The sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildSealed() {
this.assertValid("buildSealed");
const instance = this.instantiate();
return Object.seal(instance);
}
/**
* Builds and deeply seals the class instance.
*
* @returns The deeply sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepSealed() {
this.assertValid("buildDeepSealed");
return deepHarden(this.instantiate(), "seal");
}
/**
* Creates a new builder instance from an existing class instance.
* This is useful for creating builders from existing instances to modify them.
*
* @param classConstructor - The class constructor to use
* @param instance - The existing class instance to create a builder from
* @returns A new builder instance initialized with the instance's data
*
* @example
* ```typescript
* const existingPerson = new Person({ name: 'John', age: 30 });
* const builder = PersonBuilder.from(Person, existingPerson);
* const updated = builder.setProperty('age', 31).build();
* ```
*/
static from(classConstructor, instance) {
const clonedData = deepClone(instance);
return new this(classConstructor, clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = PersonBuilder.create().setProperty('name', 'John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = deepClone(this._actual);
return this.createBuilder(clonedData);
}
};
//#endregion
//#region src/cerios-class-auto-builder.ts
/**
* One runtime class per target class, rather than per factory call.
*
* Without this, `CeriosClassAutoBuilder(Person) === CeriosClassAutoBuilder(Person)` is
* false, so `instanceof` silently fails whenever the factory is called twice for the same
* class, and every call adds a distinct object shape for the engine to track. Caching is
* only sound because the runtime class captures nothing per call beyond the cache key:
* `instantiateBuilder` reads `this.getClassConstructor()` rather than the closure.
*
* @internal
*/
const RUNTIME_CACHE = /* @__PURE__ */ new WeakMap();
/**
* Creates a base class with automatic bare-name setters for every **writable** data
* property of the class T. Building instantiates the real class, so methods, getters, and
* decorators are preserved. Methods, getter-only accessors, and `readonly` fields get no
* setter - a class's `readonly` fields are established through its constructor, so seed them
* with `new Builder({ ... })` or `Builder.from(instance)`. Extend the returned class and add
* ordinary methods for any custom logic.
*
* @example
* ```typescript
* class Person {
* name!: string;
* age!: number;
* constructor(data?: Partial<Person>) { if (data) Object.assign(this, data); }
* greet(): string { return `Hi, ${this.name}`; }
* }
*
* class PersonBuilder extends CeriosClassAutoBuilder(Person) {
* static create(): PersonBuilder { return new PersonBuilder(); }
* }
*
* const person = PersonBuilder.create().name("Alice").age(30).build();
* person.greet(); // "Hi, Alice"
* ```
*
* Custom methods use a name of their own (`asAdult`, `trimmedName`) and delegate to the
* generated setter — a custom method cannot reuse a data property's exact name, because
* the method would shadow the setter it needs to call.
*
* @param classConstructor - The class constructor to instantiate on build
* @template T - The class type to build
* @returns An abstract base class to extend
*/
function CeriosClassAutoBuilder(classConstructor) {
const cached = RUNTIME_CACHE.get(classConstructor);
if (cached) return cached;
class ClassAutoBuilderRuntime extends CeriosClassBuilder {
constructor(dataOrConstructor, initOrData, validators, requiredFields) {
if (typeof dataOrConstructor === "function") super(dataOrConstructor, initOrData, validators, requiredFields);
else {
const init = initOrData ?? {};
assertBuilderInit(init);
const required = init.requiredFields === void 0 ? void 0 : new Set(toRequiredPaths(init.requiredFields));
super(classConstructor, dataOrConstructor ?? {}, init.validators, required);
}
return new Proxy(this, autoSetterHandler);
}
/**
* Copy-on-write must not go through the subclass constructor. Users write
* `constructor(data?) { super(data, { requiredFields: [...] }) }`, which cannot
* receive the base class's internal 4-argument shape - forwarding it would land the
* class constructor in the data slot and drop the validators and required fields.
* {@link createBuilderCopy} sidesteps user constructors while still carrying the
* subclass's own fields across.
*/
instantiateBuilder(data, validators, requiredFields) {
return new Proxy(createBuilderCopy(this, {
_classConstructor: this.getClassConstructor(),
_actual: data,
_validators: [...validators],
_requiredFields: new Set(requiredFields)
}), autoSetterHandler);
}
}
const Runtime = ClassAutoBuilderRuntime;
Runtime.from = function(instance) {
return new this(deepClone(instance));
};
RUNTIME_CACHE.set(classConstructor, ClassAutoBuilderRuntime);
return ClassAutoBuilderRuntime;
}
//#endregion
export { CeriosAutoBuilder, CeriosBuilder, CeriosBuilderError, CeriosClassAutoBuilder, CeriosClassBuilder };
//# sourceMappingURL=index.mjs.map
{
"name": "@cerios/cerios-builder",
"version": "1.6.0",
"version": "1.7.0",
"description": "A TypeScript builder pattern library providing compile-time type safety for object construction with method chaining and required field validation",

@@ -25,18 +25,24 @@ "keywords": [

"type": "commonjs",
"main": "./dist/index.js",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.cts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/index.mjs",
"default": "./dist/index.js"
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"default": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"scripts": {
"build": "tsup",
"build": "tsdown",
"changeset": "changeset",
"changeset:publish": "changeset publish --provenance",
"changeset:version": "changeset version && npm i",
"changeset:prepare-release": "npm run changeset:version && npm run format",
"check-exports": "attw --pack .",
"compile": "tsc --noEmit",
"dev": "tsc --watch",
"format": "oxfmt",

@@ -46,24 +52,25 @@ "format:check": "oxfmt --check",

"lint:fix": "oxlint --type-aware --fix",
"check-exports": "attw --pack .",
"compile": "tsc --noEmit",
"dev": "tsc --watch",
"release": "changeset publish",
"test": "vitest --run",
"test:coverage": "vitest --coverage",
"test:watch": "vitest --watch",
"update-all-packages": "npm-check-updates -u && npm i"
"update-all-packages": "npm-check-updates -u && npm i",
"version-packages": "changeset version && npm i",
"version-packages:prepare-release": "npm run version-packages && npm run format"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@changesets/cli": "^2.30.0",
"@types/node": "^25.4.0",
"@arethetypeswrong/cli": "^0.18.5",
"@changesets/cli": "^2.31.1",
"@types/node": "^26.1.1",
"@vitest/coverage-v8": "^4.1.10",
"husky": "^9.1.7",
"lint-staged": "^16.3.3",
"npm-check-updates": "^19.6.3",
"oxfmt": "^0.38.0",
"oxlint": "^1.53.0",
"oxlint-tsgolint": "^0.16.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
"lint-staged": "^17.1.0",
"npm-check-updates": "^22.2.9",
"oxfmt": "^0.59.0",
"oxlint": "^1.74.0",
"oxlint-tsgolint": "^0.25.0",
"tsdown": "^0.22.12",
"typescript": "^7.0.2",
"vitest": "^4.1.10"
}
}
/**
* Helper type to extract the builder type from a builder instance.
* This is useful when you want to accept a builder with some fields already set
* without manually specifying which fields are set.
*
* @deprecated Prefer `BuilderComposerFromFactory` (or `ClassBuilderComposerFromFactory`)
* for callback-based APIs. This alias remains for backward compatibility.
*
* @template B - A builder instance type
*
* @example
* ```typescript
* import { BuilderComposerFromFactory } from "@cerios/cerios-builder";
*
* // Preferred modern pattern:
* function withAddressDefaults(
* builderFn: BuilderComposerFromFactory<typeof AddressBuilder.createWithDefaults>
* ) { ... }
* ```
*/
type BuilderType<B> = B;
/**
* Helper type to extract optional keys from a type.
* Returns keys where the property can be undefined.
*
* @template T - The type to extract optional keys from
*/
type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];
/**
* Recursively makes all properties readonly for deep immutability.
* Handles arrays, objects, and primitive types.
*
* @template T - The type to make deeply readonly
*/
type DeepReadonly<T> = T extends (infer R)[] ? DeepReadonlyArray<R> : T extends (...args: unknown[]) => unknown ? T : T extends object ? DeepReadonlyObject<T> : T;
/**
* Helper type for deep readonly arrays
* @internal
*/
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {
}
/**
* Helper type for deep readonly objects
* @internal
*/
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
/**
* Unique symbol used internally to brand types and track which properties have been set in the builder's type.
*
* @internal
*/
declare const __brand: unique symbol;
/**
* Internal brand for builder type-state tracking.
* Prefer helper aliases like `BuilderStep` in public APIs.
*/
type InternalBuilderBrand<T> = {
[__brand]: T;
};
/**
* Type utility for branding builder types with information about which properties have been set.
* This is used to enforce compile-time safety for required fields in the builder pattern.
*
* @deprecated Prefer `BuilderStep`, `BuilderPreset`, `BuilderComposer`, or `BuilderComposerFromFactory` in user-facing APIs.
* This type remains exported for backward compatibility.
*
* @template T - The type representing the set of properties that have been set
* @internal
*/
type CeriosBrand<T> = InternalBuilderBrand<T>;
type RootFromPath$1<P extends string> = P extends `${infer K}.${string}` ? K : P;
type StepKey<T extends object, S extends keyof T | Path<T>> = S extends keyof T ? S : Extract<RootFromPath$1<S & string>, keyof T>;
/**
* Helper type for fluent builder methods that set one root property.
* This keeps method signatures short while preserving compile-time field tracking.
* Supports both root keys ("name") and dot-notation paths ("address.street").
*
* @template B - The current builder instance type (usually `this`)
* @template T - The target object type being built
* @template S - A root key or path in T
*/
type BuilderStep<B, T extends object, S extends keyof T | Path<T>> = B & InternalBuilderBrand<Pick<T, StepKey<T, S>>>;
/**
* Helper type for factory methods that return a preconfigured builder state.
* Useful for methods like `createWithDefaults()` where you want an explicit return type
* without losing compile-time tracking of which fields are already set.
*
* @template B - The builder instance type
* @template T - The target object type being built
* @template S - A root key or path (or union) already configured by the factory
*/
type BuilderPreset<B, T extends object, S extends keyof T | Path<T>> = BuilderStep<B, T, S>;
/**
* Helper type for callback-based builder composition APIs.
*
* Input builder:
* - If `Preset` is omitted, callback receives the base builder `B`.
* - If `Preset` is provided, callback receives a preconfigured builder state.
*
* Output builder:
* - Callback must return a fully buildable state for `T`.
*
* @template B - The builder instance type
* @template T - The target object type being built
* @template Preset - Optional preset key/path union already configured before callback execution
*/
type BuilderComposer<B, T extends object, Preset extends keyof T | Path<T> = never> = (builder: [Preset] extends [never] ? B : BuilderPreset<B, T, Preset>) => BuilderPreset<B, T, keyof T>;
type BuilderBaseFromFactoryReturn<R> = R extends (infer B) & InternalBuilderBrand<unknown> ? B : R;
type BuilderTargetFromFactoryReturn<R> = BuilderBaseFromFactoryReturn<R> extends CeriosBuilder<infer T> ? T : never;
/**
* Helper type for composition callbacks based on a builder factory method.
*
* This infers both the callback input type (including presets/defaults) and the
* fully-buildable output type directly from the factory return type.
*
* @template F - A builder factory function type (for example: `typeof MyBuilder.createWithDefaults`)
*/
type BuilderComposerFromFactory<F extends (...args: never[]) => unknown> = (builder: ReturnType<F>) => BuilderPreset<BuilderBaseFromFactoryReturn<ReturnType<F>>, BuilderTargetFromFactoryReturn<ReturnType<F>>, keyof BuilderTargetFromFactoryReturn<ReturnType<F>>>;
/**
* Helper type to represent a path through an object structure
* Handles optional properties by unwrapping them with NonNullable
*/
type PathImpl$1<T, K extends keyof T = keyof T> = K extends string | number ? NonNullable<T[K]> extends object ? NonNullable<T[K]> extends Array<unknown> ? K : K | `${K}.${PathImpl$1<NonNullable<T[K]>> & string}` : K : never;
type Path<T> = PathImpl$1<T>;
/**
* Helper type to get the value at a specific path, handling optional properties
*/
type PathValue<T, P> = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<NonNullable<T[K]>, Rest> : never : never;
/**
* Type-safe template for defining required fields using an array of paths.
* Simply list the paths that are required.
*/
type RequiredFieldsTemplate<T> = ReadonlyArray<Path<T>>;
/**
* Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields.
*
* This class is intended to be extended by concrete builder implementations for your own types.
* It provides utility methods for setting properties and building the final object, ensuring that all required fields are set at compile time.
*
* Example usage:
* ```typescript
* interface MyType { foo: string; bar: number[]; }
* class MyTypeBuilder extends CeriosBuilder<MyType> {
* static requiredTemplate: RequiredFieldsTemplate<MyType> = ['foo'];
* setFoo(value: string) { return this.setProperty('foo', value); }
* addBar(value: number) { return this.addToArrayProperty('bar', value); }
* }
* // Usage:
* const obj = new MyTypeBuilder({})
* .setFoo('hello')
* .addBar(42)
* .buildSafe(); // Validates that 'foo' is set
* ```
*
* @template T - The complete type being built
*/
declare abstract class CeriosBuilder<T extends object> {
protected readonly _actual: Partial<T>;
/**
* Template defining which fields are required for this builder.
* Subclasses should override this to specify their required fields as an array of paths.
* The template is type-safe - only valid paths from type T can be used.
*
* @deprecated Prefer passing required fields via subclass constructor through `super(data, requiredFields)`
* or setting them at runtime with `setRequiredFields()`.
*/
static requiredTemplate?: ReadonlyArray<string>;
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
private _requiredFields;
/**
* Custom validators that run during build.
* @private
*/
private _validators;
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .setRequiredFields(['path.to.field1', 'path.to.field2'])
* .setField1('value1')
* .setField2('value2')
* .buildSafe();
* ```
*/
setRequiredFields(fields: ReadonlyArray<Path<T>>): this;
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setAge(20)
* .setEmail('user@example.com')
* .build();
* ```
*/
addValidator(validator: (obj: Partial<T>) => boolean | string): this;
/**
* Gets the combined required fields from both the static template and instance-level fields.
* @private
*/
private getRequiredTemplate;
/**
* Validates that all fields in the required template have been set.
* @private
*/
private validateRequiredFields;
/**
* Runs all custom validators and returns any error messages.
* @private
*/
private runValidators;
/**
* Removes an optional property from the builder.
* Only works with optional properties (those that can be undefined).
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John')
* .setEmail('john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty<K extends OptionalKeys<T>>(key: K): this;
/**
* Clears all optional properties from the builder, keeping only required ones.
* Properties in the required template and those marked as required are preserved.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John') // required
* .setAge(30) // required
* .setEmail('john@example.com') // optional
* .setPhone('555-1234') // optional
* .clearOptionalProperties();
* // Only name and age remain
* ```
*/
clearOptionalProperties(): this;
/**
* Creates a new builder instance. Intended to be called by subclasses.
*
* @param _actual - The current partial state of the object being built
* @param _requiredFields - Optional array of required field paths to preserve across instances
* @param _validators - Optional array of validators to preserve across instances
* @protected
*/
protected constructor(_actual: Partial<T>, _requiredFields?: RequiredFieldsTemplate<T>, _validators?: Array<(obj: Partial<T>) => boolean | string>);
/**
* Sets a property value and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses.
*
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set and type state updated
* @protected
*/
protected setProperty<K extends keyof T>(key: K, value: T[K]): BuilderStep<this, T, K>;
/**
* Sets multiple property values at once and returns a new builder instance with updated type state.
* @param props - An object with one or more properties to set.
* @returns A new builder instance with the properties set and type state updated.
* @protected
*/
protected setProperties<K extends keyof T>(props: Pick<T, K>): BuilderStep<this, T, K>;
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
*
* @template P - The property path (e.g., "parent.child.grandchild")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* builder.setNestedProperty('Order.Details.CustomerId', 'value')
* ```
*/
protected setNestedProperty<P extends Path<T>>(path: P, value: PathValue<T, P>): BuilderStep<this, T, P>;
/**
* Deep clone helper for nested objects
* @private
*/
private deepClone;
/**
* Adds a value to an array property and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses for array properties.
*
* @template K - The property key (must be an array property)
* @template V - The type of the array element
* @param key - The array property key to add to
* @param value - The value to add to the array
* @returns A new builder instance with the array property updated and type state updated
* @protected
*/
protected addToArrayProperty<K extends {
[P in keyof T]: NonNullable<T[P]> extends Array<unknown> ? P : never;
}[keyof T], V extends T[K] extends Array<infer U> ? U : T[K] extends Array<infer U> | undefined ? U : never>(key: K, value: V): BuilderStep<this, T, K>;
/**
* Builds the final object with both compile-time and runtime validation.
* This is the recommended and safest way to build objects.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
build(this: this & InternalBuilderBrand<T>): T;
/**
* Builds the final object with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: No validation
*
* @returns The fully built object of type T
*/
buildWithoutRuntimeValidation(this: this & InternalBuilderBrand<T>): T;
/**
* Builds the final object with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildWithoutCompileTimeValidation(): T;
/**
* Builds the final object without any validation (neither compile-time nor runtime).
* Use this only when you're certain the object is valid and need maximum performance.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: No validation
*
* @returns The object of type T (may be incomplete)
*/
buildUnsafe(): T;
/**
* Builds a partial object, which may not have all required fields set.
* This is useful for scenarios where you want to inspect or validate the current state before finalizing.
*
* @returns The partially built object
*/
buildPartial(): Partial<T>;
/**
* Creates a new builder instance from an existing object.
* This is useful for creating builders from existing instances to modify them.
*
* @param instance - The existing object to create a builder from
* @returns A new builder instance initialized with the object's data
*
* @example
* ```typescript
* const existingPerson = { name: 'John', age: 30 };
* const builder = MyBuilder.from(existingPerson);
* const updated = builder.setAge(31).build();
* ```
*/
static from<T extends object, B extends new (data: Partial<T>) => unknown>(this: B, instance: T): InstanceType<B>;
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = new MyBuilder({}).setName('John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone(): this;
/**
* Static deep clone helper for the from() method.
* @private
*/
private static deepCloneStatic;
/**
* Builds and freezes the final object with both compile-time and runtime validation.
* The returned object is shallowly frozen - top-level properties cannot be modified,
* but nested objects remain mutable.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.freeze()
*
* @returns The frozen object of type Readonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildFrozen(this: this & InternalBuilderBrand<T>): Readonly<T>;
/**
* Builds and deeply freezes the final object with both compile-time and runtime validation.
* The returned object is recursively frozen - all nested objects and arrays are also frozen.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.freeze()
*
* @returns The deeply frozen object of type DeepReadonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildDeepFrozen(this: this & InternalBuilderBrand<T>): DeepReadonly<T>;
/**
* Builds and seals the final object with both compile-time and runtime validation.
* The returned object is shallowly sealed - properties cannot be added or removed,
* but existing properties can still be modified. Nested objects remain unsealed.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.seal()
*
* @returns The sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildSealed(this: this & InternalBuilderBrand<T>): T;
/**
* Builds and deeply seals the final object with both compile-time and runtime validation.
* The returned object is recursively sealed - properties cannot be added or removed at any level,
* but existing properties can still be modified.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.seal()
*
* @returns The deeply sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildDeepSealed(this: this & InternalBuilderBrand<T>): T;
}
/**
* Type representing a class constructor that can be instantiated with optional partial data.
* @template T - The type that the constructor creates
*/
type ClassConstructor<T> = new (data?: Partial<T>) => T;
/**
* Helper to extract only data properties (exclude methods) from a class type.
* This allows compile-time validation to work properly with classes.
*/
type DataPropertiesOnly<T> = {
[K in keyof T as T[K] extends (...args: unknown[]) => unknown ? never : K]: T[K];
};
/**
* Internal brand for class-builder type-state tracking.
* Prefer helper aliases like `ClassBuilderStep` in public APIs.
*/
type InternalClassBrand<T> = {
readonly __classBuilderBrand: T;
};
type RootFromPath<P extends string> = P extends `${infer K}.${string}` ? K : P;
type ClassStepKey<T extends object, S extends keyof T | ClassPath<T>> = S extends keyof T ? Extract<S, keyof DataPropertiesOnly<T>> : Extract<RootFromPath<S & string>, keyof DataPropertiesOnly<T>>;
/**
* Helper type for fluent class-builder methods.
* Supports both direct data-property keys ("name") and nested paths ("address.city").
*
* @template B - The current builder instance type (usually `this`)
* @template T - The class type being built
* @template S - A data-property key or class path
*/
type ClassBuilderStep<B, T extends object, S extends keyof T | ClassPath<T>> = B & InternalClassBrand<Pick<DataPropertiesOnly<T>, ClassStepKey<T, S>>>;
/**
* Helper type for factory methods that return a preconfigured class-builder state.
*
* @template B - The class-builder instance type
* @template T - The class type being built
* @template S - A data-property key or class path (or union) configured by the factory
*/
type ClassBuilderPreset<B, T extends object, S extends keyof DataPropertiesOnly<T> | ClassPath<T>> = ClassBuilderStep<B, T, S>;
/**
* Helper type for callback-based class-builder composition APIs.
*
* @template B - The class-builder instance type
* @template T - The class type being built
* @template Preset - Optional preset key/path union already configured before callback execution
*/
type ClassBuilderComposer<B, T extends object, Preset extends keyof DataPropertiesOnly<T> | ClassPath<T> = never> = (builder: [Preset] extends [never] ? B : ClassBuilderPreset<B, T, Preset>) => ClassBuilderPreset<B, T, keyof DataPropertiesOnly<T>>;
type ClassBuilderBaseFromFactoryReturn<R> = R extends (infer B) & InternalClassBrand<unknown> ? B : R;
type ClassBuilderTargetFromFactoryReturn<R> = ClassBuilderBaseFromFactoryReturn<R> extends CeriosClassBuilder<infer T> ? T : never;
/**
* Helper type for composition callbacks based on a class-builder factory method.
*
* This infers both the callback input type (including presets/defaults) and the
* fully-buildable output type directly from the factory return type.
*
* @template F - A class-builder factory function type (for example: `typeof MyBuilder.createWithDefaults`)
*/
type ClassBuilderComposerFromFactory<F extends (...args: never[]) => unknown> = (builder: ReturnType<F>) => ClassBuilderPreset<ClassBuilderBaseFromFactoryReturn<ReturnType<F>>, ClassBuilderTargetFromFactoryReturn<ReturnType<F>>, keyof DataPropertiesOnly<ClassBuilderTargetFromFactoryReturn<ReturnType<F>>>>;
/**
* Helper type to represent a path through an object structure for class properties.
* Only considers data properties (excludes methods).
* Handles optional properties by unwrapping them with NonNullable.
* @internal
*/
type PathImpl<T, K extends keyof DataPropertiesOnly<T> = keyof DataPropertiesOnly<T>> = K extends string | number ? NonNullable<DataPropertiesOnly<T>[K]> extends object ? NonNullable<DataPropertiesOnly<T>[K]> extends Array<unknown> ? K : K | `${K}.${PathImpl<NonNullable<DataPropertiesOnly<T>[K]>> & string}` : K : never;
/**
* Type representing valid dot-notation paths through class data properties.
* @template T - The class type
*/
type ClassPath<T> = PathImpl<T>;
/**
* Helper type to get the value at a specific path in a class, handling optional properties.
* Only considers data properties (excludes methods).
* @template T - The class type
* @template P - The path string
* @internal
*/
type ClassPathValue<T, P> = P extends keyof DataPropertiesOnly<T> ? DataPropertiesOnly<T>[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof DataPropertiesOnly<T> ? ClassPathValue<NonNullable<DataPropertiesOnly<T>[K]>, Rest> : never : never;
/**
* Type-safe builder specifically designed for classes.
* This builder automatically instantiates the target class and provides:
* - Compile-time validation that works with class methods
* - Automatic runtime validation of nested class instances
* - Preservation of decorators and class methods
*
* Example usage:
* ```typescript
* class Person {
* name!: string;
* age!: number;
* email?: string;
*
* constructor(data?: Partial<Person>) {
* if (data) Object.assign(this, data);
* }
*
* greet() { return `Hello, I'm ${this.name}`; }
* }
*
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .build();
* ```
*
* @template T - The class type being built
*/
declare class CeriosClassBuilder<T extends object> {
/**
* Optional static template defining which data properties are required.
* Subclasses can set this to specify required fields, which will be preserved
* when calling clearOptionalProperties().
*
* @deprecated Prefer passing required fields via subclass constructor and `super(...)`
* or setting them at runtime with `setRequiredFields()`.
*
* @example
* ```typescript
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* ```
*/
static requiredDataProperties?: ReadonlyArray<string>;
/**
* The class constructor to instantiate when building.
* @private
*/
private readonly _classConstructor;
/**
* The current partial state of the object being built.
* @private
*/
protected readonly _actual: Partial<T>;
/**
* Custom validators that run during build.
* @private
*/
private _validators;
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
private _requiredFields;
/**
* Creates a new class builder instance.
* @param classConstructor - The class constructor to use for building
* @param data - Optional initial data
* @param _validators - Optional array of validators to preserve across instances
* @param _requiredFields - Optional required fields to preserve across instances
*/
protected constructor(classConstructor: ClassConstructor<T>, data?: Partial<T>, _validators?: Array<(obj: Partial<T>) => boolean | string>, _requiredFields?: ReadonlyArray<ClassPath<T>> | Set<string>);
/**
* Gets the class constructor for this builder.
* @private
*/
private getClassConstructor;
/**
* Creates a new instance of the builder with updated data.
* Subclasses can override this to return the correct subclass type.
* @private
*/
private createBuilder;
/**
* Sets a property value and returns a new builder instance with updated type state.
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set
* @protected
*/
protected setProperty<K extends keyof DataPropertiesOnly<T>>(key: K, value: DataPropertiesOnly<T>[K]): ClassBuilderStep<this, T, K>;
/**
* Fallback overload for generic subclass scenarios where TypeScript cannot
* resolve `keyof DataPropertiesOnly<T>` from a literal key.
* This keeps fluent APIs ergonomic in shared generic base builders.
* @protected
*/
protected setProperty<K extends keyof T & string>(key: K, value: T[K]): ClassBuilderStep<this, T, K>;
protected setProperty<K extends keyof DataPropertiesOnly<T>>(key: K, value: DataPropertiesOnly<T>[K]): ClassBuilderStep<this, T, K>;
/**
* Sets multiple properties at once.
* @template K - The property keys being set
* @param props - Object with properties to set
* @returns A new builder instance with the properties set
* @protected
*/
protected setProperties<K extends keyof DataPropertiesOnly<T>>(props: Pick<DataPropertiesOnly<T>, K>): ClassBuilderStep<this, T, K>;
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
* Only supports data properties (excludes methods).
*
* @template P - The property path (e.g., "address.city")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* class Address {
* street!: string;
* city!: string;
* }
* class Person {
* name!: string;
* address!: Address;
* }
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setNestedProperty('address.city', 'New York')
* .build();
* ```
*/
protected setNestedProperty<P extends ClassPath<T>>(path: P, value: ClassPathValue<T, P>): ClassBuilderStep<this, T, P>;
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .setRequiredFields(['name', 'age'])
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .buildWithoutCompileTimeValidation();
* ```
*/
setRequiredFields(fields: ReadonlyArray<ClassPath<T>>): this;
/**
* Gets the combined required fields from both the static template and instance-level fields.
* If instance-level fields are set via setRequiredFields(), they are combined with static fields.
* @private
*/
private getRequiredTemplate;
/**
* Validates that all fields in the required template have been set.
* @private
*/
private validateRequiredFields;
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setProperty('age', 20)
* .setProperty('email', 'user@example.com')
* .build();
* ```
*/
addValidator(validator: (obj: Partial<T>) => boolean | string): this;
/**
* Runs all custom validators and returns any error messages.
* @private
*/
private runValidators;
/**
* Removes an optional property from the builder.
* Only works with optional data properties (those that can be undefined).
* Methods are automatically excluded.
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* class Person {
* name!: string;
* email?: string;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('email', 'john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty<K extends OptionalKeys<DataPropertiesOnly<T>>>(key: K): this;
/**
* Clears all optional properties from the builder, keeping only required data properties.
* Uses the combined required-field template from static defaults and instance-level fields.
* If no required fields are configured, all properties are cleared.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* class Person {
* name!: string; // required
* age!: number; // required
* email?: string; // optional
* phone?: string; // optional
* }
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .setProperty('email', 'john@example.com')
* .setProperty('phone', '555-1234')
* .clearOptionalProperties();
* // Only name and age are preserved, email and phone are cleared
* ```
*/
clearOptionalProperties(): this;
/**
* Adds a value to an array property.
* @template K - The array property key
* @template V - The array element type
* @param key - The array property key
* @param value - The value to add to the array
* @returns A new builder instance with the value added
*/
addToArrayProperty<K extends {
[P in keyof DataPropertiesOnly<T>]: NonNullable<DataPropertiesOnly<T>[P]> extends Array<unknown> ? P : never;
}[keyof DataPropertiesOnly<T>], V extends DataPropertiesOnly<T>[K] extends Array<infer U> ? U : DataPropertiesOnly<T>[K] extends Array<infer U> | undefined ? U : never>(key: K, value: V): ClassBuilderStep<this, T, K>;
/**
* Builds the final class instance with compile-time and runtime validation.
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built and validated class instance
* @throws {Error} If required fields are missing or validation fails
*/
build(this: this & InternalClassBrand<DataPropertiesOnly<T>>): T;
/**
* Builds the final class instance with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: No validation
*
* @returns The fully built class instance
*/
buildWithoutRuntimeValidation(this: this & InternalClassBrand<DataPropertiesOnly<T>>): T;
/**
* Builds the final class instance with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildWithoutCompileTimeValidation(): T;
/**
* Builds the class instance without any validation.
* Use only when you're certain the object is valid.
*
* @returns The built class instance (may be incomplete)
*/
buildUnsafe(): T;
/**
* Builds a partial object (may not have all required fields).
*
* @returns The partially built object
*/
buildPartial(): Partial<T>;
/**
* Builds and freezes the class instance (shallow freeze).
*
* @returns The frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildFrozen(this: this & InternalClassBrand<DataPropertiesOnly<T>>): Readonly<T>;
/**
* Builds and deeply freezes the class instance.
*
* @returns The deeply frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepFrozen(this: this & InternalClassBrand<DataPropertiesOnly<T>>): DeepReadonly<T>;
/**
* Builds and seals the class instance (shallow freeze).
*
* @returns The sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildSealed(this: this & InternalClassBrand<DataPropertiesOnly<T>>): T;
/**
* Builds and deeply seals the class instance.
*
* @returns The deeply sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepSealed(this: this & InternalClassBrand<DataPropertiesOnly<T>>): T;
/**
* Deep freeze helper.
* @private
*/
private deepFreeze;
/**
* Deep seal helper.
* @private
*/
private deepSeal;
/**
* Deep clone helper for nested objects.
* @private
*/
private deepClone;
/**
* Creates a new builder instance from an existing class instance.
* This is useful for creating builders from existing instances to modify them.
*
* @param classConstructor - The class constructor to use
* @param instance - The existing class instance to create a builder from
* @returns A new builder instance initialized with the instance's data
*
* @example
* ```typescript
* const existingPerson = new Person({ name: 'John', age: 30 });
* const builder = PersonBuilder.from(Person, existingPerson);
* const updated = builder.setProperty('age', 31).build();
* ```
*/
static from<T extends object, B extends new (classConstructor: ClassConstructor<T>, data: Partial<T>) => unknown>(this: B, classConstructor: ClassConstructor<T>, instance: T): InstanceType<B>;
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = PersonBuilder.create().setProperty('name', 'John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone(): this;
/**
* Static deep clone helper for the from() method.
* @private
*/
private static deepCloneStatic;
}
export { type BuilderComposer, type BuilderComposerFromFactory, type BuilderPreset, type BuilderStep, type BuilderType, type CeriosBrand, CeriosBuilder, CeriosClassBuilder, type ClassBuilderComposer, type ClassBuilderComposerFromFactory, type ClassBuilderPreset, type ClassBuilderStep, type ClassConstructor, type ClassPath, type DeepReadonly, type OptionalKeys, type RequiredFieldsTemplate };
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CeriosBuilder: () => CeriosBuilder,
CeriosClassBuilder: () => CeriosClassBuilder
});
module.exports = __toCommonJS(index_exports);
// src/cerios-builder.ts
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
function deepSeal(obj) {
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
deepSeal(value);
}
});
return Object.seal(obj);
}
var CeriosBuilder = class _CeriosBuilder {
/**
* Creates a new builder instance. Intended to be called by subclasses.
*
* @param _actual - The current partial state of the object being built
* @param _requiredFields - Optional array of required field paths to preserve across instances
* @param _validators - Optional array of validators to preserve across instances
* @protected
*/
constructor(_actual, _requiredFields, _validators) {
this._actual = _actual;
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
this._requiredFields = /* @__PURE__ */ new Set();
/**
* Custom validators that run during build.
* @private
*/
this._validators = [];
if (_requiredFields) {
this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]);
}
if (_validators) {
this._validators = [..._validators];
}
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .setRequiredFields(['path.to.field1', 'path.to.field2'])
* .setField1('value1')
* .setField2('value2')
* .buildSafe();
* ```
*/
setRequiredFields(fields) {
this._requiredFields = /* @__PURE__ */ new Set([...fields]);
return this;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = new MyBuilder({})
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setAge(20)
* .setEmail('user@example.com')
* .build();
* ```
*/
addValidator(validator) {
const BuilderClass = this.constructor;
return new BuilderClass(this._actual, Array.from(this._requiredFields), [
...this._validators,
validator
]);
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* @private
*/
getRequiredTemplate() {
var _a;
const ctor = this.constructor;
const staticFields = (_a = ctor.requiredTemplate) != null ? _a : [];
const instanceFields = Array.from(this._requiredFields);
return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!missing.includes(path)) {
missing.push(path);
}
}
}
return missing;
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
const errors = [];
for (const validator of this._validators) {
const result = validator(this._actual);
if (result === false) {
errors.push("Validation failed");
} else if (typeof result === "string") {
errors.push(result);
}
}
return errors;
}
/**
* Removes an optional property from the builder.
* Only works with optional properties (those that can be undefined).
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John')
* .setEmail('john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const BuilderClass = this.constructor;
const newData = { ...this._actual };
delete newData[key];
return new BuilderClass(
newData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Clears all optional properties from the builder, keeping only required ones.
* Properties in the required template and those marked as required are preserved.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* const builder = new MyBuilder()
* .setName('John') // required
* .setAge(30) // required
* .setEmail('john@example.com') // optional
* .setPhone('555-1234') // optional
* .clearOptionalProperties();
* // Only name and age remain
* ```
*/
clearOptionalProperties() {
const BuilderClass = this.constructor;
const requiredPaths = this.getRequiredTemplate();
const newData = {};
for (const path of requiredPaths) {
const keys = path.split(".");
if (keys.length === 1) {
const key = keys[0];
if (key in this._actual) {
newData[key] = this._actual[key];
}
} else {
const rootKey = keys[0];
if (rootKey in this._actual && !(rootKey in newData)) {
newData[rootKey] = this._actual[rootKey];
}
}
}
return new BuilderClass(
newData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets a property value and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses.
*
* @template K - The property key being set
* @param key - The property key to set
* @param value - The value to assign to the property
* @returns A new builder instance with the property set and type state updated
* @protected
*/
setProperty(key, value) {
const BuilderClass = this.constructor;
return new BuilderClass(
{
...this._actual,
[key]: value
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets multiple property values at once and returns a new builder instance with updated type state.
* @param props - An object with one or more properties to set.
* @returns A new builder instance with the properties set and type state updated.
* @protected
*/
setProperties(props) {
const BuilderClass = this.constructor;
return new BuilderClass(
{
...this._actual,
...props
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
*
* @template P - The property path (e.g., "parent.child.grandchild")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* builder.setNestedProperty('Order.Details.CustomerId', 'value')
* ```
*/
setNestedProperty(path, value) {
const BuilderClass = this.constructor;
const keys = path.split(".");
const newActual = this.deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) {
current[key] = {};
} else {
current[key] = this.deepClone(existing);
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
return new BuilderClass(
newActual,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Deep clone helper for nested objects
* @private
*/
deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
/**
* Adds a value to an array property and returns a new builder instance with updated type state.
* This method is intended to be wrapped by concrete builder methods in subclasses for array properties.
*
* @template K - The property key (must be an array property)
* @template V - The type of the array element
* @param key - The array property key to add to
* @param value - The value to add to the array
* @returns A new builder instance with the array property updated and type state updated
* @protected
*/
addToArrayProperty(key, value) {
var _a;
const BuilderClass = this.constructor;
const currentArray = (_a = this._actual[key]) != null ? _a : [];
return new BuilderClass(
{
...this._actual,
[key]: [...currentArray, value]
},
Array.from(this._requiredFields),
this._validators
);
}
/**
* Builds the final object with both compile-time and runtime validation.
* This is the recommended and safest way to build objects.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
build() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return this._actual;
}
/**
* Builds the final object with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: No validation
*
* @returns The fully built object of type T
*/
buildWithoutRuntimeValidation() {
return this._actual;
}
/**
* Builds the final object with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates all fields in the requiredTemplate
*
* @returns The fully built object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildWithoutCompileTimeValidation() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return this._actual;
}
/**
* Builds the final object without any validation (neither compile-time nor runtime).
* Use this only when you're certain the object is valid and need maximum performance.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: No validation
*
* @returns The object of type T (may be incomplete)
*/
buildUnsafe() {
return this._actual;
}
/**
* Builds a partial object, which may not have all required fields set.
* This is useful for scenarios where you want to inspect or validate the current state before finalizing.
*
* @returns The partially built object
*/
buildPartial() {
return this._actual;
}
/**
* Creates a new builder instance from an existing object.
* This is useful for creating builders from existing instances to modify them.
*
* @param instance - The existing object to create a builder from
* @returns A new builder instance initialized with the object's data
*
* @example
* ```typescript
* const existingPerson = { name: 'John', age: 30 };
* const builder = MyBuilder.from(existingPerson);
* const updated = builder.setAge(31).build();
* ```
*/
static from(instance) {
const clonedData = _CeriosBuilder.deepCloneStatic(instance);
return new this(clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = new MyBuilder({}).setName('John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const BuilderClass = this.constructor;
const clonedData = this.deepClone(this._actual);
return new BuilderClass(
clonedData,
Array.from(this._requiredFields),
this._validators
);
}
/**
* Static deep clone helper for the from() method.
* @private
*/
static deepCloneStatic(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepCloneStatic(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepCloneStatic(obj[key]);
}
}
return cloned;
}
/**
* Builds and freezes the final object with both compile-time and runtime validation.
* The returned object is shallowly frozen - top-level properties cannot be modified,
* but nested objects remain mutable.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.freeze()
*
* @returns The frozen object of type Readonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return Object.freeze(this._actual);
}
/**
* Builds and deeply freezes the final object with both compile-time and runtime validation.
* The returned object is recursively frozen - all nested objects and arrays are also frozen.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.freeze()
*
* @returns The deeply frozen object of type DeepReadonly<T>
* @throws {Error} If any required field is missing at runtime
*/
buildDeepFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return deepFreeze(this._actual);
}
/**
* Builds and seals the final object with both compile-time and runtime validation.
* The returned object is shallowly sealed - properties cannot be added or removed,
* but existing properties can still be modified. Nested objects remain unsealed.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and applies Object.seal()
*
* @returns The sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return Object.seal(this._actual);
}
/**
* Builds and deeply seals the final object with both compile-time and runtime validation.
* The returned object is recursively sealed - properties cannot be added or removed at any level,
* but existing properties can still be modified.
*
* - Compile-time: TypeScript enforces all required properties are set
* - Runtime: Validates all fields in the requiredTemplate and recursively applies Object.seal()
*
* @returns The deeply sealed object of type T
* @throws {Error} If any required field is missing at runtime
*/
buildDeepSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
return deepSeal(this._actual);
}
};
// src/cerios-class-builder.ts
var CeriosClassBuilder = class _CeriosClassBuilder {
/**
* Creates a new class builder instance.
* @param classConstructor - The class constructor to use for building
* @param data - Optional initial data
* @param _validators - Optional array of validators to preserve across instances
* @param _requiredFields - Optional required fields to preserve across instances
*/
constructor(classConstructor, data = {}, _validators, _requiredFields) {
/**
* Custom validators that run during build.
* @private
*/
this._validators = [];
/**
* Instance-level required fields that can be populated dynamically.
* This allows adding required fields at runtime via the setRequiredFields method.
* @private
*/
this._requiredFields = /* @__PURE__ */ new Set();
this._classConstructor = classConstructor;
this._actual = data;
if (_validators) {
this._validators = [..._validators];
}
if (_requiredFields) {
this._requiredFields = _requiredFields instanceof Set ? new Set(_requiredFields) : /* @__PURE__ */ new Set([..._requiredFields]);
}
}
/**
* Gets the class constructor for this builder.
* @private
*/
getClassConstructor() {
return this._classConstructor;
}
/**
* Creates a new instance of the builder with updated data.
* Subclasses can override this to return the correct subclass type.
* @private
*/
createBuilder(data) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, data, this._validators, this._requiredFields);
}
setProperty(key, value) {
const newBuilder = this.createBuilder({
...this._actual,
[key]: value
});
return newBuilder;
}
/**
* Sets multiple properties at once.
* @template K - The property keys being set
* @param props - Object with properties to set
* @returns A new builder instance with the properties set
* @protected
*/
setProperties(props) {
const newBuilder = this.createBuilder({
...this._actual,
...props
});
return newBuilder;
}
/**
* Sets a deeply nested property value and returns a new builder instance with updated type state.
* This method uses dot notation to set nested properties in a type-safe way.
* Only supports data properties (excludes methods).
*
* @template P - The property path (e.g., "address.city")
* @param path - The dot-notation path to the property
* @param value - The value to assign to the nested property
* @returns A new builder instance with the nested property set
* @protected
*
* @example
* ```typescript
* class Address {
* street!: string;
* city!: string;
* }
* class Person {
* name!: string;
* address!: Address;
* }
* const builder = new CeriosClassBuilder(Person);
* const person = builder
* .setNestedProperty('address.city', 'New York')
* .build();
* ```
*/
setNestedProperty(path, value) {
const keys = path.split(".");
const newActual = this.deepClone(this._actual);
let current = newActual;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const existing = current[key];
if (!(key in current) || typeof existing !== "object" || existing === null) {
current[key] = {};
} else {
current[key] = this.deepClone(existing);
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
const newBuilder = this.createBuilder(newActual);
return newBuilder;
}
/**
* Sets the required fields for this builder instance.
* This allows you to dynamically define which fields are required.
*
* @param fields - Array of dot-notation paths to required fields
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .setRequiredFields(['name', 'age'])
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .buildWithoutCompileTimeValidation();
* ```
*/
setRequiredFields(fields) {
const BuilderClass = this.constructor;
return new BuilderClass(this._classConstructor, this._actual, this._validators, /* @__PURE__ */ new Set([...fields]));
}
/**
* Gets the combined required fields from both the static template and instance-level fields.
* If instance-level fields are set via setRequiredFields(), they are combined with static fields.
* @private
*/
getRequiredTemplate() {
var _a;
const ctor = this.constructor;
const staticFields = (_a = ctor.requiredDataProperties) != null ? _a : [];
const instanceFields = Array.from(this._requiredFields);
if (instanceFields.length > 0) {
return [.../* @__PURE__ */ new Set([...staticFields, ...instanceFields])];
}
return staticFields;
}
/**
* Validates that all fields in the required template have been set.
* @private
*/
validateRequiredFields() {
const requiredPaths = this.getRequiredTemplate();
const missing = [];
for (const path of requiredPaths) {
const keys = path.split(".");
let current = this._actual;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current === null || current === void 0 || typeof current !== "object" || !(key in current)) {
missing.push(path);
break;
}
current = current[key];
}
if (current === null || current === void 0) {
if (!missing.includes(path)) {
missing.push(path);
}
}
}
return missing;
}
/**
* Adds a custom validator function that will be executed during build.
* Validators can return true for valid, false for invalid, or a string error message.
* Multiple validators can be added and all will be checked.
*
* @param validator - Function that validates the partial object
* @returns The builder instance for chaining
*
* @example
* ```typescript
* const builder = PersonBuilder.create()
* .addValidator(obj => obj.age ? obj.age >= 18 : 'Age must be 18 or older')
* .addValidator(obj => obj.email?.includes('@') || 'Invalid email format')
* .setProperty('age', 20)
* .setProperty('email', 'user@example.com')
* .build();
* ```
*/
addValidator(validator) {
const BuilderClass = this.constructor;
return new BuilderClass(
this._classConstructor,
this._actual,
[...this._validators, validator],
this._requiredFields
);
}
/**
* Runs all custom validators and returns any error messages.
* @private
*/
runValidators() {
const errors = [];
for (const validator of this._validators) {
const result = validator(this._actual);
if (result === false) {
errors.push("Validation failed");
} else if (typeof result === "string") {
errors.push(result);
}
}
return errors;
}
/**
* Removes an optional property from the builder.
* Only works with optional data properties (those that can be undefined).
* Methods are automatically excluded.
*
* @template K - The optional property key to remove
* @param key - The property key to remove
* @returns A new builder instance without the specified property
*
* @example
* ```typescript
* class Person {
* name!: string;
* email?: string;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('email', 'john@example.com')
* .removeOptionalProperty('email');
* // Email is now removed from the builder
* ```
*/
removeOptionalProperty(key) {
const newData = { ...this._actual };
delete newData[key];
return this.createBuilder(newData);
}
/**
* Clears all optional properties from the builder, keeping only required data properties.
* Uses the combined required-field template from static defaults and instance-level fields.
* If no required fields are configured, all properties are cleared.
*
* @returns A new builder instance with only required properties
*
* @example
* ```typescript
* class Person {
* name!: string; // required
* age!: number; // required
* email?: string; // optional
* phone?: string; // optional
* }
* class PersonBuilder extends CeriosClassBuilder<Person> {
* static requiredDataProperties = ['name', 'age'] as const;
* }
* const builder = PersonBuilder.create()
* .setProperty('name', 'John')
* .setProperty('age', 30)
* .setProperty('email', 'john@example.com')
* .setProperty('phone', '555-1234')
* .clearOptionalProperties();
* // Only name and age are preserved, email and phone are cleared
* ```
*/
clearOptionalProperties() {
const requiredProps = this.getRequiredTemplate();
const newData = {};
for (const prop of requiredProps) {
const key = prop;
if (key in this._actual) {
newData[key] = this._actual[key];
}
}
return this.createBuilder(newData);
}
/**
* Adds a value to an array property.
* @template K - The array property key
* @template V - The array element type
* @param key - The array property key
* @param value - The value to add to the array
* @returns A new builder instance with the value added
*/
addToArrayProperty(key, value) {
var _a;
const currentArray = (_a = this._actual[key]) != null ? _a : [];
const newBuilder = this.createBuilder({
...this._actual,
[key]: [...currentArray, value]
});
return newBuilder;
}
/**
* Builds the final class instance with compile-time and runtime validation.
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built and validated class instance
* @throws {Error} If required fields are missing or validation fails
*/
build() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the final class instance with only compile-time validation, skipping runtime checks.
* Use this when you want TypeScript safety but need to skip runtime validation for performance.
*
* - Compile-time: TypeScript enforces all data properties are set
* - Runtime: No validation
*
* @returns The fully built class instance
*/
buildWithoutRuntimeValidation() {
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the final class instance with only runtime validation, skipping compile-time checks.
* Use this when building from external data where compile-time checks aren't possible.
*
* - Compile-time: No TypeScript enforcement
* - Runtime: Validates required fields and custom validators
*
* @returns The fully built class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildWithoutCompileTimeValidation() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds the class instance without any validation.
* Use only when you're certain the object is valid.
*
* @returns The built class instance (may be incomplete)
*/
buildUnsafe() {
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return instance;
}
/**
* Builds a partial object (may not have all required fields).
*
* @returns The partially built object
*/
buildPartial() {
return { ...this._actual };
}
/**
* Builds and freezes the class instance (shallow freeze).
*
* @returns The frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return Object.freeze(instance);
}
/**
* Builds and deeply freezes the class instance.
*
* @returns The deeply frozen class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepFrozen() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return this.deepFreeze(instance);
}
/**
* Builds and seals the class instance (shallow freeze).
*
* @returns The sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return Object.seal(instance);
}
/**
* Builds and deeply seals the class instance.
*
* @returns The deeply sealed class instance
* @throws {Error} If required fields are missing or validation fails
*/
buildDeepSealed() {
const missing = this.validateRequiredFields();
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`);
}
const validationErrors = this.runValidators();
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join("; ")}`);
}
const ctor = this.getClassConstructor();
const instance = new ctor(this._actual);
const dataKeys = Object.keys(this._actual);
const needsAssign = dataKeys.some((key) => instance[key] === void 0 && this._actual[key] !== void 0);
if (needsAssign) {
Object.assign(instance, this._actual);
}
return this.deepSeal(instance);
}
/**
* Deep freeze helper.
* @private
*/
deepFreeze(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
this.deepFreeze(value);
}
});
return Object.freeze(obj);
}
/**
* Deep seal helper.
* @private
*/
deepSeal(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value !== null && (typeof value === "object" || typeof value === "function")) {
this.deepSeal(value);
}
});
return Object.seal(obj);
}
/**
* Deep clone helper for nested objects.
* @private
*/
deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepClone(item));
}
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = this.deepClone(obj[key]);
}
return cloned;
}
/**
* Creates a new builder instance from an existing class instance.
* This is useful for creating builders from existing instances to modify them.
*
* @param classConstructor - The class constructor to use
* @param instance - The existing class instance to create a builder from
* @returns A new builder instance initialized with the instance's data
*
* @example
* ```typescript
* const existingPerson = new Person({ name: 'John', age: 30 });
* const builder = PersonBuilder.from(Person, existingPerson);
* const updated = builder.setProperty('age', 31).build();
* ```
*/
static from(classConstructor, instance) {
const clonedData = _CeriosClassBuilder.deepCloneStatic(instance);
return new this(classConstructor, clonedData);
}
/**
* Creates a clone of the current builder instance.
* The clone has the same state but is independent - changes to one won't affect the other.
*
* @returns A new builder instance with the same state
*
* @example
* ```typescript
* const builder1 = PersonBuilder.create().setProperty('name', 'John');
* const builder2 = builder1.clone();
* // builder2 is independent of builder1
* ```
*/
clone() {
const clonedData = this.deepClone(this._actual);
return this.createBuilder(clonedData);
}
/**
* Static deep clone helper for the from() method.
* @private
*/
static deepCloneStatic(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepCloneStatic(item));
}
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = this.deepCloneStatic(obj[key]);
}
return cloned;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CeriosBuilder,
CeriosClassBuilder
});
//# sourceMappingURL=index.js.map

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display