@wener/common 1.0.5 → 2.0.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.
- package/lib/cn/DivisionCode.js +55 -32
- package/lib/cn/DivisionCode.test.js +140 -0
- package/lib/cn/Mod11Checksum.js +80 -37
- package/lib/cn/Mod31Checksum.js +89 -40
- package/lib/cn/ResidentIdentityCardNumber.js +16 -16
- package/lib/cn/ResidentIdentityCardNumber.test.js +21 -0
- package/lib/cn/UnifiedSocialCreditCode.js +32 -26
- package/lib/cn/UnifiedSocialCreditCode.test.js +16 -0
- package/lib/cn/formatDate.js +5 -7
- package/lib/cn/index.js +0 -1
- package/lib/cn/parseSex.js +0 -2
- package/lib/cn/types.d.js +0 -2
- package/lib/consola/createStandardConsolaReporter.js +6 -6
- package/lib/consola/formatLogObject.js +133 -32
- package/lib/consola/index.js +0 -1
- package/lib/data/formatSort.js +5 -6
- package/lib/data/formatSort.test.js +34 -0
- package/lib/data/index.js +0 -1
- package/lib/data/maybeNumber.js +11 -5
- package/lib/data/parseSort.js +28 -22
- package/lib/data/parseSort.test.js +188 -0
- package/lib/data/resolvePagination.js +27 -16
- package/lib/data/resolvePagination.test.js +232 -0
- package/lib/data/types.d.js +0 -2
- package/lib/index.js +0 -1
- package/lib/jsonschema/JsonSchema.js +78 -52
- package/lib/jsonschema/JsonSchema.test.js +137 -0
- package/lib/jsonschema/index.js +0 -1
- package/lib/jsonschema/types.d.js +5 -3
- package/lib/meta/defineFileType.js +103 -20
- package/lib/meta/defineInit.js +250 -31
- package/lib/meta/defineMetadata.js +140 -24
- package/lib/meta/defineMetadata.test.js +13 -0
- package/lib/meta/index.js +0 -1
- package/lib/password/PHC.js +87 -63
- package/lib/password/PHC.test.js +539 -0
- package/lib/password/Password.js +291 -29
- package/lib/password/Password.test.js +362 -0
- package/lib/password/createArgon2PasswordAlgorithm.js +191 -35
- package/lib/password/createBase64PasswordAlgorithm.js +141 -8
- package/lib/password/createBcryptPasswordAlgorithm.js +168 -13
- package/lib/password/createPBKDF2PasswordAlgorithm.js +228 -46
- package/lib/password/createScryptPasswordAlgorithm.js +211 -55
- package/lib/password/index.js +0 -1
- package/lib/password/server/index.js +0 -1
- package/lib/resource/Identifiable.js +1 -0
- package/lib/resource/getTitleOfResource.js +10 -0
- package/lib/resource/index.js +1 -0
- package/lib/resource/schema/AnyResourceSchema.js +89 -0
- package/lib/resource/schema/BaseResourceSchema.js +29 -0
- package/lib/resource/schema/ResourceActionType.js +118 -0
- package/lib/resource/schema/ResourceStatus.js +93 -0
- package/lib/resource/schema/ResourceType.js +24 -0
- package/lib/resource/schema/SchemaRegistry.js +38 -0
- package/lib/resource/schema/SexType.js +10 -0
- package/lib/resource/schema/types.js +89 -0
- package/lib/resource/schema/types.test.js +14 -0
- package/lib/schema/TypeSchema.d.js +1 -0
- package/lib/schema/createSchemaData.js +173 -0
- package/lib/schema/findJsonSchemaByPath.js +49 -0
- package/lib/schema/getSchemaCache.js +11 -0
- package/lib/schema/getSchemaOptions.js +24 -0
- package/lib/schema/index.js +5 -0
- package/lib/schema/toJsonSchema.js +441 -0
- package/lib/schema/toJsonSchema.test.js +27 -0
- package/lib/schema/validate.js +124 -0
- package/lib/search/AdvanceSearch.js +0 -1
- package/lib/search/AdvanceSearch.test.js +435 -0
- package/lib/search/formatAdvanceSearch.js +41 -27
- package/lib/search/index.js +0 -1
- package/lib/search/optimizeAdvanceSearch.js +79 -25
- package/lib/search/parseAdvanceSearch.js +5 -5
- package/lib/search/parser.d.js +0 -2
- package/lib/search/parser.js +97 -74
- package/lib/search/types.d.js +0 -2
- package/lib/tools/generateSchema.js +197 -39
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js +143 -55
- package/package.json +23 -11
- package/src/data/maybeNumber.ts +5 -1
- package/src/data/resolvePagination.test.ts +1 -1
- package/src/data/resolvePagination.ts +18 -7
- package/src/data/types.d.ts +12 -0
- package/src/jsonschema/JsonSchema.test.ts +17 -0
- package/src/jsonschema/JsonSchema.ts +2 -2
- package/src/jsonschema/types.d.ts +63 -12
- package/src/resource/Identifiable.ts +3 -0
- package/src/resource/getTitleOfResource.tsx +6 -0
- package/src/resource/index.ts +3 -0
- package/src/resource/schema/AnyResourceSchema.ts +113 -0
- package/src/resource/schema/BaseResourceSchema.ts +32 -0
- package/src/resource/schema/ResourceActionType.ts +123 -0
- package/src/resource/schema/ResourceStatus.ts +94 -0
- package/src/resource/schema/ResourceType.ts +25 -0
- package/src/resource/schema/SchemaRegistry.ts +42 -0
- package/src/resource/schema/SexType.ts +13 -0
- package/src/resource/schema/types.test.ts +18 -0
- package/src/resource/schema/types.ts +105 -0
- package/src/schema/TypeSchema.d.ts +32 -0
- package/src/schema/createSchemaData.ts +81 -0
- package/src/schema/findJsonSchemaByPath.ts +37 -0
- package/src/schema/getSchemaCache.ts +21 -0
- package/src/schema/getSchemaOptions.ts +24 -0
- package/src/schema/index.ts +6 -0
- package/src/schema/toJsonSchema.test.ts +37 -0
- package/src/schema/toJsonSchema.ts +200 -0
- package/src/schema/validate.ts +135 -0
- package/src/tools/generateSchema.ts +28 -28
- package/lib/cn/DivisionCode.js.map +0 -1
- package/lib/cn/Mod11Checksum.js.map +0 -1
- package/lib/cn/Mod31Checksum.js.map +0 -1
- package/lib/cn/ResidentIdentityCardNumber.js.map +0 -1
- package/lib/cn/UnifiedSocialCreditCode.js.map +0 -1
- package/lib/cn/formatDate.js.map +0 -1
- package/lib/cn/index.js.map +0 -1
- package/lib/cn/parseSex.js.map +0 -1
- package/lib/cn/types.d.js.map +0 -1
- package/lib/consola/createStandardConsolaReporter.js.map +0 -1
- package/lib/consola/formatLogObject.js.map +0 -1
- package/lib/consola/index.js.map +0 -1
- package/lib/data/formatSort.js.map +0 -1
- package/lib/data/index.js.map +0 -1
- package/lib/data/maybeNumber.js.map +0 -1
- package/lib/data/parseSort.js.map +0 -1
- package/lib/data/resolvePagination.js.map +0 -1
- package/lib/data/types.d.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/jsonschema/JsonSchema.js.map +0 -1
- package/lib/jsonschema/index.js.map +0 -1
- package/lib/jsonschema/types.d.js.map +0 -1
- package/lib/meta/defineFileType.js.map +0 -1
- package/lib/meta/defineInit.js.map +0 -1
- package/lib/meta/defineMetadata.js.map +0 -1
- package/lib/meta/index.js.map +0 -1
- package/lib/password/PHC.js.map +0 -1
- package/lib/password/Password.js.map +0 -1
- package/lib/password/createArgon2PasswordAlgorithm.js.map +0 -1
- package/lib/password/createBase64PasswordAlgorithm.js.map +0 -1
- package/lib/password/createBcryptPasswordAlgorithm.js.map +0 -1
- package/lib/password/createPBKDF2PasswordAlgorithm.js.map +0 -1
- package/lib/password/createScryptPasswordAlgorithm.js.map +0 -1
- package/lib/password/index.js.map +0 -1
- package/lib/password/server/index.js.map +0 -1
- package/lib/search/AdvanceSearch.js.map +0 -1
- package/lib/search/formatAdvanceSearch.js.map +0 -1
- package/lib/search/index.js.map +0 -1
- package/lib/search/optimizeAdvanceSearch.js.map +0 -1
- package/lib/search/parseAdvanceSearch.js.map +0 -1
- package/lib/search/parser.d.js.map +0 -1
- package/lib/search/parser.js.map +0 -1
- package/lib/search/types.d.js.map +0 -1
- package/lib/tools/generateSchema.js.map +0 -1
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isValid } from 'date-fns';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
const TypeIdSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^[a-zA-Z0-9]+_[a-zA-Z0-9]+$/, {
|
|
8
|
+
error: 'ID格式错误',
|
|
9
|
+
})
|
|
10
|
+
.describe('ID');
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
XXX-XXX-XXXX
|
|
14
|
+
National: (XXX) XXX-XXXX
|
|
15
|
+
Human: XXX.XXX.XXXX xXXX
|
|
16
|
+
International: +XX (XXX) XXX-XXXX
|
|
17
|
+
|
|
18
|
+
中国
|
|
19
|
+
XXX XXXX XXXX
|
|
20
|
+
XXX-XXXX-XXXX
|
|
21
|
+
XXXXXXXXXXX
|
|
22
|
+
+86 XXX XXXX XXXX
|
|
23
|
+
+86 XXXXXXXXXXX
|
|
24
|
+
|
|
25
|
+
0XX-XXXXXXXX
|
|
26
|
+
0XXX-XXXXXXXX
|
|
27
|
+
(0XX) XXXXXXXX
|
|
28
|
+
0XX XXXX XXXX
|
|
29
|
+
0XXXXXXXX
|
|
30
|
+
|
|
31
|
+
https://uibakery.io/regex-library/phone-number
|
|
32
|
+
https://fakerjs.dev/api/phone
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const PhoneNumberSchema = z
|
|
36
|
+
.string()
|
|
37
|
+
.trim()
|
|
38
|
+
.max(20)
|
|
39
|
+
.regex(/(^$)|(^[0-9]*$)/, {
|
|
40
|
+
error: '电话号码只能包含数字',
|
|
41
|
+
})
|
|
42
|
+
.meta({
|
|
43
|
+
format: 'phone',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function resourceIdOf(entity: string) {
|
|
47
|
+
return rz.resourceId.meta({ 'x-ref-entity': entity });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// allow empty string
|
|
51
|
+
const JsonDateSchema = z.coerce
|
|
52
|
+
.string()
|
|
53
|
+
.trim()
|
|
54
|
+
.overwrite((v) => {
|
|
55
|
+
if (v && !/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
56
|
+
let val = dayjs(v);
|
|
57
|
+
if (val.isValid()) {
|
|
58
|
+
return val.format('YYYY-MM-DD');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return v;
|
|
62
|
+
})
|
|
63
|
+
.regex(/(^$)|(^\d{4}-\d{2}-\d{2}$)/, { error: '错误的日期格式' })
|
|
64
|
+
.refine((value) => !value || isValid(new Date(value)), {
|
|
65
|
+
error: '错误的日期',
|
|
66
|
+
})
|
|
67
|
+
.nullish()
|
|
68
|
+
.overwrite((v) => v || undefined)
|
|
69
|
+
.meta({ format: 'date' });
|
|
70
|
+
|
|
71
|
+
const JsonDateTimeSchema = z.coerce.date().meta({ format: 'date-time', type: 'string' });
|
|
72
|
+
|
|
73
|
+
const LoginNameSchema = z
|
|
74
|
+
.string()
|
|
75
|
+
.trim()
|
|
76
|
+
.regex(/^[a-z0-9]{3,16}$/, { error: '登录名只能包含小写字母和数字,长度为3-16位' });
|
|
77
|
+
|
|
78
|
+
// maybe check valid cjk
|
|
79
|
+
const FriendlyNameSchema = z
|
|
80
|
+
.string()
|
|
81
|
+
.trim()
|
|
82
|
+
.max(50)
|
|
83
|
+
.refine((v) => !/\p{C}/u.test(v), { message: '包含无效字符' })
|
|
84
|
+
.describe('名称');
|
|
85
|
+
|
|
86
|
+
const PasswordSchema = z
|
|
87
|
+
.string()
|
|
88
|
+
.min(6, { error: '密码长度至少 6 位' })
|
|
89
|
+
.max(36, { error: '密码长度最多 36 位' })
|
|
90
|
+
.regex(/^\S*$/, { error: '密码不能包含空格' })
|
|
91
|
+
.describe('密码')
|
|
92
|
+
.meta({ sensitive: true });
|
|
93
|
+
|
|
94
|
+
export const rz = {
|
|
95
|
+
resourceIdOf: resourceIdOf,
|
|
96
|
+
date: JsonDateSchema,
|
|
97
|
+
dateTime: JsonDateTimeSchema,
|
|
98
|
+
resourceId: TypeIdSchema,
|
|
99
|
+
phoneNumber: PhoneNumberSchema,
|
|
100
|
+
loginName: LoginNameSchema,
|
|
101
|
+
password: PasswordSchema,
|
|
102
|
+
friendlyName: FriendlyNameSchema,
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
export type EnumValues<T> = T[Exclude<keyof T, '__proto__'>];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TSchema,
|
|
3
|
+
StaticDecode as TypeBoxStaticDecode,
|
|
4
|
+
StaticEncode as TypeBoxStaticEncode,
|
|
5
|
+
} from '@sinclair/typebox';
|
|
6
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
7
|
+
import type { z } from 'zod/v4';
|
|
8
|
+
import type { JsonSchemaDef } from '../jsonschema';
|
|
9
|
+
|
|
10
|
+
export type TypeSchema<I = unknown, O = I> = TSchema | z.ZodSchema<O, I> | JsonSchemaDef | StandardSchemaV1<I, O>;
|
|
11
|
+
|
|
12
|
+
export type SchemaOutput<S extends TypeSchema> =
|
|
13
|
+
S extends StandardSchemaV1<infer I, infer O>
|
|
14
|
+
? O
|
|
15
|
+
: S extends z.ZodSchema<infer O, infer I>
|
|
16
|
+
? O
|
|
17
|
+
: S extends TSchema
|
|
18
|
+
? TypeBoxStaticEncode<S>
|
|
19
|
+
: S extends JsonSchemaDef<infer I, infer O>
|
|
20
|
+
? O
|
|
21
|
+
: never;
|
|
22
|
+
|
|
23
|
+
export type SchemaInput<S extends TypeSchema> =
|
|
24
|
+
S extends StandardSchemaV1<infer I, infer O>
|
|
25
|
+
? I
|
|
26
|
+
: S extends z.ZodSchema<infer O, infer I>
|
|
27
|
+
? I
|
|
28
|
+
: S extends TSchema
|
|
29
|
+
? TypeBoxStaticDecode<S>
|
|
30
|
+
: S extends JsonSchemaDef<infer I, infer O>
|
|
31
|
+
? I
|
|
32
|
+
: never;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { match, P } from 'ts-pattern';
|
|
2
|
+
import type { JsonSchemaDef } from '../jsonschema';
|
|
3
|
+
import { toJsonSchema } from './toJsonSchema';
|
|
4
|
+
import type { SchemaOutput, TypeSchema } from './TypeSchema';
|
|
5
|
+
|
|
6
|
+
export function createSchemaData<S extends TypeSchema>(
|
|
7
|
+
ts: S,
|
|
8
|
+
options: CreateSchemaDataOptions = {},
|
|
9
|
+
): Partial<SchemaOutput<S>> {
|
|
10
|
+
const schema = toJsonSchema(ts);
|
|
11
|
+
return createJsonSchemaData(schema, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type CreateSchemaDataOptions = Partial<CreateOptions> & {
|
|
15
|
+
all?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function createJsonSchemaData(schema: JsonSchemaDef, options: CreateSchemaDataOptions): any {
|
|
19
|
+
let skip: CreateOptions['skip'] = (s, ctx) => Boolean(!ctx.required && s.nullable);
|
|
20
|
+
if (options.all) {
|
|
21
|
+
skip = () => false;
|
|
22
|
+
}
|
|
23
|
+
if (options.skip) {
|
|
24
|
+
skip = options.skip;
|
|
25
|
+
}
|
|
26
|
+
return _create(
|
|
27
|
+
schema,
|
|
28
|
+
{
|
|
29
|
+
skip,
|
|
30
|
+
},
|
|
31
|
+
{ required: false },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type CreateOptions = {
|
|
36
|
+
skip: (schema: JsonSchemaDef, ctx: { required: boolean }) => boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function _create(schema: JsonSchemaDef, options: CreateOptions, ctx: { required: boolean }): any {
|
|
40
|
+
const { skip } = options;
|
|
41
|
+
if (skip(schema, ctx)) {
|
|
42
|
+
return schema.default;
|
|
43
|
+
}
|
|
44
|
+
if (schema.default !== undefined) {
|
|
45
|
+
return schema.default;
|
|
46
|
+
}
|
|
47
|
+
return match(schema as JsonSchemaDef)
|
|
48
|
+
.returnType<any>()
|
|
49
|
+
.with({ default: P.select() }, (v) => v)
|
|
50
|
+
.with({ const: P.nonNullable }, (v) => v.const)
|
|
51
|
+
.with({ anyOf: P.nonNullable }, (schema) => {
|
|
52
|
+
return _create(schema.anyOf[0], options, { required: false });
|
|
53
|
+
})
|
|
54
|
+
.with({ oneOf: P.nonNullable }, (schema) => {
|
|
55
|
+
return _create(schema.oneOf[0], options, { required: false });
|
|
56
|
+
})
|
|
57
|
+
.with({ type: 'string' }, (schema) => '')
|
|
58
|
+
.with({ type: P.union('number', 'integer') }, (schema) => 0)
|
|
59
|
+
.with({ type: 'object' }, () => {
|
|
60
|
+
const out: Record<string, any> = {};
|
|
61
|
+
|
|
62
|
+
let required = schema.required || [];
|
|
63
|
+
for (const [k, v] of Object.entries(schema.properties || {}) as [string, JsonSchemaDef][]) {
|
|
64
|
+
const value = _create(v, options, {
|
|
65
|
+
required: required.includes(k),
|
|
66
|
+
});
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out[k] = value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return out;
|
|
74
|
+
})
|
|
75
|
+
.with({ type: 'null' }, () => null)
|
|
76
|
+
.with({ type: 'boolean' }, (schema) => false)
|
|
77
|
+
.with({ type: 'array' }, (schema) => [])
|
|
78
|
+
.otherwise(() => {
|
|
79
|
+
return undefined;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { firstOfMaybeArray } from '@wener/utils';
|
|
2
|
+
import { toJsonSchema } from './toJsonSchema';
|
|
3
|
+
import type { TypeSchema } from './TypeSchema';
|
|
4
|
+
|
|
5
|
+
export function findJsonSchemaByPath(schema: TypeSchema, objectPath: string) {
|
|
6
|
+
schema = toJsonSchema(schema);
|
|
7
|
+
if (!objectPath || !schema) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const segments = objectPath.split('.');
|
|
12
|
+
let currentSchema = schema;
|
|
13
|
+
|
|
14
|
+
for (const segment of segments) {
|
|
15
|
+
// 检查当前 schema 是否是对象类型且有 properties
|
|
16
|
+
if (
|
|
17
|
+
currentSchema
|
|
18
|
+
&& typeof currentSchema === 'object'
|
|
19
|
+
&& currentSchema.properties
|
|
20
|
+
&& currentSchema.properties[segment]
|
|
21
|
+
) {
|
|
22
|
+
currentSchema = currentSchema.properties[segment];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (/^\d$/.test(segment) || segment === '[]') {
|
|
27
|
+
if (currentSchema.items) {
|
|
28
|
+
currentSchema = firstOfMaybeArray(currentSchema.items);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return currentSchema;
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonSchemaDef } from '../jsonschema';
|
|
2
|
+
import type { TypeSchema } from './TypeSchema';
|
|
3
|
+
|
|
4
|
+
type SchemaCache = {
|
|
5
|
+
jsonschema?: JsonSchemaDef;
|
|
6
|
+
options?: Array<{ value: string; label: string }>;
|
|
7
|
+
};
|
|
8
|
+
const _cache = new WeakMap<any, SchemaCache>();
|
|
9
|
+
|
|
10
|
+
export function getSchemaCache<K extends keyof SchemaCache>(
|
|
11
|
+
obj: TypeSchema,
|
|
12
|
+
key: K,
|
|
13
|
+
compute: () => NonNullable<SchemaCache[K]>,
|
|
14
|
+
): NonNullable<SchemaCache[K]> {
|
|
15
|
+
let c = _cache.get(obj);
|
|
16
|
+
if (!c) {
|
|
17
|
+
c = {};
|
|
18
|
+
_cache.set(obj, c);
|
|
19
|
+
}
|
|
20
|
+
return (c[key] ??= compute());
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { deepFreeze } from '@wener/utils';
|
|
2
|
+
import { getSchemaCache } from './getSchemaCache';
|
|
3
|
+
import { toJsonSchema } from './toJsonSchema';
|
|
4
|
+
import type { TypeSchema } from './TypeSchema';
|
|
5
|
+
|
|
6
|
+
export function getSchemaOptions(s: TypeSchema): Array<{ value: string; label: string }> {
|
|
7
|
+
let js = toJsonSchema(s);
|
|
8
|
+
return getSchemaCache(s, 'options', () => {
|
|
9
|
+
let out =
|
|
10
|
+
(js.anyOf || js.oneOf)?.map((v) => {
|
|
11
|
+
let value = v.const;
|
|
12
|
+
return {
|
|
13
|
+
label: String(v.description || v.title || value),
|
|
14
|
+
value: value ? String(value) : '',
|
|
15
|
+
};
|
|
16
|
+
}) || [];
|
|
17
|
+
out = deepFreeze(out);
|
|
18
|
+
return out;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSchemaOptionLabel(schema: TypeSchema, value: string | undefined | null): string | undefined {
|
|
23
|
+
return getSchemaOptions(schema).find((v) => v.value === value)?.label;
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type * from './TypeSchema';
|
|
2
|
+
export { isJsonSchema, isZodSchema, isTypeBoxSchema, validate, parseData, type ValidationResult } from './validate';
|
|
3
|
+
export { getSchemaOptions, getSchemaOptionLabel } from './getSchemaOptions';
|
|
4
|
+
export { toJsonSchema } from './toJsonSchema';
|
|
5
|
+
export { findJsonSchemaByPath } from './findJsonSchemaByPath';
|
|
6
|
+
export { createSchemaData } from './createSchemaData';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { toJsonSchema } from './toJsonSchema';
|
|
5
|
+
|
|
6
|
+
describe('toJsonSchema', () => {
|
|
7
|
+
it('should handle discriminatedUnion', () => {
|
|
8
|
+
console.log(
|
|
9
|
+
inspect(
|
|
10
|
+
toJsonSchema(
|
|
11
|
+
z.discriminatedUnion('type', [
|
|
12
|
+
z.object({
|
|
13
|
+
type: z.literal('string'),
|
|
14
|
+
value: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
z.object({
|
|
17
|
+
type: z.literal('number'),
|
|
18
|
+
value: z.number(),
|
|
19
|
+
}),
|
|
20
|
+
]),
|
|
21
|
+
),
|
|
22
|
+
{
|
|
23
|
+
depth: 10,
|
|
24
|
+
colors: true,
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should cache', () => {
|
|
31
|
+
let zs = z.object({
|
|
32
|
+
name: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(toJsonSchema(zs)).toBe(toJsonSchema(zs));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { omit, remove } from 'es-toolkit';
|
|
2
|
+
import { match, P } from 'ts-pattern';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import type { JsonSchemaDef } from '../jsonschema';
|
|
5
|
+
import { getSchemaCache } from './getSchemaCache';
|
|
6
|
+
import type { TypeSchema } from './TypeSchema';
|
|
7
|
+
import { isTypeBoxSchema, isZodSchema } from './validate';
|
|
8
|
+
|
|
9
|
+
export function toJsonSchema(schema: TypeSchema): JsonSchemaDef {
|
|
10
|
+
if (isZodSchema(schema)) {
|
|
11
|
+
return getSchemaCache(schema, 'jsonschema', () => {
|
|
12
|
+
// zod v4
|
|
13
|
+
let js = z.toJSONSchema(schema, {
|
|
14
|
+
unrepresentable: 'any',
|
|
15
|
+
override: ({ zodSchema, jsonSchema: js }) => {
|
|
16
|
+
const def = zodSchema._zod.def;
|
|
17
|
+
const meta = z.globalRegistry.get(zodSchema);
|
|
18
|
+
if (meta) {
|
|
19
|
+
Object.assign(js, meta);
|
|
20
|
+
}
|
|
21
|
+
switch (def.type) {
|
|
22
|
+
case 'union':
|
|
23
|
+
if (zodSchema._zod.traits.has('ZodDiscriminatedUnion')) {
|
|
24
|
+
resolveDiscriminator(js as JsonSchemaDef);
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case 'nonoptional':
|
|
28
|
+
// js._ref maybe true
|
|
29
|
+
js.nullable = false;
|
|
30
|
+
break;
|
|
31
|
+
case 'nullable':
|
|
32
|
+
case 'optional':
|
|
33
|
+
// prefer nullable
|
|
34
|
+
match(js)
|
|
35
|
+
.with({ anyOf: [P.select(), { type: 'null' }] }, (select) => {
|
|
36
|
+
delete js['anyOf'];
|
|
37
|
+
Object.assign(js, select);
|
|
38
|
+
js.nullable = true;
|
|
39
|
+
})
|
|
40
|
+
.otherwise((js) => {
|
|
41
|
+
js.nullable = true;
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}) as JsonSchemaDef;
|
|
47
|
+
|
|
48
|
+
// remove redundant nullable, ensure required is sorted
|
|
49
|
+
visit(js, (v) => {
|
|
50
|
+
if (v.nullable === false) {
|
|
51
|
+
delete v.nullable;
|
|
52
|
+
}
|
|
53
|
+
if (v.required) {
|
|
54
|
+
v.required.sort();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// maybe freeze
|
|
58
|
+
return js;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isTypeBoxSchema(schema)) {
|
|
63
|
+
return schema as JsonSchemaDef;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return schema as JsonSchemaDef;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function visit(js: JsonSchemaDef, f: (js: JsonSchemaDef) => void) {
|
|
70
|
+
const _visit = (
|
|
71
|
+
js: JsonSchemaDef,
|
|
72
|
+
f: (js: JsonSchemaDef) => void,
|
|
73
|
+
parent: JsonSchemaDef | undefined,
|
|
74
|
+
path: string[],
|
|
75
|
+
k?: string,
|
|
76
|
+
) => {
|
|
77
|
+
if (!js) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
f(js);
|
|
81
|
+
if (js.properties) {
|
|
82
|
+
for (const [k, v] of Object.entries(js.properties)) {
|
|
83
|
+
if (v) {
|
|
84
|
+
_visit(v, f, js, path.concat(k), 'properties');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else if (js.items) {
|
|
88
|
+
if (Array.isArray(js.items)) {
|
|
89
|
+
for (const v of js.items) {
|
|
90
|
+
_visit(v, f, js, path, 'items');
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
_visit(js.items, f, js, path, 'items');
|
|
94
|
+
}
|
|
95
|
+
} else if (js.anyOf) {
|
|
96
|
+
for (const v of js.anyOf) {
|
|
97
|
+
_visit(v, f, js, path, 'anyOf');
|
|
98
|
+
}
|
|
99
|
+
} else if (js.oneOf) {
|
|
100
|
+
for (const v of js.oneOf) {
|
|
101
|
+
_visit(v, f, js, path, 'oneOf');
|
|
102
|
+
}
|
|
103
|
+
} else if (js.allOf) {
|
|
104
|
+
for (const v of js.allOf) {
|
|
105
|
+
_visit(v, f, js, path, 'allOf');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
_visit(js, f, undefined, []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveJsonSchemaDef(
|
|
113
|
+
js: JsonSchemaDef,
|
|
114
|
+
ctx?: {
|
|
115
|
+
parent: JsonSchemaDef;
|
|
116
|
+
key: string;
|
|
117
|
+
},
|
|
118
|
+
) {
|
|
119
|
+
return match(js)
|
|
120
|
+
.with(
|
|
121
|
+
{
|
|
122
|
+
anyOf: [P.select(), { type: 'null' }],
|
|
123
|
+
},
|
|
124
|
+
(select) => {
|
|
125
|
+
return { ...omit(js, ['anyOf']), ...select, nullable: true };
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
.with({ properties: P.nonNullable }, (schema: JsonSchemaDef) => {
|
|
129
|
+
for (const key in schema.properties) {
|
|
130
|
+
const prop = schema.properties[key];
|
|
131
|
+
if (prop) {
|
|
132
|
+
schema.properties[key] = resolveJsonSchemaDef(prop, { parent: schema, key });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return schema;
|
|
136
|
+
})
|
|
137
|
+
.otherwise(() => {
|
|
138
|
+
return js;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveDiscriminator(jsd: JsonSchemaDef) {
|
|
143
|
+
if (!(jsd.anyOf && jsd.anyOf.length > 1)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (jsd.discriminator) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let names: string[] = [];
|
|
151
|
+
{
|
|
152
|
+
// candidate for discriminator
|
|
153
|
+
const v = jsd.anyOf[0];
|
|
154
|
+
if (v && v.type === 'object' && v.properties) {
|
|
155
|
+
if (Array.isArray(v.required)) {
|
|
156
|
+
for (const k of v.required) {
|
|
157
|
+
if (v.properties[k].const !== undefined) {
|
|
158
|
+
names.push(k);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (names.length >= 1) {
|
|
165
|
+
for (let i = 0; i < jsd.anyOf.length; i++) {
|
|
166
|
+
if (i === 0) {
|
|
167
|
+
// skip first
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!names.length) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const v = jsd.anyOf[i];
|
|
175
|
+
if (v && v.type === 'object' && v.properties) {
|
|
176
|
+
const props = v.properties;
|
|
177
|
+
remove(names, (k) => {
|
|
178
|
+
// dont care the mapping
|
|
179
|
+
let p = props[k];
|
|
180
|
+
switch (typeof p.const) {
|
|
181
|
+
case 'string':
|
|
182
|
+
case 'number':
|
|
183
|
+
case 'boolean':
|
|
184
|
+
return false;
|
|
185
|
+
default:
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
names = [];
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (names.length === 1) {
|
|
196
|
+
jsd.discriminator = {
|
|
197
|
+
propertyName: names[0],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Kind as TypeBoxKind, type TSchema } from '@sinclair/typebox';
|
|
2
|
+
import '@sinclair/typebox';
|
|
3
|
+
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
4
|
+
import { ifPresent } from '@wener/utils';
|
|
5
|
+
import { Ajv } from 'ajv';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import type { ZodType } from 'zod/v4';
|
|
8
|
+
import type { JsonSchemaDef } from '../jsonschema';
|
|
9
|
+
import type { SchemaOutput, TypeSchema } from './TypeSchema';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
https://github.com/react-hook-form/resolvers
|
|
13
|
+
https://github.com/decs/typeschema/tree/main/packages
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function validate<S extends TypeSchema>(schema: S, data: unknown): ValidationResult<SchemaOutput<S>> {
|
|
17
|
+
if (isZodSchema(schema)) {
|
|
18
|
+
const result = schema.safeParse(data);
|
|
19
|
+
if (result.success) {
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
data: result.data as any,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
issues: result.error.issues.map(({ message, path, code }) => ({ message, path, code })),
|
|
28
|
+
// message: z.prettifyError(result.error),
|
|
29
|
+
success: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isTypeBoxSchema(schema)) {
|
|
34
|
+
const checker = TypeCompiler.Compile(schema);
|
|
35
|
+
if (checker.Check(data)) {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
data: data as any,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
issues: Array.from(checker.Errors(data)).map(({ message, path }) => {
|
|
43
|
+
return { message, path: [path] };
|
|
44
|
+
}),
|
|
45
|
+
// message: '',
|
|
46
|
+
success: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isJsonSchema(schema)) {
|
|
51
|
+
const ajv = new Ajv({
|
|
52
|
+
allErrors: true,
|
|
53
|
+
validateSchema: true,
|
|
54
|
+
});
|
|
55
|
+
addFormats(ajv);
|
|
56
|
+
const validator = ajv.compile(schema);
|
|
57
|
+
if (validator(data)) {
|
|
58
|
+
return {
|
|
59
|
+
data: data as any,
|
|
60
|
+
success: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const issues =
|
|
65
|
+
validator.errors?.map((error) => {
|
|
66
|
+
const message = error.message || 'Unknown error';
|
|
67
|
+
const path = error.instancePath
|
|
68
|
+
.split('/')
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.map((p) => p as PropertyKey);
|
|
71
|
+
return { message, path };
|
|
72
|
+
}) || [];
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
// message: 'Invalid data',
|
|
76
|
+
issues,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
// message: 'Invalid schema',
|
|
83
|
+
issues: [
|
|
84
|
+
{
|
|
85
|
+
message: 'Unknown schema type',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parseData<S extends TypeSchema>(schema: S, data: unknown): SchemaOutput<S> {
|
|
92
|
+
let result = validate(schema, data);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
return result.data;
|
|
95
|
+
}
|
|
96
|
+
throw Object.assign(
|
|
97
|
+
new Error(
|
|
98
|
+
result.issues
|
|
99
|
+
.map((v) => {
|
|
100
|
+
return `${ifPresent(v.path?.join(), (v) => `[${v}]`)}: ${v.message}`;
|
|
101
|
+
})
|
|
102
|
+
.join('; '),
|
|
103
|
+
),
|
|
104
|
+
{
|
|
105
|
+
issues: result.issues,
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatIssues(schema: TypeSchema, issues: Array<ValidationIssue>): string {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isZodSchema<I = any, O = any>(schema: any | TypeSchema<I, O>): schema is ZodType<O, I> {
|
|
115
|
+
return typeof schema === 'object' && 'parse' in schema;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isTypeBoxSchema(schema: any): schema is TSchema {
|
|
119
|
+
return typeof schema === 'object' && TypeBoxKind in schema;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isJsonSchema(schema: any): schema is JsonSchemaDef {
|
|
123
|
+
let sc: JsonSchemaDef = schema;
|
|
124
|
+
return typeof schema === 'object' && Boolean(sc.type || sc.properties || sc.anyOf || sc.oneOf || sc.$ref || sc.items);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type ValidationIssue = {
|
|
128
|
+
message: string;
|
|
129
|
+
path?: Array<PropertyKey>;
|
|
130
|
+
code?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type ValidationResult<TOutput> =
|
|
134
|
+
| { success: true; data: TOutput }
|
|
135
|
+
| { success: false; issues: Array<ValidationIssue> };
|