@fluojs/validation 1.0.5 → 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.ko.md +47 -1
- package/README.md +48 -1
- package/dist/decorators.d.ts +71 -69
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +73 -133
- package/dist/internal/decorator-factories.d.ts +12 -0
- package/dist/internal/decorator-factories.d.ts.map +1 -0
- package/dist/internal/decorator-factories.js +32 -0
- package/dist/internal/decorator-metadata.d.ts +7 -0
- package/dist/internal/decorator-metadata.d.ts.map +1 -0
- package/dist/internal/decorator-metadata.js +38 -0
- package/dist/internal/dto-materialization.d.ts +45 -0
- package/dist/internal/dto-materialization.d.ts.map +1 -0
- package/dist/internal/dto-materialization.js +204 -0
- package/dist/internal/dto-metadata-cache.d.ts +17 -0
- package/dist/internal/dto-metadata-cache.d.ts.map +1 -0
- package/dist/internal/dto-metadata-cache.js +49 -0
- package/dist/internal/enum-values.d.ts +2 -0
- package/dist/internal/enum-values.d.ts.map +1 -0
- package/dist/internal/enum-values.js +16 -0
- package/dist/internal/object-utils.d.ts +30 -0
- package/dist/internal/object-utils.d.ts.map +1 -0
- package/dist/internal/object-utils.js +53 -0
- package/dist/internal/rule-handlers.d.ts +19 -0
- package/dist/internal/rule-handlers.d.ts.map +1 -0
- package/dist/internal/rule-handlers.js +209 -0
- package/dist/internal/validation-issues.d.ts +13 -0
- package/dist/internal/validation-issues.d.ts.map +1 -0
- package/dist/internal/validation-issues.js +44 -0
- package/dist/internal/validator-js-adapter.d.ts +5 -0
- package/dist/internal/validator-js-adapter.d.ts.map +1 -0
- package/dist/internal/validator-js-adapter.js +88 -0
- package/dist/mapped-types.d.ts +1 -1
- package/dist/mapped-types.d.ts.map +1 -1
- package/dist/mapped-types.js +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/types.d.ts +18 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +2 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +24 -509
- package/package.json +4 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { appendStandardDtoValidationRule } from './decorator-metadata.js';
|
|
2
|
+
export function createValidationDecorator(ruleFactory) {
|
|
3
|
+
const decorator = (_value, context) => {
|
|
4
|
+
appendStandardDtoValidationRule(context.metadata, context.name, ruleFactory());
|
|
5
|
+
};
|
|
6
|
+
return decorator;
|
|
7
|
+
}
|
|
8
|
+
export function createValidationOptionsWithConfigDecorator(ruleFactory) {
|
|
9
|
+
return (value, options) => {
|
|
10
|
+
return createValidationDecorator(() => ruleFactory(value, options));
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function createFlagValidationDecorator(ruleFactory) {
|
|
14
|
+
return options => {
|
|
15
|
+
return createValidationDecorator(() => ruleFactory(options));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createArrayValidationDecorator(ruleFactory) {
|
|
19
|
+
return (values, options) => {
|
|
20
|
+
return createValidationDecorator(() => ruleFactory(values, options));
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createValidatorJsDecorator(validator) {
|
|
24
|
+
return (args, options) => {
|
|
25
|
+
return createValidationDecorator(() => ({
|
|
26
|
+
args,
|
|
27
|
+
kind: 'validatorjs',
|
|
28
|
+
validator,
|
|
29
|
+
...options
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MetadataPropertyKey } from '@fluojs/core';
|
|
2
|
+
import type { ClassValidationRule, DtoFieldValidationRule } from '@fluojs/core/request-pipeline';
|
|
3
|
+
export type ClassDecoratorFn = (value: Function, context: ClassDecoratorContext) => void;
|
|
4
|
+
export type FieldDecoratorFn = <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>) => void;
|
|
5
|
+
export declare function appendStandardDtoValidationRule(metadata: unknown, propertyKey: MetadataPropertyKey, rule: DtoFieldValidationRule): void;
|
|
6
|
+
export declare function appendStandardClassValidationRule(metadata: unknown, rule: ClassValidationRule): void;
|
|
7
|
+
//# sourceMappingURL=decorator-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorator-metadata.d.ts","sourceRoot":"","sources":["../../src/internal/decorator-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEjG,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AACzF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AA8CzH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,OAAO,EACjB,WAAW,EAAE,mBAAmB,EAChC,IAAI,EAAE,sBAAsB,GAC3B,IAAI,CAGN;AAED,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAEpG"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const standardDtoValidationMetadataKey = Symbol.for('fluo.standard.dto-validation');
|
|
2
|
+
const standardClassValidationMetadataKey = Symbol.for('fluo.standard.class-validation');
|
|
3
|
+
function getStandardMetadataBag(metadata) {
|
|
4
|
+
if (metadata === null || metadata === undefined) {
|
|
5
|
+
throw new Error('Decorator metadata is not available. Ensure your environment supports TC39 decorator metadata (Stage 3).');
|
|
6
|
+
}
|
|
7
|
+
return metadata;
|
|
8
|
+
}
|
|
9
|
+
function getStandardDtoValidationMap(metadata) {
|
|
10
|
+
const bag = getStandardMetadataBag(metadata);
|
|
11
|
+
const current = bag[standardDtoValidationMetadataKey];
|
|
12
|
+
if (current && Object.hasOwn(bag, standardDtoValidationMetadataKey)) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
const created = new Map();
|
|
16
|
+
for (const [propertyKey, rules] of current ?? []) {
|
|
17
|
+
created.set(propertyKey, [...rules]);
|
|
18
|
+
}
|
|
19
|
+
bag[standardDtoValidationMetadataKey] = created;
|
|
20
|
+
return created;
|
|
21
|
+
}
|
|
22
|
+
function getStandardClassValidationList(metadata) {
|
|
23
|
+
const bag = getStandardMetadataBag(metadata);
|
|
24
|
+
const current = bag[standardClassValidationMetadataKey];
|
|
25
|
+
if (current && Object.hasOwn(bag, standardClassValidationMetadataKey)) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
const created = [...(current ?? [])];
|
|
29
|
+
bag[standardClassValidationMetadataKey] = created;
|
|
30
|
+
return created;
|
|
31
|
+
}
|
|
32
|
+
export function appendStandardDtoValidationRule(metadata, propertyKey, rule) {
|
|
33
|
+
const map = getStandardDtoValidationMap(metadata);
|
|
34
|
+
map.set(propertyKey, [...(map.get(propertyKey) ?? []), rule]);
|
|
35
|
+
}
|
|
36
|
+
export function appendStandardClassValidationRule(metadata, rule) {
|
|
37
|
+
getStandardClassValidationList(metadata).push(rule);
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Constructor } from '@fluojs/core';
|
|
2
|
+
import type { MaterializeOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Carries invocation-local DTO traversal state across materialization and validation.
|
|
5
|
+
*/
|
|
6
|
+
export interface NestedTraversalContext {
|
|
7
|
+
readonly active: WeakSet<object>;
|
|
8
|
+
readonly declaredNestedKeys?: WeakMap<object, ReadonlySet<PropertyKey>>;
|
|
9
|
+
readonly hydrateExistingInstances?: boolean;
|
|
10
|
+
readonly undeclaredProperties?: MaterializeOptions['undeclaredProperties'];
|
|
11
|
+
readonly materialized?: WeakMap<object, WeakMap<Constructor, object>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Enters a traversal value when it is not already active.
|
|
15
|
+
*
|
|
16
|
+
* @param value Candidate traversal value.
|
|
17
|
+
* @param context Invocation-local traversal state.
|
|
18
|
+
* @returns Whether traversal may continue for the value.
|
|
19
|
+
*/
|
|
20
|
+
export declare function enterTraversal(value: unknown, context?: NestedTraversalContext): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Releases a traversal value after materialization or validation completes.
|
|
23
|
+
*
|
|
24
|
+
* @param value Traversal value to release.
|
|
25
|
+
* @param context Invocation-local traversal state.
|
|
26
|
+
*/
|
|
27
|
+
export declare function exitTraversal(value: unknown, context?: NestedTraversalContext): void;
|
|
28
|
+
/**
|
|
29
|
+
* Creates or reuses a nested DTO instance for a raw value.
|
|
30
|
+
*
|
|
31
|
+
* @param target Nested DTO constructor.
|
|
32
|
+
* @param rawValue Raw nested value.
|
|
33
|
+
* @param context Invocation-local traversal state.
|
|
34
|
+
* @param fieldPrefix Parent path used for undeclared-property issues.
|
|
35
|
+
* @returns The materialized DTO value or the original unsupported value.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createNestedDtoInstance<T>(target: Constructor<T>, rawValue: unknown, context?: NestedTraversalContext, fieldPrefix?: string): T;
|
|
38
|
+
/**
|
|
39
|
+
* Asserts that a root value can be validated as the requested DTO.
|
|
40
|
+
*
|
|
41
|
+
* @param value Root value to validate.
|
|
42
|
+
* @param target Requested DTO constructor.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertValidRootValue(value: unknown, target: Constructor): void;
|
|
45
|
+
//# sourceMappingURL=dto-materialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto-materialization.d.ts","sourceRoot":"","sources":["../../src/internal/dto-materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAUvE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;IACxE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,CAAC,oBAAoB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;IAC3E,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;CACvE;AAwED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAWxF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAUpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EACtB,QAAQ,EAAE,OAAO,EACjB,OAAO,CAAC,EAAE,sBAAsB,EAChC,WAAW,CAAC,EAAE,MAAM,GACnB,CAAC,CA4FH;AAyDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAM9E"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DtoValidationError } from '../errors.js';
|
|
2
|
+
import { getCachedDtoMetadata } from './dto-metadata-cache.js';
|
|
3
|
+
import { assignSafeOwnEnumerableProperties, getIterableValues, isPlainObject, isSafeOwnEnumerableProperty } from './object-utils.js';
|
|
4
|
+
import { joinFieldPath } from './validation-issues.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Carries invocation-local DTO traversal state across materialization and validation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const materializationCycleTarget = class {};
|
|
11
|
+
function getMaterializedInstance(rawValue, target, context) {
|
|
12
|
+
return context?.materialized?.get(rawValue)?.get(target);
|
|
13
|
+
}
|
|
14
|
+
function rememberMaterializedInstance(rawValue, target, instance, context) {
|
|
15
|
+
if (!context?.materialized) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let instancesByTarget = context.materialized.get(rawValue);
|
|
19
|
+
if (!instancesByTarget) {
|
|
20
|
+
instancesByTarget = new WeakMap();
|
|
21
|
+
context.materialized.set(rawValue, instancesByTarget);
|
|
22
|
+
}
|
|
23
|
+
instancesByTarget.set(target, instance);
|
|
24
|
+
}
|
|
25
|
+
function canMaterialize(value, target) {
|
|
26
|
+
return value instanceof target || isPlainObject(value);
|
|
27
|
+
}
|
|
28
|
+
function collectDeclaredKeys(instance, target) {
|
|
29
|
+
const metadata = getCachedDtoMetadata(target);
|
|
30
|
+
return new Set([...Reflect.ownKeys(instance), ...metadata.mergedPropertyKeys, ...Array.from(metadata.bindingMap.values()).flatMap(binding => binding.key === undefined ? [] : [binding.key])]);
|
|
31
|
+
}
|
|
32
|
+
function rememberConflictingNestedKeys(value, targets, context) {
|
|
33
|
+
if (!context?.declaredNestedKeys) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const declaredKeys = new Set();
|
|
37
|
+
for (const target of targets) {
|
|
38
|
+
for (const key of collectDeclaredKeys(new target(), target)) {
|
|
39
|
+
declaredKeys.add(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const entry of getIterableValues(value) ?? [value]) {
|
|
43
|
+
if (isPlainObject(entry)) {
|
|
44
|
+
context.declaredNestedKeys.set(entry, declaredKeys);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enters a traversal value when it is not already active.
|
|
51
|
+
*
|
|
52
|
+
* @param value Candidate traversal value.
|
|
53
|
+
* @param context Invocation-local traversal state.
|
|
54
|
+
* @returns Whether traversal may continue for the value.
|
|
55
|
+
*/
|
|
56
|
+
export function enterTraversal(value, context) {
|
|
57
|
+
if (!context || typeof value !== 'object' || value === null) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (context.active.has(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
context.active.add(value);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Releases a traversal value after materialization or validation completes.
|
|
69
|
+
*
|
|
70
|
+
* @param value Traversal value to release.
|
|
71
|
+
* @param context Invocation-local traversal state.
|
|
72
|
+
*/
|
|
73
|
+
export function exitTraversal(value, context) {
|
|
74
|
+
if (!context || typeof value !== 'object' || value === null) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (context.materialized?.get(value)?.has(materializationCycleTarget)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
context.active.delete(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates or reuses a nested DTO instance for a raw value.
|
|
85
|
+
*
|
|
86
|
+
* @param target Nested DTO constructor.
|
|
87
|
+
* @param rawValue Raw nested value.
|
|
88
|
+
* @param context Invocation-local traversal state.
|
|
89
|
+
* @param fieldPrefix Parent path used for undeclared-property issues.
|
|
90
|
+
* @returns The materialized DTO value or the original unsupported value.
|
|
91
|
+
*/
|
|
92
|
+
export function createNestedDtoInstance(target, rawValue, context, fieldPrefix) {
|
|
93
|
+
if (rawValue instanceof target && context?.hydrateExistingInstances !== true) {
|
|
94
|
+
return rawValue;
|
|
95
|
+
}
|
|
96
|
+
if (!canMaterialize(rawValue, target)) {
|
|
97
|
+
return rawValue;
|
|
98
|
+
}
|
|
99
|
+
const rawObject = rawValue;
|
|
100
|
+
const rememberedInstance = getMaterializedInstance(rawObject, target, context);
|
|
101
|
+
if (rememberedInstance) {
|
|
102
|
+
return rememberedInstance;
|
|
103
|
+
}
|
|
104
|
+
if (context?.active.has(rawObject)) {
|
|
105
|
+
rememberMaterializedInstance(rawObject, materializationCycleTarget, rawObject, context);
|
|
106
|
+
return rawValue;
|
|
107
|
+
}
|
|
108
|
+
const instance = rawValue instanceof target ? rawValue : new target();
|
|
109
|
+
rememberMaterializedInstance(rawObject, target, instance, context);
|
|
110
|
+
if (!enterTraversal(rawValue, context)) {
|
|
111
|
+
return rawValue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const metadata = getCachedDtoMetadata(target);
|
|
115
|
+
if (isPlainObject(rawValue)) {
|
|
116
|
+
if (context?.undeclaredProperties === 'reject') {
|
|
117
|
+
const declaredKeys = collectDeclaredKeys(instance, target);
|
|
118
|
+
const declaredNestedKeys = context.declaredNestedKeys?.get(rawValue);
|
|
119
|
+
if (declaredNestedKeys) {
|
|
120
|
+
for (const key of declaredNestedKeys) {
|
|
121
|
+
declaredKeys.add(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const issues = Reflect.ownKeys(rawValue).filter(key => isSafeOwnEnumerableProperty(rawValue, key) && !declaredKeys.has(key)).map(key => {
|
|
125
|
+
const field = fieldPrefix ? joinFieldPath(fieldPrefix, String(key)) : String(key);
|
|
126
|
+
return {
|
|
127
|
+
code: 'UNDECLARED_PROPERTY',
|
|
128
|
+
field,
|
|
129
|
+
message: `${field} is not declared by the DTO.`
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
if (issues.length > 0) {
|
|
133
|
+
throw new DtoValidationError('Validation failed.', issues);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
assignSafeOwnEnumerableProperties(instance, rawValue);
|
|
137
|
+
for (const propertyKey of metadata.mergedPropertyKeys) {
|
|
138
|
+
const sourceKey = metadata.bindingMap.get(propertyKey)?.key;
|
|
139
|
+
if (!sourceKey) continue;
|
|
140
|
+
instance[propertyKey] = rawValue[sourceKey];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const nestedEntry of metadata.nestedDtoTransforms) {
|
|
144
|
+
const nestedTargets = metadata.nestedDtoTransforms.filter(candidate => candidate.propertyKey === nestedEntry.propertyKey).map(candidate => candidate.target);
|
|
145
|
+
const hasConflictingTarget = nestedTargets.some(candidate => candidate !== nestedEntry.target);
|
|
146
|
+
if (hasConflictingTarget) {
|
|
147
|
+
rememberConflictingNestedKeys(instance[nestedEntry.propertyKey], nestedTargets, context);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const currentValue = instance[nestedEntry.propertyKey];
|
|
151
|
+
if (currentValue === undefined || currentValue === null) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const fieldPath = fieldPrefix ? joinFieldPath(fieldPrefix, String(nestedEntry.propertyKey)) : String(nestedEntry.propertyKey);
|
|
155
|
+
instance[nestedEntry.propertyKey] = transformNestedCollectionValue(currentValue, nestedEntry.target, context, fieldPath);
|
|
156
|
+
}
|
|
157
|
+
return instance;
|
|
158
|
+
} finally {
|
|
159
|
+
exitTraversal(rawValue, context);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function transformNestedValue(value, target, context, fieldPrefix) {
|
|
163
|
+
if (value === undefined || value === null) {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
if (value instanceof target && context?.hydrateExistingInstances !== true) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
if (!(value instanceof target) && !isPlainObject(value)) {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
return createNestedDtoInstance(target, value, context, fieldPrefix);
|
|
173
|
+
}
|
|
174
|
+
function transformNestedCollectionValue(value, target, context, fieldPrefix) {
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
return value.map((item, index) => transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`));
|
|
177
|
+
}
|
|
178
|
+
if (value instanceof Set) {
|
|
179
|
+
return new Set(Array.from(value.values(), (item, index) => transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`)));
|
|
180
|
+
}
|
|
181
|
+
if (value instanceof Map) {
|
|
182
|
+
return new Map(Array.from(value.entries(), ([key, item], index) => [key, transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`)]));
|
|
183
|
+
}
|
|
184
|
+
return transformNestedValue(value, target, context, fieldPrefix);
|
|
185
|
+
}
|
|
186
|
+
function buildInvalidRootIssue() {
|
|
187
|
+
return {
|
|
188
|
+
code: 'INVALID_DTO',
|
|
189
|
+
message: 'DTO root value must be a plain object.'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Asserts that a root value can be validated as the requested DTO.
|
|
195
|
+
*
|
|
196
|
+
* @param value Root value to validate.
|
|
197
|
+
* @param target Requested DTO constructor.
|
|
198
|
+
*/
|
|
199
|
+
export function assertValidRootValue(value, target) {
|
|
200
|
+
if (value instanceof target || isPlainObject(value)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
|
|
204
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Constructor, MetadataPropertyKey } from '@fluojs/core';
|
|
2
|
+
import { type DtoFieldBindingMetadata, getClassValidationRules, getDtoValidationSchema } from '@fluojs/core/request-pipeline';
|
|
3
|
+
export declare function resolveNestedDto(dto: Constructor | (() => Constructor)): Constructor;
|
|
4
|
+
type DtoValidationSchema = ReturnType<typeof getDtoValidationSchema>;
|
|
5
|
+
export interface CachedDtoMetadata {
|
|
6
|
+
bindingMap: Map<MetadataPropertyKey, DtoFieldBindingMetadata>;
|
|
7
|
+
classValidationRules: ReturnType<typeof getClassValidationRules>;
|
|
8
|
+
dtoValidationSchema: DtoValidationSchema;
|
|
9
|
+
mergedPropertyKeys: Set<MetadataPropertyKey>;
|
|
10
|
+
nestedDtoTransforms: readonly {
|
|
11
|
+
propertyKey: MetadataPropertyKey;
|
|
12
|
+
target: Constructor;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export declare function getCachedDtoMetadata(target: Constructor): CachedDtoMetadata;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=dto-metadata-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto-metadata-cache.d.ts","sourceRoot":"","sources":["../../src/internal/dto-metadata-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EACL,KAAK,uBAAuB,EAC5B,uBAAuB,EAEvB,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AAMvC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,GAAG,WAAW,CAMpF;AAED,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,GAAG,CAAC,mBAAmB,EAAE,uBAAuB,CAAC,CAAC;IAC9D,oBAAoB,EAAE,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;IACjE,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,kBAAkB,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS;QAC5B,WAAW,EAAE,mBAAmB,CAAC;QACjC,MAAM,EAAE,WAAW,CAAC;KACrB,EAAE,CAAC;CACL;AA6BD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAyB3E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/request-pipeline';
|
|
2
|
+
function isClassConstructor(dto) {
|
|
3
|
+
return typeof dto === 'function' && Function.prototype.toString.call(dto).startsWith('class ');
|
|
4
|
+
}
|
|
5
|
+
export function resolveNestedDto(dto) {
|
|
6
|
+
if (isClassConstructor(dto)) {
|
|
7
|
+
return dto;
|
|
8
|
+
}
|
|
9
|
+
return dto();
|
|
10
|
+
}
|
|
11
|
+
const dtoMetadataCache = new WeakMap();
|
|
12
|
+
function getDtoBindingMap(target) {
|
|
13
|
+
return new Map(getDtoBindingSchema(target).map(entry => [entry.propertyKey, entry.metadata]));
|
|
14
|
+
}
|
|
15
|
+
function collectNestedDtoTransforms(dtoValidationSchema) {
|
|
16
|
+
const nestedEntries = [];
|
|
17
|
+
for (const entry of dtoValidationSchema) {
|
|
18
|
+
for (const rule of entry.rules) {
|
|
19
|
+
if (rule.kind !== 'nested') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
nestedEntries.push({
|
|
23
|
+
propertyKey: entry.propertyKey,
|
|
24
|
+
target: resolveNestedDto(rule.dto)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return nestedEntries;
|
|
29
|
+
}
|
|
30
|
+
export function getCachedDtoMetadata(target) {
|
|
31
|
+
const cached = dtoMetadataCache.get(target);
|
|
32
|
+
if (cached) {
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
const bindingMap = getDtoBindingMap(target);
|
|
36
|
+
const dtoValidationSchema = getDtoValidationSchema(target);
|
|
37
|
+
const classValidationRules = getClassValidationRules(target);
|
|
38
|
+
const mergedPropertyKeys = new Set([...bindingMap.keys(), ...dtoValidationSchema.map(entry => entry.propertyKey)]);
|
|
39
|
+
const nestedDtoTransforms = collectNestedDtoTransforms(dtoValidationSchema);
|
|
40
|
+
const next = {
|
|
41
|
+
bindingMap,
|
|
42
|
+
classValidationRules,
|
|
43
|
+
dtoValidationSchema,
|
|
44
|
+
mergedPropertyKeys,
|
|
45
|
+
nestedDtoTransforms
|
|
46
|
+
};
|
|
47
|
+
dtoMetadataCache.set(target, next);
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enum-values.d.ts","sourceRoot":"","sources":["../../src/internal/enum-values.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,CAuB5G"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function normalizeEnumValues(values) {
|
|
2
|
+
if (Array.isArray(values)) {
|
|
3
|
+
return values;
|
|
4
|
+
}
|
|
5
|
+
const entries = Object.entries(values);
|
|
6
|
+
const ownMembers = new Map(entries);
|
|
7
|
+
const normalized = new Set();
|
|
8
|
+
for (const [key, value] of entries) {
|
|
9
|
+
const numericValue = Number(key);
|
|
10
|
+
const hasForwardMember = typeof value === 'string' && ownMembers.has(value);
|
|
11
|
+
const forwardValue = typeof value === 'string' ? ownMembers.get(value) : undefined;
|
|
12
|
+
const isReverseEntry = typeof value === 'string' && key === String(numericValue) && (typeof forwardValue === 'number' && key === String(forwardValue) || value === '__proto__' && !hasForwardMember && Number.isFinite(numericValue));
|
|
13
|
+
normalized.add(isReverseEntry ? numericValue : value);
|
|
14
|
+
}
|
|
15
|
+
return [...normalized];
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns collection members for supported nested collection values.
|
|
3
|
+
*
|
|
4
|
+
* @param value Candidate collection value.
|
|
5
|
+
* @returns Collection members, or `undefined` for unsupported values.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getIterableValues(value: unknown): unknown[] | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Identifies plain records accepted at DTO boundaries.
|
|
10
|
+
*
|
|
11
|
+
* @param value Candidate record value.
|
|
12
|
+
* @returns Whether the value has an object or null prototype.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isPlainObject(value: unknown): value is Record<PropertyKey, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Identifies an enumerable own property that is safe to copy onto a DTO.
|
|
17
|
+
*
|
|
18
|
+
* @param source Source object.
|
|
19
|
+
* @param key Candidate property key.
|
|
20
|
+
* @returns Whether the property is own, enumerable, and not prototype-sensitive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isSafeOwnEnumerableProperty(source: object, key: PropertyKey): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Copies safe enumerable own properties between records.
|
|
25
|
+
*
|
|
26
|
+
* @param target Destination record.
|
|
27
|
+
* @param source Source record.
|
|
28
|
+
*/
|
|
29
|
+
export declare function assignSafeOwnEnumerableProperties(target: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>): void;
|
|
30
|
+
//# sourceMappingURL=object-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-utils.d.ts","sourceRoot":"","sources":["../../src/internal/object-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,SAAS,CAKvE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAOnF;AAID;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAGrF;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,GACnC,IAAI,CAQN"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns collection members for supported nested collection values.
|
|
3
|
+
*
|
|
4
|
+
* @param value Candidate collection value.
|
|
5
|
+
* @returns Collection members, or `undefined` for unsupported values.
|
|
6
|
+
*/
|
|
7
|
+
export function getIterableValues(value) {
|
|
8
|
+
if (Array.isArray(value)) return value;
|
|
9
|
+
if (value instanceof Set) return Array.from(value.values());
|
|
10
|
+
if (value instanceof Map) return Array.from(value.values());
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Identifies plain records accepted at DTO boundaries.
|
|
16
|
+
*
|
|
17
|
+
* @param value Candidate record value.
|
|
18
|
+
* @returns Whether the value has an object or null prototype.
|
|
19
|
+
*/
|
|
20
|
+
export function isPlainObject(value) {
|
|
21
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const prototype = Object.getPrototypeOf(value);
|
|
25
|
+
return prototype === Object.prototype || prototype === null;
|
|
26
|
+
}
|
|
27
|
+
const dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Identifies an enumerable own property that is safe to copy onto a DTO.
|
|
31
|
+
*
|
|
32
|
+
* @param source Source object.
|
|
33
|
+
* @param key Candidate property key.
|
|
34
|
+
* @returns Whether the property is own, enumerable, and not prototype-sensitive.
|
|
35
|
+
*/
|
|
36
|
+
export function isSafeOwnEnumerableProperty(source, key) {
|
|
37
|
+
return !(typeof key === 'string' && dangerousKeys.has(key)) && Object.prototype.propertyIsEnumerable.call(source, key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copies safe enumerable own properties between records.
|
|
42
|
+
*
|
|
43
|
+
* @param target Destination record.
|
|
44
|
+
* @param source Source record.
|
|
45
|
+
*/
|
|
46
|
+
export function assignSafeOwnEnumerableProperties(target, source) {
|
|
47
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
48
|
+
if (!isSafeOwnEnumerableProperty(source, key)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
target[key] = source[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DtoFieldValidationRule } from '@fluojs/core/request-pipeline';
|
|
2
|
+
type RuleKind = DtoFieldValidationRule['kind'];
|
|
3
|
+
export type NonCustomRule = Exclude<DtoFieldValidationRule, {
|
|
4
|
+
kind: 'custom' | 'nested';
|
|
5
|
+
}>;
|
|
6
|
+
type RuleHandler<K extends RuleKind> = {
|
|
7
|
+
defaultCode: string;
|
|
8
|
+
describe: (field: string, rule: Extract<DtoFieldValidationRule, {
|
|
9
|
+
kind: K;
|
|
10
|
+
}>) => string;
|
|
11
|
+
validate: (rule: Extract<DtoFieldValidationRule, {
|
|
12
|
+
kind: K;
|
|
13
|
+
}>, value: unknown) => boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function getRuleHandler<K extends RuleKind>(rule: Extract<DtoFieldValidationRule, {
|
|
16
|
+
kind: K;
|
|
17
|
+
}>): RuleHandler<K>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=rule-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-handlers.d.ts","sourceRoot":"","sources":["../../src/internal/rule-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAK5E,KAAK,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,sBAAsB,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAE,CAAC,CAAC;AAE3F,KAAK,WAAW,CAAC,CAAC,SAAS,QAAQ,IAAI;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,sBAAsB,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAAK,MAAM,CAAC;IACxF,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,sBAAsB,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CAC3F,CAAC;AAgNF,wBAAgB,cAAc,CAAC,CAAC,SAAS,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,sBAAsB,EAAE;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAErH"}
|