@wener/common 1.0.3 → 1.0.4

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 (97) hide show
  1. package/lib/cn/DivisionCode.js.map +1 -1
  2. package/lib/cn/Mod11Checksum.js.map +1 -1
  3. package/lib/cn/Mod31Checksum.js.map +1 -1
  4. package/lib/cn/ResidentIdentityCardNumber.js.map +1 -1
  5. package/lib/cn/UnifiedSocialCreditCode.js.map +1 -1
  6. package/lib/cn/formatDate.js.map +1 -1
  7. package/lib/cn/parseSex.js.map +1 -1
  8. package/lib/cn/types.d.js.map +1 -1
  9. package/lib/data/formatSort.js +15 -0
  10. package/lib/data/formatSort.js.map +1 -0
  11. package/lib/data/index.js +4 -0
  12. package/lib/data/index.js.map +1 -0
  13. package/lib/data/maybeNumber.js +22 -0
  14. package/lib/data/maybeNumber.js.map +1 -0
  15. package/lib/data/parseSort.js +95 -0
  16. package/lib/data/parseSort.js.map +1 -0
  17. package/lib/data/resolvePagination.js +36 -0
  18. package/lib/data/resolvePagination.js.map +1 -0
  19. package/lib/data/types.d.js +3 -0
  20. package/lib/data/types.d.js.map +1 -0
  21. package/lib/index.js +6 -2
  22. package/lib/index.js.map +1 -1
  23. package/lib/jsonschema/JsonSchema.js +4 -4
  24. package/lib/jsonschema/JsonSchema.js.map +1 -1
  25. package/lib/jsonschema/types.d.js.map +1 -1
  26. package/lib/meta/defineFileType.js.map +1 -1
  27. package/lib/meta/defineInit.js.map +1 -1
  28. package/lib/meta/defineMetadata.js.map +1 -1
  29. package/lib/password/PHC.js +8 -8
  30. package/lib/password/PHC.js.map +1 -1
  31. package/lib/password/Password.js.map +1 -1
  32. package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -1
  33. package/lib/password/createBase64PasswordAlgorithm.js.map +1 -1
  34. package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -1
  35. package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -1
  36. package/lib/password/createScryptPasswordAlgorithm.js.map +1 -1
  37. package/lib/search/AdvanceSearch.js.map +1 -1
  38. package/lib/search/formatAdvanceSearch.js.map +1 -1
  39. package/lib/search/optimizeAdvanceSearch.js.map +1 -1
  40. package/lib/search/parseAdvanceSearch.js.map +1 -1
  41. package/lib/search/parser.d.js.map +1 -1
  42. package/lib/search/types.d.js.map +1 -1
  43. package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +1 -1
  44. package/package.json +10 -5
  45. package/src/cn/DivisionCode.test.ts +38 -45
  46. package/src/cn/DivisionCode.ts +140 -184
  47. package/src/cn/Mod11Checksum.ts +17 -17
  48. package/src/cn/Mod31Checksum.ts +25 -25
  49. package/src/cn/ResidentIdentityCardNumber.test.ts +12 -16
  50. package/src/cn/ResidentIdentityCardNumber.ts +82 -82
  51. package/src/cn/UnifiedSocialCreditCode.test.ts +11 -11
  52. package/src/cn/UnifiedSocialCreditCode.ts +115 -120
  53. package/src/cn/__snapshots__/ResidentIdentityCardNumber.test.ts.snap +1 -1
  54. package/src/cn/formatDate.ts +10 -10
  55. package/src/cn/parseSex.ts +11 -25
  56. package/src/cn/types.d.ts +43 -43
  57. package/src/data/formatSort.test.ts +13 -0
  58. package/src/data/formatSort.ts +18 -0
  59. package/src/data/index.ts +5 -0
  60. package/src/data/maybeNumber.ts +23 -0
  61. package/src/data/parseSort.test.ts +67 -0
  62. package/src/data/parseSort.ts +108 -0
  63. package/src/data/resolvePagination.test.ts +58 -0
  64. package/src/data/resolvePagination.ts +60 -0
  65. package/src/data/types.d.ts +33 -0
  66. package/src/index.ts +8 -2
  67. package/src/jsonschema/JsonSchema.test.ts +13 -22
  68. package/src/jsonschema/JsonSchema.ts +145 -177
  69. package/src/jsonschema/types.d.ts +151 -161
  70. package/src/meta/defineFileType.tsx +54 -54
  71. package/src/meta/defineInit.ts +32 -53
  72. package/src/meta/defineMetadata.test.ts +5 -7
  73. package/src/meta/defineMetadata.ts +28 -46
  74. package/src/password/PHC.test.ts +186 -277
  75. package/src/password/PHC.ts +243 -243
  76. package/src/password/Password.test.ts +38 -50
  77. package/src/password/Password.ts +73 -95
  78. package/src/password/createArgon2PasswordAlgorithm.ts +65 -69
  79. package/src/password/createBase64PasswordAlgorithm.ts +9 -9
  80. package/src/password/createBcryptPasswordAlgorithm.ts +20 -22
  81. package/src/password/createPBKDF2PasswordAlgorithm.ts +49 -61
  82. package/src/password/createScryptPasswordAlgorithm.ts +48 -59
  83. package/src/search/AdvanceSearch.test.ts +136 -143
  84. package/src/search/AdvanceSearch.ts +6 -6
  85. package/src/search/formatAdvanceSearch.ts +44 -53
  86. package/src/search/optimizeAdvanceSearch.ts +70 -83
  87. package/src/search/parseAdvanceSearch.ts +16 -19
  88. package/src/search/parser.d.ts +3 -3
  89. package/src/search/types.d.ts +28 -54
  90. package/src/tools/renderJsonSchemaToMarkdownDoc.ts +69 -69
  91. package/lib/normalizePagination.js +0 -14
  92. package/lib/normalizePagination.js.map +0 -1
  93. package/lib/parseSort.js +0 -106
  94. package/lib/parseSort.js.map +0 -1
  95. package/src/normalizePagination.ts +0 -25
  96. package/src/parseSort.test.ts +0 -42
  97. package/src/parseSort.ts +0 -133
@@ -0,0 +1,108 @@
1
+ import { arrayOfMaybeArray, type MaybeArray } from '@wener/utils';
2
+
3
+ export type SortRule = { field: string; order: 'asc' | 'desc'; nulls?: 'last' | 'first' };
4
+
5
+ /**
6
+ * Parses various format of sort specifications into standardized SortRule objects
7
+ *
8
+ * Supported formats:
9
+ * - "field asc|desc"
10
+ * - "field asc|desc nulls first|last"
11
+ * - "+field" for ascending, "-field" for descending
12
+ * - Object notation: { field: string, order?: string, nulls?: string }
13
+ *
14
+ * @example
15
+ * parseSort('name desc') // [{ field: 'name', order: 'desc' }]
16
+ * parseSort('+name,-age') // [{ field: 'name', order: 'asc' }, { field: 'age', order: 'desc' }]
17
+ * parseSort('name asc nulls last') // [{ field: 'name', order: 'asc', nulls: 'last' }]
18
+ * parseSort([{ field: 'name' }]) // [{ field: 'name', order: 'asc' }]
19
+ */
20
+ export function parseSort(
21
+ order: MaybeArray<{ field?: string; order?: string; nulls?: string } | string> | undefined | null,
22
+ ): SortRule[] {
23
+ if (!order) {
24
+ return [];
25
+ }
26
+
27
+ return arrayOfMaybeArray(order).flatMap((v): MaybeArray<SortRule> => {
28
+ if (!v) return [];
29
+ if (typeof v === 'object') {
30
+ if (!v.field) {
31
+ return [];
32
+ }
33
+ const rule: SortRule = {
34
+ field: v.field,
35
+ order: normalizeOrder(v.order),
36
+ };
37
+
38
+ if (v.nulls) {
39
+ rule.nulls = normalizeNulls(v.nulls);
40
+ }
41
+ return rule;
42
+ }
43
+ return v
44
+ .split(',')
45
+ .map((v) => v.trim())
46
+ .filter(Boolean)
47
+ .map(_parse);
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Normalizes order values to 'asc' or 'desc'
53
+ */
54
+ function normalizeOrder(order?: string): SortRule['order'] {
55
+ return order?.toLowerCase() === 'asc' ? 'asc' : 'desc';
56
+ }
57
+
58
+ /**
59
+ * Normalizes nulls values to 'last' or 'first'
60
+ */
61
+ function normalizeNulls(nulls?: string): SortRule['nulls'] {
62
+ return nulls?.toLowerCase() === 'last' ? 'last' : 'first';
63
+ }
64
+
65
+ function _parse(v: string): SortRule {
66
+ const sp = v.split(/\s+/);
67
+ let field = '';
68
+ let order: SortRule['order'];
69
+ let nulls: SortRule['nulls'];
70
+
71
+ // Handle first token which should be the field name (possibly with +/- prefix)
72
+ const f = sp.shift();
73
+ if (!f) return { field: '', order: 'asc' }; // Defensive programming
74
+
75
+ if (f.startsWith('-') || f.startsWith('+')) {
76
+ field = f.slice(1).trim();
77
+ order = f.startsWith('-') ? 'desc' : 'asc';
78
+ } else {
79
+ field = f.trim();
80
+ }
81
+
82
+ // Process remaining tokens
83
+ while (sp.length > 0) {
84
+ const token = sp.shift()?.trim()?.toLowerCase();
85
+ if (!token) continue;
86
+
87
+ switch (token) {
88
+ case 'asc':
89
+ case 'desc':
90
+ order = token;
91
+ break;
92
+
93
+ case 'nulls':
94
+ nulls = sp.shift()?.trim()?.toLowerCase() === 'last' ? 'last' : 'first';
95
+ break;
96
+
97
+ case 'last':
98
+ case 'first':
99
+ nulls = token;
100
+ break;
101
+ }
102
+ }
103
+
104
+ order ||= 'asc';
105
+
106
+ // Only include nulls if specified
107
+ return nulls ? { field, order, nulls } : { field, order };
108
+ }
@@ -0,0 +1,58 @@
1
+ import { expect, test } from 'vitest';
2
+ import { resolvePagination } from './resolvePagination';
3
+
4
+ test(resolvePagination.name, () => {
5
+ const testCases: Array<[string, any, any]> = [
6
+ [
7
+ 'pageSize with null pageIndex',
8
+ { pageSize: '10', pageIndex: null },
9
+ { limit: 10, offset: 0, pageSize: 10, pageNumber: 1, pageIndex: 0 },
10
+ ],
11
+ [
12
+ 'pageSize with pageIndex',
13
+ { pageSize: '10', pageIndex: 1 },
14
+ { limit: 10, offset: 10, pageSize: 10, pageNumber: 2, pageIndex: 1 },
15
+ ],
16
+ [
17
+ 'pageSize with pageIndex 2',
18
+ { pageSize: '10', pageIndex: 2 },
19
+ { limit: 10, offset: 20, pageSize: 10, pageNumber: 3, pageIndex: 2 },
20
+ ],
21
+ [
22
+ 'limit with null offset',
23
+ { limit: '10', offset: null },
24
+ { limit: 10, offset: 0, pageSize: 10, pageNumber: 1, pageIndex: 0 },
25
+ ],
26
+ [
27
+ 'limit with offset',
28
+ { limit: '10', offset: '20' },
29
+ { limit: 10, offset: 20, pageSize: 10, pageNumber: 3, pageIndex: 2 },
30
+ ],
31
+ [
32
+ 'null limit with offset',
33
+ { limit: null, offset: '20' },
34
+ { limit: 20, offset: 20, pageSize: 20, pageNumber: 2, pageIndex: 1 },
35
+ ],
36
+ ['empty input', {}, { limit: 20, offset: 0, pageSize: 20, pageNumber: 1, pageIndex: 0 }],
37
+ [
38
+ 'negative values',
39
+ { pageSize: '-5', pageIndex: '-1', limit: '-10', offset: '-20' },
40
+ { limit: 1, offset: 0, pageSize: 1, pageNumber: 1, pageIndex: 0 },
41
+ ],
42
+ [
43
+ 'custom pageNumber',
44
+ { pageNumber: 3, pageSize: 15 },
45
+ { limit: 15, offset: 30, pageSize: 15, pageNumber: 3, pageIndex: 2 },
46
+ ],
47
+ ];
48
+
49
+ for (const [description, input, expected] of testCases) {
50
+ const result = resolvePagination(input);
51
+ expect(result, description).toMatchObject(expected);
52
+ }
53
+ });
54
+
55
+ test(`${resolvePagination.name} with options`, () => {
56
+ expect(resolvePagination({ pageSize: '200' }, { maxPageSize: 50 })).toMatchObject({ pageSize: 50, limit: 50 });
57
+ expect(resolvePagination({}, { pageSize: 30 })).toMatchObject({ pageSize: 30, limit: 30 });
58
+ });
@@ -0,0 +1,60 @@
1
+ import { maybeFunction, type MaybeFunction } from '@wener/utils';
2
+ import { clamp, mapValues, omitBy, pick } from 'es-toolkit';
3
+ import { maybeNumber, type MaybeNumber } from './maybeNumber';
4
+
5
+ export type PaginationInput = {
6
+ limit?: MaybeNumber;
7
+ offset?: MaybeNumber;
8
+ pageSize?: MaybeNumber;
9
+ pageNumber?: MaybeNumber;
10
+ pageIndex?: MaybeNumber;
11
+ };
12
+
13
+ export type ResolvedPagination = {
14
+ limit: number;
15
+ offset: number;
16
+ pageSize: number;
17
+ pageNumber: number;
18
+ pageIndex: number;
19
+ };
20
+
21
+ export function resolvePagination(
22
+ page: PaginationInput,
23
+ options: {
24
+ pageSize?: MaybeFunction<number, [number | undefined]>;
25
+ maxPageSize?: number;
26
+ } = {},
27
+ ): ResolvedPagination {
28
+ let out = omitBy(
29
+ mapValues(
30
+ //
31
+ pick(page, ['limit', 'offset', 'pageSize', 'pageNumber', 'pageIndex']),
32
+ // to Number
33
+ (v) => maybeNumber(v),
34
+ ),
35
+ (v) => v === undefined || v === null,
36
+ );
37
+ let { pageSize } = out;
38
+ if (options.pageSize) {
39
+ pageSize = maybeFunction(options.pageSize, pageSize);
40
+ }
41
+ pageSize ??= 20;
42
+ pageSize = clamp(pageSize, 1, options.maxPageSize ?? 100);
43
+
44
+ let { pageNumber = 1, pageIndex, limit, offset } = out;
45
+ // page index over page number
46
+ pageNumber = Math.max(pageNumber, 1);
47
+ pageIndex = Math.max(pageIndex ?? pageNumber - 1, 0);
48
+ limit = Math.max(1, limit ?? pageSize);
49
+ offset = Math.max(0, offset ?? pageSize * pageIndex);
50
+
51
+ pageSize = limit;
52
+ pageIndex = Math.floor(offset / pageSize);
53
+ return {
54
+ limit,
55
+ offset,
56
+ pageSize,
57
+ pageNumber: pageIndex + 1,
58
+ pageIndex,
59
+ };
60
+ }
@@ -0,0 +1,33 @@
1
+ export type ListQueryInput = {
2
+ /** Filter string */
3
+ filter?: string;
4
+ /** Filter array */
5
+ filters?: string[];
6
+ /** Filter by IDs */
7
+ ids?: string[];
8
+ /** Search query */
9
+ search?: string;
10
+ /** Items per page */
11
+ limit?: number;
12
+ /** Offset for pagination */
13
+ offset?: number;
14
+ /** Ordering criteria */
15
+ order?: string[];
16
+ /** Page index (0-based) */
17
+ pageIndex?: number;
18
+ /** Page number (1-based) */
19
+ pageNumber?: number;
20
+ /** Items per page */
21
+ pageSize?: number;
22
+ /** Cursor for pagination */
23
+ cursor?: string;
24
+ /** Whether to include deleted items */
25
+ deleted?: boolean;
26
+ };
27
+
28
+ type ListResult<T = any> = {
29
+ /** List of items */
30
+ data: T[];
31
+ /** Total number of items */
32
+ total: number;
33
+ };
package/src/index.ts CHANGED
@@ -1,2 +1,8 @@
1
- export { normalizePagination } from './normalizePagination';
2
- export { parseSort, type SortRule } from './parseSort';
1
+ /**
2
+ * @deprecated
3
+ */
4
+ export { parseSort, type SortRule } from './data/parseSort';
5
+ /**
6
+ * @deprecated
7
+ */
8
+ export { resolvePagination as normalizePagination } from './data/resolvePagination';
@@ -2,26 +2,17 @@ import { describe, expect, it } from 'vitest';
2
2
  import { JsonSchema } from './JsonSchema';
3
3
 
4
4
  describe('jsonschema', () => {
5
- it('should create from schema', () => {
6
- for (const [a, b] of [
7
- [{ type: 'string' }, ''],
8
- [{ type: 'number' }, 0],
9
- [{ type: 'number', default: 1 }, 1],
10
- [{ type: 'integer' }, 0],
11
- [{ type: 'array' }, []],
12
- [{ type: 'object' }, {}],
13
- [
14
- {
15
- type: 'object',
16
- properties: {
17
- a: { type: 'string' },
18
- },
19
- required: ['a'],
20
- },
21
- { a: '' },
22
- ],
23
- ]) {
24
- expect(JsonSchema.create(a)).toEqual(b);
25
- }
26
- });
5
+ it('should create from schema', () => {
6
+ for (const [a, b] of [
7
+ [{ type: 'string' }, ''],
8
+ [{ type: 'number' }, 0],
9
+ [{ type: 'number', default: 1 }, 1],
10
+ [{ type: 'integer' }, 0],
11
+ [{ type: 'array' }, []],
12
+ [{ type: 'object' }, {}],
13
+ [{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] }, { a: '' }],
14
+ ]) {
15
+ expect(JsonSchema.create(a)).toEqual(b);
16
+ }
17
+ });
27
18
  });
@@ -8,190 +8,158 @@ import { match, P } from 'ts-pattern';
8
8
  import type { JsonSchemaDef } from './types';
9
9
 
10
10
  function _createAjv(opt: Options) {
11
- const ajv = new Ajv(opt);
12
- addKeywords(ajv);
13
- addFormats(ajv);
14
- return ajv;
11
+ const ajv = new Ajv(opt);
12
+ addKeywords(ajv);
13
+ addFormats(ajv);
14
+ return ajv;
15
15
  }
16
16
 
17
- type ValidateOptions = {
18
- mutate?: boolean;
19
- clone?: boolean;
20
- ajv?: Ajv;
21
- };
17
+ type ValidateOptions = { mutate?: boolean; clone?: boolean; ajv?: Ajv };
22
18
 
23
19
  type ValidateResult<T> =
24
- | {
25
- data: T;
26
- success: true;
27
- message: undefined;
28
- }
29
- | {
30
- data: undefined;
31
- success: false;
32
- message: string;
33
- errors: ErrorObject[];
34
- };
35
-
36
- function validate({
37
- schema,
38
- data,
39
- mutate,
40
- clone,
41
- ajv,
42
- }: ValidateOptions & {
43
- schema: any;
44
- data: any;
45
- }) {
46
- let opts: Options = {
47
- // strict: 'log',
48
- strict: true,
49
- strictSchema: 'log', // skip unknown keywords in schema
50
- };
51
-
52
- if (mutate) {
53
- Object.assign(opts, {
54
- removeAdditional: true,
55
- useDefaults: true,
56
- coerceTypes: true,
57
- allErrors: true,
58
- });
59
- }
60
-
61
- if (clone) {
62
- data = structuredClone(data);
63
- }
64
-
65
- if (!ajv) {
66
- ajv = JsonSchema.createAjv(opts);
67
- }
68
-
69
- const validate = ajv.compile(schema);
70
-
71
- // consider reusing validate instance
72
-
73
- const valid = validate(data);
74
- const errors = validate.errors;
75
- localize(errors);
76
-
77
- return {
78
- data,
79
- success: valid,
80
- message: ajv.errorsText(errors),
81
- errors: errors,
82
- };
20
+ | { data: T; success: true; message: undefined }
21
+ | { data: undefined; success: false; message: string; errors: ErrorObject[] };
22
+
23
+ function validate({ schema, data, mutate, clone, ajv }: ValidateOptions & { schema: any; data: any }) {
24
+ let opts: Options = {
25
+ // strict: 'log',
26
+ strict: true,
27
+ strictSchema: 'log', // skip unknown keywords in schema
28
+ };
29
+
30
+ if (mutate) {
31
+ Object.assign(opts, { removeAdditional: true, useDefaults: true, coerceTypes: true, allErrors: true });
32
+ }
33
+
34
+ if (clone) {
35
+ data = structuredClone(data);
36
+ }
37
+
38
+ if (!ajv) {
39
+ ajv = JsonSchema.createAjv(opts);
40
+ }
41
+
42
+ const validate = ajv.compile(schema);
43
+
44
+ // consider reusing validate instance
45
+
46
+ const valid = validate(data);
47
+ const errors = validate.errors;
48
+ localize(errors);
49
+
50
+ return { data, success: valid, message: ajv.errorsText(errors), errors: errors };
83
51
  }
84
52
 
85
53
  type TypeOfSchema<S> = S extends TSchema ? Static<S> : any;
86
54
 
87
55
  export namespace JsonSchema {
88
- export let schemas: JsonSchemaDef[] = [];
89
-
90
- export const createAjv = _createAjv;
91
-
92
- export function addSchema(
93
- schema: JsonSchemaDef,
94
- {
95
- onConflict = 'throw',
96
- }: {
97
- onConflict?: 'throw' | 'ignore' | 'replace' | ((old: JsonSchemaDef, neo: JsonSchemaDef) => JsonSchemaDef);
98
- } = {},
99
- ) {
100
- if (!schema.$id) throw new Error('Schema must have $id');
101
- switch (onConflict) {
102
- case 'ignore':
103
- onConflict = (old) => old;
104
- break;
105
- case 'replace':
106
- onConflict = (_, neo) => neo;
107
- break;
108
- case 'throw':
109
- onConflict = (old, neo) => {
110
- throw new Error(`Schema ${neo.$id} already exists`);
111
- };
112
- break;
113
- }
114
- let idx = schemas.findIndex((s) => s.$id === schema.$id);
115
- if (idx >= 0) {
116
- schemas[idx] = onConflict(schemas[idx], schema);
117
- } else {
118
- schemas.push(schema);
119
- }
120
- }
121
-
122
- /**
123
- * Check data is valid, will not use default
124
- */
125
- export function check<S>(schema: S, data: any): ValidateResult<TypeOfSchema<S>> {
126
- return validate({ schema, data, mutate: false, clone: true }) as any;
127
- }
128
-
129
- /**
130
- * Parse data with default value and coerceTypes
131
- */
132
- export function safeParse<S>(schema: S, data: any): ValidateResult<TypeOfSchema<S>> {
133
- return validate({ schema, data, mutate: true, clone: true }) as any;
134
- }
135
-
136
- export function parse<S>(schema: S, data: any): TypeOfSchema<S> {
137
- const { data: out, message, errors } = validate({ schema, data, mutate: true, clone: true });
138
- if (errors) {
139
- throw new Error(message);
140
- }
141
- return out;
142
- }
143
-
144
- export function create<S>(schema: S, data?: any): TypeOfSchema<S> {
145
- // will not ensure value match the rule
146
- return match(schema as JsonSchemaDef)
147
- .returnType<any>()
148
- .with({ const: P.select() }, (v) => v)
149
- .with({ default: P.select() }, (v) => v)
150
- .with({ anyOf: P.nonNullable }, (schema) => {
151
- return create(schema.anyOf[0]);
152
- })
153
- .with({ oneOf: P.nonNullable }, (schema) => {
154
- return create(schema.oneOf[0]);
155
- })
156
- .with({ type: 'string' }, (schema) => '')
157
- .with({ type: P.union('number', 'integer') }, (schema) => 0)
158
- .with({ type: 'object' }, (schema) => {
159
- let out = validate({ schema, data: data ?? {}, mutate: true });
160
- if (!out.success) {
161
- // fallback
162
- let obj = data || {};
163
- schema.required?.forEach((key) => {
164
- if (!(key in obj)) {
165
- let last = obj[key];
166
- let prop = schema.properties?.[key];
167
- if (prop && isNil(last)) obj[key] = JsonSchema.create(prop, last);
168
- }
169
- });
170
- out = validate({ schema, data: obj, mutate: true });
171
- if (!out.success) {
172
- console.warn(`Failed to create object with schema: ${out.message}`);
173
- }
174
- }
175
- return out.data;
176
- })
177
- .with({ type: 'null' }, () => null)
178
- .with({ type: 'boolean' }, (schema) => false)
179
- .with({ type: 'array' }, (schema) => [])
180
- .otherwise(() => {
181
- return undefined;
182
- });
183
- }
184
-
185
- export function isPrimitiveType(schema: any): boolean {
186
- return match(schema as JsonSchemaDef)
187
- .returnType<boolean>()
188
- .with({ type: P.union('number', 'integer', 'string', 'boolean') }, () => true)
189
- .with({ anyOf: P.nonNullable }, (schema) => {
190
- return isPrimitiveType(schema.anyOf[0]);
191
- })
192
- .with({ oneOf: P.nonNullable }, (schema) => {
193
- return isPrimitiveType(schema.oneOf[0]);
194
- })
195
- .otherwise(() => false);
196
- }
56
+ export let schemas: JsonSchemaDef[] = [];
57
+
58
+ export const createAjv = _createAjv;
59
+
60
+ export function addSchema(
61
+ schema: JsonSchemaDef,
62
+ {
63
+ onConflict = 'throw',
64
+ }: {
65
+ onConflict?: 'throw' | 'ignore' | 'replace' | ((old: JsonSchemaDef, neo: JsonSchemaDef) => JsonSchemaDef);
66
+ } = {},
67
+ ) {
68
+ if (!schema.$id) throw new Error('Schema must have $id');
69
+ switch (onConflict) {
70
+ case 'ignore':
71
+ onConflict = (old) => old;
72
+ break;
73
+ case 'replace':
74
+ onConflict = (_, neo) => neo;
75
+ break;
76
+ case 'throw':
77
+ onConflict = (old, neo) => {
78
+ throw new Error(`Schema ${neo.$id} already exists`);
79
+ };
80
+ break;
81
+ }
82
+ let idx = schemas.findIndex((s) => s.$id === schema.$id);
83
+ if (idx >= 0) {
84
+ schemas[idx] = onConflict(schemas[idx], schema);
85
+ } else {
86
+ schemas.push(schema);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check data is valid, will not use default
92
+ */
93
+ export function check<S>(schema: S, data: any): ValidateResult<TypeOfSchema<S>> {
94
+ return validate({ schema, data, mutate: false, clone: true }) as any;
95
+ }
96
+
97
+ /**
98
+ * Parse data with default value and coerceTypes
99
+ */
100
+ export function safeParse<S>(schema: S, data: any): ValidateResult<TypeOfSchema<S>> {
101
+ return validate({ schema, data, mutate: true, clone: true }) as any;
102
+ }
103
+
104
+ export function parse<S>(schema: S, data: any): TypeOfSchema<S> {
105
+ const { data: out, message, errors } = validate({ schema, data, mutate: true, clone: true });
106
+ if (errors) {
107
+ throw new Error(message);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ export function create<S>(schema: S, data?: any): TypeOfSchema<S> {
113
+ // will not ensure value match the rule
114
+ return match(schema as JsonSchemaDef)
115
+ .returnType<any>()
116
+ .with({ const: P.select() }, (v) => v)
117
+ .with({ default: P.select() }, (v) => v)
118
+ .with({ anyOf: P.nonNullable }, (schema) => {
119
+ return create(schema.anyOf[0]);
120
+ })
121
+ .with({ oneOf: P.nonNullable }, (schema) => {
122
+ return create(schema.oneOf[0]);
123
+ })
124
+ .with({ type: 'string' }, (schema) => '')
125
+ .with({ type: P.union('number', 'integer') }, (schema) => 0)
126
+ .with({ type: 'object' }, (schema) => {
127
+ let out = validate({ schema, data: data ?? {}, mutate: true });
128
+ if (!out.success) {
129
+ // fallback
130
+ let obj = data || {};
131
+ schema.required?.forEach((key) => {
132
+ if (!(key in obj)) {
133
+ let last = obj[key];
134
+ let prop = schema.properties?.[key];
135
+ if (prop && isNil(last)) obj[key] = JsonSchema.create(prop, last);
136
+ }
137
+ });
138
+ out = validate({ schema, data: obj, mutate: true });
139
+ if (!out.success) {
140
+ console.warn(`Failed to create object with schema: ${out.message}`);
141
+ }
142
+ }
143
+ return out.data;
144
+ })
145
+ .with({ type: 'null' }, () => null)
146
+ .with({ type: 'boolean' }, (schema) => false)
147
+ .with({ type: 'array' }, (schema) => [])
148
+ .otherwise(() => {
149
+ return undefined;
150
+ });
151
+ }
152
+
153
+ export function isPrimitiveType(schema: any): boolean {
154
+ return match(schema as JsonSchemaDef)
155
+ .returnType<boolean>()
156
+ .with({ type: P.union('number', 'integer', 'string', 'boolean') }, () => true)
157
+ .with({ anyOf: P.nonNullable }, (schema) => {
158
+ return isPrimitiveType(schema.anyOf[0]);
159
+ })
160
+ .with({ oneOf: P.nonNullable }, (schema) => {
161
+ return isPrimitiveType(schema.oneOf[0]);
162
+ })
163
+ .otherwise(() => false);
164
+ }
197
165
  }