@fluojs/validation 1.0.0-beta.1

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.
@@ -0,0 +1,207 @@
1
+ import { appendClassValidationRule, appendDtoFieldValidationRule, defineDtoFieldBindingMetadata, getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/internal';
2
+ function setClassName(target, name) {
3
+ Object.defineProperty(target, 'name', {
4
+ configurable: true,
5
+ value: name
6
+ });
7
+ }
8
+ function createDerivedDto(name, initializer) {
9
+ class DerivedDto {
10
+ constructor() {
11
+ initializer(this);
12
+ }
13
+ }
14
+ setClassName(DerivedDto, name);
15
+ return DerivedDto;
16
+ }
17
+ function collectDtoKeys(dto) {
18
+ const keys = new Set();
19
+ for (const entry of getDtoBindingSchema(dto)) {
20
+ keys.add(entry.propertyKey);
21
+ }
22
+ for (const entry of getDtoValidationSchema(dto)) {
23
+ keys.add(entry.propertyKey);
24
+ }
25
+ return [...keys];
26
+ }
27
+ function copyDtoMetadata(source, target, include) {
28
+ for (const entry of getDtoBindingSchema(source)) {
29
+ if (!include(entry.propertyKey)) {
30
+ continue;
31
+ }
32
+ defineDtoFieldBindingMetadata(target.prototype, entry.propertyKey, entry.metadata);
33
+ }
34
+ for (const entry of getDtoValidationSchema(source)) {
35
+ if (!include(entry.propertyKey)) {
36
+ continue;
37
+ }
38
+ for (const rule of entry.rules) {
39
+ appendDtoFieldValidationRule(target.prototype, entry.propertyKey, rule);
40
+ }
41
+ }
42
+ for (const rule of getClassValidationRules(source)) {
43
+ appendClassValidationRule(target, rule);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Derive a DTO class that keeps only the selected properties from a base DTO.
49
+ *
50
+ * Validation and binding metadata are copied only for the requested keys, which
51
+ * keeps request materialization aligned with the subset contract documented in
52
+ * the base DTO.
53
+ *
54
+ * @typeParam TBase Base DTO constructor to derive from.
55
+ * @typeParam TKey String keys preserved on the derived DTO.
56
+ * @param BaseDto Source DTO that already declares binding and validation metadata.
57
+ * @param keys Property names to keep on the derived DTO.
58
+ * @returns A derived DTO constructor that materializes and validates only the selected fields.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * class UserDto {
63
+ * email = '';
64
+ * name = '';
65
+ * }
66
+ *
67
+ * class UserEmailDto extends PickType(UserDto, ['email']) {}
68
+ * ```
69
+ */
70
+ export function PickType(BaseDto, keys) {
71
+ const selected = new Set(keys);
72
+ const baseKeys = collectDtoKeys(BaseDto);
73
+ const PickedDto = createDerivedDto(`${BaseDto.name}PickType`, instance => {
74
+ for (const key of baseKeys) {
75
+ if (selected.has(key)) {
76
+ instance[key] = undefined;
77
+ }
78
+ }
79
+ });
80
+ copyDtoMetadata(BaseDto, PickedDto, propertyKey => selected.has(propertyKey));
81
+ return PickedDto;
82
+ }
83
+
84
+ /**
85
+ * Derive a DTO class that removes specific properties from a base DTO.
86
+ *
87
+ * Binding and validation metadata for omitted properties are intentionally not
88
+ * copied, so downstream transports cannot bind or validate those fields on the
89
+ * derived contract.
90
+ *
91
+ * @typeParam TBase Base DTO constructor to derive from.
92
+ * @typeParam TKey String keys removed from the derived DTO.
93
+ * @param BaseDto Source DTO that already declares binding and validation metadata.
94
+ * @param keys Property names to exclude from the derived DTO.
95
+ * @returns A derived DTO constructor that preserves every base field except the omitted keys.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * class UserDto {
100
+ * id = '';
101
+ * passwordHash = '';
102
+ * }
103
+ *
104
+ * class PublicUserDto extends OmitType(UserDto, ['passwordHash']) {}
105
+ * ```
106
+ */
107
+ export function OmitType(BaseDto, keys) {
108
+ const omitted = new Set(keys);
109
+ const baseKeys = collectDtoKeys(BaseDto);
110
+ const OmittedDto = createDerivedDto(`${BaseDto.name}OmitType`, instance => {
111
+ for (const key of baseKeys) {
112
+ if (!omitted.has(key)) {
113
+ instance[key] = undefined;
114
+ }
115
+ }
116
+ });
117
+ copyDtoMetadata(BaseDto, OmittedDto, propertyKey => !omitted.has(propertyKey));
118
+ return OmittedDto;
119
+ }
120
+ /**
121
+ * Combine multiple DTO classes into one intersection DTO.
122
+ *
123
+ * The derived constructor initializes every property discovered across the
124
+ * source DTOs and copies each source DTO's binding and validation metadata.
125
+ *
126
+ * @typeParam TBaseDtos Tuple of DTO constructors to merge.
127
+ * @param baseDtos DTO constructors whose fields and rules should be combined.
128
+ * @returns A DTO constructor whose instance type is the intersection of every input DTO.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * class PaginationDto {
133
+ * page = 1;
134
+ * }
135
+ *
136
+ * class SearchDto {
137
+ * query = '';
138
+ * }
139
+ *
140
+ * class SearchPageDto extends IntersectionType(PaginationDto, SearchDto) {}
141
+ * ```
142
+ */
143
+ export function IntersectionType(...baseDtos) {
144
+ const baseKeySets = baseDtos.map(dto => collectDtoKeys(dto));
145
+ const IntersectionDto = createDerivedDto(`${baseDtos.map(dto => dto.name).join('') || 'Anonymous'}IntersectionType`, instance => {
146
+ for (const baseKeys of baseKeySets) {
147
+ for (const key of baseKeys) {
148
+ instance[key] = undefined;
149
+ }
150
+ }
151
+ });
152
+ for (const BaseDto of baseDtos) {
153
+ copyDtoMetadata(BaseDto, IntersectionDto, () => true);
154
+ }
155
+ return IntersectionDto;
156
+ }
157
+
158
+ /**
159
+ * Derive a DTO class where every bound field from the base DTO becomes optional.
160
+ *
161
+ * Existing validators are preserved, and an `optional` validation rule is added
162
+ * when the base field did not already declare one.
163
+ *
164
+ * @typeParam TBase Base DTO constructor to derive from.
165
+ * @param BaseDto Source DTO that defines the original field contract.
166
+ * @returns A derived DTO constructor suited for patch/update style payloads.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * class CreateUserDto {
171
+ * email = '';
172
+ * name = '';
173
+ * }
174
+ *
175
+ * class UpdateUserDto extends PartialType(CreateUserDto) {}
176
+ * ```
177
+ */
178
+ export function PartialType(BaseDto) {
179
+ const baseKeys = collectDtoKeys(BaseDto);
180
+ const PartialDto = createDerivedDto(`${BaseDto.name}PartialType`, instance => {
181
+ for (const key of baseKeys) {
182
+ instance[key] = undefined;
183
+ }
184
+ });
185
+ for (const entry of getDtoBindingSchema(BaseDto)) {
186
+ defineDtoFieldBindingMetadata(PartialDto.prototype, entry.propertyKey, {
187
+ ...entry.metadata,
188
+ optional: true
189
+ });
190
+ }
191
+ const validationSchema = getDtoValidationSchema(BaseDto);
192
+ for (const entry of validationSchema) {
193
+ const hasOptional = entry.rules.some(rule => rule.kind === 'optional');
194
+ for (const rule of entry.rules) {
195
+ appendDtoFieldValidationRule(PartialDto.prototype, entry.propertyKey, rule);
196
+ }
197
+ if (!hasOptional) {
198
+ appendDtoFieldValidationRule(PartialDto.prototype, entry.propertyKey, {
199
+ kind: 'optional'
200
+ });
201
+ }
202
+ }
203
+ for (const rule of getClassValidationRules(BaseDto)) {
204
+ appendClassValidationRule(PartialDto, rule);
205
+ }
206
+ return PartialDto;
207
+ }
@@ -0,0 +1,6 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { CustomClassValidator } from '@fluojs/core/internal';
3
+ export type StandardSchemaV1Like<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output>;
4
+ export declare function isStandardSchemaLike(value: unknown): value is StandardSchemaV1Like;
5
+ export declare function createClassValidatorFromStandardSchema(schema: StandardSchemaV1Like): CustomClassValidator;
6
+ //# sourceMappingURL=standard-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-schema.d.ts","sourceRoot":"","sources":["../src/standard-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAIlE,MAAM,MAAM,oBAAoB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AA0CpG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAkBlF;AA4CD,wBAAgB,sCAAsC,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAgBzG"}
@@ -0,0 +1,75 @@
1
+ function toFieldPath(path) {
2
+ if (!path || path.length === 0) {
3
+ return undefined;
4
+ }
5
+ let result = '';
6
+ for (const segment of path) {
7
+ if (typeof segment === 'symbol') {
8
+ continue;
9
+ }
10
+ if (typeof segment === 'number') {
11
+ result += `[${String(segment)}]`;
12
+ continue;
13
+ }
14
+ result += result.length === 0 ? segment : `.${segment}`;
15
+ }
16
+ return result;
17
+ }
18
+ function normalizeCode(code, fallback) {
19
+ if (!code || code.length === 0) {
20
+ return fallback;
21
+ }
22
+ return code.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase() || fallback;
23
+ }
24
+ export function isStandardSchemaLike(value) {
25
+ if (typeof value !== 'object' && typeof value !== 'function' || value === null) {
26
+ return false;
27
+ }
28
+ const standard = value['~standard'];
29
+ if (typeof standard !== 'object' || standard === null) {
30
+ return false;
31
+ }
32
+ const candidate = standard;
33
+ return candidate.version === 1 && typeof candidate.vendor === 'string' && typeof candidate.validate === 'function';
34
+ }
35
+ function toStandardSchemaPath(path) {
36
+ if (!path || path.length === 0) {
37
+ return undefined;
38
+ }
39
+ const segments = [];
40
+ for (const segment of path) {
41
+ if (typeof segment === 'string' || typeof segment === 'number') {
42
+ segments.push(segment);
43
+ continue;
44
+ }
45
+ if (typeof segment === 'object' && segment !== null && 'key' in segment && (typeof segment.key === 'string' || typeof segment.key === 'number' || typeof segment.key === 'symbol')) {
46
+ segments.push(segment.key);
47
+ }
48
+ }
49
+ return segments.length > 0 ? segments : undefined;
50
+ }
51
+ function toStandardValidationIssue(issue) {
52
+ return {
53
+ code: normalizeCode(issue.code ?? issue.kind ?? issue.type, 'INVALID_FIELD'),
54
+ field: toFieldPath(toStandardSchemaPath(issue.path)) ?? issue.propString,
55
+ message: issue.message
56
+ };
57
+ }
58
+ function isStandardSchemaFailureResult(result) {
59
+ return typeof result === 'object' && result !== null && 'issues' in result;
60
+ }
61
+ export function createClassValidatorFromStandardSchema(schema) {
62
+ return async value => {
63
+ const result = await schema['~standard'].validate(value);
64
+ if (!isStandardSchemaFailureResult(result) || result.issues === undefined) {
65
+ return true;
66
+ }
67
+ if (!Array.isArray(result.issues)) {
68
+ return [{
69
+ code: 'INVALID_SCHEMA_RESULT',
70
+ message: 'Standard Schema validator returned malformed issues.'
71
+ }];
72
+ }
73
+ return result.issues.length > 0 ? result.issues.map(issue => toStandardValidationIssue(issue)) : true;
74
+ };
75
+ }
@@ -0,0 +1,21 @@
1
+ import type { Constructor, MaybePromise, MetadataSource } from '@fluojs/core';
2
+ export interface ValidationIssue {
3
+ /** Stable issue code for programmatic error handling. */
4
+ code: string;
5
+ /** Dot/bracket field path when the issue is field-scoped. */
6
+ field?: string;
7
+ /** Human-readable explanation for the failed rule. */
8
+ message: string;
9
+ /** Optional metadata source that produced this rule. */
10
+ source?: MetadataSource;
11
+ }
12
+ /**
13
+ * Validation engine contract used by HTTP binding and app-level validation flows.
14
+ */
15
+ export interface Validator {
16
+ /** Validates an existing instance without materializing nested objects. */
17
+ validate(value: unknown, target: Constructor): MaybePromise<void>;
18
+ /** Materializes and validates a value into a typed DTO instance. */
19
+ materialize<T>(value: unknown, target: Constructor<T>): MaybePromise<T>;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9E,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClE,oEAAoE;IACpE,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;CACzE"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { type Constructor } from '@fluojs/core';
2
+ import type { Validator } from './types.js';
3
+ export declare class DefaultValidator implements Validator {
4
+ validate(value: unknown, target: Constructor): Promise<void>;
5
+ materialize<T>(value: unknown, target: Constructor<T>): Promise<T>;
6
+ }
7
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAC;AAatB,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAC;AAywB7D,qBAAa,gBAAiB,YAAW,SAAS;IAC1C,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAUzE"}