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.
package/README.md CHANGED
@@ -337,17 +337,49 @@ export class User {
337
337
 
338
338
  ```typescript
339
339
  // user.dtos.ts
340
- import { createMetalCrudDtoClasses } from "adorn-api";
340
+ import { createMetalCrudDtoClasses, t } from "adorn-api";
341
341
  import { User } from "./user.entity";
342
342
 
343
343
  export const {
344
- GetUserDto,
345
- CreateUserDto,
346
- UpdateUserDto,
347
- ReplaceUserDto,
348
- UserQueryDto,
349
- UserPagedResponseDto
350
- } = createMetalCrudDtoClasses(User);
344
+ response: UserDto,
345
+ create: CreateUserDto,
346
+ replace: ReplaceUserDto,
347
+ update: UpdateUserDto,
348
+ params: UserParamsDto,
349
+ queryDto: UserQueryDto,
350
+ optionsQueryDto: UserOptionsQueryDto,
351
+ pagedResponseDto: UserPagedResponseDto,
352
+ optionDto: UserOptionDto,
353
+ optionsDto: UserOptionsDto,
354
+ errors: UserErrors,
355
+ filterMappings: USER_FILTER_MAPPINGS,
356
+ sortableColumns: USER_SORTABLE_COLUMNS
357
+ } = createMetalCrudDtoClasses(User, {
358
+ mutationExclude: ["id", "createdAt"],
359
+ query: {
360
+ filters: {
361
+ nameContains: {
362
+ schema: t.string({ minLength: 1 }),
363
+ field: "name",
364
+ operator: "contains"
365
+ },
366
+ emailContains: {
367
+ schema: t.string({ minLength: 1 }),
368
+ field: "email",
369
+ operator: "contains"
370
+ }
371
+ },
372
+ sortableColumns: {
373
+ id: "id",
374
+ name: "name",
375
+ createdAt: "createdAt"
376
+ },
377
+ options: {
378
+ labelField: "name"
379
+ }
380
+ },
381
+ errors: true
382
+ });
351
383
  ```
352
384
 
353
385
  ### 3. Create a CRUD Controller
@@ -365,19 +397,28 @@ import {
365
397
  Body,
366
398
  Query,
367
399
  Returns,
400
+ parseFilter,
368
401
  parsePagination,
402
+ parseSort,
403
+ t,
369
404
  type RequestContext
370
405
  } from "adorn-api";
371
406
  import { applyFilter, toPagedResponse } from "metal-orm";
372
407
  import { createSession } from "./db";
373
408
  import { User } from "./user.entity";
374
409
  import {
375
- GetUserDto,
410
+ UserDto,
376
411
  CreateUserDto,
377
- UpdateUserDto,
378
412
  ReplaceUserDto,
413
+ UpdateUserDto,
414
+ UserParamsDto,
379
415
  UserQueryDto,
380
- UserPagedResponseDto
416
+ UserOptionsQueryDto,
417
+ UserPagedResponseDto,
418
+ UserOptionsDto,
419
+ UserErrors,
420
+ USER_FILTER_MAPPINGS,
421
+ USER_SORTABLE_COLUMNS
381
422
  } from "./user.dtos";
382
423
 
383
424
  @Controller("/users")
@@ -386,26 +427,52 @@ export class UserController {
386
427
  @Query(UserQueryDto)
387
428
  @Returns(UserPagedResponseDto)
388
429
  async list(ctx: RequestContext<unknown, UserQueryDto>) {
389
- const { page, pageSize } = parsePagination(ctx.query);
430
+ const query = (ctx.query ?? {}) as Record<string, unknown>;
431
+ const { page, pageSize } = parsePagination(query);
432
+ const filters = parseFilter(query, USER_FILTER_MAPPINGS);
433
+ const sort = parseSort(query, USER_SORTABLE_COLUMNS, {
434
+ defaultSortBy: "id"
435
+ });
436
+ const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
390
437
  const session = createSession();
391
438
 
392
439
  try {
393
- const query = applyFilter(
394
- User.select().orderBy(User.id, "ASC"),
440
+ const ormQuery = applyFilter(
441
+ User.select().orderBy(User.id, direction),
395
442
  User,
396
- ctx.query
443
+ filters
397
444
  );
398
445
 
399
- const paged = await query.executePaged(session, { page, pageSize });
446
+ const paged = await ormQuery.executePaged(session, { page, pageSize });
400
447
  return toPagedResponse(paged);
401
448
  } finally {
402
449
  await session.dispose();
403
450
  }
404
451
  }
405
452
 
453
+ @Get("/options")
454
+ @Query(UserOptionsQueryDto)
455
+ @Returns(UserOptionsDto)
456
+ async options(ctx: RequestContext<unknown, UserOptionsQueryDto>) {
457
+ const query = (ctx.query ?? {}) as Record<string, unknown>;
458
+ const { page, pageSize } = parsePagination(query);
459
+ const filters = parseFilter(query, USER_FILTER_MAPPINGS);
460
+ // run your options query using the same mappings + generated DTOs
461
+ return {
462
+ items: [],
463
+ totalItems: 0,
464
+ page,
465
+ pageSize,
466
+ totalPages: 1,
467
+ hasNextPage: false,
468
+ hasPrevPage: false
469
+ };
470
+ }
471
+
406
472
  @Get("/:id")
407
473
  @Params({ id: t.integer() })
408
- @Returns(GetUserDto)
474
+ @Returns(UserDto)
475
+ @UserErrors
409
476
  async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
410
477
  const session = createSession();
411
478
 
@@ -421,6 +488,48 @@ export class UserController {
421
488
  }
422
489
  ```
423
490
 
491
+ ### Migration Guide (Breaking)
492
+
493
+ Before (duplicated config):
494
+
495
+ ```typescript
496
+ const UserQueryDto = createPagedFilterQueryDtoClass({
497
+ filters: {
498
+ nameContains: { schema: t.string(), operator: "contains" }
499
+ }
500
+ });
501
+
502
+ const USER_FILTER_MAPPINGS = {
503
+ nameContains: { field: "name", operator: "contains" as const }
504
+ };
505
+ ```
506
+
507
+ After (single source of truth):
508
+
509
+ ```typescript
510
+ const {
511
+ queryDto: UserQueryDto,
512
+ filterMappings: USER_FILTER_MAPPINGS
513
+ } = createMetalCrudDtoClasses(User, {
514
+ query: {
515
+ filters: {
516
+ nameContains: {
517
+ schema: t.string({ minLength: 1 }),
518
+ field: "name",
519
+ operator: "contains"
520
+ }
521
+ }
522
+ }
523
+ });
524
+ ```
525
+
526
+ Breaking changes summary:
527
+ - `createMetalCrudDtoClasses` now generates query/options/paged/error artifacts directly.
528
+ - Query filter definitions now include schema + operator + field mapping in one `query.filters` block.
529
+ - Sort allowlist now lives in `query.sortableColumns` and feeds both DTO schemas and runtime metadata.
530
+ - Generated outputs now include `queryDto`, `optionsQueryDto`, `pagedResponseDto`, `optionDto`, `optionsDto`, `errors`, `filterMappings`, and `sortableColumns`.
531
+ - Consumers no longer need internal `dist/...` imports for query/filter metadata types; all relevant types/utilities are publicly exported from `adorn-api`.
532
+
424
533
  ### Deep Relation Filters
425
534
 
426
535
  `parseFilter` now accepts nested relation paths, so you can filter deep chains like Alpha → Bravo → Charlie → Delta. If
@@ -1,5 +1,6 @@
1
+ import type { FieldOverride } from "../../core/decorators";
1
2
  import type { DtoConstructor } from "../../core/types";
2
- import type { MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, NestedCreateDtoOptions } from "./types";
3
- export declare function createMetalCrudDtos(target: any, options?: MetalCrudDtoOptions): MetalCrudDtoDecorators;
4
- export declare function createMetalCrudDtoClasses(target: any, options?: MetalCrudDtoClassOptions): MetalCrudDtoClasses;
5
- export declare function createNestedCreateDtoClass(target: any, overrides: Record<string, any>, options: NestedCreateDtoOptions): DtoConstructor;
3
+ import type { MetalCrudDtoClassOptions, MetalCrudDtoClasses, MetalCrudDtoDecorators, MetalCrudDtoOptions, MetalDtoTarget, NestedCreateDtoOptions } from "./types";
4
+ export declare function createMetalCrudDtos<TEntity extends Record<string, unknown>>(target: MetalDtoTarget, options?: MetalCrudDtoOptions<TEntity>): MetalCrudDtoDecorators;
5
+ export declare function createMetalCrudDtoClasses<TEntity extends Record<string, unknown>>(target: MetalDtoTarget, options?: MetalCrudDtoClassOptions<TEntity>): MetalCrudDtoClasses<TEntity>;
6
+ export declare function createNestedCreateDtoClass(target: MetalDtoTarget, overrides: Record<string, FieldOverride>, options: NestedCreateDtoOptions): DtoConstructor;
@@ -37,7 +37,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.createMetalCrudDtos = createMetalCrudDtos;
38
38
  exports.createMetalCrudDtoClasses = createMetalCrudDtoClasses;
39
39
  exports.createNestedCreateDtoClass = createNestedCreateDtoClass;
40
+ const decorators_1 = require("../../core/decorators");
41
+ const metadata_1 = require("../../core/metadata");
42
+ const schema_1 = require("../../core/schema");
40
43
  const dto_1 = require("./dto");
44
+ const error_dtos_1 = require("./error-dtos");
45
+ const paged_dtos_1 = require("./paged-dtos");
41
46
  function createMetalCrudDtos(target, options = {}) {
42
47
  const mutationExclude = options.mutationExclude;
43
48
  const immutable = options.immutable;
@@ -69,7 +74,7 @@ function createMetalCrudDtos(target, options = {}) {
69
74
  };
70
75
  }
71
76
  function createMetalCrudDtoClasses(target, options = {}) {
72
- const { baseName, names, ...crudOptions } = options;
77
+ const { baseName, names, query, errors: errorOptions, ...crudOptions } = options;
73
78
  const decorators = createMetalCrudDtos(target, crudOptions);
74
79
  const entityName = baseName ?? getTargetName(target);
75
80
  const defaultNames = {
@@ -77,14 +82,100 @@ function createMetalCrudDtoClasses(target, options = {}) {
77
82
  create: `Create${entityName}Dto`,
78
83
  replace: `Replace${entityName}Dto`,
79
84
  update: `Update${entityName}Dto`,
80
- params: `${entityName}ParamsDto`
85
+ params: `${entityName}ParamsDto`,
86
+ queryDto: `${entityName}QueryDto`,
87
+ optionsQueryDto: `${entityName}OptionsQueryDto`,
88
+ pagedResponseDto: `${entityName}PagedResponseDto`,
89
+ optionDto: `${entityName}OptionDto`,
90
+ optionsDto: `${entityName}OptionsDto`
81
91
  };
82
- const classes = {};
92
+ const crudClasses = {};
83
93
  for (const key of Object.keys(decorators)) {
84
94
  const name = names?.[key] ?? defaultNames[key];
85
- classes[key] = buildDtoClass(name, decorators[key]);
95
+ crudClasses[key] = buildDtoClass(name, decorators[key]);
86
96
  }
87
- return classes;
97
+ const strict = options.strict ?? false;
98
+ const queryOptions = query ?? {};
99
+ const sortByKey = queryOptions.sortByKey ?? "sortBy";
100
+ const sortDirectionKey = queryOptions.sortDirectionKey ?? "sortDirection";
101
+ const defaultPageSize = queryOptions.defaultPageSize ?? 25;
102
+ const maxPageSize = queryOptions.maxPageSize ?? 100;
103
+ const filters = queryOptions.filters ?? {};
104
+ const sortableColumns = cloneSortableColumns(queryOptions.sortableColumns ?? {});
105
+ const filterMappings = buildFilterMappings(filters);
106
+ const optionsQueryConfig = queryOptions.options ?? {};
107
+ const optionsEnabled = optionsQueryConfig.enabled ?? true;
108
+ const optionsSearchKey = optionsQueryConfig.searchKey ?? "search";
109
+ const optionsLabelField = (optionsQueryConfig.labelField ?? "nome");
110
+ const optionsValueField = (optionsQueryConfig.valueField ?? "id");
111
+ const optionsSearchOperator = optionsQueryConfig.searchOperator ?? "contains";
112
+ const optionsDefaultPageSize = optionsQueryConfig.defaultPageSize ?? defaultPageSize;
113
+ const optionsMaxPageSize = optionsQueryConfig.maxPageSize ?? maxPageSize;
114
+ if (optionsEnabled && !(optionsSearchKey in filterMappings)) {
115
+ filterMappings[optionsSearchKey] = {
116
+ field: optionsLabelField,
117
+ operator: optionsSearchOperator
118
+ };
119
+ }
120
+ const queryDtoName = names?.queryDto ?? defaultNames.queryDto;
121
+ const optionsQueryDtoName = names?.optionsQueryDto ?? defaultNames.optionsQueryDto;
122
+ const pagedResponseDtoName = names?.pagedResponseDto ?? defaultNames.pagedResponseDto;
123
+ const optionDtoName = names?.optionDto ?? defaultNames.optionDto;
124
+ const optionsDtoName = names?.optionsDto ?? defaultNames.optionsDto;
125
+ const queryDto = createQueryDtoClass({
126
+ name: queryDtoName,
127
+ filters,
128
+ sortableColumns,
129
+ sortByKey,
130
+ sortDirectionKey,
131
+ defaultSortBy: queryOptions.defaultSortBy,
132
+ defaultSortDirection: queryOptions.defaultSortDirection ?? "asc",
133
+ defaultPageSize,
134
+ maxPageSize
135
+ });
136
+ const optionsQueryDto = createOptionsQueryDtoClass({
137
+ name: optionsQueryDtoName,
138
+ filters,
139
+ sortableColumns,
140
+ sortByKey,
141
+ sortDirectionKey,
142
+ defaultSortBy: queryOptions.defaultSortBy,
143
+ defaultSortDirection: queryOptions.defaultSortDirection ?? "asc",
144
+ defaultPageSize: optionsDefaultPageSize,
145
+ maxPageSize: optionsMaxPageSize,
146
+ searchKey: optionsEnabled ? optionsSearchKey : undefined
147
+ });
148
+ const optionDto = buildDtoClass(optionDtoName, (0, dto_1.MetalDto)(target, {
149
+ include: Array.from(new Set([optionsValueField, optionsLabelField])),
150
+ strict
151
+ }));
152
+ const responseDto = crudClasses.response;
153
+ const pagedResponseDto = (0, paged_dtos_1.createPagedResponseDtoClass)({
154
+ name: pagedResponseDtoName,
155
+ itemDto: responseDto,
156
+ description: `Paged ${entityName} response.`
157
+ });
158
+ const optionsDto = (0, paged_dtos_1.createPagedResponseDtoClass)({
159
+ name: optionsDtoName,
160
+ itemDto: optionDto,
161
+ description: `${entityName} options response.`
162
+ });
163
+ const errors = buildStandardCrudErrors(entityName, errorOptions);
164
+ return {
165
+ response: crudClasses.response,
166
+ create: crudClasses.create,
167
+ replace: crudClasses.replace,
168
+ update: crudClasses.update,
169
+ params: crudClasses.params,
170
+ queryDto,
171
+ optionsQueryDto,
172
+ pagedResponseDto,
173
+ optionDto,
174
+ optionsDto,
175
+ errors,
176
+ filterMappings,
177
+ sortableColumns
178
+ };
88
179
  }
89
180
  function createNestedCreateDtoClass(target, overrides, options) {
90
181
  const { additionalExclude, name, parentEntity: _parentEntity, ...metalDtoOptions } = options;
@@ -114,6 +205,108 @@ function createNestedCreateDtoClass(target, overrides, options) {
114
205
  Object.defineProperty(NestedCreateDto, "name", { value: name, configurable: true });
115
206
  return NestedCreateDto;
116
207
  }
208
+ function createQueryDtoClass(options) {
209
+ const fields = buildQueryFields(options);
210
+ return createRegisteredDtoClass(options.name, fields);
211
+ }
212
+ function createOptionsQueryDtoClass(options) {
213
+ const fields = buildQueryFields(options);
214
+ if (options.searchKey) {
215
+ fields[options.searchKey] = {
216
+ schema: schema_1.t.optional(schema_1.t.string({ minLength: 1 })),
217
+ optional: true
218
+ };
219
+ }
220
+ return createRegisteredDtoClass(options.name, fields);
221
+ }
222
+ function buildQueryFields(options) {
223
+ const fields = {
224
+ page: {
225
+ schema: schema_1.t.optional(schema_1.t.integer({ minimum: 1, default: 1 })),
226
+ optional: true
227
+ },
228
+ pageSize: {
229
+ schema: schema_1.t.optional(schema_1.t.integer({
230
+ minimum: 1,
231
+ maximum: options.maxPageSize,
232
+ default: options.defaultPageSize
233
+ })),
234
+ optional: true
235
+ }
236
+ };
237
+ for (const [queryKey, def] of Object.entries(options.filters)) {
238
+ fields[queryKey] = { schema: schema_1.t.optional(def.schema), optional: true };
239
+ }
240
+ const sortableKeys = Object.keys(options.sortableColumns);
241
+ if (sortableKeys.length > 0) {
242
+ const sortByOptions = options.defaultSortBy
243
+ ? { default: options.defaultSortBy }
244
+ : {};
245
+ fields[options.sortByKey] = {
246
+ schema: schema_1.t.optional(schema_1.t.enum(sortableKeys, sortByOptions)),
247
+ optional: true
248
+ };
249
+ fields[options.sortDirectionKey] = {
250
+ schema: schema_1.t.optional(schema_1.t.enum(["asc", "desc"], { default: options.defaultSortDirection })),
251
+ optional: true
252
+ };
253
+ }
254
+ return fields;
255
+ }
256
+ function createRegisteredDtoClass(name, fields) {
257
+ const DtoClass = class {
258
+ };
259
+ Object.defineProperty(DtoClass, "name", { value: name, configurable: true });
260
+ (0, metadata_1.registerDto)(DtoClass, { name, fields });
261
+ return DtoClass;
262
+ }
263
+ function buildFilterMappings(filters) {
264
+ const mappings = {};
265
+ for (const [queryKey, def] of Object.entries(filters)) {
266
+ mappings[queryKey] = {
267
+ field: def.field,
268
+ operator: def.operator ?? "equals"
269
+ };
270
+ }
271
+ return mappings;
272
+ }
273
+ function cloneSortableColumns(sortableColumns) {
274
+ return Object.fromEntries(Object.entries(sortableColumns).map(([key, value]) => [key, value]));
275
+ }
276
+ function buildStandardCrudErrors(entityName, options) {
277
+ if (!options) {
278
+ return undefined;
279
+ }
280
+ const config = typeof options === "boolean" ? {} : options;
281
+ if (config.enabled === false) {
282
+ return undefined;
283
+ }
284
+ const responses = [];
285
+ const invalidId = config.invalidId;
286
+ const notFound = config.notFound;
287
+ if (invalidId !== false) {
288
+ responses.push({
289
+ status: 400,
290
+ description: invalidId?.description ?? `Invalid ${entityName} id.`,
291
+ contentType: invalidId?.contentType
292
+ });
293
+ }
294
+ if (notFound !== false) {
295
+ responses.push({
296
+ status: 404,
297
+ description: notFound?.description ?? `${entityName} not found.`,
298
+ contentType: notFound?.contentType
299
+ });
300
+ }
301
+ if (!responses.length) {
302
+ return undefined;
303
+ }
304
+ const schema = config.schema ?? (0, error_dtos_1.createErrorDtoClass)({
305
+ withDetails: config.withDetails ?? false,
306
+ includeTraceId: config.includeTraceId ?? true
307
+ });
308
+ return (0, decorators_1.Errors)(schema, responses);
309
+ }
117
310
  function buildDtoClass(name, decorator) {
118
311
  const DtoClass = class {
119
312
  };
@@ -144,7 +337,7 @@ function mergeStringArrays(...entries) {
144
337
  }
145
338
  function buildCrudOptions(base, overrides, extra = {}) {
146
339
  const mergedOverrides = mergeOverrides(overrides, base?.overrides);
147
- const output = { ...base, ...extra };
340
+ const output = { ...(base ?? {}), ...extra };
148
341
  if (mergedOverrides) {
149
342
  output.overrides = mergedOverrides;
150
343
  }
@@ -5,7 +5,7 @@ import type { Filter, FilterFieldInput, FilterMapping, FilterOperator, ParseFilt
5
5
  * @param mappings - Filter field mappings
6
6
  * @returns Parsed filter or undefined
7
7
  */
8
- export declare function parseFilter<T, K extends keyof T>(query: Record<string, unknown> | undefined, mappings: Record<string, FilterMapping<T>>): Filter<T, K> | undefined;
8
+ export declare function parseFilter<T, K extends keyof T>(query: object | undefined, mappings: Record<string, FilterMapping<T>>): Filter<T, K> | undefined;
9
9
  export declare function parseFilter<T, K extends keyof T>(options: ParseFilterOptions<T>): Filter<T, K> | undefined;
10
10
  /**
11
11
  * Creates filter mappings for an entity.
@@ -1,6 +1,7 @@
1
1
  export { MetalDto } from "./dto";
2
2
  export { parsePagination } from "./pagination";
3
3
  export { parseFilter, createFilterMappings } from "./filters";
4
+ export { parseSort } from "./sort";
4
5
  export { createPagedQueryDtoClass, createPagedResponseDtoClass, createPagedFilterQueryDtoClass } from "./paged-dtos";
5
6
  export { createMetalCrudDtos, createMetalCrudDtoClasses, createNestedCreateDtoClass } from "./crud-dtos";
6
7
  export { createMetalTreeDtoClasses } from "./tree-dtos";
@@ -8,4 +9,4 @@ export { createMetalDtoOverrides, type CreateMetalDtoOverridesOptions } from "./
8
9
  export { createErrorDtoClass, StandardErrorDto, SimpleErrorDto, BasicErrorDto } from "./error-dtos";
9
10
  export { withSession, parseIdOrThrow, compactUpdates, applyInput, getEntityOrThrow } from "./utils";
10
11
  export { validateEntityMetadata, hasValidEntityMetadata } from "./field-builder";
11
- export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNames, NestedCreateDtoOptions, MetalTreeDtoClassOptions, MetalTreeDtoClasses, MetalTreeDtoClassNames, MetalTreeListEntryOptions, ErrorDtoOptions, CreateSessionFn } from "./types";
12
+ export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, ParseSortOptions, ParsedSort, SortDirection, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudQueryFilterDef, MetalCrudSortableColumns, MetalCrudOptionsQueryOptions, MetalCrudQueryOptions, MetalCrudStandardErrorsOptions, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNameKey, MetalCrudDtoClassNames, RouteErrorsDecorator, NestedCreateDtoOptions, MetalTreeDtoClassOptions, MetalTreeDtoClasses, MetalTreeDtoClassNames, MetalTreeListEntryOptions, ErrorDtoOptions, CreateSessionFn } from "./types";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasValidEntityMetadata = exports.validateEntityMetadata = exports.getEntityOrThrow = exports.applyInput = exports.compactUpdates = exports.parseIdOrThrow = exports.withSession = exports.BasicErrorDto = exports.SimpleErrorDto = exports.StandardErrorDto = exports.createErrorDtoClass = exports.createMetalDtoOverrides = exports.createMetalTreeDtoClasses = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.createFilterMappings = exports.parseFilter = exports.parsePagination = exports.MetalDto = void 0;
3
+ exports.hasValidEntityMetadata = exports.validateEntityMetadata = exports.getEntityOrThrow = exports.applyInput = exports.compactUpdates = exports.parseIdOrThrow = exports.withSession = exports.BasicErrorDto = exports.SimpleErrorDto = exports.StandardErrorDto = exports.createErrorDtoClass = exports.createMetalDtoOverrides = exports.createMetalTreeDtoClasses = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.parseSort = exports.createFilterMappings = exports.parseFilter = exports.parsePagination = exports.MetalDto = void 0;
4
4
  // Ensure standard decorator metadata is available for metal-orm transformers.
5
5
  const symbolMetadata = Symbol.metadata;
6
6
  if (!symbolMetadata) {
@@ -13,6 +13,8 @@ Object.defineProperty(exports, "parsePagination", { enumerable: true, get: funct
13
13
  var filters_1 = require("./filters");
14
14
  Object.defineProperty(exports, "parseFilter", { enumerable: true, get: function () { return filters_1.parseFilter; } });
15
15
  Object.defineProperty(exports, "createFilterMappings", { enumerable: true, get: function () { return filters_1.createFilterMappings; } });
16
+ var sort_1 = require("./sort");
17
+ Object.defineProperty(exports, "parseSort", { enumerable: true, get: function () { return sort_1.parseSort; } });
16
18
  var paged_dtos_1 = require("./paged-dtos");
17
19
  Object.defineProperty(exports, "createPagedQueryDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedQueryDtoClass; } });
18
20
  Object.defineProperty(exports, "createPagedResponseDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedResponseDtoClass; } });
@@ -5,4 +5,4 @@ import type { PaginationConfig, ParsedPagination } from "./types";
5
5
  * @param config - Pagination configuration
6
6
  * @returns Parsed pagination result
7
7
  */
8
- export declare function parsePagination(query: Record<string, unknown>, config?: PaginationConfig): ParsedPagination;
8
+ export declare function parsePagination(query: object, config?: PaginationConfig): ParsedPagination;
@@ -10,11 +10,12 @@ const coerce_1 = require("../../core/coerce");
10
10
  */
11
11
  function parsePagination(query, config = {}) {
12
12
  const { defaultPageSize = 25, maxPageSize = 100 } = config;
13
- const page = coerce_1.coerce.integer(query.page, {
13
+ const q = query;
14
+ const page = coerce_1.coerce.integer(q.page, {
14
15
  min: 1,
15
16
  clamp: true
16
17
  }) ?? 1;
17
- const pageSize = coerce_1.coerce.integer(query.pageSize, {
18
+ const pageSize = coerce_1.coerce.integer(q.pageSize, {
18
19
  min: 1,
19
20
  max: maxPageSize,
20
21
  clamp: true
@@ -0,0 +1,7 @@
1
+ import type { FilterFieldInput, ParseSortOptions, ParsedSort } from "./types";
2
+ /**
3
+ * Parses sort parameters from query params using an allowed sortable column map.
4
+ * Returns undefined when no valid sort column is selected.
5
+ */
6
+ export declare function parseSort<T>(query: Record<string, unknown> | undefined, sortableColumns: Record<string, FilterFieldInput<T>>, options?: Omit<ParseSortOptions<T>, "query" | "sortableColumns">): ParsedSort<T> | undefined;
7
+ export declare function parseSort<T>(options: ParseSortOptions<T>): ParsedSort<T> | undefined;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSort = parseSort;
4
+ function parseSort(queryOrOptions, sortableColumns, options) {
5
+ const resolved = sortableColumns
6
+ ? {
7
+ query: queryOrOptions,
8
+ sortableColumns,
9
+ ...(options ?? {})
10
+ }
11
+ : queryOrOptions;
12
+ const query = resolved?.query;
13
+ const allowed = resolved?.sortableColumns;
14
+ if (!query || !allowed || Object.keys(allowed).length === 0) {
15
+ return undefined;
16
+ }
17
+ const sortByKey = resolved.sortByKey ?? "sortBy";
18
+ const sortDirectionKey = resolved.sortDirectionKey ?? "sortDirection";
19
+ const defaultSortBy = resolved.defaultSortBy;
20
+ const defaultDirection = resolved.defaultSortDirection ?? "asc";
21
+ const requestedSortBy = toTrimmedString(query[sortByKey]);
22
+ const selectedSortBy = selectSortBy(requestedSortBy, defaultSortBy, allowed);
23
+ if (!selectedSortBy) {
24
+ return undefined;
25
+ }
26
+ const requestedDirection = toTrimmedString(query[sortDirectionKey]);
27
+ const sortDirection = normalizeDirection(requestedDirection, defaultDirection);
28
+ return {
29
+ sortBy: selectedSortBy,
30
+ sortDirection,
31
+ field: allowed[selectedSortBy]
32
+ };
33
+ }
34
+ function toTrimmedString(value) {
35
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
36
+ }
37
+ function selectSortBy(requestedSortBy, defaultSortBy, allowed) {
38
+ if (requestedSortBy && requestedSortBy in allowed) {
39
+ return requestedSortBy;
40
+ }
41
+ if (defaultSortBy && defaultSortBy in allowed) {
42
+ return defaultSortBy;
43
+ }
44
+ return undefined;
45
+ }
46
+ function normalizeDirection(raw, fallback) {
47
+ if (raw === "desc") {
48
+ return "desc";
49
+ }
50
+ if (raw === "asc") {
51
+ return "asc";
52
+ }
53
+ return fallback;
54
+ }