@zipbul/baker 2.1.0 → 3.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/CHANGELOG.md +256 -0
- package/MIGRATION-3.0.md +104 -0
- package/README.md +121 -75
- package/dist/index.d.ts +8 -7
- package/dist/index.js +10 -229
- package/dist/src/collect.d.ts +13 -10
- package/dist/src/collect.js +26 -0
- package/dist/src/configure.d.ts +8 -11
- package/dist/src/configure.js +43 -0
- package/dist/src/create-rule.d.ts +1 -1
- package/dist/src/create-rule.js +41 -0
- package/dist/src/decorators/field.d.ts +22 -18
- package/dist/src/decorators/field.js +268 -0
- package/dist/src/decorators/index.d.ts +1 -0
- package/dist/src/decorators/index.js +2 -2
- package/dist/src/decorators/recipe.d.ts +17 -0
- package/dist/src/decorators/recipe.js +23 -0
- package/dist/src/errors.d.ts +28 -17
- package/dist/src/errors.js +52 -0
- package/dist/src/functions/check-call-options.d.ts +8 -0
- package/dist/src/functions/check-call-options.js +51 -0
- package/dist/src/functions/deserialize.d.ts +14 -6
- package/dist/src/functions/deserialize.js +57 -0
- package/dist/src/functions/serialize.d.ts +10 -3
- package/dist/src/functions/serialize.js +52 -0
- package/dist/src/functions/validate.d.ts +13 -8
- package/dist/src/functions/validate.js +49 -0
- package/dist/src/interfaces.d.ts +1 -1
- package/dist/src/interfaces.js +4 -0
- package/dist/src/meta-access.d.ts +19 -0
- package/dist/src/meta-access.js +75 -0
- package/dist/src/registry.js +8 -0
- package/dist/src/rule-metadata.d.ts +11 -0
- package/dist/src/rule-metadata.js +17 -0
- package/dist/src/rule-plan.d.ts +29 -0
- package/dist/src/rule-plan.js +117 -0
- package/dist/src/rules/array.d.ts +7 -6
- package/dist/src/rules/array.js +96 -0
- package/dist/src/rules/common.d.ts +2 -2
- package/dist/src/rules/common.js +77 -0
- package/dist/src/rules/date.js +35 -0
- package/dist/src/rules/index.d.ts +2 -4
- package/dist/src/rules/index.js +8 -11
- package/dist/src/rules/locales.d.ts +5 -4
- package/dist/src/rules/locales.js +249 -0
- package/dist/src/rules/number.d.ts +2 -2
- package/dist/src/rules/number.js +79 -0
- package/dist/src/rules/object.d.ts +1 -1
- package/dist/src/rules/object.js +49 -0
- package/dist/src/rules/string.d.ts +83 -80
- package/dist/src/rules/string.js +1998 -0
- package/dist/src/rules/typechecker.d.ts +6 -6
- package/dist/src/rules/typechecker.js +143 -0
- package/dist/src/seal/circular-analyzer.js +63 -0
- package/dist/src/seal/codegen-utils.d.ts +7 -0
- package/dist/src/seal/codegen-utils.js +18 -0
- package/dist/src/seal/deserialize-builder.d.ts +8 -3
- package/dist/src/seal/deserialize-builder.js +1546 -0
- package/dist/src/seal/expose-validator.d.ts +3 -2
- package/dist/src/seal/expose-validator.js +65 -0
- package/dist/src/seal/seal-state.d.ts +10 -0
- package/dist/src/seal/seal-state.js +18 -0
- package/dist/src/seal/seal.d.ts +22 -21
- package/dist/src/seal/seal.js +431 -0
- package/dist/src/seal/serialize-builder.d.ts +3 -2
- package/dist/src/seal/serialize-builder.js +374 -0
- package/dist/src/seal/validate-meta.d.ts +13 -0
- package/dist/src/seal/validate-meta.js +61 -0
- package/dist/src/symbols.d.ts +1 -1
- package/dist/src/symbols.js +13 -2
- package/dist/src/transformers/collection.transformer.js +25 -0
- package/dist/src/transformers/date.transformer.js +18 -0
- package/dist/src/transformers/index.js +6 -2
- package/dist/src/transformers/luxon.transformer.d.ts +4 -2
- package/dist/src/transformers/luxon.transformer.js +34 -0
- package/dist/src/transformers/moment.transformer.d.ts +4 -2
- package/dist/src/transformers/moment.transformer.js +32 -0
- package/dist/src/transformers/number.transformer.js +8 -0
- package/dist/src/transformers/string.transformer.js +12 -0
- package/dist/src/types.d.ts +68 -28
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +4 -2
- package/dist/src/utils.js +10 -0
- package/package.json +80 -67
- package/dist/index-fnv35wrf.js +0 -3
- package/dist/index-k3d659ad.js +0 -3
- package/dist/index-s0n74vx1.js +0 -3
- package/dist/index-xdn55cz3.js +0 -1
- package/dist/src/functions/_run-sealed.d.ts +0 -7
- package/dist/src/functions/index.d.ts +0 -3
- package/dist/src/seal/index.d.ts +0 -5
package/dist/src/collect.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RawClassMeta, RawPropertyMeta } from './types';
|
|
2
|
+
import { RAW } from './symbols';
|
|
3
|
+
type MetaObject = Record<PropertyKey, unknown> & {
|
|
4
|
+
[RAW]?: RawClassMeta;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
|
-
* Returns the RawPropertyMeta for the given propertyKey on
|
|
4
|
-
*
|
|
5
|
-
*
|
|
7
|
+
* Returns the RawPropertyMeta for the given propertyKey on the class's decorator metadata.
|
|
8
|
+
* Creates the RAW slot and the per-key default meta if absent.
|
|
9
|
+
*
|
|
10
|
+
* The own-RAW check is required: a subclass's metadata inherits the parent's RAW via the
|
|
11
|
+
* metadata prototype chain, so a bare assignment would pollute the parent. Creating a fresh
|
|
12
|
+
* own RAW (null prototype) keeps child fields isolated.
|
|
6
13
|
*/
|
|
7
|
-
export declare function ensureMeta(
|
|
8
|
-
export
|
|
9
|
-
export declare function collectTransform(target: object, key: string, transformDef: TransformDef): void;
|
|
10
|
-
export declare function collectExpose(target: object, key: string, exposeDef: ExposeDef): void;
|
|
11
|
-
export declare function collectExclude(target: object, key: string, excludeDef: ExcludeDef): void;
|
|
12
|
-
export declare function collectType(target: object, key: string, typeDef: TypeDef): void;
|
|
14
|
+
export declare function ensureMeta(metadata: MetaObject, key: string): RawPropertyMeta;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RAW } from './symbols.js';
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// ensureMeta — Internal utility (§3.1)
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Returns the RawPropertyMeta for the given propertyKey on the class's decorator metadata.
|
|
7
|
+
* Creates the RAW slot and the per-key default meta if absent.
|
|
8
|
+
*
|
|
9
|
+
* The own-RAW check is required: a subclass's metadata inherits the parent's RAW via the
|
|
10
|
+
* metadata prototype chain, so a bare assignment would pollute the parent. Creating a fresh
|
|
11
|
+
* own RAW (null prototype) keeps child fields isolated.
|
|
12
|
+
*/
|
|
13
|
+
export function ensureMeta(metadata, key) {
|
|
14
|
+
if (!Object.hasOwn(metadata, RAW)) {
|
|
15
|
+
metadata[RAW] = Object.create(null);
|
|
16
|
+
}
|
|
17
|
+
const raw = metadata[RAW];
|
|
18
|
+
return (raw[key] ??= {
|
|
19
|
+
validation: [],
|
|
20
|
+
transform: [],
|
|
21
|
+
expose: [],
|
|
22
|
+
exclude: null,
|
|
23
|
+
type: null,
|
|
24
|
+
flags: {},
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/src/configure.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SealOptions } from './interfaces';
|
|
2
|
-
|
|
2
|
+
interface BakerConfig {
|
|
3
3
|
/** Automatic type conversion ("123" → 123). @default false */
|
|
4
4
|
autoConvert?: boolean;
|
|
5
5
|
/** Use class default values when key is missing from input. @default false */
|
|
@@ -11,17 +11,14 @@ export interface BakerConfig {
|
|
|
11
11
|
/** Include field exclusion reasons as comments in generated code. @default false */
|
|
12
12
|
debug?: boolean;
|
|
13
13
|
}
|
|
14
|
-
export interface ConfigureResult {
|
|
15
|
-
warnings: string[];
|
|
16
|
-
}
|
|
17
14
|
/**
|
|
18
|
-
* Baker global configuration. Call before
|
|
15
|
+
* Baker global configuration. Call before `seal()`.
|
|
19
16
|
* If not called, defaults are applied.
|
|
20
|
-
*
|
|
21
|
-
* @returns `{ warnings }` — contains warning messages if called after seal.
|
|
22
17
|
*/
|
|
23
|
-
|
|
24
|
-
/** @internal — used by seal */
|
|
25
|
-
|
|
18
|
+
declare function configure(config: BakerConfig): void;
|
|
19
|
+
/** @internal — used by seal. Returns the frozen global options; the only way to change them is configure(). */
|
|
20
|
+
declare function getGlobalOptions(): SealOptions;
|
|
26
21
|
/** @internal — reset to defaults on unseal */
|
|
27
|
-
|
|
22
|
+
declare function resetConfigForTesting(): void;
|
|
23
|
+
export { configure, getGlobalOptions, resetConfigForTesting };
|
|
24
|
+
export type { BakerConfig };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BakerError } from './errors.js';
|
|
2
|
+
import { isSealed } from './seal/seal-state.js';
|
|
3
|
+
const BAKER_CONFIG_KEYS = new Set([
|
|
4
|
+
'autoConvert',
|
|
5
|
+
'allowClassDefaults',
|
|
6
|
+
'stopAtFirstError',
|
|
7
|
+
'forbidUnknown',
|
|
8
|
+
'debug',
|
|
9
|
+
]);
|
|
10
|
+
let globalOptionsState = Object.freeze({});
|
|
11
|
+
/**
|
|
12
|
+
* Baker global configuration. Call before `seal()`.
|
|
13
|
+
* If not called, defaults are applied.
|
|
14
|
+
*/
|
|
15
|
+
function configure(config) {
|
|
16
|
+
if (isSealed()) {
|
|
17
|
+
throw new BakerError('[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().');
|
|
18
|
+
}
|
|
19
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
20
|
+
throw new BakerError(`[baker] configure() requires a plain object. Received: ${config === null ? 'null' : Array.isArray(config) ? 'array' : typeof config}.`);
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(config)) {
|
|
23
|
+
if (!BAKER_CONFIG_KEYS.has(key)) {
|
|
24
|
+
throw new BakerError(`[baker] configure(): unknown key '${key}'. ` + `Valid keys: ${[...BAKER_CONFIG_KEYS].join(', ')}.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
globalOptionsState = Object.freeze({
|
|
28
|
+
enableImplicitConversion: config.autoConvert ?? false,
|
|
29
|
+
exposeDefaultValues: config.allowClassDefaults ?? false,
|
|
30
|
+
stopAtFirstError: config.stopAtFirstError ?? false,
|
|
31
|
+
whitelist: config.forbidUnknown ?? false,
|
|
32
|
+
debug: config.debug ?? false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** @internal — used by seal. Returns the frozen global options; the only way to change them is configure(). */
|
|
36
|
+
function getGlobalOptions() {
|
|
37
|
+
return globalOptionsState;
|
|
38
|
+
}
|
|
39
|
+
/** @internal — reset to defaults on unseal */
|
|
40
|
+
function resetConfigForTesting() {
|
|
41
|
+
globalOptionsState = Object.freeze({});
|
|
42
|
+
}
|
|
43
|
+
export { configure, getGlobalOptions, resetConfigForTesting };
|
|
@@ -7,7 +7,7 @@ export interface CreateRuleOptions {
|
|
|
7
7
|
/** Rule parameters */
|
|
8
8
|
constraints?: Record<string, unknown>;
|
|
9
9
|
/** Type assumed by this rule — used for type gate optimization */
|
|
10
|
-
requiresType?: 'string' | 'number' | 'boolean' | 'date';
|
|
10
|
+
requiresType?: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object';
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Creates a user-defined validation rule.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BakerError } from './errors.js';
|
|
2
|
+
import { defineRuleMetadata } from './rule-metadata.js';
|
|
3
|
+
import { isAsyncFunction, isPromiseLike } from './utils.js';
|
|
4
|
+
export function createRule(nameOrOptions, validateFn) {
|
|
5
|
+
const name = typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions.name;
|
|
6
|
+
const validate = typeof nameOrOptions === 'string' ? validateFn : nameOrOptions.validate;
|
|
7
|
+
// The overloads require `validate`; guard the untyped-JS path instead of asserting with `!`,
|
|
8
|
+
// so misuse fails clearly at creation rather than as a confusing TypeError at validation time.
|
|
9
|
+
if (typeof validate !== 'function') {
|
|
10
|
+
throw new BakerError(`createRule(${name}): a validate function is required.`);
|
|
11
|
+
}
|
|
12
|
+
const constraints = typeof nameOrOptions === 'object' ? nameOrOptions.constraints : undefined;
|
|
13
|
+
const requiresType = typeof nameOrOptions === 'object' ? nameOrOptions.requiresType : undefined;
|
|
14
|
+
const isAsyncFn = isAsyncFunction(validate);
|
|
15
|
+
// Validation function wrapper — enforces that sync rules stay sync.
|
|
16
|
+
const fn = function (value) {
|
|
17
|
+
const result = validate(value);
|
|
18
|
+
if (!isAsyncFn && isPromiseLike(result)) {
|
|
19
|
+
throw new BakerError(`createRule(${name}): sync rule returned Promise. Declare the validator with async if it is asynchronous.`);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
// .emit() — generates function call code via the refs array
|
|
24
|
+
fn.emit = function (varName, ctx) {
|
|
25
|
+
const i = ctx.addRef(fn);
|
|
26
|
+
return `if(!(${isAsyncFn ? 'await ' : ''}refs[${i}](${varName}))) ${ctx.fail(name)};`;
|
|
27
|
+
};
|
|
28
|
+
const meta = {
|
|
29
|
+
emit: fn.emit,
|
|
30
|
+
ruleName: name,
|
|
31
|
+
isAsync: isAsyncFn,
|
|
32
|
+
};
|
|
33
|
+
if (constraints !== undefined) {
|
|
34
|
+
meta.constraints = constraints;
|
|
35
|
+
}
|
|
36
|
+
if (requiresType !== undefined) {
|
|
37
|
+
meta.requiresType = requiresType;
|
|
38
|
+
}
|
|
39
|
+
defineRuleMetadata(fn, meta);
|
|
40
|
+
return fn;
|
|
41
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EmittableRule, Transformer } from '../types';
|
|
2
|
-
|
|
1
|
+
import type { ClassCtor, EmittableRule, Transformer } from '../types';
|
|
2
|
+
interface ArrayOfMarker {
|
|
3
3
|
readonly [key: symbol]: true;
|
|
4
4
|
readonly rules: EmittableRule[];
|
|
5
5
|
}
|
|
@@ -7,13 +7,15 @@ export interface ArrayOfMarker {
|
|
|
7
7
|
* Apply rules to each element of an array.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
|
-
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* \@Field(arrayOf(isString(), minLength(1)))
|
|
11
12
|
* tags!: string[];
|
|
13
|
+
* ```
|
|
12
14
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
declare function arrayOf(...rules: EmittableRule[]): ArrayOfMarker;
|
|
16
|
+
interface FieldOptions {
|
|
15
17
|
/** Nested DTO type. Thunk — supports circular references. [Dto] for arrays. */
|
|
16
|
-
type?: () =>
|
|
18
|
+
type?: () => ClassCtor | ClassCtor[] | MapConstructor | SetConstructor;
|
|
17
19
|
/** Polymorphic discriminator configuration — used with type */
|
|
18
20
|
discriminator?: {
|
|
19
21
|
property: string;
|
|
@@ -41,7 +43,7 @@ export interface FieldOptions {
|
|
|
41
43
|
/** Groups — field visibility control + conditional validation rule application */
|
|
42
44
|
groups?: string[];
|
|
43
45
|
/** Conditional validation — skip all field validation when false */
|
|
44
|
-
when?: (obj: Record<string,
|
|
46
|
+
when?: (obj: Record<string, unknown>) => boolean;
|
|
45
47
|
/** Transformer or array of transformers (serialize direction applies in reverse order) */
|
|
46
48
|
transform?: Transformer | Transformer[];
|
|
47
49
|
/** Error message on validation failure — applied to all rules of the field (rule's own message takes precedence) */
|
|
@@ -53,17 +55,19 @@ export interface FieldOptions {
|
|
|
53
55
|
/** Error context on validation failure — applied to all rules of the field (rule's own context takes precedence) */
|
|
54
56
|
context?: unknown;
|
|
55
57
|
/** Nested DTO class thunk for Map values — used with type: () => Map */
|
|
56
|
-
mapValue?: () =>
|
|
58
|
+
mapValue?: () => ClassCtor;
|
|
57
59
|
/** Nested DTO class thunk for Set elements — used with type: () => Set */
|
|
58
|
-
setValue?: () =>
|
|
60
|
+
setValue?: () => ClassCtor;
|
|
59
61
|
}
|
|
60
62
|
type RuleArg = EmittableRule | ArrayOfMarker;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
type FieldDecorator = (value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
64
|
+
/** `@Field`() — empty field registration */
|
|
65
|
+
declare function Field(): FieldDecorator;
|
|
66
|
+
/** `@Field`(isString(), email()) — variadic rules */
|
|
67
|
+
declare function Field(...rules: RuleArg[]): FieldDecorator;
|
|
68
|
+
/** `@Field`({ type: () => Dto }) — options object */
|
|
69
|
+
declare function Field(options: FieldOptions): FieldDecorator;
|
|
70
|
+
/** `@Field`(isString(), { optional: true }) — rules + options mixed */
|
|
71
|
+
declare function Field(...rulesAndOptions: [...RuleArg[], FieldOptions]): FieldDecorator;
|
|
72
|
+
export { arrayOf, Field };
|
|
73
|
+
export type { ArrayOfMarker, FieldOptions };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { ensureMeta } from '../collect.js';
|
|
2
|
+
import { BakerError } from '../errors.js';
|
|
3
|
+
import { isAsyncFunction, isPromiseLike } from '../utils.js';
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// arrayOf — Array element validation marker (replaces each: true)
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
const ARRAY_OF = Symbol.for('baker:arrayOf');
|
|
8
|
+
/**
|
|
9
|
+
* Apply rules to each element of an array.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* \@Field(arrayOf(isString(), minLength(1)))
|
|
14
|
+
* tags!: string[];
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function arrayOf(...rules) {
|
|
18
|
+
const marker = { rules, [ARRAY_OF]: true };
|
|
19
|
+
return marker;
|
|
20
|
+
}
|
|
21
|
+
function isArrayOfMarker(arg) {
|
|
22
|
+
return typeof arg === 'object' && arg !== null && arg[ARRAY_OF] === true;
|
|
23
|
+
}
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// FieldOptions detection — distinguish from EmittableRule/ArrayOfMarker
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
const FIELD_OPTION_KEYS = new Set([
|
|
28
|
+
'type',
|
|
29
|
+
'discriminator',
|
|
30
|
+
'keepDiscriminatorProperty',
|
|
31
|
+
'rules',
|
|
32
|
+
'optional',
|
|
33
|
+
'nullable',
|
|
34
|
+
'name',
|
|
35
|
+
'deserializeName',
|
|
36
|
+
'serializeName',
|
|
37
|
+
'exclude',
|
|
38
|
+
'groups',
|
|
39
|
+
'when',
|
|
40
|
+
'transform',
|
|
41
|
+
'message',
|
|
42
|
+
'context',
|
|
43
|
+
'mapValue',
|
|
44
|
+
'setValue',
|
|
45
|
+
]);
|
|
46
|
+
function isFieldOptions(arg) {
|
|
47
|
+
if (typeof arg === 'function') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (typeof arg !== 'object' || arg === null) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (isArrayOfMarker(arg)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// Treat as FieldOptions if at least one known key exists
|
|
57
|
+
const keys = Object.keys(arg);
|
|
58
|
+
if (keys.length === 0) {
|
|
59
|
+
return true;
|
|
60
|
+
} // @Field({})
|
|
61
|
+
return keys.some(k => FIELD_OPTION_KEYS.has(k));
|
|
62
|
+
}
|
|
63
|
+
/** W5: assert that a value is a valid baker rule (has `.emit` fn + `.ruleName` string). */
|
|
64
|
+
function assertRule(value, fieldKey, slot) {
|
|
65
|
+
const loc = slot ? `${fieldKey} ${slot}` : fieldKey;
|
|
66
|
+
const validForms = ` Valid @Field forms: @Field(), @Field(rule, ...), @Field(options), @Field(rule, ..., options).`;
|
|
67
|
+
if (typeof value === 'function') {
|
|
68
|
+
const fn = value;
|
|
69
|
+
if (typeof fn.emit !== 'function' || typeof fn.ruleName !== 'string') {
|
|
70
|
+
const hint = fn.name
|
|
71
|
+
? ` Did you forget to call '${fn.name}()'? Factories must be invoked (e.g., '${fn.name}()'). Rule constants are passed directly (e.g., 'isString' without parentheses).`
|
|
72
|
+
: ` Use createRule() or import a rule from @zipbul/baker/rules.`;
|
|
73
|
+
throw new BakerError(`@Field on ${loc}: argument is not a baker rule.${hint}${validForms}`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw new BakerError(`@Field on ${loc}: expected a baker rule (function with .emit and .ruleName), got ${value === null ? 'null' : typeof value}. Use createRule() or import a rule from @zipbul/baker/rules.${validForms}`);
|
|
78
|
+
}
|
|
79
|
+
/** Normalize 4 overload signatures into `{ rules, options }` */
|
|
80
|
+
function parseFieldArgs(args) {
|
|
81
|
+
if (args.length === 0) {
|
|
82
|
+
// Form 1: @Field()
|
|
83
|
+
return { rules: [], options: {} };
|
|
84
|
+
}
|
|
85
|
+
if (args.length === 1 && isFieldOptions(args[0])) {
|
|
86
|
+
// Form 3: @Field({ type: () => Dto })
|
|
87
|
+
const options = args[0];
|
|
88
|
+
return { rules: options.rules ?? [], options };
|
|
89
|
+
}
|
|
90
|
+
// Form 2 or 4
|
|
91
|
+
const lastArg = args[args.length - 1];
|
|
92
|
+
if (isFieldOptions(lastArg)) {
|
|
93
|
+
// Form 4: @Field(isString(), { optional: true })
|
|
94
|
+
const options = lastArg;
|
|
95
|
+
let rules = args.slice(0, -1);
|
|
96
|
+
if (options.rules) {
|
|
97
|
+
rules = [...rules, ...options.rules];
|
|
98
|
+
}
|
|
99
|
+
return { rules, options };
|
|
100
|
+
}
|
|
101
|
+
// Form 2: @Field(isString(), email())
|
|
102
|
+
return { rules: args, options: {} };
|
|
103
|
+
}
|
|
104
|
+
/** Register validation rules + handle arrayOf */
|
|
105
|
+
function applyValidation(meta, rules, options) {
|
|
106
|
+
for (const rule of rules) {
|
|
107
|
+
if (isArrayOfMarker(rule)) {
|
|
108
|
+
for (const innerRule of rule.rules) {
|
|
109
|
+
const rd = { rule: innerRule, each: true };
|
|
110
|
+
if (options.groups !== undefined) {
|
|
111
|
+
rd.groups = options.groups;
|
|
112
|
+
}
|
|
113
|
+
if (options.message !== undefined) {
|
|
114
|
+
rd.message = options.message;
|
|
115
|
+
}
|
|
116
|
+
if (options.context !== undefined) {
|
|
117
|
+
rd.context = options.context;
|
|
118
|
+
}
|
|
119
|
+
meta.validation.push(rd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const rd = { rule: rule };
|
|
124
|
+
if (options.groups !== undefined) {
|
|
125
|
+
rd.groups = options.groups;
|
|
126
|
+
}
|
|
127
|
+
if (options.message !== undefined) {
|
|
128
|
+
rd.message = options.message;
|
|
129
|
+
}
|
|
130
|
+
if (options.context !== undefined) {
|
|
131
|
+
rd.context = options.context;
|
|
132
|
+
}
|
|
133
|
+
meta.validation.push(rd);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Handle expose 5-branch logic */
|
|
138
|
+
function applyExpose(meta, options) {
|
|
139
|
+
if (options.name) {
|
|
140
|
+
const ed = { name: options.name };
|
|
141
|
+
if (options.groups !== undefined) {
|
|
142
|
+
ed.groups = options.groups;
|
|
143
|
+
}
|
|
144
|
+
meta.expose.push(ed);
|
|
145
|
+
}
|
|
146
|
+
else if (options.deserializeName || options.serializeName) {
|
|
147
|
+
if (options.deserializeName) {
|
|
148
|
+
const ed = { name: options.deserializeName, deserializeOnly: true };
|
|
149
|
+
if (options.groups !== undefined) {
|
|
150
|
+
ed.groups = options.groups;
|
|
151
|
+
}
|
|
152
|
+
meta.expose.push(ed);
|
|
153
|
+
}
|
|
154
|
+
if (options.serializeName) {
|
|
155
|
+
const ed = { name: options.serializeName, serializeOnly: true };
|
|
156
|
+
if (options.groups !== undefined) {
|
|
157
|
+
ed.groups = options.groups;
|
|
158
|
+
}
|
|
159
|
+
meta.expose.push(ed);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (options.groups) {
|
|
163
|
+
meta.expose.push({ groups: options.groups });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
meta.expose.push({});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Register Transformer — split into direction-specific TransformDefs */
|
|
170
|
+
function wrapTransform(propertyKey, direction, fn) {
|
|
171
|
+
const isAsync = isAsyncFunction(fn);
|
|
172
|
+
const wrapped = (params => {
|
|
173
|
+
const result = fn(params);
|
|
174
|
+
if (!isAsync && isPromiseLike(result)) {
|
|
175
|
+
throw new BakerError(`@Field(${propertyKey}) ${direction} transform returned Promise. Declare the transform with async if it is asynchronous.`);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
});
|
|
179
|
+
return { fn: wrapped, isAsync };
|
|
180
|
+
}
|
|
181
|
+
/** Register Transformer — split into direction-specific TransformDefs */
|
|
182
|
+
function applyTransform(meta, propertyKey, options) {
|
|
183
|
+
if (!options.transform) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const transformers = Array.isArray(options.transform) ? options.transform : [options.transform];
|
|
187
|
+
for (const t of transformers) {
|
|
188
|
+
const deserialize = wrapTransform(propertyKey, 'deserialize', t.deserialize);
|
|
189
|
+
const serialize = wrapTransform(propertyKey, 'serialize', t.serialize);
|
|
190
|
+
meta.transform.push({ fn: deserialize.fn, isAsync: deserialize.isAsync, options: { deserializeOnly: true } }, { fn: serialize.fn, isAsync: serialize.isAsync, options: { serializeOnly: true } });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function Field(...args) {
|
|
194
|
+
return (_value, context) => {
|
|
195
|
+
if (context.static) {
|
|
196
|
+
throw new BakerError(`@Field cannot decorate static fields.`);
|
|
197
|
+
}
|
|
198
|
+
if (context.private) {
|
|
199
|
+
throw new BakerError(`@Field cannot decorate private fields.`);
|
|
200
|
+
}
|
|
201
|
+
if (typeof context.name === 'symbol') {
|
|
202
|
+
throw new BakerError(`@Field: symbol property keys are not supported. Use a string property name.`);
|
|
203
|
+
}
|
|
204
|
+
const propertyKey = context.name;
|
|
205
|
+
const meta = ensureMeta(context.metadata, propertyKey);
|
|
206
|
+
const { rules, options } = parseFieldArgs(args);
|
|
207
|
+
// `name` is bidirectional; `deserializeName`/`serializeName` are per-direction. Combining them
|
|
208
|
+
// is contradictory — reject it instead of silently dropping the per-direction names. Truthiness
|
|
209
|
+
// matches applyExpose: an empty-string name is treated as "no name" consistently throughout.
|
|
210
|
+
if (options.name && (options.deserializeName || options.serializeName)) {
|
|
211
|
+
throw new BakerError(`@Field on ${propertyKey}: 'name' cannot be combined with 'deserializeName'/'serializeName'. Use one or the other.`);
|
|
212
|
+
}
|
|
213
|
+
// W5: validate each rule shape — `.emit` function + `.ruleName` string required.
|
|
214
|
+
// Catches D2/D4: `@Field(isString())` (boolean), `@Field(isNumber)` (factory unstamped), `@Field(() => true)`.
|
|
215
|
+
for (let i = 0; i < rules.length; i++) {
|
|
216
|
+
const r = rules[i];
|
|
217
|
+
if (isArrayOfMarker(r)) {
|
|
218
|
+
for (let j = 0; j < r.rules.length; j++) {
|
|
219
|
+
assertRule(r.rules[j], propertyKey, `arrayOf[${j}]`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
assertRule(r, propertyKey);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
applyValidation(meta, rules, options);
|
|
227
|
+
// ── flags ──
|
|
228
|
+
if (options.optional) {
|
|
229
|
+
meta.flags.isOptional = true;
|
|
230
|
+
}
|
|
231
|
+
if (options.nullable) {
|
|
232
|
+
meta.flags.isNullable = true;
|
|
233
|
+
}
|
|
234
|
+
if (options.when) {
|
|
235
|
+
meta.flags.validateIf = options.when;
|
|
236
|
+
}
|
|
237
|
+
// ── type (nested DTO + discriminator + collection) ──
|
|
238
|
+
if (options.type) {
|
|
239
|
+
const td = { fn: options.type };
|
|
240
|
+
if (options.discriminator !== undefined) {
|
|
241
|
+
td.discriminator = options.discriminator;
|
|
242
|
+
}
|
|
243
|
+
if (options.keepDiscriminatorProperty !== undefined) {
|
|
244
|
+
td.keepDiscriminatorProperty = options.keepDiscriminatorProperty;
|
|
245
|
+
}
|
|
246
|
+
const cv = options.mapValue ?? options.setValue;
|
|
247
|
+
if (cv !== undefined) {
|
|
248
|
+
td.collectionValue = cv;
|
|
249
|
+
}
|
|
250
|
+
meta.type = td;
|
|
251
|
+
}
|
|
252
|
+
applyExpose(meta, options);
|
|
253
|
+
// ── exclude ──
|
|
254
|
+
if (options.exclude) {
|
|
255
|
+
if (options.exclude === true) {
|
|
256
|
+
meta.exclude = {};
|
|
257
|
+
}
|
|
258
|
+
else if (options.exclude === 'deserializeOnly') {
|
|
259
|
+
meta.exclude = { deserializeOnly: true };
|
|
260
|
+
}
|
|
261
|
+
else if (options.exclude === 'serializeOnly') {
|
|
262
|
+
meta.exclude = { serializeOnly: true };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
applyTransform(meta, propertyKey, options);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export { arrayOf, Field };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export { Field, arrayOf } from './field.js';
|
|
2
|
+
export { Recipe } from './recipe.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
|
|
3
|
+
*
|
|
4
|
+
* Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
|
|
5
|
+
* the owning class. `@Recipe` runs after the field decorators and registers the class itself.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* \@Recipe
|
|
10
|
+
* class UserDto {
|
|
11
|
+
* \@Field(isString()) name!: string;
|
|
12
|
+
* }
|
|
13
|
+
* seal();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare function Recipe<T extends Function>(value: T, _context: ClassDecoratorContext): void;
|
|
17
|
+
export { Recipe };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { globalRegistry } from '../registry.js';
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// @Recipe — class decorator that registers a DTO for argless seal()
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
|
|
7
|
+
*
|
|
8
|
+
* Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
|
|
9
|
+
* the owning class. `@Recipe` runs after the field decorators and registers the class itself.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* \@Recipe
|
|
14
|
+
* class UserDto {
|
|
15
|
+
* \@Field(isString()) name!: string;
|
|
16
|
+
* }
|
|
17
|
+
* seal();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function Recipe(value, _context) {
|
|
21
|
+
globalRegistry.add(value);
|
|
22
|
+
}
|
|
23
|
+
export { Recipe };
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - 'conversionFailed': when type conversion fails in enableImplicitConversion
|
|
10
10
|
* - 'whitelistViolation': when undeclared fields exist in input with whitelist: true
|
|
11
11
|
*
|
|
12
|
-
* Future extension fields (
|
|
12
|
+
* Future extension fields (expected, actual, etc.) must be added as Optional.
|
|
13
13
|
*/
|
|
14
|
-
export interface
|
|
14
|
+
export interface BakerIssue {
|
|
15
15
|
readonly path: string;
|
|
16
16
|
readonly code: string;
|
|
17
17
|
/** User-defined error message — included only when the decorator message option is set */
|
|
@@ -19,32 +19,43 @@ export interface BakerError {
|
|
|
19
19
|
/** User-defined context — included only when the decorator context option is set */
|
|
20
20
|
readonly context?: unknown;
|
|
21
21
|
}
|
|
22
|
-
/** Symbol tag for
|
|
22
|
+
/** Symbol tag for isBakerIssueSet() type guard — collision-proof discriminator */
|
|
23
23
|
export declare const BAKER_ERROR: unique symbol;
|
|
24
|
-
/** Validation failure — returned by deserialize() on invalid input */
|
|
25
|
-
export interface
|
|
24
|
+
/** Validation failure — returned by deserialize()/validate() on invalid input */
|
|
25
|
+
export interface BakerIssueSet {
|
|
26
26
|
readonly [BAKER_ERROR]: true;
|
|
27
|
-
readonly errors: readonly
|
|
27
|
+
readonly errors: readonly BakerIssue[];
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Type guard — narrows deserialize() result to
|
|
30
|
+
* Type guard — narrows deserialize()/validate() result to BakerIssueSet.
|
|
31
31
|
*
|
|
32
32
|
* @example
|
|
33
33
|
* const result = await deserialize(UserDto, input);
|
|
34
|
-
* if (
|
|
35
|
-
* result.errors // readonly
|
|
34
|
+
* if (isBakerIssueSet(result)) {
|
|
35
|
+
* result.errors // readonly BakerIssue[]
|
|
36
36
|
* } else {
|
|
37
37
|
* result // UserDto
|
|
38
38
|
* }
|
|
39
39
|
*/
|
|
40
|
-
export declare function
|
|
41
|
-
/** @internal — create
|
|
42
|
-
export declare function
|
|
40
|
+
export declare function isBakerIssueSet(value: unknown): value is BakerIssueSet;
|
|
41
|
+
/** @internal — create BakerIssueSet object */
|
|
42
|
+
export declare function toBakerIssueSet(errors: BakerIssue[]): BakerIssueSet;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* -
|
|
46
|
-
*
|
|
44
|
+
* The single error thrown by baker for any developer/config/schema misuse — i.e. anything
|
|
45
|
+
* discoverable without external input. End-user input-data failures are NOT thrown; they are
|
|
46
|
+
* returned as a {@link BakerIssueSet}.
|
|
47
|
+
*
|
|
48
|
+
* Thrown when, e.g.:
|
|
49
|
+
* - deserialize()/serialize()/validate() is called on an unsealed class
|
|
50
|
+
* - configure() is called after seal(), or with an unknown key
|
|
51
|
+
* - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
|
|
52
|
+
* - per-call options contain unsupported keys
|
|
53
|
+
* - @Field receives a non-rule value, or a rule/transformer factory is misused
|
|
54
|
+
* - a user @Type/collectionValue thunk throws (wrapped, with the original error as `cause`)
|
|
55
|
+
* - an optional peer dependency (luxon/moment) is missing
|
|
47
56
|
*/
|
|
48
|
-
export declare class
|
|
49
|
-
constructor(message: string
|
|
57
|
+
export declare class BakerError extends Error {
|
|
58
|
+
constructor(message: string, options?: {
|
|
59
|
+
cause?: unknown;
|
|
60
|
+
});
|
|
50
61
|
}
|