@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.
Files changed (152) hide show
  1. package/lib/cn/DivisionCode.js +55 -32
  2. package/lib/cn/DivisionCode.test.js +140 -0
  3. package/lib/cn/Mod11Checksum.js +80 -37
  4. package/lib/cn/Mod31Checksum.js +89 -40
  5. package/lib/cn/ResidentIdentityCardNumber.js +16 -16
  6. package/lib/cn/ResidentIdentityCardNumber.test.js +21 -0
  7. package/lib/cn/UnifiedSocialCreditCode.js +32 -26
  8. package/lib/cn/UnifiedSocialCreditCode.test.js +16 -0
  9. package/lib/cn/formatDate.js +5 -7
  10. package/lib/cn/index.js +0 -1
  11. package/lib/cn/parseSex.js +0 -2
  12. package/lib/cn/types.d.js +0 -2
  13. package/lib/consola/createStandardConsolaReporter.js +6 -6
  14. package/lib/consola/formatLogObject.js +133 -32
  15. package/lib/consola/index.js +0 -1
  16. package/lib/data/formatSort.js +5 -6
  17. package/lib/data/formatSort.test.js +34 -0
  18. package/lib/data/index.js +0 -1
  19. package/lib/data/maybeNumber.js +11 -5
  20. package/lib/data/parseSort.js +28 -22
  21. package/lib/data/parseSort.test.js +188 -0
  22. package/lib/data/resolvePagination.js +27 -16
  23. package/lib/data/resolvePagination.test.js +232 -0
  24. package/lib/data/types.d.js +0 -2
  25. package/lib/index.js +0 -1
  26. package/lib/jsonschema/JsonSchema.js +78 -52
  27. package/lib/jsonschema/JsonSchema.test.js +137 -0
  28. package/lib/jsonschema/index.js +0 -1
  29. package/lib/jsonschema/types.d.js +5 -3
  30. package/lib/meta/defineFileType.js +103 -20
  31. package/lib/meta/defineInit.js +250 -31
  32. package/lib/meta/defineMetadata.js +140 -24
  33. package/lib/meta/defineMetadata.test.js +13 -0
  34. package/lib/meta/index.js +0 -1
  35. package/lib/password/PHC.js +87 -63
  36. package/lib/password/PHC.test.js +539 -0
  37. package/lib/password/Password.js +291 -29
  38. package/lib/password/Password.test.js +362 -0
  39. package/lib/password/createArgon2PasswordAlgorithm.js +191 -35
  40. package/lib/password/createBase64PasswordAlgorithm.js +141 -8
  41. package/lib/password/createBcryptPasswordAlgorithm.js +168 -13
  42. package/lib/password/createPBKDF2PasswordAlgorithm.js +228 -46
  43. package/lib/password/createScryptPasswordAlgorithm.js +211 -55
  44. package/lib/password/index.js +0 -1
  45. package/lib/password/server/index.js +0 -1
  46. package/lib/resource/Identifiable.js +1 -0
  47. package/lib/resource/getTitleOfResource.js +10 -0
  48. package/lib/resource/index.js +1 -0
  49. package/lib/resource/schema/AnyResourceSchema.js +89 -0
  50. package/lib/resource/schema/BaseResourceSchema.js +29 -0
  51. package/lib/resource/schema/ResourceActionType.js +118 -0
  52. package/lib/resource/schema/ResourceStatus.js +93 -0
  53. package/lib/resource/schema/ResourceType.js +24 -0
  54. package/lib/resource/schema/SchemaRegistry.js +38 -0
  55. package/lib/resource/schema/SexType.js +10 -0
  56. package/lib/resource/schema/types.js +89 -0
  57. package/lib/resource/schema/types.test.js +14 -0
  58. package/lib/schema/TypeSchema.d.js +1 -0
  59. package/lib/schema/createSchemaData.js +173 -0
  60. package/lib/schema/findJsonSchemaByPath.js +49 -0
  61. package/lib/schema/getSchemaCache.js +11 -0
  62. package/lib/schema/getSchemaOptions.js +24 -0
  63. package/lib/schema/index.js +5 -0
  64. package/lib/schema/toJsonSchema.js +441 -0
  65. package/lib/schema/toJsonSchema.test.js +27 -0
  66. package/lib/schema/validate.js +124 -0
  67. package/lib/search/AdvanceSearch.js +0 -1
  68. package/lib/search/AdvanceSearch.test.js +435 -0
  69. package/lib/search/formatAdvanceSearch.js +41 -27
  70. package/lib/search/index.js +0 -1
  71. package/lib/search/optimizeAdvanceSearch.js +79 -25
  72. package/lib/search/parseAdvanceSearch.js +5 -5
  73. package/lib/search/parser.d.js +0 -2
  74. package/lib/search/parser.js +97 -74
  75. package/lib/search/types.d.js +0 -2
  76. package/lib/tools/generateSchema.js +197 -39
  77. package/lib/tools/renderJsonSchemaToMarkdownDoc.js +143 -55
  78. package/package.json +23 -11
  79. package/src/data/maybeNumber.ts +5 -1
  80. package/src/data/resolvePagination.test.ts +1 -1
  81. package/src/data/resolvePagination.ts +18 -7
  82. package/src/data/types.d.ts +12 -0
  83. package/src/jsonschema/JsonSchema.test.ts +17 -0
  84. package/src/jsonschema/JsonSchema.ts +2 -2
  85. package/src/jsonschema/types.d.ts +63 -12
  86. package/src/resource/Identifiable.ts +3 -0
  87. package/src/resource/getTitleOfResource.tsx +6 -0
  88. package/src/resource/index.ts +3 -0
  89. package/src/resource/schema/AnyResourceSchema.ts +113 -0
  90. package/src/resource/schema/BaseResourceSchema.ts +32 -0
  91. package/src/resource/schema/ResourceActionType.ts +123 -0
  92. package/src/resource/schema/ResourceStatus.ts +94 -0
  93. package/src/resource/schema/ResourceType.ts +25 -0
  94. package/src/resource/schema/SchemaRegistry.ts +42 -0
  95. package/src/resource/schema/SexType.ts +13 -0
  96. package/src/resource/schema/types.test.ts +18 -0
  97. package/src/resource/schema/types.ts +105 -0
  98. package/src/schema/TypeSchema.d.ts +32 -0
  99. package/src/schema/createSchemaData.ts +81 -0
  100. package/src/schema/findJsonSchemaByPath.ts +37 -0
  101. package/src/schema/getSchemaCache.ts +21 -0
  102. package/src/schema/getSchemaOptions.ts +24 -0
  103. package/src/schema/index.ts +6 -0
  104. package/src/schema/toJsonSchema.test.ts +37 -0
  105. package/src/schema/toJsonSchema.ts +200 -0
  106. package/src/schema/validate.ts +135 -0
  107. package/src/tools/generateSchema.ts +28 -28
  108. package/lib/cn/DivisionCode.js.map +0 -1
  109. package/lib/cn/Mod11Checksum.js.map +0 -1
  110. package/lib/cn/Mod31Checksum.js.map +0 -1
  111. package/lib/cn/ResidentIdentityCardNumber.js.map +0 -1
  112. package/lib/cn/UnifiedSocialCreditCode.js.map +0 -1
  113. package/lib/cn/formatDate.js.map +0 -1
  114. package/lib/cn/index.js.map +0 -1
  115. package/lib/cn/parseSex.js.map +0 -1
  116. package/lib/cn/types.d.js.map +0 -1
  117. package/lib/consola/createStandardConsolaReporter.js.map +0 -1
  118. package/lib/consola/formatLogObject.js.map +0 -1
  119. package/lib/consola/index.js.map +0 -1
  120. package/lib/data/formatSort.js.map +0 -1
  121. package/lib/data/index.js.map +0 -1
  122. package/lib/data/maybeNumber.js.map +0 -1
  123. package/lib/data/parseSort.js.map +0 -1
  124. package/lib/data/resolvePagination.js.map +0 -1
  125. package/lib/data/types.d.js.map +0 -1
  126. package/lib/index.js.map +0 -1
  127. package/lib/jsonschema/JsonSchema.js.map +0 -1
  128. package/lib/jsonschema/index.js.map +0 -1
  129. package/lib/jsonschema/types.d.js.map +0 -1
  130. package/lib/meta/defineFileType.js.map +0 -1
  131. package/lib/meta/defineInit.js.map +0 -1
  132. package/lib/meta/defineMetadata.js.map +0 -1
  133. package/lib/meta/index.js.map +0 -1
  134. package/lib/password/PHC.js.map +0 -1
  135. package/lib/password/Password.js.map +0 -1
  136. package/lib/password/createArgon2PasswordAlgorithm.js.map +0 -1
  137. package/lib/password/createBase64PasswordAlgorithm.js.map +0 -1
  138. package/lib/password/createBcryptPasswordAlgorithm.js.map +0 -1
  139. package/lib/password/createPBKDF2PasswordAlgorithm.js.map +0 -1
  140. package/lib/password/createScryptPasswordAlgorithm.js.map +0 -1
  141. package/lib/password/index.js.map +0 -1
  142. package/lib/password/server/index.js.map +0 -1
  143. package/lib/search/AdvanceSearch.js.map +0 -1
  144. package/lib/search/formatAdvanceSearch.js.map +0 -1
  145. package/lib/search/index.js.map +0 -1
  146. package/lib/search/optimizeAdvanceSearch.js.map +0 -1
  147. package/lib/search/parseAdvanceSearch.js.map +0 -1
  148. package/lib/search/parser.d.js.map +0 -1
  149. package/lib/search/parser.js.map +0 -1
  150. package/lib/search/types.d.js.map +0 -1
  151. package/lib/tools/generateSchema.js.map +0 -1
  152. 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> };