@cleverbrush/schema 1.1.10 → 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.
- package/README.md +1314 -101
- package/dist/builders/AnySchemaBuilder.d.ts +55 -15
- package/dist/builders/AnySchemaBuilder.js +2 -112
- package/dist/builders/AnySchemaBuilder.js.map +1 -0
- package/dist/builders/ArraySchemaBuilder.d.ts +124 -23
- package/dist/builders/ArraySchemaBuilder.js +2 -284
- package/dist/builders/ArraySchemaBuilder.js.map +1 -0
- package/dist/builders/BooleanSchemaBuilder.d.ts +95 -20
- package/dist/builders/BooleanSchemaBuilder.js +2 -150
- package/dist/builders/BooleanSchemaBuilder.js.map +1 -0
- package/dist/builders/DateSchemaBuilder.d.ts +135 -37
- package/dist/builders/DateSchemaBuilder.js +2 -433
- package/dist/builders/DateSchemaBuilder.js.map +1 -0
- package/dist/builders/ExternSchemaBuilder.d.ts +200 -0
- package/dist/builders/ExternSchemaBuilder.js +2 -0
- package/dist/builders/ExternSchemaBuilder.js.map +1 -0
- package/dist/builders/FunctionSchemaBuilder.d.ts +58 -15
- package/dist/builders/FunctionSchemaBuilder.js +2 -113
- package/dist/builders/FunctionSchemaBuilder.js.map +1 -0
- package/dist/builders/LazySchemaBuilder.d.ts +167 -0
- package/dist/builders/NullSchemaBuilder.d.ts +162 -0
- package/dist/builders/NumberSchemaBuilder.d.ts +143 -31
- package/dist/builders/NumberSchemaBuilder.js +2 -386
- package/dist/builders/NumberSchemaBuilder.js.map +1 -0
- package/dist/builders/ObjectSchemaBuilder.d.ts +266 -61
- package/dist/builders/ObjectSchemaBuilder.js +2 -589
- package/dist/builders/ObjectSchemaBuilder.js.map +1 -0
- package/dist/builders/PropertyValidationResult.d.ts +68 -0
- package/dist/builders/RecordSchemaBuilder.d.ts +341 -0
- package/dist/builders/RecordSchemaBuilder.js +2 -0
- package/dist/builders/RecordSchemaBuilder.js.map +1 -0
- package/dist/builders/SchemaBuilder.d.ts +818 -30
- package/dist/builders/StringSchemaBuilder.d.ts +152 -37
- package/dist/builders/StringSchemaBuilder.js +2 -414
- package/dist/builders/StringSchemaBuilder.js.map +1 -0
- package/dist/builders/TupleSchemaBuilder.d.ts +248 -0
- package/dist/builders/TupleSchemaBuilder.js +2 -0
- package/dist/builders/TupleSchemaBuilder.js.map +1 -0
- package/dist/builders/UnionSchemaBuilder.d.ts +133 -39
- package/dist/builders/UnionSchemaBuilder.js +2 -216
- package/dist/builders/UnionSchemaBuilder.js.map +1 -0
- package/dist/chunk-BFCMDDTX.js +2 -0
- package/dist/chunk-BFCMDDTX.js.map +1 -0
- package/dist/chunk-CTP4RHDG.js +2 -0
- package/dist/chunk-CTP4RHDG.js.map +1 -0
- package/dist/chunk-EUQ5AE3H.js +2 -0
- package/dist/chunk-EUQ5AE3H.js.map +1 -0
- package/dist/chunk-FWUEUW2N.js +2 -0
- package/dist/chunk-FWUEUW2N.js.map +1 -0
- package/dist/chunk-IJ6FO37G.js +2 -0
- package/dist/chunk-IJ6FO37G.js.map +1 -0
- package/dist/chunk-KESLT5EE.js +2 -0
- package/dist/chunk-KESLT5EE.js.map +1 -0
- package/dist/chunk-KFTO7TMB.js +2 -0
- package/dist/chunk-KFTO7TMB.js.map +1 -0
- package/dist/chunk-MKAYFPAR.js +2 -0
- package/dist/chunk-MKAYFPAR.js.map +1 -0
- package/dist/chunk-NXPH3ZUW.js +2 -0
- package/dist/chunk-NXPH3ZUW.js.map +1 -0
- package/dist/chunk-S5TC6NSU.js +2 -0
- package/dist/chunk-S5TC6NSU.js.map +1 -0
- package/dist/chunk-YWBNVHR6.js +2 -0
- package/dist/chunk-YWBNVHR6.js.map +1 -0
- package/dist/chunk-Z7QZ7IXT.js +2 -0
- package/dist/chunk-Z7QZ7IXT.js.map +1 -0
- package/dist/chunk-ZAN4ZHCJ.js +2 -0
- package/dist/chunk-ZAN4ZHCJ.js.map +1 -0
- package/dist/chunk-ZUPYV5TI.js +2 -0
- package/dist/chunk-ZUPYV5TI.js.map +1 -0
- package/dist/core.d.ts +23 -0
- package/dist/core.js +2 -0
- package/dist/core.js.map +1 -0
- package/dist/extension.d.ts +413 -0
- package/dist/extensions/array.d.ts +112 -0
- package/dist/extensions/enum.d.ts +190 -0
- package/dist/extensions/index.d.ts +108 -0
- package/dist/extensions/nullable.d.ts +26 -0
- package/dist/extensions/number.d.ts +228 -0
- package/dist/extensions/string.d.ts +332 -0
- package/dist/extensions/util.d.ts +45 -0
- package/dist/index.d.ts +7 -20
- package/dist/index.js +2 -19
- package/dist/index.js.map +1 -0
- package/dist/utils/transaction.d.ts +27 -4
- package/package.json +74 -6
- package/dist/builders/SchemaBuilder.js +0 -275
- 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 {};
|