adorn-api 1.1.2 → 1.1.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.
@@ -1,17 +1,31 @@
1
-
1
+ import type { ErrorResponseOptions, FieldOverride } from "../../core/decorators";
2
+ import { Errors } from "../../core/decorators";
3
+ import { registerDto, type FieldMeta } from "../../core/metadata";
4
+ import { t } from "../../core/schema";
2
5
  import type { DtoConstructor } from "../../core/types";
3
6
  import { MetalDto } from "./dto";
7
+ import { createErrorDtoClass } from "./error-dtos";
8
+ import { createPagedResponseDtoClass } from "./paged-dtos";
4
9
  import type {
5
- MetalCrudDtoOptions,
10
+ FilterMapping,
11
+ MetalCrudDtoClassNameKey,
6
12
  MetalCrudDtoClassOptions,
7
- MetalCrudDtoDecorators,
8
13
  MetalCrudDtoClasses,
9
- NestedCreateDtoOptions
14
+ MetalCrudDtoDecorators,
15
+ MetalCrudDtoOptions,
16
+ MetalCrudQueryFilterDef,
17
+ MetalCrudSortableColumns,
18
+ MetalCrudStandardErrorsOptions,
19
+ MetalDtoOptions,
20
+ MetalDtoTarget,
21
+ NestedCreateDtoOptions,
22
+ RouteErrorsDecorator,
23
+ SortDirection
10
24
  } from "./types";
11
-
12
- export function createMetalCrudDtos(
13
- target: any,
14
- options: MetalCrudDtoOptions = {}
25
+
26
+ export function createMetalCrudDtos<TEntity extends Record<string, unknown>>(
27
+ target: MetalDtoTarget,
28
+ options: MetalCrudDtoOptions<TEntity> = {}
15
29
  ): MetalCrudDtoDecorators {
16
30
  const mutationExclude = options.mutationExclude;
17
31
  const immutable = options.immutable;
@@ -45,101 +59,357 @@ export function createMetalCrudDtos(
45
59
  params: MetalDto(target, params)
46
60
  };
47
61
  }
48
-
49
- export function createMetalCrudDtoClasses(
50
- target: any,
51
- options: MetalCrudDtoClassOptions = {}
52
- ): MetalCrudDtoClasses {
53
- const { baseName, names, ...crudOptions } = options;
54
- const decorators = createMetalCrudDtos(target, crudOptions);
55
- const entityName = baseName ?? getTargetName(target);
56
- const defaultNames: Record<keyof MetalCrudDtoDecorators, string> = {
57
- response: `${entityName}Dto`,
58
- create: `Create${entityName}Dto`,
59
- replace: `Replace${entityName}Dto`,
60
- update: `Update${entityName}Dto`,
61
- params: `${entityName}ParamsDto`
62
- };
63
-
64
- const classes: Partial<MetalCrudDtoClasses> = {};
65
- for (const key of Object.keys(decorators) as Array<keyof MetalCrudDtoDecorators>) {
66
- const name = names?.[key] ?? defaultNames[key];
67
- classes[key] = buildDtoClass(name, decorators[key]);
68
- }
69
- return classes as MetalCrudDtoClasses;
70
- }
71
-
72
- export function createNestedCreateDtoClass(
73
- target: any,
74
- overrides: Record<string, any>,
75
- options: NestedCreateDtoOptions
76
- ): DtoConstructor {
77
- const { additionalExclude, name, parentEntity: _parentEntity, ...metalDtoOptions } = options;
78
-
79
- const allExcludes = mergeStringArrays(
80
- ["id", "createdAt"],
81
- additionalExclude,
82
- metalDtoOptions.exclude
83
- );
84
-
85
- @MetalDto(target, {
86
- ...metalDtoOptions,
87
- mode: "create",
88
- exclude: allExcludes,
89
- overrides
90
- })
91
- class NestedCreateDto {}
92
-
93
- Object.defineProperty(NestedCreateDto, "name", { value: name, configurable: true });
94
-
95
- return NestedCreateDto;
96
- }
97
-
98
- function buildDtoClass(name: string, decorator: (target: DtoConstructor) => void): DtoConstructor {
99
- const DtoClass = class {};
100
- Object.defineProperty(DtoClass, "name", { value: name, configurable: true });
101
- decorator(DtoClass);
102
- return DtoClass;
103
- }
104
-
105
- function getTargetName(target: any): string {
106
- if (typeof target === "function" && target.name) {
107
- return target.name;
108
- }
109
- return "Entity";
110
- }
111
-
112
- function mergeOverrides(
113
- base?: Record<string, any>,
114
- override?: Record<string, any>
115
- ): Record<string, any> | undefined {
116
- if (!base && !override) {
117
- return undefined;
118
- }
119
- return { ...(base ?? {}), ...(override ?? {}) };
120
- }
121
-
122
- function mergeStringArrays(
123
- ...entries: Array<string[] | undefined>
124
- ): string[] | undefined {
125
- const merged = new Set<string>();
126
- for (const entry of entries) {
127
- for (const value of entry ?? []) {
128
- merged.add(value);
129
- }
130
- }
131
- return merged.size ? Array.from(merged) : undefined;
132
- }
133
-
134
- function buildCrudOptions(
135
- base: any | undefined,
136
- overrides: Record<string, any> | undefined,
137
- extra: any = {}
138
- ): any {
139
- const mergedOverrides = mergeOverrides(overrides, base?.overrides);
140
- const output: any = { ...base, ...extra };
141
- if (mergedOverrides) {
142
- output.overrides = mergedOverrides;
143
- }
144
- return output;
145
- }
62
+
63
+ export function createMetalCrudDtoClasses<TEntity extends Record<string, unknown>>(
64
+ target: MetalDtoTarget,
65
+ options: MetalCrudDtoClassOptions<TEntity> = {}
66
+ ): MetalCrudDtoClasses<TEntity> {
67
+ const { baseName, names, query, errors: errorOptions, ...crudOptions } = options;
68
+ const decorators = createMetalCrudDtos(target, crudOptions);
69
+ const entityName = baseName ?? getTargetName(target);
70
+
71
+ const defaultNames: Record<MetalCrudDtoClassNameKey, string> = {
72
+ response: `${entityName}Dto`,
73
+ create: `Create${entityName}Dto`,
74
+ replace: `Replace${entityName}Dto`,
75
+ update: `Update${entityName}Dto`,
76
+ params: `${entityName}ParamsDto`,
77
+ queryDto: `${entityName}QueryDto`,
78
+ optionsQueryDto: `${entityName}OptionsQueryDto`,
79
+ pagedResponseDto: `${entityName}PagedResponseDto`,
80
+ optionDto: `${entityName}OptionDto`,
81
+ optionsDto: `${entityName}OptionsDto`
82
+ };
83
+
84
+ const crudClasses: Partial<MetalCrudDtoClasses<TEntity>> = {};
85
+ for (const key of Object.keys(decorators) as Array<keyof MetalCrudDtoDecorators>) {
86
+ const name = names?.[key] ?? defaultNames[key];
87
+ crudClasses[key] = buildDtoClass(name, decorators[key]);
88
+ }
89
+
90
+ const strict = options.strict ?? false;
91
+ const queryOptions = query ?? {};
92
+ const sortByKey = queryOptions.sortByKey ?? "sortBy";
93
+ const sortDirectionKey = queryOptions.sortDirectionKey ?? "sortDirection";
94
+ const defaultPageSize = queryOptions.defaultPageSize ?? 25;
95
+ const maxPageSize = queryOptions.maxPageSize ?? 100;
96
+ const filters = queryOptions.filters ?? {};
97
+ const sortableColumns = cloneSortableColumns(queryOptions.sortableColumns ?? {});
98
+ const filterMappings = buildFilterMappings(filters);
99
+
100
+ const optionsQueryConfig = queryOptions.options ?? {};
101
+ const optionsEnabled = optionsQueryConfig.enabled ?? true;
102
+ const optionsSearchKey = optionsQueryConfig.searchKey ?? "search";
103
+ const optionsLabelField = (optionsQueryConfig.labelField ?? "nome") as keyof TEntity & string;
104
+ const optionsValueField = (optionsQueryConfig.valueField ?? "id") as keyof TEntity & string;
105
+ const optionsSearchOperator = optionsQueryConfig.searchOperator ?? "contains";
106
+ const optionsDefaultPageSize = optionsQueryConfig.defaultPageSize ?? defaultPageSize;
107
+ const optionsMaxPageSize = optionsQueryConfig.maxPageSize ?? maxPageSize;
108
+
109
+ if (optionsEnabled && !(optionsSearchKey in filterMappings)) {
110
+ filterMappings[optionsSearchKey] = {
111
+ field: optionsLabelField,
112
+ operator: optionsSearchOperator
113
+ };
114
+ }
115
+
116
+ const queryDtoName = names?.queryDto ?? defaultNames.queryDto;
117
+ const optionsQueryDtoName = names?.optionsQueryDto ?? defaultNames.optionsQueryDto;
118
+ const pagedResponseDtoName = names?.pagedResponseDto ?? defaultNames.pagedResponseDto;
119
+ const optionDtoName = names?.optionDto ?? defaultNames.optionDto;
120
+ const optionsDtoName = names?.optionsDto ?? defaultNames.optionsDto;
121
+
122
+ const queryDto = createQueryDtoClass({
123
+ name: queryDtoName,
124
+ filters,
125
+ sortableColumns,
126
+ sortByKey,
127
+ sortDirectionKey,
128
+ defaultSortBy: queryOptions.defaultSortBy,
129
+ defaultSortDirection: queryOptions.defaultSortDirection ?? "asc",
130
+ defaultPageSize,
131
+ maxPageSize
132
+ });
133
+
134
+ const optionsQueryDto = createOptionsQueryDtoClass({
135
+ name: optionsQueryDtoName,
136
+ filters,
137
+ sortableColumns,
138
+ sortByKey,
139
+ sortDirectionKey,
140
+ defaultSortBy: queryOptions.defaultSortBy,
141
+ defaultSortDirection: queryOptions.defaultSortDirection ?? "asc",
142
+ defaultPageSize: optionsDefaultPageSize,
143
+ maxPageSize: optionsMaxPageSize,
144
+ searchKey: optionsEnabled ? optionsSearchKey : undefined
145
+ });
146
+
147
+ const optionDto = buildDtoClass(
148
+ optionDtoName,
149
+ MetalDto(target, {
150
+ include: Array.from(new Set([optionsValueField, optionsLabelField])),
151
+ strict
152
+ })
153
+ );
154
+
155
+ const responseDto = crudClasses.response as DtoConstructor;
156
+ const pagedResponseDto = createPagedResponseDtoClass({
157
+ name: pagedResponseDtoName,
158
+ itemDto: responseDto,
159
+ description: `Paged ${entityName} response.`
160
+ });
161
+ const optionsDto = createPagedResponseDtoClass({
162
+ name: optionsDtoName,
163
+ itemDto: optionDto,
164
+ description: `${entityName} options response.`
165
+ });
166
+
167
+ const errors = buildStandardCrudErrors(entityName, errorOptions);
168
+
169
+ return {
170
+ response: crudClasses.response as DtoConstructor,
171
+ create: crudClasses.create as DtoConstructor,
172
+ replace: crudClasses.replace as DtoConstructor,
173
+ update: crudClasses.update as DtoConstructor,
174
+ params: crudClasses.params as DtoConstructor,
175
+ queryDto,
176
+ optionsQueryDto,
177
+ pagedResponseDto,
178
+ optionDto,
179
+ optionsDto,
180
+ errors,
181
+ filterMappings,
182
+ sortableColumns
183
+ };
184
+ }
185
+
186
+ export function createNestedCreateDtoClass(
187
+ target: MetalDtoTarget,
188
+ overrides: Record<string, FieldOverride>,
189
+ options: NestedCreateDtoOptions
190
+ ): DtoConstructor {
191
+ const { additionalExclude, name, parentEntity: _parentEntity, ...metalDtoOptions } = options;
192
+
193
+ const allExcludes = mergeStringArrays(
194
+ ["id", "createdAt"],
195
+ additionalExclude,
196
+ metalDtoOptions.exclude
197
+ );
198
+
199
+ @MetalDto(target, {
200
+ ...metalDtoOptions,
201
+ mode: "create",
202
+ exclude: allExcludes,
203
+ overrides
204
+ })
205
+ class NestedCreateDto {}
206
+
207
+ Object.defineProperty(NestedCreateDto, "name", { value: name, configurable: true });
208
+
209
+ return NestedCreateDto;
210
+ }
211
+
212
+ interface CreateQueryDtoClassOptions<TEntity extends Record<string, unknown>> {
213
+ name: string;
214
+ filters: Record<string, MetalCrudQueryFilterDef<TEntity>>;
215
+ sortableColumns: MetalCrudSortableColumns<TEntity>;
216
+ sortByKey: string;
217
+ sortDirectionKey: string;
218
+ defaultSortBy?: string;
219
+ defaultSortDirection: SortDirection;
220
+ defaultPageSize: number;
221
+ maxPageSize: number;
222
+ }
223
+
224
+ interface CreateOptionsQueryDtoClassOptions<TEntity extends Record<string, unknown>>
225
+ extends CreateQueryDtoClassOptions<TEntity> {
226
+ searchKey?: string;
227
+ }
228
+
229
+ function createQueryDtoClass<TEntity extends Record<string, unknown>>(
230
+ options: CreateQueryDtoClassOptions<TEntity>
231
+ ): DtoConstructor {
232
+ const fields = buildQueryFields(options);
233
+ return createRegisteredDtoClass(options.name, fields);
234
+ }
235
+
236
+ function createOptionsQueryDtoClass<TEntity extends Record<string, unknown>>(
237
+ options: CreateOptionsQueryDtoClassOptions<TEntity>
238
+ ): DtoConstructor {
239
+ const fields = buildQueryFields(options);
240
+ if (options.searchKey) {
241
+ fields[options.searchKey] = {
242
+ schema: t.optional(t.string({ minLength: 1 })),
243
+ optional: true
244
+ };
245
+ }
246
+ return createRegisteredDtoClass(options.name, fields);
247
+ }
248
+
249
+ function buildQueryFields<TEntity extends Record<string, unknown>>(
250
+ options: CreateQueryDtoClassOptions<TEntity>
251
+ ): Record<string, FieldMeta> {
252
+ const fields: Record<string, FieldMeta> = {
253
+ page: {
254
+ schema: t.optional(t.integer({ minimum: 1, default: 1 })),
255
+ optional: true
256
+ },
257
+ pageSize: {
258
+ schema: t.optional(
259
+ t.integer({
260
+ minimum: 1,
261
+ maximum: options.maxPageSize,
262
+ default: options.defaultPageSize
263
+ })
264
+ ),
265
+ optional: true
266
+ }
267
+ };
268
+
269
+ for (const [queryKey, def] of Object.entries(options.filters)) {
270
+ fields[queryKey] = { schema: t.optional(def.schema), optional: true };
271
+ }
272
+
273
+ const sortableKeys = Object.keys(options.sortableColumns);
274
+ if (sortableKeys.length > 0) {
275
+ const sortByOptions = options.defaultSortBy
276
+ ? { default: options.defaultSortBy }
277
+ : {};
278
+ fields[options.sortByKey] = {
279
+ schema: t.optional(t.enum(sortableKeys, sortByOptions)),
280
+ optional: true
281
+ };
282
+ fields[options.sortDirectionKey] = {
283
+ schema: t.optional(
284
+ t.enum(["asc", "desc"], { default: options.defaultSortDirection })
285
+ ),
286
+ optional: true
287
+ };
288
+ }
289
+
290
+ return fields;
291
+ }
292
+
293
+ function createRegisteredDtoClass(
294
+ name: string,
295
+ fields: Record<string, FieldMeta>
296
+ ): DtoConstructor {
297
+ const DtoClass = class {};
298
+ Object.defineProperty(DtoClass, "name", { value: name, configurable: true });
299
+ registerDto(DtoClass, { name, fields });
300
+ return DtoClass as DtoConstructor;
301
+ }
302
+
303
+ function buildFilterMappings<TEntity extends Record<string, unknown>>(
304
+ filters: Record<string, MetalCrudQueryFilterDef<TEntity>>
305
+ ): Record<string, FilterMapping<TEntity>> {
306
+ const mappings: Record<string, FilterMapping<TEntity>> = {};
307
+ for (const [queryKey, def] of Object.entries(filters)) {
308
+ mappings[queryKey] = {
309
+ field: def.field,
310
+ operator: def.operator ?? "equals"
311
+ };
312
+ }
313
+ return mappings;
314
+ }
315
+
316
+ function cloneSortableColumns<TEntity extends Record<string, unknown>>(
317
+ sortableColumns: MetalCrudSortableColumns<TEntity>
318
+ ): MetalCrudSortableColumns<TEntity> {
319
+ return Object.fromEntries(
320
+ Object.entries(sortableColumns).map(([key, value]) => [key, value])
321
+ ) as MetalCrudSortableColumns<TEntity>;
322
+ }
323
+
324
+ function buildStandardCrudErrors(
325
+ entityName: string,
326
+ options: boolean | MetalCrudStandardErrorsOptions | undefined
327
+ ): RouteErrorsDecorator | undefined {
328
+ if (!options) {
329
+ return undefined;
330
+ }
331
+
332
+ const config: MetalCrudStandardErrorsOptions =
333
+ typeof options === "boolean" ? {} : options;
334
+ if (config.enabled === false) {
335
+ return undefined;
336
+ }
337
+
338
+ const responses: ErrorResponseOptions[] = [];
339
+ const invalidId = config.invalidId;
340
+ const notFound = config.notFound;
341
+
342
+ if (invalidId !== false) {
343
+ responses.push({
344
+ status: 400,
345
+ description: invalidId?.description ?? `Invalid ${entityName} id.`,
346
+ contentType: invalidId?.contentType
347
+ });
348
+ }
349
+ if (notFound !== false) {
350
+ responses.push({
351
+ status: 404,
352
+ description: notFound?.description ?? `${entityName} not found.`,
353
+ contentType: notFound?.contentType
354
+ });
355
+ }
356
+
357
+ if (!responses.length) {
358
+ return undefined;
359
+ }
360
+
361
+ const schema = config.schema ?? createErrorDtoClass({
362
+ withDetails: config.withDetails ?? false,
363
+ includeTraceId: config.includeTraceId ?? true
364
+ });
365
+ return Errors(schema, responses);
366
+ }
367
+
368
+ function buildDtoClass(name: string, decorator: (target: DtoConstructor) => void): DtoConstructor {
369
+ const DtoClass = class {};
370
+ Object.defineProperty(DtoClass, "name", { value: name, configurable: true });
371
+ decorator(DtoClass);
372
+ return DtoClass as DtoConstructor;
373
+ }
374
+
375
+ function getTargetName(target: unknown): string {
376
+ if (typeof target === "function" && target.name) {
377
+ return target.name;
378
+ }
379
+ return "Entity";
380
+ }
381
+
382
+ function mergeOverrides(
383
+ base?: Record<string, FieldOverride>,
384
+ override?: Record<string, FieldOverride>
385
+ ): Record<string, FieldOverride> | undefined {
386
+ if (!base && !override) {
387
+ return undefined;
388
+ }
389
+ return { ...(base ?? {}), ...(override ?? {}) };
390
+ }
391
+
392
+ function mergeStringArrays(
393
+ ...entries: Array<string[] | undefined>
394
+ ): string[] | undefined {
395
+ const merged = new Set<string>();
396
+ for (const entry of entries) {
397
+ for (const value of entry ?? []) {
398
+ merged.add(value);
399
+ }
400
+ }
401
+ return merged.size ? Array.from(merged) : undefined;
402
+ }
403
+
404
+ function buildCrudOptions(
405
+ base: MetalDtoOptions | undefined,
406
+ overrides: Record<string, FieldOverride> | undefined,
407
+ extra: Partial<MetalDtoOptions> = {}
408
+ ): MetalDtoOptions {
409
+ const mergedOverrides = mergeOverrides(overrides, base?.overrides);
410
+ const output: MetalDtoOptions = { ...(base ?? {}), ...extra };
411
+ if (mergedOverrides) {
412
+ output.overrides = mergedOverrides;
413
+ }
414
+ return output;
415
+ }
@@ -7,14 +7,14 @@ import type { Filter, FilterFieldInput, FilterMapping, FilterOperator, ParseFilt
7
7
  * @returns Parsed filter or undefined
8
8
  */
9
9
  export function parseFilter<T, K extends keyof T>(
10
- query: Record<string, unknown> | undefined,
10
+ query: object | undefined,
11
11
  mappings: Record<string, FilterMapping<T>>
12
12
  ): Filter<T, K> | undefined;
13
13
  export function parseFilter<T, K extends keyof T>(
14
14
  options: ParseFilterOptions<T>
15
15
  ): Filter<T, K> | undefined;
16
16
  export function parseFilter<T, K extends keyof T>(
17
- queryOrOptions: Record<string, unknown> | ParseFilterOptions<T> | undefined,
17
+ queryOrOptions: object | ParseFilterOptions<T> | undefined,
18
18
  mappings?: Record<string, FilterMapping<T>>
19
19
  ): Filter<T, K> | undefined {
20
20
  const options = mappings ? undefined : (queryOrOptions as ParseFilterOptions<T> | undefined);
@@ -49,13 +49,13 @@ export function parseFilter<T, K extends keyof T>(
49
49
 
50
50
  return hasValues ? (filter as Filter<T, K>) : undefined;
51
51
  }
52
-
53
- /**
54
- * Creates filter mappings for an entity.
55
- * @param entity - Entity type
56
- * @param fields - Array of field definitions
57
- * @returns Filter mappings
58
- */
52
+
53
+ /**
54
+ * Creates filter mappings for an entity.
55
+ * @param entity - Entity type
56
+ * @param fields - Array of field definitions
57
+ * @returns Filter mappings
58
+ */
59
59
  export function createFilterMappings<T extends Record<string, unknown>>(
60
60
  _entity: T,
61
61
  fields: Array<{ queryKey: string; field: FilterFieldInput<T>; operator?: FilterOperator }>
@@ -12,10 +12,14 @@ export {
12
12
  parsePagination
13
13
  } from "./pagination";
14
14
 
15
- export {
16
- parseFilter,
17
- createFilterMappings
18
- } from "./filters";
15
+ export {
16
+ parseFilter,
17
+ createFilterMappings
18
+ } from "./filters";
19
+
20
+ export {
21
+ parseSort
22
+ } from "./sort";
19
23
 
20
24
  export {
21
25
  createPagedQueryDtoClass,
@@ -73,15 +77,25 @@ export type {
73
77
  FilterFieldInput,
74
78
  RelationQuantifier,
75
79
  ParseFilterOptions,
76
- PagedQueryDtoOptions,
77
- PagedResponseDtoOptions,
78
- PagedFilterQueryDtoOptions,
79
- FilterFieldDef,
80
- MetalCrudDtoOptions,
81
- MetalCrudDtoClassOptions,
82
- MetalCrudDtoDecorators,
80
+ ParseSortOptions,
81
+ ParsedSort,
82
+ SortDirection,
83
+ PagedQueryDtoOptions,
84
+ PagedResponseDtoOptions,
85
+ PagedFilterQueryDtoOptions,
86
+ FilterFieldDef,
87
+ MetalCrudQueryFilterDef,
88
+ MetalCrudSortableColumns,
89
+ MetalCrudOptionsQueryOptions,
90
+ MetalCrudQueryOptions,
91
+ MetalCrudStandardErrorsOptions,
92
+ MetalCrudDtoOptions,
93
+ MetalCrudDtoClassOptions,
94
+ MetalCrudDtoDecorators,
83
95
  MetalCrudDtoClasses,
96
+ MetalCrudDtoClassNameKey,
84
97
  MetalCrudDtoClassNames,
98
+ RouteErrorsDecorator,
85
99
  NestedCreateDtoOptions,
86
100
  MetalTreeDtoClassOptions,
87
101
  MetalTreeDtoClasses,
@@ -1,28 +1,29 @@
1
- import type { PaginationConfig, ParsedPagination } from "./types";
2
- import { coerce } from "../../core/coerce";
3
-
4
- /**
5
- * Parses pagination parameters from query parameters.
6
- * @param query - Query parameters
7
- * @param config - Pagination configuration
8
- * @returns Parsed pagination result
9
- */
10
- export function parsePagination(
11
- query: Record<string, unknown>,
12
- config: PaginationConfig = {}
13
- ): ParsedPagination {
14
- const { defaultPageSize = 25, maxPageSize = 100 } = config;
15
-
16
- const page = coerce.integer(query.page as string | number, {
17
- min: 1,
18
- clamp: true
19
- }) ?? 1;
20
-
21
- const pageSize = coerce.integer(query.pageSize as string | number, {
22
- min: 1,
23
- max: maxPageSize,
24
- clamp: true
25
- }) ?? defaultPageSize;
26
-
27
- return { page, pageSize };
28
- }
1
+ import type { PaginationConfig, ParsedPagination } from "./types";
2
+ import { coerce } from "../../core/coerce";
3
+
4
+ /**
5
+ * Parses pagination parameters from query parameters.
6
+ * @param query - Query parameters
7
+ * @param config - Pagination configuration
8
+ * @returns Parsed pagination result
9
+ */
10
+ export function parsePagination(
11
+ query: object,
12
+ config: PaginationConfig = {}
13
+ ): ParsedPagination {
14
+ const { defaultPageSize = 25, maxPageSize = 100 } = config;
15
+ const q = query as Record<string, unknown>;
16
+
17
+ const page = coerce.integer(q.page as string | number, {
18
+ min: 1,
19
+ clamp: true
20
+ }) ?? 1;
21
+
22
+ const pageSize = coerce.integer(q.pageSize as string | number, {
23
+ min: 1,
24
+ max: maxPageSize,
25
+ clamp: true
26
+ }) ?? defaultPageSize;
27
+
28
+ return { page, pageSize };
29
+ }