@cerios/cerios-builder
Advanced tools
+104
-3
@@ -18,2 +18,18 @@ /** | ||
| /** | ||
| * Helper type to represent a path through an object structure | ||
| */ | ||
| type PathImpl<T, K extends keyof T = keyof T> = K extends string | number ? T[K] extends Record<string, any> ? T[K] extends Array<any> ? K : K | `${K}.${PathImpl<T[K]> & string}` : K : never; | ||
| type Path<T> = PathImpl<T>; | ||
| type PathValue<T, P> = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<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>>; | ||
| /** | ||
| * Cache the root key extraction to avoid repeated computation | ||
| * @internal | ||
| */ | ||
| type RootKey<P extends string> = P extends `${infer K}.${string}` ? K : P; | ||
| /** | ||
| * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields. | ||
@@ -28,2 +44,3 @@ * | ||
| * class MyTypeBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = ['foo']; | ||
| * setFoo(value: string) { return this.setProperty('foo', value); } | ||
@@ -36,3 +53,3 @@ * addBar(value: number) { return this.addToArrayProperty('bar', value); } | ||
| * .addBar(42) | ||
| * .build(); | ||
| * .buildSafe(); // Validates that 'foo' is set | ||
| * ``` | ||
@@ -45,8 +62,48 @@ * | ||
| /** | ||
| * 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. | ||
| */ | ||
| 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; | ||
| /** | ||
| * 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; | ||
| /** | ||
| * 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; | ||
| /** | ||
| * 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 | ||
| * @protected | ||
| */ | ||
| protected constructor(_actual: Partial<T>); | ||
| protected constructor(_actual: Partial<T>, _requiredFields?: RequiredFieldsTemplate<T>); | ||
| /** | ||
@@ -71,2 +128,23 @@ * Sets a property value and returns a new builder instance with updated type state. | ||
| /** | ||
| * 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>): this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>>; | ||
| /** | ||
| * 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. | ||
@@ -96,2 +174,25 @@ * This method is intended to be wrapped by concrete builder methods in subclasses for array properties. | ||
| /** | ||
| * Builds the final object with runtime validation using the requiredTemplate. | ||
| * This method validates that all fields in the requiredTemplate array are present. | ||
| * | ||
| * @returns The fully built object of type T | ||
| * @throws {Error} If any required field is missing at runtime | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * class MyBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = [ | ||
| * 'path.to.field1', | ||
| * 'path.to.field2' | ||
| * ]; | ||
| * } | ||
| * | ||
| * const obj = new MyBuilder({}) | ||
| * .setRequiredField1("value1") | ||
| * .setRequiredField2("value2") | ||
| * .buildSafe(); // Validates that both required fields are present | ||
| * ``` | ||
| */ | ||
| buildSafe(): T; | ||
| /** | ||
| * Builds a partial object, which may not have all required fields set. | ||
@@ -105,2 +206,2 @@ * This is useful for scenarios where you want to inspect or validate the current state before finalizing. | ||
| export { type CeriosBrand, CeriosBuilder }; | ||
| export { type CeriosBrand, CeriosBuilder, type RequiredFieldsTemplate }; |
+104
-3
@@ -18,2 +18,18 @@ /** | ||
| /** | ||
| * Helper type to represent a path through an object structure | ||
| */ | ||
| type PathImpl<T, K extends keyof T = keyof T> = K extends string | number ? T[K] extends Record<string, any> ? T[K] extends Array<any> ? K : K | `${K}.${PathImpl<T[K]> & string}` : K : never; | ||
| type Path<T> = PathImpl<T>; | ||
| type PathValue<T, P> = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<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>>; | ||
| /** | ||
| * Cache the root key extraction to avoid repeated computation | ||
| * @internal | ||
| */ | ||
| type RootKey<P extends string> = P extends `${infer K}.${string}` ? K : P; | ||
| /** | ||
| * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields. | ||
@@ -28,2 +44,3 @@ * | ||
| * class MyTypeBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = ['foo']; | ||
| * setFoo(value: string) { return this.setProperty('foo', value); } | ||
@@ -36,3 +53,3 @@ * addBar(value: number) { return this.addToArrayProperty('bar', value); } | ||
| * .addBar(42) | ||
| * .build(); | ||
| * .buildSafe(); // Validates that 'foo' is set | ||
| * ``` | ||
@@ -45,8 +62,48 @@ * | ||
| /** | ||
| * 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. | ||
| */ | ||
| 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; | ||
| /** | ||
| * 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; | ||
| /** | ||
| * 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; | ||
| /** | ||
| * 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 | ||
| * @protected | ||
| */ | ||
| protected constructor(_actual: Partial<T>); | ||
| protected constructor(_actual: Partial<T>, _requiredFields?: RequiredFieldsTemplate<T>); | ||
| /** | ||
@@ -71,2 +128,23 @@ * Sets a property value and returns a new builder instance with updated type state. | ||
| /** | ||
| * 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>): this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>>; | ||
| /** | ||
| * 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. | ||
@@ -96,2 +174,25 @@ * This method is intended to be wrapped by concrete builder methods in subclasses for array properties. | ||
| /** | ||
| * Builds the final object with runtime validation using the requiredTemplate. | ||
| * This method validates that all fields in the requiredTemplate array are present. | ||
| * | ||
| * @returns The fully built object of type T | ||
| * @throws {Error} If any required field is missing at runtime | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * class MyBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = [ | ||
| * 'path.to.field1', | ||
| * 'path.to.field2' | ||
| * ]; | ||
| * } | ||
| * | ||
| * const obj = new MyBuilder({}) | ||
| * .setRequiredField1("value1") | ||
| * .setRequiredField2("value2") | ||
| * .buildSafe(); // Validates that both required fields are present | ||
| * ``` | ||
| */ | ||
| buildSafe(): T; | ||
| /** | ||
| * Builds a partial object, which may not have all required fields set. | ||
@@ -105,2 +206,2 @@ * This is useful for scenarios where you want to inspect or validate the current state before finalizing. | ||
| export { type CeriosBrand, CeriosBuilder }; | ||
| export { type CeriosBrand, CeriosBuilder, type RequiredFieldsTemplate }; |
+172
-13
@@ -33,8 +33,74 @@ "use strict"; | ||
| * @param _actual - The current partial state of the object being built | ||
| * @param _requiredFields - Optional array of required field paths to preserve across instances | ||
| * @protected | ||
| */ | ||
| constructor(_actual) { | ||
| constructor(_actual, _requiredFields) { | ||
| 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(); | ||
| if (_requiredFields) { | ||
| this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]); | ||
| } | ||
| } | ||
| /** | ||
| * 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; | ||
| } | ||
| /** | ||
| * Gets the combined required fields from both the static template and instance-level fields. | ||
| * @private | ||
| */ | ||
| getRequiredTemplate() { | ||
| const ctor = this.constructor; | ||
| const staticFields = ctor.requiredTemplate || []; | ||
| 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 || !(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; | ||
| } | ||
| /** | ||
| * Sets a property value and returns a new builder instance with updated type state. | ||
@@ -51,6 +117,9 @@ * This method is intended to be wrapped by concrete builder methods in subclasses. | ||
| const BuilderClass = this.constructor; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| [key]: value | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| [key]: value | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
@@ -65,8 +134,66 @@ /** | ||
| const BuilderClass = this.constructor; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| ...props | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| ...props | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
| /** | ||
| * 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]; | ||
| if (!(key in current) || typeof current[key] !== "object" || current[key] === null) { | ||
| current[key] = {}; | ||
| } else { | ||
| current[key] = this.deepClone(current[key]); | ||
| } | ||
| current = current[key]; | ||
| } | ||
| current[keys[keys.length - 1]] = value; | ||
| return new BuilderClass( | ||
| newActual, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
| /** | ||
| * 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 = {}; | ||
| const hasOwn = Object.prototype.hasOwnProperty; | ||
| for (const key in obj) { | ||
| if (hasOwn.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. | ||
@@ -86,6 +213,9 @@ * This method is intended to be wrapped by concrete builder methods in subclasses for array properties. | ||
| const currentArray = (_a = this._actual[key]) != null ? _a : []; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| [key]: [...currentArray, value] | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| [key]: [...currentArray, value] | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
@@ -105,2 +235,31 @@ /** | ||
| /** | ||
| * Builds the final object with runtime validation using the requiredTemplate. | ||
| * This method validates that all fields in the requiredTemplate array are present. | ||
| * | ||
| * @returns The fully built object of type T | ||
| * @throws {Error} If any required field is missing at runtime | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * class MyBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = [ | ||
| * 'path.to.field1', | ||
| * 'path.to.field2' | ||
| * ]; | ||
| * } | ||
| * | ||
| * const obj = new MyBuilder({}) | ||
| * .setRequiredField1("value1") | ||
| * .setRequiredField2("value2") | ||
| * .buildSafe(); // Validates that both required fields are present | ||
| * ``` | ||
| */ | ||
| buildSafe() { | ||
| const missing = this.validateRequiredFields(); | ||
| if (missing.length > 0) { | ||
| throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); | ||
| } | ||
| return this._actual; | ||
| } | ||
| /** | ||
| * Builds a partial object, which may not have all required fields set. | ||
@@ -107,0 +266,0 @@ * This is useful for scenarios where you want to inspect or validate the current state before finalizing. |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/index.ts","../src/cerios-builder.ts"],"sourcesContent":["export { CeriosBrand, CeriosBuilder } from \"./cerios-builder.js\";\n","/**\n * Unique symbol used internally to brand types and track which properties have been set in the builder's type.\n *\n * @internal\n */\ndeclare const __brand: unique symbol;\n\n/**\n * Type utility for branding builder types with information about which properties have been set.\n * This is used to enforce compile-time safety for required fields in the builder pattern.\n *\n * @template T - The type representing the set of properties that have been set\n * @internal\n */\nexport type CeriosBrand<T> = { [__brand]: T };\n\n/**\n * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields.\n *\n * This class is intended to be extended by concrete builder implementations for your own types.\n * It provides utility methods for setting properties and building the final object, ensuring that all required fields are set at compile time.\n *\n * Example usage:\n * ```typescript\n * interface MyType { foo: string; bar: number[]; }\n * class MyTypeBuilder extends CeriosBuilder<MyType> {\n * setFoo(value: string) { return this.setProperty('foo', value); }\n * addBar(value: number) { return this.addToArrayProperty('bar', value); }\n * }\n * // Usage:\n * const obj = new MyTypeBuilder({})\n * .setFoo('hello')\n * .addBar(42)\n * .build();\n * ```\n *\n * @template T - The complete type being built\n */\nexport abstract class CeriosBuilder<T extends object> {\n\t/**\n\t * Creates a new builder instance. Intended to be called by subclasses.\n\t *\n\t * @param _actual - The current partial state of the object being built\n\t * @protected\n\t */\n\tprotected constructor(protected readonly _actual: Partial<T>) {}\n\n\t/**\n\t * Sets a property value and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses.\n\t *\n\t * @template K - The property key being set\n\t * @param key - The property key to set\n\t * @param value - The value to assign to the property\n\t * @returns A new builder instance with the property set and type state updated\n\t * @protected\n\t */\n\tprotected setProperty<K extends keyof T>(key: K, value: T[K]): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t[key]: value,\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets multiple property values at once and returns a new builder instance with updated type state.\n\t * @param props - An object with one or more properties to set.\n\t * @returns A new builder instance with the properties set and type state updated.\n\t * @protected\n\t */\n\tprotected setProperties<K extends keyof T>(props: Pick<T, K>): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t...props,\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Adds a value to an array property and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses for array properties.\n\t *\n\t * @template K - The property key (must be an array property)\n\t * @template V - The type of the array element\n\t * @param key - The array property key to add to\n\t * @param value - The value to add to the array\n\t * @returns A new builder instance with the array property updated and type state updated\n\t * @protected\n\t */\n\tprotected addToArrayProperty<\n\t\tK extends { [P in keyof T]: NonNullable<T[P]> extends Array<any> ? P : never }[keyof T],\n\t\tV extends T[K] extends Array<infer U> ? U : T[K] extends Array<infer U> | undefined ? U : never,\n\t>(key: K, value: V): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\tconst currentArray = (this._actual[key] as Array<V> | undefined) ?? [];\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t[key]: [...currentArray, value],\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Builds the final object. This method uses TypeScript's contextual typing to ensure\n\t * all required fields are set before allowing build() to be called.\n\t *\n\t * The type constraint checks that all required properties are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {TypeError} If called without all required fields set (compile-time error)\n\t */\n\tbuild(this: this & CeriosBrand<T>): T {\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds a partial object, which may not have all required fields set.\n\t * This is useful for scenarios where you want to inspect or validate the current state before finalizing.\n\t *\n\t * @returns The partially built object\n\t */\n\tbuildPartial(): Partial<T> {\n\t\treturn this._actual as Partial<T>;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsCO,IAAe,gBAAf,MAA+C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3C,YAA+B,SAAqB;AAArB;AAAA,EAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYrD,YAA+B,KAAQ,OAA6C;AAC7F,UAAM,eAAe,KAAK;AAC1B,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,CAAC,GAAG,GAAG;AAAA,IACR,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,cAAiC,OAAmD;AAC7F,UAAM,eAAe,KAAK;AAC1B,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACJ,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaU,mBAGR,KAAQ,OAA0C;AA7FrD;AA8FE,UAAM,eAAe,KAAK;AAC1B,UAAM,gBAAgB,UAAK,QAAQ,GAAG,MAAhB,YAA8C,CAAC;AACrE,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,CAAC,GAAG,GAAG,CAAC,GAAG,cAAc,KAAK;AAAA,IAC/B,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,QAAsC;AACrC,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,eAA2B;AAC1B,WAAO,KAAK;AAAA,EACb;AACD;","names":[]} | ||
| {"version":3,"sources":["../src/index.ts","../src/cerios-builder.ts"],"sourcesContent":["export { CeriosBrand, CeriosBuilder, RequiredFieldsTemplate } from \"./cerios-builder.js\";\n","/**\n * Unique symbol used internally to brand types and track which properties have been set in the builder's type.\n *\n * @internal\n */\ndeclare const __brand: unique symbol;\n\n/**\n * Type utility for branding builder types with information about which properties have been set.\n * This is used to enforce compile-time safety for required fields in the builder pattern.\n *\n * @template T - The type representing the set of properties that have been set\n * @internal\n */\nexport type CeriosBrand<T> = { [__brand]: T };\n\n/**\n * Helper type to represent a path through an object structure\n */\ntype PathImpl<T, K extends keyof T = keyof T> = K extends string | number\n\t? T[K] extends Record<string, any>\n\t\t? T[K] extends Array<any>\n\t\t\t? K\n\t\t\t: K | `${K}.${PathImpl<T[K]> & string}`\n\t\t: K\n\t: never;\n\nexport type Path<T> = PathImpl<T>;\n\ntype PathValue<T, P> = P extends keyof T\n\t? T[P]\n\t: P extends `${infer K}.${infer Rest}`\n\t\t? K extends keyof T\n\t\t\t? PathValue<T[K], Rest>\n\t\t\t: never\n\t\t: never;\n\n/**\n * Type-safe template for defining required fields using an array of paths.\n * Simply list the paths that are required.\n */\nexport type RequiredFieldsTemplate<T> = ReadonlyArray<Path<T>>;\n\n/**\n * Cache the root key extraction to avoid repeated computation\n * @internal\n */\ntype RootKey<P extends string> = P extends `${infer K}.${string}` ? K : P;\n\n/**\n * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields.\n *\n * This class is intended to be extended by concrete builder implementations for your own types.\n * It provides utility methods for setting properties and building the final object, ensuring that all required fields are set at compile time.\n *\n * Example usage:\n * ```typescript\n * interface MyType { foo: string; bar: number[]; }\n * class MyTypeBuilder extends CeriosBuilder<MyType> {\n * static requiredTemplate: RequiredFieldsTemplate<MyType> = ['foo'];\n * setFoo(value: string) { return this.setProperty('foo', value); }\n * addBar(value: number) { return this.addToArrayProperty('bar', value); }\n * }\n * // Usage:\n * const obj = new MyTypeBuilder({})\n * .setFoo('hello')\n * .addBar(42)\n * .buildSafe(); // Validates that 'foo' is set\n * ```\n *\n * @template T - The complete type being built\n */\nexport abstract class CeriosBuilder<T extends object> {\n\t/**\n\t * Template defining which fields are required for this builder.\n\t * Subclasses should override this to specify their required fields as an array of paths.\n\t * The template is type-safe - only valid paths from type T can be used.\n\t */\n\tstatic requiredTemplate?: ReadonlyArray<string>;\n\n\t/**\n\t * Instance-level required fields that can be populated dynamically.\n\t * This allows adding required fields at runtime via the setRequiredFields method.\n\t * @private\n\t */\n\tprivate _requiredFields: Set<string> = new Set();\n\n\t/**\n\t * Sets the required fields for this builder instance.\n\t * This allows you to dynamically define which fields are required.\n\t *\n\t * @param fields - Array of dot-notation paths to required fields\n\t * @returns The builder instance for chaining\n\t *\n\t * @example\n\t * ```typescript\n\t * const builder = new MyBuilder({})\n\t * .setRequiredFields(['path.to.field1', 'path.to.field2'])\n\t * .setField1('value1')\n\t * .setField2('value2')\n\t * .buildSafe();\n\t * ```\n\t */\n\tsetRequiredFields(fields: ReadonlyArray<Path<T>>): this {\n\t\tthis._requiredFields = new Set([...fields] as string[]);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gets the combined required fields from both the static template and instance-level fields.\n\t * @private\n\t */\n\tprivate getRequiredTemplate(): ReadonlyArray<string> {\n\t\tconst ctor = this.constructor as typeof CeriosBuilder;\n\t\tconst staticFields = ctor.requiredTemplate || [];\n\t\tconst instanceFields = Array.from(this._requiredFields);\n\n\t\t// Combine and deduplicate\n\t\treturn [...new Set([...staticFields, ...instanceFields])];\n\t}\n\n\t/**\n\t * Validates that all fields in the required template have been set.\n\t * @private\n\t */\n\tprivate validateRequiredFields(): string[] {\n\t\tconst requiredPaths = this.getRequiredTemplate();\n\t\tconst missing: string[] = [];\n\n\t\tfor (const path of requiredPaths) {\n\t\t\tconst keys = path.split(\".\");\n\t\t\tlet current: any = this._actual;\n\n\t\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\t\tconst key = keys[i];\n\t\t\t\tif (current === null || current === undefined || !(key in current)) {\n\t\t\t\t\tmissing.push(path);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcurrent = current[key];\n\t\t\t}\n\n\t\t\t// Check if the final value is null or undefined\n\t\t\tif (current === null || current === undefined) {\n\t\t\t\tif (!missing.includes(path)) {\n\t\t\t\t\tmissing.push(path);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn missing;\n\t}\n\n\t/**\n\t * Creates a new builder instance. Intended to be called by subclasses.\n\t *\n\t * @param _actual - The current partial state of the object being built\n\t * @param _requiredFields - Optional array of required field paths to preserve across instances\n\t * @protected\n\t */\n\tprotected constructor(\n\t\tprotected readonly _actual: Partial<T>,\n\t\t_requiredFields?: RequiredFieldsTemplate<T>\n\t) {\n\t\tif (_requiredFields) {\n\t\t\tthis._requiredFields = new Set([..._requiredFields] as string[]);\n\t\t}\n\t}\n\n\t/**\n\t * Sets a property value and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses.\n\t *\n\t * @template K - The property key being set\n\t * @param key - The property key to set\n\t * @param value - The value to assign to the property\n\t * @returns A new builder instance with the property set and type state updated\n\t * @protected\n\t */\n\tprotected setProperty<K extends keyof T>(key: K, value: T[K]): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t[key]: value,\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets multiple property values at once and returns a new builder instance with updated type state.\n\t * @param props - An object with one or more properties to set.\n\t * @returns A new builder instance with the properties set and type state updated.\n\t * @protected\n\t */\n\tprotected setProperties<K extends keyof T>(props: Pick<T, K>): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t...props,\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets a deeply nested property value and returns a new builder instance with updated type state.\n\t * This method uses dot notation to set nested properties in a type-safe way.\n\t *\n\t * @template P - The property path (e.g., \"parent.child.grandchild\")\n\t * @param path - The dot-notation path to the property\n\t * @param value - The value to assign to the nested property\n\t * @returns A new builder instance with the nested property set\n\t * @protected\n\t *\n\t * @example\n\t * ```typescript\n\t * builder.setNestedProperty('Order.Details.CustomerId', 'value')\n\t * ```\n\t */\n\tprotected setNestedProperty<P extends Path<T>>(\n\t\tpath: P,\n\t\tvalue: PathValue<T, P>\n\t): this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\tconst keys = (path as string).split(\".\");\n\t\tconst newActual = this.deepClone(this._actual);\n\n\t\tlet current: any = newActual;\n\t\tfor (let i = 0; i < keys.length - 1; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tif (!(key in current) || typeof current[key] !== \"object\" || current[key] === null) {\n\t\t\t\tcurrent[key] = {};\n\t\t\t} else {\n\t\t\t\tcurrent[key] = this.deepClone(current[key]);\n\t\t\t}\n\t\t\tcurrent = current[key];\n\t\t}\n\n\t\tcurrent[keys[keys.length - 1]] = value;\n\n\t\treturn new BuilderClass(\n\t\t\tnewActual,\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>>;\n\t}\n\n\t/**\n\t * Deep clone helper for nested objects\n\t * @private\n\t */\n\tprivate deepClone<V>(obj: V): V {\n\t\tif (obj === null || typeof obj !== \"object\") {\n\t\t\treturn obj;\n\t\t}\n\t\tif (Array.isArray(obj)) {\n\t\t\treturn obj.map(item => this.deepClone(item)) as any;\n\t\t}\n\t\tconst cloned: any = {};\n\t\tconst hasOwn = Object.prototype.hasOwnProperty;\n\t\tfor (const key in obj) {\n\t\t\tif (hasOwn.call(obj, key)) {\n\t\t\t\tcloned[key] = this.deepClone(obj[key]);\n\t\t\t}\n\t\t}\n\t\treturn cloned;\n\t}\n\n\t/**\n\t * Adds a value to an array property and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses for array properties.\n\t *\n\t * @template K - The property key (must be an array property)\n\t * @template V - The type of the array element\n\t * @param key - The array property key to add to\n\t * @param value - The value to add to the array\n\t * @returns A new builder instance with the array property updated and type state updated\n\t * @protected\n\t */\n\tprotected addToArrayProperty<\n\t\tK extends { [P in keyof T]: NonNullable<T[P]> extends Array<any> ? P : never }[keyof T],\n\t\tV extends T[K] extends Array<infer U> ? U : T[K] extends Array<infer U> | undefined ? U : never,\n\t>(key: K, value: V): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\tconst currentArray = (this._actual[key] as Array<V> | undefined) ?? [];\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t[key]: [...currentArray, value],\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Builds the final object. This method uses TypeScript's contextual typing to ensure\n\t * all required fields are set before allowing build() to be called.\n\t *\n\t * The type constraint checks that all required properties are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {TypeError} If called without all required fields set (compile-time error)\n\t */\n\tbuild(this: this & CeriosBrand<T>): T {\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds the final object with runtime validation using the requiredTemplate.\n\t * This method validates that all fields in the requiredTemplate array are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {Error} If any required field is missing at runtime\n\t *\n\t * @example\n\t * ```typescript\n\t * class MyBuilder extends CeriosBuilder<MyType> {\n\t * static requiredTemplate: RequiredFieldsTemplate<MyType> = [\n\t * 'path.to.field1',\n\t * 'path.to.field2'\n\t * ];\n\t * }\n\t *\n\t * const obj = new MyBuilder({})\n\t * .setRequiredField1(\"value1\")\n\t * .setRequiredField2(\"value2\")\n\t * .buildSafe(); // Validates that both required fields are present\n\t * ```\n\t */\n\tbuildSafe(): T {\n\t\tconst missing = this.validateRequiredFields();\n\n\t\tif (missing.length > 0) {\n\t\t\tthrow new Error(`Missing required fields: ${missing.join(\", \")}. Please set these fields before calling build.`);\n\t\t}\n\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds a partial object, which may not have all required fields set.\n\t * This is useful for scenarios where you want to inspect or validate the current state before finalizing.\n\t *\n\t * @returns The partially built object\n\t */\n\tbuildPartial(): Partial<T> {\n\t\treturn this._actual as Partial<T>;\n\t}\n}\n"],"mappings":"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","names":[]} |
+172
-13
@@ -7,8 +7,74 @@ // src/cerios-builder.ts | ||
| * @param _actual - The current partial state of the object being built | ||
| * @param _requiredFields - Optional array of required field paths to preserve across instances | ||
| * @protected | ||
| */ | ||
| constructor(_actual) { | ||
| constructor(_actual, _requiredFields) { | ||
| 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(); | ||
| if (_requiredFields) { | ||
| this._requiredFields = /* @__PURE__ */ new Set([..._requiredFields]); | ||
| } | ||
| } | ||
| /** | ||
| * 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; | ||
| } | ||
| /** | ||
| * Gets the combined required fields from both the static template and instance-level fields. | ||
| * @private | ||
| */ | ||
| getRequiredTemplate() { | ||
| const ctor = this.constructor; | ||
| const staticFields = ctor.requiredTemplate || []; | ||
| 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 || !(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; | ||
| } | ||
| /** | ||
| * Sets a property value and returns a new builder instance with updated type state. | ||
@@ -25,6 +91,9 @@ * This method is intended to be wrapped by concrete builder methods in subclasses. | ||
| const BuilderClass = this.constructor; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| [key]: value | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| [key]: value | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
@@ -39,8 +108,66 @@ /** | ||
| const BuilderClass = this.constructor; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| ...props | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| ...props | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
| /** | ||
| * 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]; | ||
| if (!(key in current) || typeof current[key] !== "object" || current[key] === null) { | ||
| current[key] = {}; | ||
| } else { | ||
| current[key] = this.deepClone(current[key]); | ||
| } | ||
| current = current[key]; | ||
| } | ||
| current[keys[keys.length - 1]] = value; | ||
| return new BuilderClass( | ||
| newActual, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
| /** | ||
| * 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 = {}; | ||
| const hasOwn = Object.prototype.hasOwnProperty; | ||
| for (const key in obj) { | ||
| if (hasOwn.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. | ||
@@ -60,6 +187,9 @@ * This method is intended to be wrapped by concrete builder methods in subclasses for array properties. | ||
| const currentArray = (_a = this._actual[key]) != null ? _a : []; | ||
| return new BuilderClass({ | ||
| ...this._actual, | ||
| [key]: [...currentArray, value] | ||
| }); | ||
| return new BuilderClass( | ||
| { | ||
| ...this._actual, | ||
| [key]: [...currentArray, value] | ||
| }, | ||
| Array.from(this._requiredFields) | ||
| ); | ||
| } | ||
@@ -79,2 +209,31 @@ /** | ||
| /** | ||
| * Builds the final object with runtime validation using the requiredTemplate. | ||
| * This method validates that all fields in the requiredTemplate array are present. | ||
| * | ||
| * @returns The fully built object of type T | ||
| * @throws {Error} If any required field is missing at runtime | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * class MyBuilder extends CeriosBuilder<MyType> { | ||
| * static requiredTemplate: RequiredFieldsTemplate<MyType> = [ | ||
| * 'path.to.field1', | ||
| * 'path.to.field2' | ||
| * ]; | ||
| * } | ||
| * | ||
| * const obj = new MyBuilder({}) | ||
| * .setRequiredField1("value1") | ||
| * .setRequiredField2("value2") | ||
| * .buildSafe(); // Validates that both required fields are present | ||
| * ``` | ||
| */ | ||
| buildSafe() { | ||
| const missing = this.validateRequiredFields(); | ||
| if (missing.length > 0) { | ||
| throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); | ||
| } | ||
| return this._actual; | ||
| } | ||
| /** | ||
| * Builds a partial object, which may not have all required fields set. | ||
@@ -81,0 +240,0 @@ * This is useful for scenarios where you want to inspect or validate the current state before finalizing. |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/cerios-builder.ts"],"sourcesContent":["/**\n * Unique symbol used internally to brand types and track which properties have been set in the builder's type.\n *\n * @internal\n */\ndeclare const __brand: unique symbol;\n\n/**\n * Type utility for branding builder types with information about which properties have been set.\n * This is used to enforce compile-time safety for required fields in the builder pattern.\n *\n * @template T - The type representing the set of properties that have been set\n * @internal\n */\nexport type CeriosBrand<T> = { [__brand]: T };\n\n/**\n * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields.\n *\n * This class is intended to be extended by concrete builder implementations for your own types.\n * It provides utility methods for setting properties and building the final object, ensuring that all required fields are set at compile time.\n *\n * Example usage:\n * ```typescript\n * interface MyType { foo: string; bar: number[]; }\n * class MyTypeBuilder extends CeriosBuilder<MyType> {\n * setFoo(value: string) { return this.setProperty('foo', value); }\n * addBar(value: number) { return this.addToArrayProperty('bar', value); }\n * }\n * // Usage:\n * const obj = new MyTypeBuilder({})\n * .setFoo('hello')\n * .addBar(42)\n * .build();\n * ```\n *\n * @template T - The complete type being built\n */\nexport abstract class CeriosBuilder<T extends object> {\n\t/**\n\t * Creates a new builder instance. Intended to be called by subclasses.\n\t *\n\t * @param _actual - The current partial state of the object being built\n\t * @protected\n\t */\n\tprotected constructor(protected readonly _actual: Partial<T>) {}\n\n\t/**\n\t * Sets a property value and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses.\n\t *\n\t * @template K - The property key being set\n\t * @param key - The property key to set\n\t * @param value - The value to assign to the property\n\t * @returns A new builder instance with the property set and type state updated\n\t * @protected\n\t */\n\tprotected setProperty<K extends keyof T>(key: K, value: T[K]): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t[key]: value,\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets multiple property values at once and returns a new builder instance with updated type state.\n\t * @param props - An object with one or more properties to set.\n\t * @returns A new builder instance with the properties set and type state updated.\n\t * @protected\n\t */\n\tprotected setProperties<K extends keyof T>(props: Pick<T, K>): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t...props,\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Adds a value to an array property and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses for array properties.\n\t *\n\t * @template K - The property key (must be an array property)\n\t * @template V - The type of the array element\n\t * @param key - The array property key to add to\n\t * @param value - The value to add to the array\n\t * @returns A new builder instance with the array property updated and type state updated\n\t * @protected\n\t */\n\tprotected addToArrayProperty<\n\t\tK extends { [P in keyof T]: NonNullable<T[P]> extends Array<any> ? P : never }[keyof T],\n\t\tV extends T[K] extends Array<infer U> ? U : T[K] extends Array<infer U> | undefined ? U : never,\n\t>(key: K, value: V): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any) => any;\n\t\tconst currentArray = (this._actual[key] as Array<V> | undefined) ?? [];\n\t\treturn new BuilderClass({\n\t\t\t...this._actual,\n\t\t\t[key]: [...currentArray, value],\n\t\t}) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Builds the final object. This method uses TypeScript's contextual typing to ensure\n\t * all required fields are set before allowing build() to be called.\n\t *\n\t * The type constraint checks that all required properties are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {TypeError} If called without all required fields set (compile-time error)\n\t */\n\tbuild(this: this & CeriosBrand<T>): T {\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds a partial object, which may not have all required fields set.\n\t * This is useful for scenarios where you want to inspect or validate the current state before finalizing.\n\t *\n\t * @returns The partially built object\n\t */\n\tbuildPartial(): Partial<T> {\n\t\treturn this._actual as Partial<T>;\n\t}\n}\n"],"mappings":";AAsCO,IAAe,gBAAf,MAA+C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3C,YAA+B,SAAqB;AAArB;AAAA,EAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYrD,YAA+B,KAAQ,OAA6C;AAC7F,UAAM,eAAe,KAAK;AAC1B,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,CAAC,GAAG,GAAG;AAAA,IACR,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,cAAiC,OAAmD;AAC7F,UAAM,eAAe,KAAK;AAC1B,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACJ,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaU,mBAGR,KAAQ,OAA0C;AA7FrD;AA8FE,UAAM,eAAe,KAAK;AAC1B,UAAM,gBAAgB,UAAK,QAAQ,GAAG,MAAhB,YAA8C,CAAC;AACrE,WAAO,IAAI,aAAa;AAAA,MACvB,GAAG,KAAK;AAAA,MACR,CAAC,GAAG,GAAG,CAAC,GAAG,cAAc,KAAK;AAAA,IAC/B,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,QAAsC;AACrC,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,eAA2B;AAC1B,WAAO,KAAK;AAAA,EACb;AACD;","names":[]} | ||
| {"version":3,"sources":["../src/cerios-builder.ts"],"sourcesContent":["/**\n * Unique symbol used internally to brand types and track which properties have been set in the builder's type.\n *\n * @internal\n */\ndeclare const __brand: unique symbol;\n\n/**\n * Type utility for branding builder types with information about which properties have been set.\n * This is used to enforce compile-time safety for required fields in the builder pattern.\n *\n * @template T - The type representing the set of properties that have been set\n * @internal\n */\nexport type CeriosBrand<T> = { [__brand]: T };\n\n/**\n * Helper type to represent a path through an object structure\n */\ntype PathImpl<T, K extends keyof T = keyof T> = K extends string | number\n\t? T[K] extends Record<string, any>\n\t\t? T[K] extends Array<any>\n\t\t\t? K\n\t\t\t: K | `${K}.${PathImpl<T[K]> & string}`\n\t\t: K\n\t: never;\n\nexport type Path<T> = PathImpl<T>;\n\ntype PathValue<T, P> = P extends keyof T\n\t? T[P]\n\t: P extends `${infer K}.${infer Rest}`\n\t\t? K extends keyof T\n\t\t\t? PathValue<T[K], Rest>\n\t\t\t: never\n\t\t: never;\n\n/**\n * Type-safe template for defining required fields using an array of paths.\n * Simply list the paths that are required.\n */\nexport type RequiredFieldsTemplate<T> = ReadonlyArray<Path<T>>;\n\n/**\n * Cache the root key extraction to avoid repeated computation\n * @internal\n */\ntype RootKey<P extends string> = P extends `${infer K}.${string}` ? K : P;\n\n/**\n * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields.\n *\n * This class is intended to be extended by concrete builder implementations for your own types.\n * It provides utility methods for setting properties and building the final object, ensuring that all required fields are set at compile time.\n *\n * Example usage:\n * ```typescript\n * interface MyType { foo: string; bar: number[]; }\n * class MyTypeBuilder extends CeriosBuilder<MyType> {\n * static requiredTemplate: RequiredFieldsTemplate<MyType> = ['foo'];\n * setFoo(value: string) { return this.setProperty('foo', value); }\n * addBar(value: number) { return this.addToArrayProperty('bar', value); }\n * }\n * // Usage:\n * const obj = new MyTypeBuilder({})\n * .setFoo('hello')\n * .addBar(42)\n * .buildSafe(); // Validates that 'foo' is set\n * ```\n *\n * @template T - The complete type being built\n */\nexport abstract class CeriosBuilder<T extends object> {\n\t/**\n\t * Template defining which fields are required for this builder.\n\t * Subclasses should override this to specify their required fields as an array of paths.\n\t * The template is type-safe - only valid paths from type T can be used.\n\t */\n\tstatic requiredTemplate?: ReadonlyArray<string>;\n\n\t/**\n\t * Instance-level required fields that can be populated dynamically.\n\t * This allows adding required fields at runtime via the setRequiredFields method.\n\t * @private\n\t */\n\tprivate _requiredFields: Set<string> = new Set();\n\n\t/**\n\t * Sets the required fields for this builder instance.\n\t * This allows you to dynamically define which fields are required.\n\t *\n\t * @param fields - Array of dot-notation paths to required fields\n\t * @returns The builder instance for chaining\n\t *\n\t * @example\n\t * ```typescript\n\t * const builder = new MyBuilder({})\n\t * .setRequiredFields(['path.to.field1', 'path.to.field2'])\n\t * .setField1('value1')\n\t * .setField2('value2')\n\t * .buildSafe();\n\t * ```\n\t */\n\tsetRequiredFields(fields: ReadonlyArray<Path<T>>): this {\n\t\tthis._requiredFields = new Set([...fields] as string[]);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gets the combined required fields from both the static template and instance-level fields.\n\t * @private\n\t */\n\tprivate getRequiredTemplate(): ReadonlyArray<string> {\n\t\tconst ctor = this.constructor as typeof CeriosBuilder;\n\t\tconst staticFields = ctor.requiredTemplate || [];\n\t\tconst instanceFields = Array.from(this._requiredFields);\n\n\t\t// Combine and deduplicate\n\t\treturn [...new Set([...staticFields, ...instanceFields])];\n\t}\n\n\t/**\n\t * Validates that all fields in the required template have been set.\n\t * @private\n\t */\n\tprivate validateRequiredFields(): string[] {\n\t\tconst requiredPaths = this.getRequiredTemplate();\n\t\tconst missing: string[] = [];\n\n\t\tfor (const path of requiredPaths) {\n\t\t\tconst keys = path.split(\".\");\n\t\t\tlet current: any = this._actual;\n\n\t\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\t\tconst key = keys[i];\n\t\t\t\tif (current === null || current === undefined || !(key in current)) {\n\t\t\t\t\tmissing.push(path);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcurrent = current[key];\n\t\t\t}\n\n\t\t\t// Check if the final value is null or undefined\n\t\t\tif (current === null || current === undefined) {\n\t\t\t\tif (!missing.includes(path)) {\n\t\t\t\t\tmissing.push(path);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn missing;\n\t}\n\n\t/**\n\t * Creates a new builder instance. Intended to be called by subclasses.\n\t *\n\t * @param _actual - The current partial state of the object being built\n\t * @param _requiredFields - Optional array of required field paths to preserve across instances\n\t * @protected\n\t */\n\tprotected constructor(\n\t\tprotected readonly _actual: Partial<T>,\n\t\t_requiredFields?: RequiredFieldsTemplate<T>\n\t) {\n\t\tif (_requiredFields) {\n\t\t\tthis._requiredFields = new Set([..._requiredFields] as string[]);\n\t\t}\n\t}\n\n\t/**\n\t * Sets a property value and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses.\n\t *\n\t * @template K - The property key being set\n\t * @param key - The property key to set\n\t * @param value - The value to assign to the property\n\t * @returns A new builder instance with the property set and type state updated\n\t * @protected\n\t */\n\tprotected setProperty<K extends keyof T>(key: K, value: T[K]): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t[key]: value,\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets multiple property values at once and returns a new builder instance with updated type state.\n\t * @param props - An object with one or more properties to set.\n\t * @returns A new builder instance with the properties set and type state updated.\n\t * @protected\n\t */\n\tprotected setProperties<K extends keyof T>(props: Pick<T, K>): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t...props,\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Sets a deeply nested property value and returns a new builder instance with updated type state.\n\t * This method uses dot notation to set nested properties in a type-safe way.\n\t *\n\t * @template P - The property path (e.g., \"parent.child.grandchild\")\n\t * @param path - The dot-notation path to the property\n\t * @param value - The value to assign to the nested property\n\t * @returns A new builder instance with the nested property set\n\t * @protected\n\t *\n\t * @example\n\t * ```typescript\n\t * builder.setNestedProperty('Order.Details.CustomerId', 'value')\n\t * ```\n\t */\n\tprotected setNestedProperty<P extends Path<T>>(\n\t\tpath: P,\n\t\tvalue: PathValue<T, P>\n\t): this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\tconst keys = (path as string).split(\".\");\n\t\tconst newActual = this.deepClone(this._actual);\n\n\t\tlet current: any = newActual;\n\t\tfor (let i = 0; i < keys.length - 1; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tif (!(key in current) || typeof current[key] !== \"object\" || current[key] === null) {\n\t\t\t\tcurrent[key] = {};\n\t\t\t} else {\n\t\t\t\tcurrent[key] = this.deepClone(current[key]);\n\t\t\t}\n\t\t\tcurrent = current[key];\n\t\t}\n\n\t\tcurrent[keys[keys.length - 1]] = value;\n\n\t\treturn new BuilderClass(\n\t\t\tnewActual,\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, Extract<RootKey<P & string>, keyof T>>>;\n\t}\n\n\t/**\n\t * Deep clone helper for nested objects\n\t * @private\n\t */\n\tprivate deepClone<V>(obj: V): V {\n\t\tif (obj === null || typeof obj !== \"object\") {\n\t\t\treturn obj;\n\t\t}\n\t\tif (Array.isArray(obj)) {\n\t\t\treturn obj.map(item => this.deepClone(item)) as any;\n\t\t}\n\t\tconst cloned: any = {};\n\t\tconst hasOwn = Object.prototype.hasOwnProperty;\n\t\tfor (const key in obj) {\n\t\t\tif (hasOwn.call(obj, key)) {\n\t\t\t\tcloned[key] = this.deepClone(obj[key]);\n\t\t\t}\n\t\t}\n\t\treturn cloned;\n\t}\n\n\t/**\n\t * Adds a value to an array property and returns a new builder instance with updated type state.\n\t * This method is intended to be wrapped by concrete builder methods in subclasses for array properties.\n\t *\n\t * @template K - The property key (must be an array property)\n\t * @template V - The type of the array element\n\t * @param key - The array property key to add to\n\t * @param value - The value to add to the array\n\t * @returns A new builder instance with the array property updated and type state updated\n\t * @protected\n\t */\n\tprotected addToArrayProperty<\n\t\tK extends { [P in keyof T]: NonNullable<T[P]> extends Array<any> ? P : never }[keyof T],\n\t\tV extends T[K] extends Array<infer U> ? U : T[K] extends Array<infer U> | undefined ? U : never,\n\t>(key: K, value: V): this & CeriosBrand<Pick<T, K>> {\n\t\tconst BuilderClass = this.constructor as new (data: any, requiredFields?: RequiredFieldsTemplate<T>) => any;\n\t\tconst currentArray = (this._actual[key] as Array<V> | undefined) ?? [];\n\t\treturn new BuilderClass(\n\t\t\t{\n\t\t\t\t...this._actual,\n\t\t\t\t[key]: [...currentArray, value],\n\t\t\t},\n\t\t\tArray.from(this._requiredFields) as unknown as RequiredFieldsTemplate<T>\n\t\t) as this & CeriosBrand<Pick<T, K>>;\n\t}\n\n\t/**\n\t * Builds the final object. This method uses TypeScript's contextual typing to ensure\n\t * all required fields are set before allowing build() to be called.\n\t *\n\t * The type constraint checks that all required properties are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {TypeError} If called without all required fields set (compile-time error)\n\t */\n\tbuild(this: this & CeriosBrand<T>): T {\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds the final object with runtime validation using the requiredTemplate.\n\t * This method validates that all fields in the requiredTemplate array are present.\n\t *\n\t * @returns The fully built object of type T\n\t * @throws {Error} If any required field is missing at runtime\n\t *\n\t * @example\n\t * ```typescript\n\t * class MyBuilder extends CeriosBuilder<MyType> {\n\t * static requiredTemplate: RequiredFieldsTemplate<MyType> = [\n\t * 'path.to.field1',\n\t * 'path.to.field2'\n\t * ];\n\t * }\n\t *\n\t * const obj = new MyBuilder({})\n\t * .setRequiredField1(\"value1\")\n\t * .setRequiredField2(\"value2\")\n\t * .buildSafe(); // Validates that both required fields are present\n\t * ```\n\t */\n\tbuildSafe(): T {\n\t\tconst missing = this.validateRequiredFields();\n\n\t\tif (missing.length > 0) {\n\t\t\tthrow new Error(`Missing required fields: ${missing.join(\", \")}. Please set these fields before calling build.`);\n\t\t}\n\n\t\treturn this._actual as T;\n\t}\n\n\t/**\n\t * Builds a partial object, which may not have all required fields set.\n\t * This is useful for scenarios where you want to inspect or validate the current state before finalizing.\n\t *\n\t * @returns The partially built object\n\t */\n\tbuildPartial(): Partial<T> {\n\t\treturn this._actual as Partial<T>;\n\t}\n}\n"],"mappings":"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","names":[]} |
+1
-1
| { | ||
| "name": "@cerios/cerios-builder", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "author": "Ronald Veth - Cerios", | ||
@@ -5,0 +5,0 @@ "description": "A TypeScript builder pattern library providing compile-time type safety for object construction with method chaining and required field validation", |
+819
-19
@@ -8,5 +8,8 @@ # @cerios/cerios-builder | ||
| - **Type-Safe Building**: Compile-time validation ensures all required properties are set | ||
| - **Nested Properties Support**: Build deeply nested objects with dot-notation paths | ||
| - **Runtime Validation**: Optional runtime validation with `buildSafe()` and required field templates | ||
| - **Dynamic Required Fields**: Add required fields at runtime based on your business logic | ||
| - **Method Chaining**: Fluent API for readable object construction | ||
| - **Partial Building**: Build incomplete objects when needed | ||
| - **Zero Runtime Overhead**: All type checking happens at compile time | ||
| - **Zero Runtime Overhead**: All type checking happens at compile time (unless using `buildSafe()`) | ||
| - **Extensible**: Easy to create custom builder methods | ||
@@ -27,2 +30,61 @@ - **TypeScript First**: Built with TypeScript, for TypeScript | ||
| ## 🎯 Quick Start | ||
| ```typescript | ||
| import { CeriosBuilder, RequiredFieldsTemplate } from '@cerios/cerios-builder'; | ||
| // 1. Define your types | ||
| type User = { | ||
| id: string; | ||
| name: string; | ||
| email: string; | ||
| age?: number; | ||
| }; | ||
| // 2. Create your builder | ||
| class UserBuilder extends CeriosBuilder<User> { | ||
| static create() { | ||
| return new UserBuilder({}); | ||
| } | ||
| id(value: string) { | ||
| return this.setProperty('id', value); | ||
| } | ||
| name(value: string) { | ||
| return this.setProperty('name', value); | ||
| } | ||
| email(value: string) { | ||
| return this.setProperty('email', value); | ||
| } | ||
| age(value: number) { | ||
| return this.setProperty('age', value); | ||
| } | ||
| } | ||
| // 3. Build your object | ||
| const user = UserBuilder.create() | ||
| .id('123') | ||
| .name('John Doe') | ||
| .email('john@example.com') | ||
| .age(30) | ||
| .build(); | ||
| ``` | ||
| ## 📖 Feature Overview | ||
| | Feature | Method | Use Case | | ||
| |---------|--------|----------| | ||
| | **Simple Properties** | `setProperty()` | Set flat object properties | | ||
| | **Nested Properties** | `setNestedProperty()` | Set deeply nested properties with dot notation | | ||
| | **Array Properties** | `addToArrayProperty()` | Add values to array properties | | ||
| | **Multiple Properties** | `setProperties()` | Set multiple properties at once | | ||
| | **Required Template** | `static requiredTemplate` | Define required fields for runtime validation | | ||
| | **Dynamic Requirements** | `setRequiredFields()` | Add required fields at runtime | | ||
| | **Compile-Time Build** | `build()` | Build with TypeScript type checking | | ||
| | **Runtime Validation** | `buildSafe()` | Build with runtime validation | | ||
| | **Partial Build** | `buildPartial()` | Build incomplete objects | | ||
| ## 🔧 Basic Usage | ||
@@ -126,2 +188,571 @@ | ||
| ### Deeply Nested Properties | ||
| Build complex nested structures with type-safe dot-notation paths: | ||
| ```typescript | ||
| import { CeriosBuilder, RequiredFieldsTemplate } from '@cerios/cerios-builder'; | ||
| type Address = { | ||
| Street: string; | ||
| City: string; | ||
| PostalCode?: string; | ||
| Country: string; | ||
| }; | ||
| type OrderDetails = { | ||
| OrderNumber?: string; | ||
| CustomerId: string; | ||
| TotalAmount: number; | ||
| Status: string; | ||
| ShippingAddress: Address; | ||
| }; | ||
| type Order = { | ||
| Details: OrderDetails; | ||
| }; | ||
| type OrderRequest = { | ||
| Order: Order; | ||
| }; | ||
| class OrderRequestBuilder extends CeriosBuilder<OrderRequest> { | ||
| static create() { | ||
| return new OrderRequestBuilder({}, [ | ||
| 'Order.Details.CustomerId', | ||
| 'Order.Details.TotalAmount', | ||
| 'Order.Details.Status', | ||
| 'Order.Details.ShippingAddress.Street', | ||
| 'Order.Details.ShippingAddress.City', | ||
| 'Order.Details.ShippingAddress.Country', | ||
| ]); | ||
| } | ||
| static createWithDefaults() { | ||
| return this.create().status('pending'); | ||
| } | ||
| orderNumber(value: string) { | ||
| return this.setNestedProperty('Order.Details.OrderNumber', value); | ||
| } | ||
| customerId(value: string) { | ||
| return this.setNestedProperty('Order.Details.CustomerId', value); | ||
| } | ||
| totalAmount(value: number) { | ||
| return this.setNestedProperty('Order.Details.TotalAmount', value); | ||
| } | ||
| status(value: string) { | ||
| return this.setNestedProperty('Order.Details.Status', value); | ||
| } | ||
| shippingStreet(value: string) { | ||
| return this.setNestedProperty('Order.Details.ShippingAddress.Street', value); | ||
| } | ||
| shippingCity(value: string) { | ||
| return this.setNestedProperty('Order.Details.ShippingAddress.City', value); | ||
| } | ||
| shippingPostalCode(value: string) { | ||
| return this.setNestedProperty('Order.Details.ShippingAddress.PostalCode', value); | ||
| } | ||
| shippingCountry(value: string) { | ||
| return this.setNestedProperty('Order.Details.ShippingAddress.Country', value); | ||
| } | ||
| } | ||
| // Usage with runtime validation | ||
| const order = OrderRequestBuilder.createWithDefaults() | ||
| .customerId('CUST-001') | ||
| .totalAmount(299.99) | ||
| .orderNumber('ORD-12345') | ||
| .shippingStreet('123 Main St') | ||
| .shippingCity('New York') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ Validates all required fields are set | ||
| // ❌ This will throw an error at runtime | ||
| try { | ||
| const invalidOrder = OrderRequestBuilder.createWithDefaults() | ||
| .customerId('CUST-002') | ||
| .buildSafe(); // Missing TotalAmount and shipping address fields | ||
| } catch (error) { | ||
| console.error(error.message); | ||
| // "Missing required fields: Order.Details.TotalAmount, Order.Details.ShippingAddress.Street, ..." | ||
| } | ||
| ``` | ||
| ### Runtime Validation with Required Templates | ||
| Define which fields are required and validate them at runtime: | ||
| ```typescript | ||
| class ProductBuilder extends CeriosBuilder<Product> { | ||
| static create() { | ||
| return new ProductBuilder({}, [ | ||
| 'id', | ||
| 'name', | ||
| 'price', | ||
| ]); | ||
| } | ||
| id(value: string) { | ||
| return this.setProperty('id', value); | ||
| } | ||
| name(value: string) { | ||
| return this.setProperty('name', value); | ||
| } | ||
| price(value: number) { | ||
| return this.setProperty('price', value); | ||
| } | ||
| description(value: string) { | ||
| return this.setProperty('description', value); | ||
| } | ||
| } | ||
| // Use buildSafe() for runtime validation | ||
| const product = ProductBuilder.create() | ||
| .id('PROD-001') | ||
| .name('Laptop') | ||
| .price(999.99) | ||
| .buildSafe(); // ✅ All required fields are set | ||
| // This will throw an error | ||
| const invalidProduct = ProductBuilder.create() | ||
| .id('PROD-002') | ||
| .name('Mouse') | ||
| .buildSafe(); // ❌ Error: Missing required fields: price | ||
| ``` | ||
| ### Dynamic Required Fields | ||
| Add required fields at runtime based on business logic: | ||
| ```typescript | ||
| class EmployeeBuilder extends CeriosBuilder<Employee> { | ||
| static create() { | ||
| return new EmployeeBuilder({}, [ | ||
| 'firstName', | ||
| 'lastName', | ||
| 'email', | ||
| ]); | ||
| } | ||
| firstName(value: string) { | ||
| return this.setProperty('firstName', value); | ||
| } | ||
| lastName(value: string) { | ||
| return this.setProperty('lastName', value); | ||
| } | ||
| email(value: string) { | ||
| return this.setProperty('email', value); | ||
| } | ||
| employeeId(value: string) { | ||
| return this.setProperty('employeeId', value); | ||
| } | ||
| phone(value: string) { | ||
| return this.setProperty('phone', value); | ||
| } | ||
| } | ||
| // Scenario 1: New employee (no ID required) | ||
| const newEmployee = EmployeeBuilder.create() | ||
| .firstName('John') | ||
| .lastName('Doe') | ||
| .email('john.doe@company.com') | ||
| .buildSafe(); // ✅ Only validates firstName, lastName, email | ||
| // Scenario 2: Existing employee (ID required) | ||
| const existingEmployee = EmployeeBuilder.create() | ||
| .setRequiredFields(['employeeId']) // Add employeeId as required dynamically | ||
| .firstName('Jane') | ||
| .lastName('Smith') | ||
| .email('jane.smith@company.com') | ||
| .employeeId('EMP-12345') // Must provide employeeId | ||
| .buildSafe(); // ✅ Validates firstName, lastName, email, AND employeeId | ||
| // Scenario 3: Employee with contact requirement | ||
| const contactEmployee = EmployeeBuilder.create() | ||
| .setRequiredFields(['phone', 'employeeId']) // Add multiple dynamic fields | ||
| .firstName('Bob') | ||
| .lastName('Johnson') | ||
| .email('bob.johnson@company.com') | ||
| .phone('+1-555-0123') | ||
| .employeeId('EMP-67890') | ||
| .buildSafe(); // ✅ Validates all fields including phone and employeeId | ||
| ``` | ||
| ### Four Ways to Set Up Required Fields for Deeply Nested Properties | ||
| When working with deeply nested structures, you have **four approaches** to configure required field validation: | ||
| #### 1. Static Template in Constructor (Recommended for Fixed Requirements) | ||
| Define required fields once at the class level and pass them through the constructor. Best for when required fields never change. | ||
| ```typescript | ||
| type Address = { | ||
| Street: string; | ||
| City: string; | ||
| Country: string; | ||
| }; | ||
| type OrderDetails = { | ||
| CustomerId: string; | ||
| TotalAmount: number; | ||
| ShippingAddress: Address; | ||
| }; | ||
| type Order = { | ||
| Details: OrderDetails; | ||
| }; | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| // Method 1: Static template defined at class level | ||
| static requiredTemplate: RequiredFieldsTemplate<Order> = [ | ||
| 'Details.CustomerId', | ||
| 'Details.TotalAmount', | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| ]; | ||
| static create() { | ||
| // Pass template through constructor | ||
| return new OrderBuilder({}); | ||
| } | ||
| customerId(value: string) { | ||
| return this.setNestedProperty('Details.CustomerId', value); | ||
| } | ||
| totalAmount(value: number) { | ||
| return this.setNestedProperty('Details.TotalAmount', value); | ||
| } | ||
| shippingStreet(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Street', value); | ||
| } | ||
| shippingCity(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.City', value); | ||
| } | ||
| shippingCountry(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Country', value); | ||
| } | ||
| } | ||
| // Usage: All required fields validated by template | ||
| const order = OrderBuilder.create() | ||
| .customerId('CUST-001') | ||
| .totalAmount(299.99) | ||
| .shippingStreet('123 Main St') | ||
| .shippingCity('New York') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ Validates all 5 required nested fields | ||
| ``` | ||
| #### 2. Inline Constructor Template (Simple and Direct) | ||
| Pass the required template directly in the constructor without defining a static property. Best for simple cases or when you want to keep everything in one place. | ||
| ```typescript | ||
| type Address = { | ||
| Street: string; | ||
| City: string; | ||
| Country: string; | ||
| PostalCode?: string; | ||
| }; | ||
| type OrderDetails = { | ||
| CustomerId: string; | ||
| TotalAmount: number; | ||
| ShippingAddress: Address; | ||
| }; | ||
| type Order = { | ||
| Details: OrderDetails; | ||
| }; | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| // Method 2: Pass template directly in constructor (no static property needed) | ||
| static create() { | ||
| return new OrderBuilder({}, [ | ||
| 'Details.CustomerId', | ||
| 'Details.TotalAmount', | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| ]); | ||
| } | ||
| customerId(value: string) { | ||
| return this.setNestedProperty('Details.CustomerId', value); | ||
| } | ||
| totalAmount(value: number) { | ||
| return this.setNestedProperty('Details.TotalAmount', value); | ||
| } | ||
| shippingStreet(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Street', value); | ||
| } | ||
| shippingCity(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.City', value); | ||
| } | ||
| shippingCountry(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Country', value); | ||
| } | ||
| shippingPostalCode(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.PostalCode', value); | ||
| } | ||
| } | ||
| // Usage: Same validation as Method 1, but simpler setup | ||
| const order = OrderBuilder.create() | ||
| .customerId('CUST-001') | ||
| .totalAmount(299.99) | ||
| .shippingStreet('123 Main St') | ||
| .shippingCity('New York') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ Validates all required nested fields | ||
| // ❌ Still validates properly | ||
| try { | ||
| const invalidOrder = OrderBuilder.create() | ||
| .customerId('CUST-002') | ||
| .buildSafe(); | ||
| } catch (error) { | ||
| console.error(error.message); | ||
| // "Missing required fields: Details.TotalAmount, Details.ShippingAddress.Street, ..." | ||
| } | ||
| ``` | ||
| #### 3. Dynamic Required Fields via `setRequiredFields()` (Best for Conditional Logic) | ||
| Add required fields at runtime based on business logic. Combines static template with dynamic additions. | ||
| ```typescript | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| static requiredTemplate: RequiredFieldsTemplate<Order> = [ | ||
| 'Details.CustomerId', | ||
| 'Details.TotalAmount', | ||
| ]; | ||
| static create() { | ||
| return new OrderBuilder({}); | ||
| } | ||
| // Method 3: Add required fields dynamically for specific scenarios | ||
| static createInternational() { | ||
| return this.create() | ||
| .setRequiredFields([ | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| 'Details.ShippingAddress.PostalCode', // Extra requirement for international | ||
| ]); | ||
| } | ||
| static createDomestic() { | ||
| return this.create() | ||
| .setRequiredFields([ | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| ]); // No postal code required | ||
| } | ||
| customerId(value: string) { | ||
| return this.setNestedProperty('Details.CustomerId', value); | ||
| } | ||
| totalAmount(value: number) { | ||
| return this.setNestedProperty('Details.TotalAmount', value); | ||
| } | ||
| shippingStreet(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Street', value); | ||
| } | ||
| shippingCity(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.City', value); | ||
| } | ||
| shippingCountry(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Country', value); | ||
| } | ||
| shippingPostalCode(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.PostalCode', value); | ||
| } | ||
| } | ||
| // Domestic order: postal code is optional | ||
| const domesticOrder = OrderBuilder.createDomestic() | ||
| .customerId('CUST-001') | ||
| .totalAmount(99.99) | ||
| .shippingStreet('123 Main St') | ||
| .shippingCity('New York') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ Valid without postal code | ||
| // International order: postal code is required | ||
| const internationalOrder = OrderBuilder.createInternational() | ||
| .customerId('CUST-002') | ||
| .totalAmount(299.99) | ||
| .shippingStreet('10 Downing Street') | ||
| .shippingCity('London') | ||
| .shippingCountry('UK') | ||
| .shippingPostalCode('SW1A 2AA') // Must provide postal code | ||
| .buildSafe(); // ✅ All fields including postal code validated | ||
| // ❌ This will fail - missing postal code for international | ||
| try { | ||
| const invalidOrder = OrderBuilder.createInternational() | ||
| .customerId('CUST-003') | ||
| .totalAmount(150.00) | ||
| .shippingStreet('Rue de Rivoli') | ||
| .shippingCity('Paris') | ||
| .shippingCountry('France') | ||
| .buildSafe(); // Missing PostalCode | ||
| } catch (error) { | ||
| console.error(error.message); | ||
| // "Missing required fields: Details.ShippingAddress.PostalCode" | ||
| } | ||
| ``` | ||
| #### 4. Inline Dynamic Fields (Best for One-Off Requirements) | ||
| Add required fields inline during the building process. Useful for special cases. | ||
| ```typescript | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| static create() { | ||
| return new OrderBuilder({}, ['Details.CustomerId']); | ||
| } | ||
| customerId(value: string) { | ||
| return this.setNestedProperty('Details.CustomerId', value); | ||
| } | ||
| totalAmount(value: number) { | ||
| return this.setNestedProperty('Details.TotalAmount', value); | ||
| } | ||
| shippingStreet(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Street', value); | ||
| } | ||
| shippingCity(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.City', value); | ||
| } | ||
| shippingCountry(value: string) { | ||
| return this.setNestedProperty('Details.ShippingAddress.Country', value); | ||
| } | ||
| } | ||
| // Method 4: Inline requirement based on runtime data | ||
| function createOrder(isPaidOrder: boolean, requiresShipping: boolean) { | ||
| let builder = OrderBuilder.create() | ||
| .customerId('CUST-001'); | ||
| // Add requirements inline based on conditions | ||
| if (isPaidOrder) { | ||
| builder = builder.setRequiredFields(['Details.TotalAmount']); | ||
| } | ||
| if (requiresShipping) { | ||
| builder = builder.setRequiredFields([ | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| ]); | ||
| } | ||
| return builder; | ||
| } | ||
| // Free order without shipping (digital product) | ||
| const digitalOrder = createOrder(false, false) | ||
| .buildSafe(); // ✅ Only CustomerId required | ||
| // Paid order without shipping (in-store pickup) | ||
| const pickupOrder = createOrder(true, false) | ||
| .totalAmount(49.99) | ||
| .buildSafe(); // ✅ CustomerId + TotalAmount required | ||
| // Paid order with shipping | ||
| const shippedOrder = createOrder(true, true) | ||
| .totalAmount(99.99) | ||
| .shippingStreet('456 Oak Ave') | ||
| .shippingCity('Boston') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ All fields validated | ||
| ``` | ||
| #### Choosing the Right Approach | ||
| | Approach | When to Use | Pros | Cons | | ||
| |----------|-------------|------|------| | ||
| | **Static Template** | Requirements never change, need reusability | Simple, clear, type-safe, reusable | Inflexible, extra line of code | | ||
| | **Inline Constructor** | Simple cases, one factory method | Minimal code, clear, type-safe | Template not reusable elsewhere | | ||
| | **Dynamic via Factory** | Multiple predefined scenarios | Reusable, organized, type-safe | More boilerplate | | ||
| | **Inline Dynamic** | Runtime-dependent logic | Maximum flexibility | Less discoverable | | ||
| **Best Practice**: Combine approaches! Use a static template for core required fields, then add dynamic requirements for conditional scenarios: | ||
| ```typescript | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| // Core fields always required | ||
| static requiredTemplate: RequiredFieldsTemplate<Order> = [ | ||
| 'Details.CustomerId', | ||
| ]; | ||
| static create() { | ||
| return new OrderBuilder({}); | ||
| } | ||
| // Scenario-specific factory methods | ||
| static createPaid() { | ||
| return this.create().setRequiredFields(['Details.TotalAmount']); | ||
| } | ||
| static createShippable() { | ||
| return this.createPaid().setRequiredFields([ | ||
| 'Details.ShippingAddress.Street', | ||
| 'Details.ShippingAddress.City', | ||
| 'Details.ShippingAddress.Country', | ||
| ]); | ||
| } | ||
| // ... builder methods | ||
| } | ||
| // Usage is clear and type-safe | ||
| const order = OrderBuilder.createShippable() | ||
| .customerId('CUST-001') | ||
| .totalAmount(199.99) | ||
| .shippingStreet('789 Maple Dr') | ||
| .shippingCity('Seattle') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ All required fields validated | ||
| ``` | ||
| ### Building Test Data | ||
@@ -361,11 +992,18 @@ | ||
| // ❌ No compile-time safety | ||
| const user = { | ||
| id: "123", | ||
| name: "John", | ||
| // Oops! Forgot required email field | ||
| roles: ["admin"] | ||
| const order = { | ||
| Order: { | ||
| Details: { | ||
| CustomerId: "CUST-001", | ||
| TotalAmount: 299.99, | ||
| // Oops! Forgot required Status field | ||
| ShippingAddress: { | ||
| Street: "123 Main St", | ||
| // Oops! Forgot required City and Country | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // ❌ Runtime error waiting to happen | ||
| userService.createUser(user); | ||
| orderService.createOrder(order); | ||
| ``` | ||
@@ -375,14 +1013,31 @@ | ||
| ```typescript | ||
| // ✅ Compile-time safety | ||
| const user = UserBuilder.create() | ||
| .id("123") | ||
| .name("John") | ||
| .email("john@example.com") // Required - won't compile without it | ||
| .addRole("admin") | ||
| .build(); | ||
| // ✅ Type-safe nested property building | ||
| const order = OrderRequestBuilder.createWithDefaults() | ||
| .customerId('CUST-001') | ||
| .totalAmount(299.99) | ||
| .shippingStreet('123 Main St') | ||
| .shippingCity('New York') | ||
| .shippingCountry('USA') | ||
| .buildSafe(); // ✅ Runtime validation ensures all required fields are set | ||
| // ✅ Guaranteed to have all required fields | ||
| userService.createUser(user); | ||
| // ✅ Clear error messages | ||
| try { | ||
| const invalid = OrderRequestBuilder.createWithDefaults() | ||
| .customerId('CUST-002') | ||
| .buildSafe(); | ||
| } catch (error) { | ||
| console.error(error.message); | ||
| // "Missing required fields: Order.Details.TotalAmount, | ||
| // Order.Details.ShippingAddress.Street, ..." | ||
| } | ||
| ``` | ||
| ### Benefits | ||
| - **Type-Safe Paths**: Dot notation with full IntelliSense support | ||
| - **Runtime Validation**: Optional runtime checks with clear error messages | ||
| - **Flexible Requirements**: Static templates + dynamic required fields | ||
| - **Deep Nesting**: Handle complex nested structures easily | ||
| - **Developer Experience**: Better autocomplete and error messages | ||
| ## 📚 API Reference | ||
@@ -394,9 +1049,51 @@ | ||
| #### Methods | ||
| #### Static Properties | ||
| - `requiredTemplate?: RequiredFieldsTemplate<T>` - Optional array of required field paths for runtime validation | ||
| #### Instance Methods | ||
| - `setProperty<K>(key: K, value: T[K])` - Sets a property and returns a new builder instance | ||
| - `setProperties<K>(props: Pick<T, K>)` - Sets multiple properties at once and returns a new builder instance | ||
| - `setNestedProperty<P>(path: P, value: PathValue<T, P>)` - Sets a deeply nested property using dot notation | ||
| - `addToArrayProperty<K, V>(key: K, value: V)` - Adds a value to an array property and returns a new builder instance | ||
| - `build()` - Builds the final object (only available when all required properties are set) | ||
| - `setRequiredFields(fields: ReadonlyArray<Path<T>>)` - Sets required fields dynamically for this instance | ||
| - `build()` - Builds the final object (only available when all required properties are set via type system) | ||
| - `buildSafe()` - Builds the final object with runtime validation of required fields from template | ||
| - `buildPartial()` - Builds a partial object with currently set properties | ||
| ### RequiredFieldsTemplate<T> | ||
| Type-safe array of paths for defining required fields. | ||
| ```typescript | ||
| type RequiredFieldsTemplate<T> = ReadonlyArray<Path<T>>; | ||
| ``` | ||
| Example: | ||
| ```typescript | ||
| static requiredTemplate: RequiredFieldsTemplate<OrderRequest> = [ | ||
| 'Order.Details.CustomerId', | ||
| 'Order.Details.TotalAmount', | ||
| 'Order.Details.ShippingAddress.Street', | ||
| ]; | ||
| ``` | ||
| ### Path<T> | ||
| Type utility that generates all valid dot-notation paths for a type. | ||
| ```typescript | ||
| // For a type like: | ||
| type User = { | ||
| profile: { | ||
| name: string; | ||
| email: string; | ||
| }; | ||
| }; | ||
| // Path<User> includes: | ||
| // 'profile' | 'profile.name' | 'profile.email' | ||
| ``` | ||
| ### CeriosBrand<T> | ||
@@ -406,4 +1103,107 @@ | ||
| ## 📄 License | ||
| ## � Build Methods Explained | ||
| ### `build()` - Compile-Time Safety | ||
| Use `build()` when you want **compile-time type safety**. TypeScript will enforce that all required properties are set before allowing the build. | ||
| ```typescript | ||
| type User = { | ||
| id: string; | ||
| name: string; | ||
| email: string; | ||
| }; | ||
| const user = UserBuilder.create() | ||
| .id('123') | ||
| .name('John') | ||
| .email('john@example.com') | ||
| .build(); // ✅ Compiles - all required fields set | ||
| const invalid = UserBuilder.create() | ||
| .id('123') | ||
| .name('John') | ||
| .build(); // ❌ TypeScript error - missing email | ||
| ``` | ||
| ### `buildSafe()` - Runtime Validation | ||
| Use `buildSafe()` when you want **runtime validation** using the `requiredTemplate`. This is useful when: | ||
| - Building from user input or external data | ||
| - You want to validate deeply nested required fields | ||
| - You need dynamic required fields based on business logic | ||
| ```typescript | ||
| class OrderBuilder extends CeriosBuilder<Order> { | ||
| static requiredTemplate: RequiredFieldsTemplate<Order> = [ | ||
| 'Details.CustomerId', | ||
| 'Details.TotalAmount', | ||
| ]; | ||
| static create() { | ||
| return new OrderBuilder({}); | ||
| } | ||
| // ... methods | ||
| } | ||
| const order = OrderBuilder.create() | ||
| .customerId('CUST-001') | ||
| .totalAmount(100) | ||
| .buildSafe(); // ✅ Runtime validation passes | ||
| const invalid = OrderBuilder.create() | ||
| .customerId('CUST-002') | ||
| .buildSafe(); // ❌ Throws: "Missing required fields: Details.TotalAmount" | ||
| ``` | ||
| ### `buildPartial()` - No Validation | ||
| Use `buildPartial()` when you need to build an incomplete object: | ||
| ```typescript | ||
| const partial = UserBuilder.create() | ||
| .name('Incomplete User') | ||
| .buildPartial(); // Returns Partial<User> | ||
| // Useful for: | ||
| // - Progressive form filling | ||
| // - Template objects | ||
| // - Partial updates | ||
| ``` | ||
| ## 💡 Best Practices | ||
| 1. **Use `setNestedProperty()` for deep structures**: Instead of building nested objects separately, use dot notation for better type safety and cleaner code. | ||
| 2. **Define `requiredTemplate` for runtime validation**: When working with external data or complex validation rules, define a required template and use `buildSafe()`. | ||
| 3. **Use `setRequiredFields()` for conditional requirements**: When requirements change based on context (e.g., new vs existing records), use dynamic required fields. | ||
| 4. **Create factory methods**: Use static `create()` and `createWithDefaults()` methods for better ergonomics: | ||
| ```typescript | ||
| static createWithDefaults() { | ||
| return this.create() | ||
| .status('pending') | ||
| .createdAt(new Date()); | ||
| } | ||
| ``` | ||
| 5. **Combine with validation libraries**: Use `buildSafe()` with libraries like Zod for comprehensive validation: | ||
| ```typescript | ||
| const data = builder.buildSafe(); | ||
| const validated = orderSchema.parse(data); // Zod validation | ||
| ``` | ||
| 6. **Keep builders focused**: One builder per entity type - don't try to handle multiple unrelated types in one builder. | ||
| 7. **Use type-safe paths**: The `RequiredFieldsTemplate` type ensures you can only specify valid paths: | ||
| ```typescript | ||
| static requiredTemplate: RequiredFieldsTemplate<Order> = [ | ||
| 'Details.CustomerId', // ✅ Valid path | ||
| 'Details.InvalidField', // ❌ TypeScript error | ||
| ]; | ||
| ``` | ||
| ## �📄 License | ||
| MIT © [Cerios](LICENSE) | ||
@@ -410,0 +1210,0 @@ |
102357
152.31%714
142.03%1208
196.08%