@wener/common 1.0.2 → 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.
- package/lib/cn/DivisionCode.js +311 -0
- package/lib/cn/DivisionCode.js.map +1 -0
- package/lib/cn/Mod11Checksum.js +42 -0
- package/lib/cn/Mod11Checksum.js.map +1 -0
- package/lib/cn/Mod31Checksum.js +48 -0
- package/lib/cn/Mod31Checksum.js.map +1 -0
- package/lib/cn/ResidentIdentityCardNumber.js +50 -0
- package/lib/cn/ResidentIdentityCardNumber.js.map +1 -0
- package/lib/cn/UnifiedSocialCreditCode.js +118 -0
- package/lib/cn/UnifiedSocialCreditCode.js.map +1 -0
- package/lib/cn/formatDate.js +15 -0
- package/lib/cn/formatDate.js.map +1 -0
- package/lib/cn/index.js +4 -0
- package/lib/cn/index.js.map +1 -0
- package/lib/cn/parseSex.js +22 -0
- package/lib/cn/parseSex.js.map +1 -0
- package/lib/cn/types.d.js +8 -0
- package/lib/cn/types.d.js.map +1 -0
- package/lib/data/formatSort.js +15 -0
- package/lib/data/formatSort.js.map +1 -0
- package/lib/data/index.js +4 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/maybeNumber.js +22 -0
- package/lib/data/maybeNumber.js.map +1 -0
- package/lib/data/parseSort.js +95 -0
- package/lib/data/parseSort.js.map +1 -0
- package/lib/data/resolvePagination.js +36 -0
- package/lib/data/resolvePagination.js.map +1 -0
- package/lib/data/types.d.js +3 -0
- package/lib/data/types.d.js.map +1 -0
- package/lib/index.js +6 -2
- package/lib/index.js.map +1 -1
- package/lib/jsonschema/JsonSchema.js +4 -4
- package/lib/jsonschema/JsonSchema.js.map +1 -1
- package/lib/jsonschema/types.d.js.map +1 -1
- package/lib/meta/defineFileType.js +44 -0
- package/lib/meta/defineFileType.js.map +1 -0
- package/lib/meta/defineInit.js.map +1 -1
- package/lib/meta/defineMetadata.js +14 -3
- package/lib/meta/defineMetadata.js.map +1 -1
- package/lib/meta/index.js +1 -0
- package/lib/meta/index.js.map +1 -1
- package/lib/password/PHC.js +8 -8
- package/lib/password/PHC.js.map +1 -1
- package/lib/password/Password.js.map +1 -1
- package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -1
- package/lib/password/createBase64PasswordAlgorithm.js.map +1 -1
- package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -1
- package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -1
- package/lib/password/createScryptPasswordAlgorithm.js.map +1 -1
- package/lib/search/AdvanceSearch.js.map +1 -1
- package/lib/search/formatAdvanceSearch.js +1 -1
- package/lib/search/formatAdvanceSearch.js.map +1 -1
- package/lib/search/optimizeAdvanceSearch.js.map +1 -1
- package/lib/search/parseAdvanceSearch.js.map +1 -1
- package/lib/search/parser.d.js.map +1 -1
- package/lib/search/parser.js +380 -76
- package/lib/search/parser.js.map +1 -1
- package/lib/search/types.d.js.map +1 -1
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +1 -1
- package/package.json +14 -5
- package/src/cn/DivisionCode.test.ts +43 -0
- package/src/cn/DivisionCode.ts +209 -0
- package/src/cn/Mod11Checksum.ts +24 -0
- package/src/cn/Mod31Checksum.ts +36 -0
- package/src/cn/ResidentIdentityCardNumber.test.ts +17 -0
- package/src/cn/ResidentIdentityCardNumber.ts +96 -0
- package/src/cn/UnifiedSocialCreditCode.test.ts +16 -0
- package/src/cn/UnifiedSocialCreditCode.ts +143 -0
- package/src/cn/__snapshots__/ResidentIdentityCardNumber.test.ts.snap +15 -0
- package/src/cn/__snapshots__/UnifiedSocialCreditCode.test.ts.snap +41 -0
- package/src/cn/formatDate.ts +12 -0
- package/src/cn/index.ts +3 -0
- package/src/cn/parseSex.ts +13 -0
- package/src/cn/types.d.ts +51 -0
- package/src/data/formatSort.test.ts +13 -0
- package/src/data/formatSort.ts +18 -0
- package/src/data/index.ts +5 -0
- package/src/data/maybeNumber.ts +23 -0
- package/src/data/parseSort.test.ts +67 -0
- package/src/data/parseSort.ts +108 -0
- package/src/data/resolvePagination.test.ts +58 -0
- package/src/data/resolvePagination.ts +60 -0
- package/src/data/types.d.ts +33 -0
- package/src/index.ts +8 -2
- package/src/jsonschema/JsonSchema.test.ts +13 -22
- package/src/jsonschema/JsonSchema.ts +145 -177
- package/src/jsonschema/types.d.ts +151 -161
- package/src/meta/defineFileType.tsx +68 -0
- package/src/meta/defineInit.ts +32 -53
- package/src/meta/defineMetadata.test.ts +5 -7
- package/src/meta/defineMetadata.ts +35 -40
- package/src/meta/index.ts +2 -0
- package/src/password/PHC.test.ts +186 -277
- package/src/password/PHC.ts +243 -243
- package/src/password/Password.test.ts +38 -50
- package/src/password/Password.ts +73 -95
- package/src/password/createArgon2PasswordAlgorithm.ts +65 -69
- package/src/password/createBase64PasswordAlgorithm.ts +9 -9
- package/src/password/createBcryptPasswordAlgorithm.ts +20 -22
- package/src/password/createPBKDF2PasswordAlgorithm.ts +49 -61
- package/src/password/createScryptPasswordAlgorithm.ts +48 -59
- package/src/search/AdvanceSearch.test.ts +136 -143
- package/src/search/AdvanceSearch.ts +6 -6
- package/src/search/__snapshots__/AdvanceSearch.test.ts.snap +3 -3
- package/src/search/formatAdvanceSearch.ts +44 -53
- package/src/search/optimizeAdvanceSearch.ts +70 -83
- package/src/search/parseAdvanceSearch.ts +16 -19
- package/src/search/parser.d.ts +3 -3
- package/src/search/parser.js +325 -73
- package/src/search/parser.peggy +42 -9
- package/src/search/types.d.ts +28 -54
- package/src/tools/renderJsonSchemaToMarkdownDoc.ts +69 -69
- package/lib/normalizePagination.js +0 -14
- package/lib/normalizePagination.js.map +0 -1
- package/lib/parseSort.js +0 -91
- package/lib/parseSort.js.map +0 -1
- package/src/normalizePagination.ts +0 -25
- package/src/parseSort.test.ts +0 -42
- package/src/parseSort.ts +0 -115
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ResidentIdentityCardNumber > should parse 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"age": 76,
|
|
6
|
+
"birthDate": "19491231",
|
|
7
|
+
"checksum": "X",
|
|
8
|
+
"division": "110105",
|
|
9
|
+
"female": true,
|
|
10
|
+
"male": false,
|
|
11
|
+
"sequence": 2,
|
|
12
|
+
"sex": "Female",
|
|
13
|
+
"valid": true,
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`UnifiedSocialCreditCode > should parse 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"bureau": "9",
|
|
6
|
+
"checksum": "P",
|
|
7
|
+
"codes": [
|
|
8
|
+
"9",
|
|
9
|
+
"1",
|
|
10
|
+
"330106",
|
|
11
|
+
"673959654",
|
|
12
|
+
],
|
|
13
|
+
"division": "330106",
|
|
14
|
+
"labels": [
|
|
15
|
+
"工商",
|
|
16
|
+
"企业",
|
|
17
|
+
],
|
|
18
|
+
"subject": "673959654",
|
|
19
|
+
"subjectType": "1",
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
exports[`UnifiedSocialCreditCode > should parse 2`] = `
|
|
24
|
+
{
|
|
25
|
+
"bureau": "9",
|
|
26
|
+
"checksum": "R",
|
|
27
|
+
"codes": [
|
|
28
|
+
"9",
|
|
29
|
+
"1",
|
|
30
|
+
"330106",
|
|
31
|
+
"MA2CFLDG4",
|
|
32
|
+
],
|
|
33
|
+
"division": "330106",
|
|
34
|
+
"labels": [
|
|
35
|
+
"工商",
|
|
36
|
+
"企业",
|
|
37
|
+
],
|
|
38
|
+
"subject": "MA2CFLDG4",
|
|
39
|
+
"subjectType": "1",
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatDate(date: Date, format: 'yyyyMMDD') {
|
|
2
|
+
switch (format) {
|
|
3
|
+
case 'yyyyMMDD': {
|
|
4
|
+
const year = date.getFullYear();
|
|
5
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
6
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
7
|
+
return `${year}${month}${day}`;
|
|
8
|
+
}
|
|
9
|
+
default:
|
|
10
|
+
throw new Error(`Invalid format`);
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/cn/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function parseSex(s: string): undefined | { sex: 'Male' | 'Female'; male: boolean; female: boolean } {
|
|
2
|
+
if (!s) return undefined;
|
|
3
|
+
|
|
4
|
+
switch (s.toLowerCase()) {
|
|
5
|
+
case '男':
|
|
6
|
+
case 'male':
|
|
7
|
+
return { sex: 'Male', male: true, female: false };
|
|
8
|
+
case '女':
|
|
9
|
+
case 'female':
|
|
10
|
+
return { sex: 'Female', male: false, female: true };
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 居民身份证
|
|
3
|
+
*
|
|
4
|
+
* @see https://en.wikipedia.org/wiki/Resident_Identity_Card
|
|
5
|
+
* @see https://en.wikipedia.org/wiki/Foreign_Permanent_Resident_ID_Card
|
|
6
|
+
*/
|
|
7
|
+
export interface ResidentIdentityCardInfo {
|
|
8
|
+
/**
|
|
9
|
+
* @title 姓名
|
|
10
|
+
*/
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* @title 性别
|
|
14
|
+
*/
|
|
15
|
+
sex: 'Male' | 'Female';
|
|
16
|
+
/**
|
|
17
|
+
* @title 民族
|
|
18
|
+
* 例如 '汉'/'满'/'回'
|
|
19
|
+
*/
|
|
20
|
+
ethnicity: string;
|
|
21
|
+
/**
|
|
22
|
+
* @title 出生日期
|
|
23
|
+
* @format date
|
|
24
|
+
*/
|
|
25
|
+
birthDate: string;
|
|
26
|
+
/**
|
|
27
|
+
* @title 地址
|
|
28
|
+
*
|
|
29
|
+
* - 通常为 domicile/户籍地
|
|
30
|
+
*/
|
|
31
|
+
address: string;
|
|
32
|
+
/**
|
|
33
|
+
* @title 身份证号
|
|
34
|
+
*/
|
|
35
|
+
identityCardNumber: string;
|
|
36
|
+
/**
|
|
37
|
+
* @title 签发机关
|
|
38
|
+
*/
|
|
39
|
+
issuer: string;
|
|
40
|
+
/**
|
|
41
|
+
* @title 有效期开始日期
|
|
42
|
+
* @format date
|
|
43
|
+
*/
|
|
44
|
+
validStartDate: string;
|
|
45
|
+
/**
|
|
46
|
+
* @title 有效期结束日期
|
|
47
|
+
* @format date
|
|
48
|
+
* @description 如长期有效则为 空
|
|
49
|
+
*/
|
|
50
|
+
validEndDate?: string;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { formatSort } from './formatSort';
|
|
3
|
+
|
|
4
|
+
test('formatSort', () => {
|
|
5
|
+
expect(formatSort([{ field: 'name', order: 'asc' }])).toEqual(['name asc']);
|
|
6
|
+
expect(formatSort([{ field: 'age', order: 'desc', nulls: 'last' }])).toEqual(['age desc nulls last']);
|
|
7
|
+
expect(
|
|
8
|
+
formatSort([
|
|
9
|
+
{ field: 'name', order: 'asc' },
|
|
10
|
+
{ field: 'age', order: 'desc' },
|
|
11
|
+
]),
|
|
12
|
+
).toEqual(['name asc', 'age desc']);
|
|
13
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SortRule } from './parseSort';
|
|
2
|
+
|
|
3
|
+
export function formatSort(s: SortRule[]): string[] {
|
|
4
|
+
return s
|
|
5
|
+
.map(({ field, order, nulls }) => {
|
|
6
|
+
if (!field) return '';
|
|
7
|
+
|
|
8
|
+
let r = field;
|
|
9
|
+
if (order) {
|
|
10
|
+
r += ` ${order}`;
|
|
11
|
+
}
|
|
12
|
+
if (nulls) {
|
|
13
|
+
r += ` nulls ${nulls}`;
|
|
14
|
+
}
|
|
15
|
+
return r;
|
|
16
|
+
})
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type MaybeNumber = number | null | string | undefined | bigint;
|
|
2
|
+
|
|
3
|
+
export function maybeNumber(v: MaybeNumber) {
|
|
4
|
+
if (v === null || v === undefined) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
switch (typeof v) {
|
|
9
|
+
case 'number':
|
|
10
|
+
return v;
|
|
11
|
+
case 'bigint':
|
|
12
|
+
return Number(v);
|
|
13
|
+
case 'string':
|
|
14
|
+
if (v === '') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const n = Number(v);
|
|
19
|
+
if (isNaN(n)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { formatSort } from './formatSort';
|
|
3
|
+
import { parseSort } from './parseSort';
|
|
4
|
+
|
|
5
|
+
describe('parseSort', () => {
|
|
6
|
+
describe('basic parsing', () => {
|
|
7
|
+
test('handles empty inputs', () => {
|
|
8
|
+
expect(parseSort('')).toEqual([]);
|
|
9
|
+
expect(parseSort([])).toEqual([]);
|
|
10
|
+
expect(parseSort(null)).toEqual([]);
|
|
11
|
+
expect(parseSort(undefined)).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('handles simple field names', () => {
|
|
15
|
+
expect(parseSort('a')).toEqual([{ field: 'a', order: 'asc' }]);
|
|
16
|
+
expect(parseSort(['a'])).toEqual([{ field: 'a', order: 'asc' }]);
|
|
17
|
+
expect(parseSort('a.b')).toEqual([{ field: 'a.b', order: 'asc' }]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('handles prefix notation', () => {
|
|
21
|
+
expect(parseSort('-a')).toEqual([{ field: 'a', order: 'desc' }]);
|
|
22
|
+
expect(parseSort('+b')).toEqual([{ field: 'b', order: 'asc' }]);
|
|
23
|
+
expect(parseSort('-a,+b')).toEqual([
|
|
24
|
+
{ field: 'a', order: 'desc' },
|
|
25
|
+
{ field: 'b', order: 'asc' },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('explicit ordering', () => {
|
|
31
|
+
test('handles explicit order keywords', () => {
|
|
32
|
+
expect(parseSort('a asc')).toEqual([{ field: 'a', order: 'asc' }]);
|
|
33
|
+
expect(parseSort('a desc')).toEqual([{ field: 'a', order: 'desc' }]);
|
|
34
|
+
expect(parseSort('-a asc')).toEqual([{ field: 'a', order: 'asc' }]); // explicit order overrides prefix
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('nulls handling', () => {
|
|
39
|
+
test('handles nulls specification', () => {
|
|
40
|
+
expect(parseSort('a asc nulls last')).toEqual([{ field: 'a', order: 'asc', nulls: 'last' }]);
|
|
41
|
+
expect(parseSort('a asc nulls first')).toEqual([{ field: 'a', order: 'asc', nulls: 'first' }]);
|
|
42
|
+
expect(parseSort('a desc nulls last')).toEqual([{ field: 'a', order: 'desc', nulls: 'last' }]);
|
|
43
|
+
expect(parseSort('a nulls first')).toEqual([{ field: 'a', order: 'asc', nulls: 'first' }]);
|
|
44
|
+
expect(parseSort('-a nulls first')).toEqual([{ field: 'a', order: 'desc', nulls: 'first' }]);
|
|
45
|
+
|
|
46
|
+
// Alternative syntax
|
|
47
|
+
expect(parseSort('a asc last')).toEqual([{ field: 'a', order: 'asc', nulls: 'last' }]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('object notation', () => {
|
|
52
|
+
test('handles object input', () => {
|
|
53
|
+
expect(parseSort([{ field: 'a', order: 'asc' }])).toEqual([{ field: 'a', order: 'asc' }]);
|
|
54
|
+
expect(parseSort([{ field: 'a', order: 'asc', nulls: 'last' }])).toEqual([
|
|
55
|
+
{ field: 'a', order: 'asc', nulls: 'last' },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('invalid inputs', () => {
|
|
61
|
+
test('handles invalid inputs gracefully', () => {
|
|
62
|
+
expect(parseSort([{ order: 'asc' }])).toEqual([]);
|
|
63
|
+
expect(parseSort(['a,,', { field: '', order: 'asc' }])).toEqual([{ field: 'a', order: 'asc' }]);
|
|
64
|
+
expect(parseSort([',,', { field: 'a', order: 'asc' }])).toEqual([{ field: 'a', order: 'asc' }]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
});
|