@cleverbrush/schema 1.1.11 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +1314 -101
  2. package/dist/builders/AnySchemaBuilder.d.ts +54 -14
  3. package/dist/builders/AnySchemaBuilder.js +2 -112
  4. package/dist/builders/AnySchemaBuilder.js.map +1 -0
  5. package/dist/builders/ArraySchemaBuilder.d.ts +124 -23
  6. package/dist/builders/ArraySchemaBuilder.js +2 -284
  7. package/dist/builders/ArraySchemaBuilder.js.map +1 -0
  8. package/dist/builders/BooleanSchemaBuilder.d.ts +94 -19
  9. package/dist/builders/BooleanSchemaBuilder.js +2 -150
  10. package/dist/builders/BooleanSchemaBuilder.js.map +1 -0
  11. package/dist/builders/DateSchemaBuilder.d.ts +134 -34
  12. package/dist/builders/DateSchemaBuilder.js +2 -433
  13. package/dist/builders/DateSchemaBuilder.js.map +1 -0
  14. package/dist/builders/ExternSchemaBuilder.d.ts +200 -0
  15. package/dist/builders/ExternSchemaBuilder.js +2 -0
  16. package/dist/builders/ExternSchemaBuilder.js.map +1 -0
  17. package/dist/builders/FunctionSchemaBuilder.d.ts +57 -14
  18. package/dist/builders/FunctionSchemaBuilder.js +2 -113
  19. package/dist/builders/FunctionSchemaBuilder.js.map +1 -0
  20. package/dist/builders/LazySchemaBuilder.d.ts +167 -0
  21. package/dist/builders/NullSchemaBuilder.d.ts +162 -0
  22. package/dist/builders/NumberSchemaBuilder.d.ts +143 -31
  23. package/dist/builders/NumberSchemaBuilder.js +2 -386
  24. package/dist/builders/NumberSchemaBuilder.js.map +1 -0
  25. package/dist/builders/ObjectSchemaBuilder.d.ts +266 -61
  26. package/dist/builders/ObjectSchemaBuilder.js +2 -589
  27. package/dist/builders/ObjectSchemaBuilder.js.map +1 -0
  28. package/dist/builders/PropertyValidationResult.d.ts +68 -0
  29. package/dist/builders/RecordSchemaBuilder.d.ts +341 -0
  30. package/dist/builders/RecordSchemaBuilder.js +2 -0
  31. package/dist/builders/RecordSchemaBuilder.js.map +1 -0
  32. package/dist/builders/SchemaBuilder.d.ts +818 -30
  33. package/dist/builders/StringSchemaBuilder.d.ts +152 -37
  34. package/dist/builders/StringSchemaBuilder.js +2 -414
  35. package/dist/builders/StringSchemaBuilder.js.map +1 -0
  36. package/dist/builders/TupleSchemaBuilder.d.ts +248 -0
  37. package/dist/builders/TupleSchemaBuilder.js +2 -0
  38. package/dist/builders/TupleSchemaBuilder.js.map +1 -0
  39. package/dist/builders/UnionSchemaBuilder.d.ts +133 -39
  40. package/dist/builders/UnionSchemaBuilder.js +2 -216
  41. package/dist/builders/UnionSchemaBuilder.js.map +1 -0
  42. package/dist/chunk-BFCMDDTX.js +2 -0
  43. package/dist/chunk-BFCMDDTX.js.map +1 -0
  44. package/dist/chunk-CTP4RHDG.js +2 -0
  45. package/dist/chunk-CTP4RHDG.js.map +1 -0
  46. package/dist/chunk-EUQ5AE3H.js +2 -0
  47. package/dist/chunk-EUQ5AE3H.js.map +1 -0
  48. package/dist/chunk-FWUEUW2N.js +2 -0
  49. package/dist/chunk-FWUEUW2N.js.map +1 -0
  50. package/dist/chunk-IJ6FO37G.js +2 -0
  51. package/dist/chunk-IJ6FO37G.js.map +1 -0
  52. package/dist/chunk-KESLT5EE.js +2 -0
  53. package/dist/chunk-KESLT5EE.js.map +1 -0
  54. package/dist/chunk-KFTO7TMB.js +2 -0
  55. package/dist/chunk-KFTO7TMB.js.map +1 -0
  56. package/dist/chunk-MKAYFPAR.js +2 -0
  57. package/dist/chunk-MKAYFPAR.js.map +1 -0
  58. package/dist/chunk-NXPH3ZUW.js +2 -0
  59. package/dist/chunk-NXPH3ZUW.js.map +1 -0
  60. package/dist/chunk-S5TC6NSU.js +2 -0
  61. package/dist/chunk-S5TC6NSU.js.map +1 -0
  62. package/dist/chunk-YWBNVHR6.js +2 -0
  63. package/dist/chunk-YWBNVHR6.js.map +1 -0
  64. package/dist/chunk-Z7QZ7IXT.js +2 -0
  65. package/dist/chunk-Z7QZ7IXT.js.map +1 -0
  66. package/dist/chunk-ZAN4ZHCJ.js +2 -0
  67. package/dist/chunk-ZAN4ZHCJ.js.map +1 -0
  68. package/dist/chunk-ZUPYV5TI.js +2 -0
  69. package/dist/chunk-ZUPYV5TI.js.map +1 -0
  70. package/dist/core.d.ts +23 -0
  71. package/dist/core.js +2 -0
  72. package/dist/core.js.map +1 -0
  73. package/dist/extension.d.ts +413 -0
  74. package/dist/extensions/array.d.ts +112 -0
  75. package/dist/extensions/enum.d.ts +190 -0
  76. package/dist/extensions/index.d.ts +108 -0
  77. package/dist/extensions/nullable.d.ts +26 -0
  78. package/dist/extensions/number.d.ts +228 -0
  79. package/dist/extensions/string.d.ts +332 -0
  80. package/dist/extensions/util.d.ts +45 -0
  81. package/dist/index.d.ts +7 -20
  82. package/dist/index.js +2 -19
  83. package/dist/index.js.map +1 -0
  84. package/dist/utils/transaction.d.ts +27 -4
  85. package/package.json +74 -6
  86. package/dist/builders/SchemaBuilder.js +0 -275
  87. package/dist/utils/transaction.js +0 -178
@@ -0,0 +1,413 @@
1
+ /**
2
+ * @module extension
3
+ *
4
+ * The **extension system** for `@cleverbrush/schema` allows third-party and
5
+ * first-party code to add custom methods to any schema builder type
6
+ * (`string`, `number`, `date`, `object`, …) without modifying the core
7
+ * library.
8
+ *
9
+ * ## Overview
10
+ *
11
+ * Extensions follow a two-step workflow:
12
+ *
13
+ * 1. **Define** an extension with {@link defineExtension} — declare which
14
+ * builder types it targets and what methods it adds.
15
+ * 2. **Apply** one or more extensions with {@link withExtensions} — get back
16
+ * augmented factory functions (`string()`, `number()`, …) whose return
17
+ * types include the new methods.
18
+ *
19
+ * ## Ergonomic authoring
20
+ *
21
+ * Extension methods do **not** need to call `withExtension()` manually.
22
+ * The system automatically attaches metadata using the method name as the
23
+ * extension key and the method arguments as the value. This keeps extension
24
+ * definitions concise:
25
+ *
26
+ * ```ts
27
+ * const slugExt = defineExtension({
28
+ * string: {
29
+ * slug(this: StringSchemaBuilder) {
30
+ * return this.addValidator((val) => {
31
+ * const valid = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(val);
32
+ * return { valid, errors: valid ? [] : [{ message: 'must be a valid URL slug' }] };
33
+ * });
34
+ * }
35
+ * }
36
+ * });
37
+ * ```
38
+ *
39
+ * If you need **custom metadata** (e.g. a different key or a transformed
40
+ * value), call `this.withExtension(key, value)` explicitly — the auto-infer
41
+ * logic will detect the existing key and skip the automatic attachment.
42
+ *
43
+ * ## Stacking and composition
44
+ *
45
+ * Multiple extensions can target the same builder type. Pass them all to
46
+ * `withExtensions()` and the methods are merged. A runtime error is thrown
47
+ * if two extensions define the same method name on the same builder type.
48
+ *
49
+ * ```ts
50
+ * const s = withExtensions(emailExt, slugExt, rangeExt);
51
+ * const schema = s.string().email().slug(); // both methods available
52
+ * ```
53
+ *
54
+ * ## Introspection
55
+ *
56
+ * Extension metadata is accessible via `schema.introspect().extensions`.
57
+ * Each key corresponds to an extension method name and its value is whatever
58
+ * was passed (or auto-inferred) as the extension data.
59
+ *
60
+ * @see {@link defineExtension} — define an extension
61
+ * @see {@link withExtensions} — apply extensions to builder factories
62
+ * @see {@link ExtensionConfig} — shape of the configuration object
63
+ * @see {@link ExtensionDescriptor} — branded descriptor returned by `defineExtension`
64
+ */
65
+ import { AnySchemaBuilder } from './builders/AnySchemaBuilder.js';
66
+ import { ArraySchemaBuilder } from './builders/ArraySchemaBuilder.js';
67
+ import { BooleanSchemaBuilder } from './builders/BooleanSchemaBuilder.js';
68
+ import { DateSchemaBuilder } from './builders/DateSchemaBuilder.js';
69
+ import { FunctionSchemaBuilder } from './builders/FunctionSchemaBuilder.js';
70
+ import { NumberSchemaBuilder } from './builders/NumberSchemaBuilder.js';
71
+ import { ObjectSchemaBuilder } from './builders/ObjectSchemaBuilder.js';
72
+ import { RecordSchemaBuilder } from './builders/RecordSchemaBuilder.js';
73
+ import type { SchemaBuilder } from './builders/SchemaBuilder.js';
74
+ import { StringSchemaBuilder } from './builders/StringSchemaBuilder.js';
75
+ import { TupleSchemaBuilder } from './builders/TupleSchemaBuilder.js';
76
+ import { UnionSchemaBuilder } from './builders/UnionSchemaBuilder.js';
77
+ /**
78
+ * Maps each builder type name to the corresponding generic builder class.
79
+ *
80
+ * Used internally to type-check extension method `this` bindings — for
81
+ * example, an extension targeting `"string"` receives `this: StringSchemaBuilder`.
82
+ *
83
+ * @internal Not exported — used only by the extension type machinery.
84
+ */
85
+ type BuilderMap = {
86
+ string: StringSchemaBuilder<any, any, any, any, any>;
87
+ number: NumberSchemaBuilder<any, any, any, any, any>;
88
+ boolean: BooleanSchemaBuilder<any, any, any, any, any, any, any>;
89
+ date: DateSchemaBuilder<any, any, any, any, any>;
90
+ object: ObjectSchemaBuilder<any, any, any, any, any, any>;
91
+ array: ArraySchemaBuilder<any, any, any, any, any, any, any>;
92
+ tuple: TupleSchemaBuilder<any, any, any, any, any, any, any>;
93
+ record: RecordSchemaBuilder<any, any, any, any, any, any, any>;
94
+ union: UnionSchemaBuilder<any, any, any, any, any, any>;
95
+ func: FunctionSchemaBuilder<any, any, any, any, any>;
96
+ any: AnySchemaBuilder<any, any, any, any, any, any>;
97
+ };
98
+ type BuilderTypeName = keyof BuilderMap;
99
+ /**
100
+ * Defines the shape of an extension configuration object passed to
101
+ * {@link defineExtension}.
102
+ *
103
+ * Each key is a **builder type name** — one of `"string"`, `"number"`,
104
+ * `"boolean"`, `"date"`, `"object"`, `"array"`, `"union"`, `"func"`, or
105
+ * `"any"`. The value is a record of **method implementations** to add to
106
+ * that builder type.
107
+ *
108
+ * Method implementations receive `this` bound to the target builder instance
109
+ * (e.g. `StringSchemaBuilder` for the `"string"` key) and **must** return a
110
+ * builder of the same type to support fluent chaining.
111
+ *
112
+ * @remarks
113
+ * Extension methods that only add validators/preprocessors do not need to
114
+ * call `this.withExtension()` — the system will auto-attach metadata using
115
+ * the method name as the key and the arguments as the value. Call
116
+ * `this.withExtension(key, value)` explicitly only when you need custom
117
+ * metadata (e.g. a transformed value or a different key).
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * // Minimal extension config — auto-inferred metadata
122
+ * const config: ExtensionConfig = {
123
+ * string: {
124
+ * slug(this: StringSchemaBuilder) {
125
+ * return this.addValidator((v) => ({ valid: /^[a-z0-9-]+$/.test(v), errors: [] }));
126
+ * }
127
+ * },
128
+ * number: {
129
+ * port(this: NumberSchemaBuilder) {
130
+ * return this.isInteger().min(1).max(65535);
131
+ * }
132
+ * }
133
+ * };
134
+ * ```
135
+ *
136
+ * @see {@link defineExtension}
137
+ */
138
+ export type ExtensionConfig = {
139
+ [K in BuilderTypeName]?: Record<string, (this: BuilderMap[K], ...args: any[]) => any>;
140
+ };
141
+ /**
142
+ * A branded descriptor returned by {@link defineExtension}.
143
+ *
144
+ * The descriptor captures the extension's method signatures at the **type
145
+ * level** so that {@link withExtensions} can produce correctly-typed factory
146
+ * functions. At runtime it holds the (possibly wrapped) configuration object.
147
+ *
148
+ * Extension descriptors are intentionally **opaque** — consumers should not
149
+ * access `config` directly. Instead, pass descriptors to
150
+ * {@link withExtensions} to obtain augmented builder factories.
151
+ *
152
+ * @typeParam T - The concrete {@link ExtensionConfig} shape. Inferred
153
+ * automatically by `defineExtension`; you rarely need to specify it.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * // The type is inferred — no need to annotate
158
+ * const myExt: ExtensionDescriptor<{ string: { slug: ... } }> = defineExtension({ ... });
159
+ * ```
160
+ *
161
+ * @see {@link defineExtension}
162
+ * @see {@link withExtensions}
163
+ */
164
+ export type ExtensionDescriptor<T extends ExtensionConfig = ExtensionConfig> = {
165
+ readonly __brand: unique symbol;
166
+ readonly config: T;
167
+ };
168
+ /** Extracts the method signatures an extension adds to a given builder type. */
169
+ type ExtractMethods<TExt extends ExtensionConfig, TType extends BuilderTypeName> = TExt[TType] extends Record<string, (...args: any[]) => any> ? TExt[TType] : {};
170
+ /** Merges the methods from multiple extensions for a given builder type. */
171
+ type MergeExtensionMethods<TExts extends readonly ExtensionDescriptor<any>[], TType extends BuilderTypeName> = TExts extends readonly [
172
+ ExtensionDescriptor<infer TFirst>,
173
+ ...infer TRest extends readonly ExtensionDescriptor<any>[]
174
+ ] ? ExtractMethods<TFirst, TType> & MergeExtensionMethods<TRest, TType> : {};
175
+ /**
176
+ * Intersected onto consumer-facing builder types to make `withExtension`
177
+ * and `getExtension` uncallable (`never`). Using an intersection instead
178
+ * of `Omit` preserves the class identity so extended builders remain
179
+ * assignable to `SchemaBuilder<any, any, any, any, any>`.
180
+ */
181
+ export type HiddenExtensionMethods = {
182
+ /** @internal Extension-author only — use inside `defineExtension()`. */
183
+ withExtension: never;
184
+ /** @internal Extension-author only — use inside `defineExtension()`. */
185
+ getExtension: never;
186
+ };
187
+ /**
188
+ * Overrides extension method return types so they always return the full
189
+ * extended builder type. This ensures extension methods preserve all other
190
+ * extension methods through chaining (e.g. `s.string().email().slug()`).
191
+ *
192
+ * The self-reference (`FixedMethods` appears in its own mapped return
193
+ * types) is resolved lazily by TypeScript because the recursion sits
194
+ * inside a function-return position within a conditional mapped type.
195
+ */
196
+ export type FixedMethods<TRawMethods, TBase> = {
197
+ [K in keyof TRawMethods]: TRawMethods[K] extends (this: any, ...args: infer A) => any ? (...args: A) => TBase & FixedMethods<TRawMethods, TBase> & HiddenExtensionMethods : TRawMethods[K];
198
+ };
199
+ /**
200
+ * Produces the consumer-facing type for an extended builder: the base
201
+ * builder intersected with its fixed extension methods, with
202
+ * `withExtension` / `getExtension` overridden to `never` so they
203
+ * don't appear as callable in consumer code.
204
+ */
205
+ export type CleanExtended<TBuilder, TExt> = TBuilder & FixedMethods<TExt, TBuilder> & HiddenExtensionMethods;
206
+ type ExtendedStringFactory<TExt> = {
207
+ (): CleanExtended<StringSchemaBuilder<string, true, false, false, TExt>, TExt>;
208
+ <T extends string>(equals: T): CleanExtended<StringSchemaBuilder<T, true, false, false, TExt>, TExt>;
209
+ };
210
+ type ExtendedNumberFactory<TExt> = {
211
+ (): CleanExtended<NumberSchemaBuilder<number, true, false, false, TExt>, TExt>;
212
+ <T extends number>(equals: T): CleanExtended<NumberSchemaBuilder<T, true, false, false, TExt>, TExt>;
213
+ };
214
+ type ExtendedBooleanFactory<TExt> = () => CleanExtended<BooleanSchemaBuilder<boolean, true, false, undefined, false, TExt>, TExt>;
215
+ type ExtendedDateFactory<TExt> = () => CleanExtended<DateSchemaBuilder<Date, true, false, false, TExt>, TExt>;
216
+ type ExtendedObjectFactory<TExt> = <P extends Record<string, SchemaBuilder<any, any, any, any, any>>>(properties?: P) => CleanExtended<ObjectSchemaBuilder<P, true, false, undefined, false, TExt>, TExt>;
217
+ type ExtendedArrayFactory<TExt> = <TElementSchema extends SchemaBuilder<any, any, any, any, any>>(elementSchema?: TElementSchema) => CleanExtended<ArraySchemaBuilder<TElementSchema, true, false, undefined, false, TExt>, TExt>;
218
+ type ExtendedUnionFactory<TExt> = <T extends SchemaBuilder<any, any, any, any, any>>(schema: T) => CleanExtended<UnionSchemaBuilder<[T], true, false, undefined, false, TExt>, TExt>;
219
+ type ExtendedFuncFactory<TExt> = () => CleanExtended<FunctionSchemaBuilder<true, false, undefined, false, TExt>, TExt>;
220
+ type ExtendedAnyFactory<TExt> = () => CleanExtended<AnySchemaBuilder<true, false, undefined, false, TExt>, TExt>;
221
+ type ExtendedTupleFactory<TExt> = <const TElements extends readonly SchemaBuilder<any, any, any, any, any>[]>(elements: [...TElements]) => CleanExtended<TupleSchemaBuilder<TElements, true, false, undefined, false, TExt>, TExt>;
222
+ type ExtendedRecordFactory<TExt> = <TKeySchema extends StringSchemaBuilder<any, any, any, any>, TValueSchema extends SchemaBuilder<any, any, any, any, any>>(keySchema: TKeySchema, valueSchema: TValueSchema) => CleanExtended<RecordSchemaBuilder<TKeySchema, TValueSchema, true, false, undefined, false, TExt>, TExt>;
223
+ /**
224
+ * The return type of {@link withExtensions}.
225
+ *
226
+ * Contains a factory function for every builder type (`string`, `number`,
227
+ * `boolean`, `date`, `object`, `array`, `union`, `func`, `any`). Each
228
+ * factory returns a builder whose type includes the methods contributed
229
+ * by all provided extension descriptors.
230
+ *
231
+ * @typeParam TExts - Tuple of extension descriptors passed to `withExtensions`.
232
+ *
233
+ * @see {@link withExtensions}
234
+ */
235
+ type WithExtensionsResult<TExts extends readonly ExtensionDescriptor<any>[]> = {
236
+ string: ExtendedStringFactory<MergeExtensionMethods<TExts, 'string'>>;
237
+ number: ExtendedNumberFactory<MergeExtensionMethods<TExts, 'number'>>;
238
+ boolean: ExtendedBooleanFactory<MergeExtensionMethods<TExts, 'boolean'>>;
239
+ date: ExtendedDateFactory<MergeExtensionMethods<TExts, 'date'>>;
240
+ object: ExtendedObjectFactory<MergeExtensionMethods<TExts, 'object'>>;
241
+ array: ExtendedArrayFactory<MergeExtensionMethods<TExts, 'array'>>;
242
+ tuple: ExtendedTupleFactory<MergeExtensionMethods<TExts, 'tuple'>>;
243
+ record: ExtendedRecordFactory<MergeExtensionMethods<TExts, 'record'>>;
244
+ union: ExtendedUnionFactory<MergeExtensionMethods<TExts, 'union'>>;
245
+ func: ExtendedFuncFactory<MergeExtensionMethods<TExts, 'func'>>;
246
+ any: ExtendedAnyFactory<MergeExtensionMethods<TExts, 'any'>>;
247
+ };
248
+ /**
249
+ * Defines an extension targeting one or more schema builder types.
250
+ *
251
+ * Each extension is a plain object keyed by builder type name (`"string"`,
252
+ * `"number"`, `"date"`, …) whose values are method implementations.
253
+ * Methods receive `this` bound to the builder instance and must return a
254
+ * builder to support fluent chaining.
255
+ *
256
+ * ## Ergonomic metadata (auto-infer)
257
+ *
258
+ * Extension methods **do not** have to call `this.withExtension()`. The
259
+ * system wraps each method and automatically attaches
260
+ * `withExtension(methodName, args)` to the returned builder when the key
261
+ * is not already present. This eliminates the most common source of
262
+ * duplication in extension code.
263
+ *
264
+ * - **Zero-arg methods** → metadata value is `true`
265
+ * - **Single-arg methods** → metadata value is the argument itself
266
+ * - **Multi-arg methods** → metadata value is the arguments array
267
+ *
268
+ * If you need **custom metadata** (e.g. a different key, a transformed
269
+ * value, or a structured object), call `this.withExtension(key, value)`
270
+ * explicitly inside the method — the auto-infer logic detects the existing
271
+ * key and skips automatic attachment.
272
+ *
273
+ * ## Validation
274
+ *
275
+ * `defineExtension` validates the configuration eagerly:
276
+ * - Unknown builder type names throw immediately.
277
+ * - {@link RESERVED_METHODS | Reserved method names} (e.g. `validate`,
278
+ * `introspect`) cannot be overridden.
279
+ * - Non-function values in the method record are rejected.
280
+ *
281
+ * @param config - An {@link ExtensionConfig} object mapping builder type
282
+ * names to method records.
283
+ * @returns A branded {@link ExtensionDescriptor} ready to pass to
284
+ * {@link withExtensions}.
285
+ *
286
+ * @example Simple extension (auto-inferred metadata)
287
+ * ```ts
288
+ * const slugExt = defineExtension({
289
+ * string: {
290
+ * slug(this: StringSchemaBuilder) {
291
+ * return this.addValidator((val) => {
292
+ * const valid = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(val);
293
+ * return { valid, errors: valid ? [] : [{ message: 'invalid slug' }] };
294
+ * });
295
+ * }
296
+ * }
297
+ * });
298
+ *
299
+ * // Usage:
300
+ * const s = withExtensions(slugExt);
301
+ * const schema = s.string().slug();
302
+ * schema.introspect().extensions.slug; // true
303
+ * ```
304
+ *
305
+ * @example Extension with custom metadata
306
+ * ```ts
307
+ * const currencyExt = defineExtension({
308
+ * number: {
309
+ * currency(this: NumberSchemaBuilder, opts?: { maxDecimals?: number }) {
310
+ * const maxDec = opts?.maxDecimals ?? 2;
311
+ * return this.withExtension('currency', { maxDecimals: maxDec })
312
+ * .min(0)
313
+ * .addValidator((val) => {
314
+ * const decimals = (String(val).split('.')[1] ?? '').length;
315
+ * const valid = decimals <= maxDec;
316
+ * return { valid, errors: valid ? [] : [{ message: `max ${maxDec} decimals` }] };
317
+ * });
318
+ * }
319
+ * }
320
+ * });
321
+ * ```
322
+ *
323
+ * @example Multi-builder extension
324
+ * ```ts
325
+ * const myExt = defineExtension({
326
+ * string: {
327
+ * email(this: StringSchemaBuilder) { return this.addValidator(...); }
328
+ * },
329
+ * number: {
330
+ * port(this: NumberSchemaBuilder) { return this.isInteger().min(1).max(65535); }
331
+ * }
332
+ * });
333
+ * ```
334
+ *
335
+ * @throws {Error} If a builder type name is unknown.
336
+ * @throws {Error} If a method name is reserved.
337
+ * @throws {Error} If a method value is not a function.
338
+ *
339
+ * @see {@link withExtensions} — apply the defined extension
340
+ * @see {@link ExtensionConfig} — configuration shape
341
+ */
342
+ export declare function defineExtension<T extends ExtensionConfig>(config: T): ExtensionDescriptor<T>;
343
+ /**
344
+ * Creates a set of schema factory functions with the provided extensions
345
+ * applied.
346
+ *
347
+ * Each factory function (`string()`, `number()`, `date()`, …) returned by
348
+ * `withExtensions` produces builder instances whose prototypes include the
349
+ * extension methods. All built-in builder methods remain available and
350
+ * fully chainable alongside the new ones.
351
+ *
352
+ * ## Stacking multiple extensions
353
+ *
354
+ * Pass any number of {@link ExtensionDescriptor}s — their methods are
355
+ * merged per builder type. If two extensions define the **same** method
356
+ * name on the same builder type, a runtime error is thrown to prevent
357
+ * silent conflicts.
358
+ *
359
+ * ## Type safety
360
+ *
361
+ * The return type is fully inferred: TypeScript knows exactly which
362
+ * extension methods are available on each builder factory. Extension
363
+ * methods return the full extended builder type, so chaining like
364
+ * `s.string().email().slug().minLength(3)` is fully typed.
365
+ *
366
+ * ## Builder types without extensions
367
+ *
368
+ * Builders that have no methods from any of the provided extensions
369
+ * use the standard (unextended) factory, so there is zero overhead.
370
+ *
371
+ * @param extensions - One or more {@link ExtensionDescriptor}s created
372
+ * by {@link defineExtension}.
373
+ * @returns An object with factory functions for all builder types
374
+ * (`string`, `number`, `boolean`, `date`, `object`, `array`, `union`,
375
+ * `func`, `any`), each returning augmented builders.
376
+ *
377
+ * @example Basic usage
378
+ * ```ts
379
+ * const s = withExtensions(emailExt, rangeExt);
380
+ *
381
+ * // string() now has .email()
382
+ * const emailSchema = s.string().email().minLength(5);
383
+ *
384
+ * // number() now has .range()
385
+ * const rangeSchema = s.number().range(0, 100);
386
+ *
387
+ * // builders without targeted extensions work as normal
388
+ * const dateSchema = s.date();
389
+ * ```
390
+ *
391
+ * @example Stacking extensions on the same builder
392
+ * ```ts
393
+ * const s = withExtensions(emailExt, slugExt, trimmedExt);
394
+ * const schema = s.string().email().slug().trimmed();
395
+ * ```
396
+ *
397
+ * @example Using extensions in object schemas
398
+ * ```ts
399
+ * const s = withExtensions(emailExt, portExt);
400
+ * const ServerConfig = s.object({
401
+ * host: s.string().email(),
402
+ * port: s.number().port()
403
+ * });
404
+ * ```
405
+ *
406
+ * @throws {Error} If two extensions define the same method name on the
407
+ * same builder type.
408
+ *
409
+ * @see {@link defineExtension} — create extension descriptors
410
+ * @see {@link ExtensionDescriptor}
411
+ */
412
+ export declare function withExtensions<const TExts extends readonly ExtensionDescriptor<any>[]>(...extensions: TExts): WithExtensionsResult<TExts>;
413
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Built-in array extensions for `@cleverbrush/schema`.
3
+ *
4
+ * Provides common array validators: {@link arrayExtensions | nonempty}
5
+ * and {@link arrayExtensions | unique}.
6
+ *
7
+ * These are pre-applied in the default `@cleverbrush/schema` import.
8
+ * Import from `@cleverbrush/schema/core` to get bare builders without these extensions.
9
+ *
10
+ * @module
11
+ */
12
+ import type { ArraySchemaBuilder } from '../builders/ArraySchemaBuilder.js';
13
+ import type { SchemaBuilder, ValidationErrorMessageProvider } from '../builders/SchemaBuilder.js';
14
+ import type { HiddenExtensionMethods } from '../extension.js';
15
+ /** Return type shared by every method on {@link ArrayBuiltinExtensions}. */
16
+ type ArrayExtReturn<TElementSchema extends SchemaBuilder<any, any, any, any, any> = SchemaBuilder<any, any, any>> = ArraySchemaBuilder<TElementSchema, true, false, undefined, false, ArrayBuiltinExtensions<TElementSchema>> & ArrayBuiltinExtensions<TElementSchema> & HiddenExtensionMethods;
17
+ /**
18
+ * Methods added to `ArraySchemaBuilder` by the built-in array extension pack.
19
+ *
20
+ * **WORKAROUND:** This interface duplicates the method signatures from
21
+ * `arrayExtensions` so that JSDoc survives into the published `.d.ts`
22
+ * files. TypeScript strips JSDoc when method signatures are reconstructed
23
+ * through the `FixedMethods` mapped type (conditional `infer` loses
24
+ * comments). Remove this interface once TypeScript preserves JSDoc
25
+ * through mapped types / conditional type inference.
26
+ *
27
+ * @see https://github.com/microsoft/TypeScript/issues/50715
28
+ */
29
+ export interface ArrayBuiltinExtensions<TElementSchema extends SchemaBuilder<any, any, any, any, any> = SchemaBuilder<any, any, any>> {
30
+ /**
31
+ * Validates that the array contains at least one element.
32
+ *
33
+ * @param errorMessage - custom error message or function to generate one
34
+ * @returns a new schema builder with the nonempty validator applied
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * array().nonempty();
39
+ * array().nonempty('At least one item required');
40
+ * ```
41
+ */
42
+ nonempty(errorMessage?: ValidationErrorMessageProvider<ArraySchemaBuilder<any>>): ArrayExtReturn<TElementSchema>;
43
+ /**
44
+ * Validates that all elements in the array are unique.
45
+ *
46
+ * For primitive elements, uses strict equality. For objects, pass a `keyFn`
47
+ * that extracts a comparison key from each element.
48
+ *
49
+ * @param keyFn - optional function to extract a comparison key from each element
50
+ * @param errorMessage - custom error message or function to generate one
51
+ * @returns a new schema builder with the unique validator applied
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * array().unique();
56
+ * array().unique((item) => item.id);
57
+ * array().unique(undefined, 'No duplicates allowed');
58
+ * ```
59
+ */
60
+ unique(keyFn?: (item: any) => unknown, errorMessage?: ValidationErrorMessageProvider<ArraySchemaBuilder<any>>): ArrayExtReturn<TElementSchema>;
61
+ }
62
+ /**
63
+ * Extension descriptor that adds common array validators
64
+ * to `ArraySchemaBuilder`.
65
+ *
66
+ * Included methods: `nonempty`, `unique`.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { withExtensions } from '@cleverbrush/schema/core';
71
+ * import { arrayExtensions } from '@cleverbrush/schema';
72
+ *
73
+ * const s = withExtensions(arrayExtensions);
74
+ * const schema = s.array().nonempty().unique();
75
+ * ```
76
+ */
77
+ export declare const arrayExtensions: import("../extension.js").ExtensionDescriptor<{
78
+ array: {
79
+ /**
80
+ * Validates that the array contains at least one element.
81
+ *
82
+ * @param errorMessage - custom error message or function to generate one
83
+ * @returns a new schema builder with the nonempty validator applied
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * array().nonempty();
88
+ * array().nonempty('At least one item required');
89
+ * ```
90
+ */
91
+ nonempty(this: ArraySchemaBuilder<any>, errorMessage?: ValidationErrorMessageProvider<ArraySchemaBuilder<any>>): ArraySchemaBuilder<any, true, false, undefined, false, {}, any[] | unknown[]>;
92
+ /**
93
+ * Validates that all elements in the array are unique.
94
+ *
95
+ * For primitive elements, uses strict equality. For objects, pass a `keyFn`
96
+ * that extracts a comparison key from each element.
97
+ *
98
+ * @param keyFn - optional function to extract a comparison key from each element
99
+ * @param errorMessage - custom error message or function to generate one
100
+ * @returns a new schema builder with the unique validator applied
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * array().unique();
105
+ * array().unique((item) => item.id);
106
+ * array().unique(undefined, 'No duplicates allowed');
107
+ * ```
108
+ */
109
+ unique(this: ArraySchemaBuilder<any>, keyFn?: (item: any) => unknown, errorMessage?: ValidationErrorMessageProvider<ArraySchemaBuilder<any>>): ArraySchemaBuilder<any, true, false, undefined, false, {}, any[] | unknown[]>;
110
+ };
111
+ }>;
112
+ export {};