@wener/common 1.0.3 → 1.0.5

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 (109) 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/consola/createStandardConsolaReporter.js +18 -0
  10. package/lib/consola/createStandardConsolaReporter.js.map +1 -0
  11. package/lib/consola/formatLogObject.js +125 -0
  12. package/lib/consola/formatLogObject.js.map +1 -0
  13. package/lib/consola/index.js +3 -0
  14. package/lib/consola/index.js.map +1 -0
  15. package/lib/data/formatSort.js +15 -0
  16. package/lib/data/formatSort.js.map +1 -0
  17. package/lib/data/index.js +4 -0
  18. package/lib/data/index.js.map +1 -0
  19. package/lib/data/maybeNumber.js +22 -0
  20. package/lib/data/maybeNumber.js.map +1 -0
  21. package/lib/data/parseSort.js +95 -0
  22. package/lib/data/parseSort.js.map +1 -0
  23. package/lib/data/resolvePagination.js +36 -0
  24. package/lib/data/resolvePagination.js.map +1 -0
  25. package/lib/data/types.d.js +3 -0
  26. package/lib/data/types.d.js.map +1 -0
  27. package/lib/index.js +6 -2
  28. package/lib/index.js.map +1 -1
  29. package/lib/jsonschema/JsonSchema.js +6 -6
  30. package/lib/jsonschema/JsonSchema.js.map +1 -1
  31. package/lib/jsonschema/types.d.js.map +1 -1
  32. package/lib/meta/defineFileType.js.map +1 -1
  33. package/lib/meta/defineInit.js.map +1 -1
  34. package/lib/meta/defineMetadata.js.map +1 -1
  35. package/lib/password/PHC.js +8 -8
  36. package/lib/password/PHC.js.map +1 -1
  37. package/lib/password/Password.js.map +1 -1
  38. package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -1
  39. package/lib/password/createBase64PasswordAlgorithm.js.map +1 -1
  40. package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -1
  41. package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -1
  42. package/lib/password/createScryptPasswordAlgorithm.js.map +1 -1
  43. package/lib/search/AdvanceSearch.js.map +1 -1
  44. package/lib/search/formatAdvanceSearch.js.map +1 -1
  45. package/lib/search/optimizeAdvanceSearch.js.map +1 -1
  46. package/lib/search/parseAdvanceSearch.js.map +1 -1
  47. package/lib/search/parser.d.js.map +1 -1
  48. package/lib/search/types.d.js.map +1 -1
  49. package/lib/tools/generateSchema.js +43 -0
  50. package/lib/tools/generateSchema.js.map +1 -0
  51. package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +1 -1
  52. package/package.json +19 -5
  53. package/src/cn/DivisionCode.test.ts +38 -45
  54. package/src/cn/DivisionCode.ts +140 -184
  55. package/src/cn/Mod11Checksum.ts +17 -17
  56. package/src/cn/Mod31Checksum.ts +25 -25
  57. package/src/cn/ResidentIdentityCardNumber.test.ts +12 -16
  58. package/src/cn/ResidentIdentityCardNumber.ts +82 -82
  59. package/src/cn/UnifiedSocialCreditCode.test.ts +11 -11
  60. package/src/cn/UnifiedSocialCreditCode.ts +115 -120
  61. package/src/cn/__snapshots__/ResidentIdentityCardNumber.test.ts.snap +1 -1
  62. package/src/cn/formatDate.ts +10 -10
  63. package/src/cn/parseSex.ts +11 -25
  64. package/src/cn/types.d.ts +43 -43
  65. package/src/consola/createStandardConsolaReporter.ts +31 -0
  66. package/src/consola/formatLogObject.ts +171 -0
  67. package/src/consola/index.ts +2 -0
  68. package/src/data/formatSort.test.ts +13 -0
  69. package/src/data/formatSort.ts +18 -0
  70. package/src/data/index.ts +5 -0
  71. package/src/data/maybeNumber.ts +23 -0
  72. package/src/data/parseSort.test.ts +67 -0
  73. package/src/data/parseSort.ts +108 -0
  74. package/src/data/resolvePagination.test.ts +58 -0
  75. package/src/data/resolvePagination.ts +60 -0
  76. package/src/data/types.d.ts +33 -0
  77. package/src/index.ts +8 -2
  78. package/src/jsonschema/JsonSchema.test.ts +13 -22
  79. package/src/jsonschema/JsonSchema.ts +146 -178
  80. package/src/jsonschema/types.d.ts +151 -161
  81. package/src/meta/defineFileType.tsx +54 -54
  82. package/src/meta/defineInit.ts +32 -53
  83. package/src/meta/defineMetadata.test.ts +5 -7
  84. package/src/meta/defineMetadata.ts +28 -46
  85. package/src/password/PHC.test.ts +186 -277
  86. package/src/password/PHC.ts +243 -243
  87. package/src/password/Password.test.ts +38 -50
  88. package/src/password/Password.ts +73 -95
  89. package/src/password/createArgon2PasswordAlgorithm.ts +65 -69
  90. package/src/password/createBase64PasswordAlgorithm.ts +9 -9
  91. package/src/password/createBcryptPasswordAlgorithm.ts +20 -22
  92. package/src/password/createPBKDF2PasswordAlgorithm.ts +49 -61
  93. package/src/password/createScryptPasswordAlgorithm.ts +48 -59
  94. package/src/search/AdvanceSearch.test.ts +136 -143
  95. package/src/search/AdvanceSearch.ts +6 -6
  96. package/src/search/formatAdvanceSearch.ts +44 -53
  97. package/src/search/optimizeAdvanceSearch.ts +70 -83
  98. package/src/search/parseAdvanceSearch.ts +16 -19
  99. package/src/search/parser.d.ts +3 -3
  100. package/src/search/types.d.ts +28 -54
  101. package/src/tools/generateSchema.ts +39 -0
  102. package/src/tools/renderJsonSchemaToMarkdownDoc.ts +69 -69
  103. package/lib/normalizePagination.js +0 -14
  104. package/lib/normalizePagination.js.map +0 -1
  105. package/lib/parseSort.js +0 -106
  106. package/lib/parseSort.js.map +0 -1
  107. package/src/normalizePagination.ts +0 -25
  108. package/src/parseSort.test.ts +0 -42
  109. package/src/parseSort.ts +0 -133
package/src/cn/types.d.ts CHANGED
@@ -5,47 +5,47 @@
5
5
  * @see https://en.wikipedia.org/wiki/Foreign_Permanent_Resident_ID_Card
6
6
  */
7
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;
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
51
  }
@@ -0,0 +1,31 @@
1
+ import type { ConsolaOptions, ConsolaReporter, LogObject } from 'consola/core';
2
+ import { formatLogObject } from './formatLogObject';
3
+
4
+ type Formatter = (
5
+ o: LogObject,
6
+ ctx: {
7
+ options: ConsolaOptions;
8
+ },
9
+ ) => string;
10
+
11
+ export function createStandardConsolaReporter({
12
+ format = formatLogObject,
13
+ }: {
14
+ format?: Formatter;
15
+ } = {}): ConsolaReporter {
16
+ return {
17
+ log: (o, ctx) => {
18
+ let out = format(o, ctx);
19
+ let fn = console.log;
20
+
21
+ const { level } = o;
22
+ if (level < 1) {
23
+ fn = console.error;
24
+ } else if (level === 1) {
25
+ fn = console.warn;
26
+ }
27
+
28
+ fn(out);
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,171 @@
1
+ import colors from 'chalk';
2
+ import type { ConsolaOptions, FormatOptions, LogObject, LogType } from 'consola/core';
3
+ import dayjs from 'dayjs';
4
+ import { isDevelopment } from 'std-env';
5
+
6
+ const levelColors: Record<LogType, (str: string) => string> = {
7
+ trace: colors.gray,
8
+ debug: colors.cyan,
9
+ info: colors.blueBright,
10
+ warn: colors.yellow,
11
+ error: colors.red,
12
+ fatal: colors.bgRed.white,
13
+ silent: colors.white,
14
+ log: colors.white,
15
+ success: colors.green,
16
+ fail: colors.red,
17
+ ready: colors.green,
18
+ start: colors.cyan,
19
+ box: colors.white,
20
+ verbose: colors.green,
21
+ };
22
+
23
+ const levelShort: Record<LogType, string> = {
24
+ trace: 'TRAC',
25
+ debug: 'DEBG',
26
+ info: 'INFO',
27
+ warn: 'WARN',
28
+ error: 'ERRO',
29
+ fatal: 'FATL',
30
+ silent: 'SLNT',
31
+ log: 'LOG ', // Added space to make it 4 characters
32
+ success: 'SUCC',
33
+ fail: 'FAIL',
34
+ ready: 'READ',
35
+ start: 'STRT',
36
+ box: 'BOX ', // Added space to make it 4 characters
37
+ verbose: 'VERB',
38
+ };
39
+ const start = Date.now();
40
+
41
+ export function formatLogObject(
42
+ o: LogObject,
43
+ ctx: {
44
+ options: ConsolaOptions;
45
+ },
46
+ ) {
47
+ let { date, type, tag } = o;
48
+ type = type === 'log' ? 'info' : type;
49
+
50
+ const color = levelColors[type] || colors.white;
51
+ const levelText = levelShort[type] || type.toUpperCase().slice(0, 4); // Get first 4 chars, consistent uppercase
52
+
53
+ let line = '';
54
+ let out: string[] = [];
55
+
56
+ // Timestamp
57
+ if (isDevelopment) {
58
+ // process.hrtime.bigint()
59
+ let diff = (date.getTime() - start) / 1000;
60
+
61
+ out.push(colors.gray(diff.toFixed(3).padStart(7, ' ')));
62
+ } else {
63
+ out.push(colors.gray(dayjs(date).format('YYYY-MM-DD HH:mm:ss.SSS')));
64
+ }
65
+
66
+ // Log Level (colored)
67
+ // Pad to 4 characters
68
+ out.push(color(levelText.padEnd(4, ' ')));
69
+
70
+ if (tag) {
71
+ out.push(colors.yellow(`[${tag}]`)); // Added color for tag
72
+ }
73
+
74
+ {
75
+ const [message, ...additional] = formatArgs(o.args, ctx).split('\n');
76
+
77
+ out.push(characterFormat(message));
78
+
79
+ line = out.join(' ');
80
+ if (additional.length > 0) {
81
+ line += characterFormat(additional.join('\n'));
82
+ }
83
+
84
+ if (type === 'trace') {
85
+ const _err = new Error('Trace: ' + o.message);
86
+ line += formatStack(_err.stack || '', _err.message);
87
+ }
88
+ }
89
+
90
+ // if (!message && typeof args[0] === 'string') {
91
+ // message = args.shift();
92
+ // }
93
+ // if (message) {
94
+ // out.push(message);
95
+ // }
96
+ // if (args.length) {
97
+ // out.push(...args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a)))); // Handle non-string args
98
+ // }
99
+
100
+ // todo format error
101
+ // https://github.com/unjs/consola/blob/main/src/reporters/fancy.ts
102
+
103
+ // return out.join(' '); // Increased spacing for better readability
104
+ return line;
105
+ }
106
+
107
+ function characterFormat(str: string) {
108
+ return (
109
+ str
110
+ // highlight backticks
111
+ .replace(/`([^`]+)`/gm, (_, m) => colors.cyan(m))
112
+ // underline underscores
113
+ .replace(/\s+_([^_]+)_\s+/gm, (_, m) => ` ${colors.underline(m)} `)
114
+ );
115
+ }
116
+
117
+ function parseStack(stack: string, message: string) {
118
+ // const cwd = process.cwd() + sep;
119
+
120
+ const lines = stack
121
+ .split('\n')
122
+ .splice(message.split('\n').length)
123
+ .map(
124
+ (l) => l.trim().replace('file://', ''),
125
+ // .replace(cwd, '')
126
+ );
127
+
128
+ return lines;
129
+ }
130
+
131
+ function formatStack(stack: string, message: string, opts?: FormatOptions) {
132
+ const indent = ' '.repeat((opts?.errorLevel || 0) + 1);
133
+ return (
134
+ `\n${indent}`
135
+ + parseStack(stack, message)
136
+ .map(
137
+ (line) =>
138
+ ' ' + line.replace(/^at +/, (m) => colors.gray(m)).replace(/\((.+)\)/, (_, m) => `(${colors.cyan(m)})`),
139
+ )
140
+ .join(`\n${indent}`)
141
+ );
142
+ }
143
+
144
+ function formatArgs(args: any[], opts: FormatOptions) {
145
+ const _args = args.map((arg) => {
146
+ if (arg && typeof arg.stack === 'string') {
147
+ return formatError(arg, opts);
148
+ }
149
+ return arg;
150
+ });
151
+
152
+ // Only supported with Node >= 10
153
+ // https://nodejs.org/api/util.html#util_util_inspect_object_options
154
+ return formatWithOptions(opts, ..._args);
155
+ }
156
+
157
+ function formatWithOptions(o: any, ...params: any[]) {
158
+ // import { formatWithOptions } from 'node:util';
159
+ return params.join(' ');
160
+ }
161
+
162
+ function formatError(err: any, opts: FormatOptions): string {
163
+ const message = err.message ?? formatWithOptions(opts, err);
164
+ const stack = err.stack ? formatStack(err.stack, message, opts) : '';
165
+
166
+ const level = opts?.errorLevel || 0;
167
+ const causedPrefix = level > 0 ? `${' '.repeat(level)}[cause]: ` : '';
168
+ const causedError = err.cause ? '\n\n' + formatError(err.cause, { ...opts, errorLevel: level + 1 }) : '';
169
+
170
+ return causedPrefix + message + '\n' + stack + causedError;
171
+ }
@@ -0,0 +1,2 @@
1
+ export { formatLogObject } from './formatLogObject';
2
+ export { createStandardConsolaReporter } from './createStandardConsolaReporter';
@@ -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,5 @@
1
+ export type * from './types';
2
+
3
+ export { resolvePagination, type PaginationInput, type ResolvedPagination } from './resolvePagination';
4
+ export { parseSort, type SortRule } from './parseSort';
5
+ export { formatSort } from './formatSort';
@@ -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 ?? 1000);
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
+ };