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.
@@ -0,0 +1,82 @@
1
+ import type { FilterFieldInput, ParseSortOptions, ParsedSort, SortDirection } from "./types";
2
+
3
+ /**
4
+ * Parses sort parameters from query params using an allowed sortable column map.
5
+ * Returns undefined when no valid sort column is selected.
6
+ */
7
+ export function parseSort<T>(
8
+ query: Record<string, unknown> | undefined,
9
+ sortableColumns: Record<string, FilterFieldInput<T>>,
10
+ options?: Omit<ParseSortOptions<T>, "query" | "sortableColumns">
11
+ ): ParsedSort<T> | undefined;
12
+ export function parseSort<T>(options: ParseSortOptions<T>): ParsedSort<T> | undefined;
13
+ export function parseSort<T>(
14
+ queryOrOptions: Record<string, unknown> | ParseSortOptions<T> | undefined,
15
+ sortableColumns?: Record<string, FilterFieldInput<T>>,
16
+ options?: Omit<ParseSortOptions<T>, "query" | "sortableColumns">
17
+ ): ParsedSort<T> | undefined {
18
+ const resolved = sortableColumns
19
+ ? {
20
+ query: queryOrOptions as Record<string, unknown> | undefined,
21
+ sortableColumns,
22
+ ...(options ?? {})
23
+ }
24
+ : (queryOrOptions as ParseSortOptions<T> | undefined);
25
+
26
+ const query = resolved?.query;
27
+ const allowed = resolved?.sortableColumns;
28
+ if (!query || !allowed || Object.keys(allowed).length === 0) {
29
+ return undefined;
30
+ }
31
+
32
+ const sortByKey = resolved.sortByKey ?? "sortBy";
33
+ const sortDirectionKey = resolved.sortDirectionKey ?? "sortDirection";
34
+ const defaultSortBy = resolved.defaultSortBy;
35
+ const defaultDirection = resolved.defaultSortDirection ?? "asc";
36
+
37
+ const requestedSortBy = toTrimmedString(query[sortByKey]);
38
+ const selectedSortBy = selectSortBy(requestedSortBy, defaultSortBy, allowed);
39
+ if (!selectedSortBy) {
40
+ return undefined;
41
+ }
42
+
43
+ const requestedDirection = toTrimmedString(query[sortDirectionKey]);
44
+ const sortDirection = normalizeDirection(requestedDirection, defaultDirection);
45
+
46
+ return {
47
+ sortBy: selectedSortBy,
48
+ sortDirection,
49
+ field: allowed[selectedSortBy]
50
+ };
51
+ }
52
+
53
+ function toTrimmedString(value: unknown): string | undefined {
54
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
55
+ }
56
+
57
+ function selectSortBy<T>(
58
+ requestedSortBy: string | undefined,
59
+ defaultSortBy: string | undefined,
60
+ allowed: Record<string, FilterFieldInput<T>>
61
+ ): string | undefined {
62
+ if (requestedSortBy && requestedSortBy in allowed) {
63
+ return requestedSortBy;
64
+ }
65
+ if (defaultSortBy && defaultSortBy in allowed) {
66
+ return defaultSortBy;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function normalizeDirection(
72
+ raw: string | undefined,
73
+ fallback: SortDirection
74
+ ): SortDirection {
75
+ if (raw === "desc") {
76
+ return "desc";
77
+ }
78
+ if (raw === "asc") {
79
+ return "asc";
80
+ }
81
+ return fallback;
82
+ }
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { BelongsToReference, HasManyCollection, HasOneReference, ManyToManyCollection } from "metal-orm";
3
- import type { DtoOptions, FieldOverride } from "../../core/decorators";
3
+ import type { DtoOptions, ErrorResponseOptions, FieldOverride } from "../../core/decorators";
4
4
  import type { SchemaNode } from "../../core/schema";
5
5
  import type { DtoConstructor } from "../../core/types";
6
6
 
@@ -139,6 +139,41 @@ export interface ParseFilterOptions<T = Record<string, unknown>> {
139
139
  fieldMappings?: Record<string, FilterMapping<T>>;
140
140
  }
141
141
 
142
+ /**
143
+ * Sort direction.
144
+ */
145
+ export type SortDirection = "asc" | "desc";
146
+
147
+ /**
148
+ * Sort parsing options.
149
+ */
150
+ export interface ParseSortOptions<T = Record<string, unknown>> {
151
+ /** Query parameters */
152
+ query?: Record<string, unknown>;
153
+ /** Allowed sortable columns */
154
+ sortableColumns?: Record<string, FilterFieldInput<T>>;
155
+ /** Sort field query key */
156
+ sortByKey?: string;
157
+ /** Sort direction query key */
158
+ sortDirectionKey?: string;
159
+ /** Default sort field */
160
+ defaultSortBy?: string;
161
+ /** Default sort direction */
162
+ defaultSortDirection?: SortDirection;
163
+ }
164
+
165
+ /**
166
+ * Parsed sort result.
167
+ */
168
+ export interface ParsedSort<T = Record<string, unknown>> {
169
+ /** Requested sort key */
170
+ sortBy?: string;
171
+ /** Direction */
172
+ sortDirection: SortDirection;
173
+ /** Resolved entity field */
174
+ field?: FilterFieldInput<T>;
175
+ }
176
+
142
177
  /**
143
178
  * Filter operator.
144
179
  */
@@ -204,12 +239,87 @@ export interface PagedResponseDtoOptions {
204
239
  /**
205
240
  * Filter field definition.
206
241
  */
207
- export interface FilterFieldDef {
208
- /** Field schema */
209
- schema?: SchemaNode;
210
- /** Filter operator */
211
- operator?: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "gte" | "lt" | "lte";
212
- }
242
+ export interface FilterFieldDef {
243
+ /** Field schema */
244
+ schema?: SchemaNode;
245
+ /** Filter operator */
246
+ operator?: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "gte" | "lt" | "lte";
247
+ }
248
+
249
+ /**
250
+ * Single filter definition for CRUD query generation.
251
+ */
252
+ export interface MetalCrudQueryFilterDef<T = Record<string, unknown>> {
253
+ /** Field schema for query validation/OpenAPI */
254
+ schema: SchemaNode;
255
+ /** Entity field path mapping */
256
+ field: FilterFieldInput<T>;
257
+ /** Filter operator (default: equals) */
258
+ operator?: FilterOperator;
259
+ }
260
+
261
+ /**
262
+ * Sortable columns mapping for CRUD query generation.
263
+ * Key is accepted `sortBy` value and value is mapped entity field path.
264
+ */
265
+ export type MetalCrudSortableColumns<T = Record<string, unknown>> =
266
+ Record<string, FilterFieldInput<T>>;
267
+
268
+ /**
269
+ * Options-query generation options.
270
+ */
271
+ export interface MetalCrudOptionsQueryOptions<T = Record<string, unknown>>
272
+ extends PaginationConfig {
273
+ /** Whether options query artifacts are generated (default: true) */
274
+ enabled?: boolean;
275
+ /** Query key used for label search (default: "search") */
276
+ searchKey?: string;
277
+ /** Entity field used as option label (default: "nome") */
278
+ labelField?: keyof T & string;
279
+ /** Entity field used as option value (default: "id") */
280
+ valueField?: keyof T & string;
281
+ /** Operator used for label search (default: "contains") */
282
+ searchOperator?: FilterOperator;
283
+ }
284
+
285
+ /**
286
+ * Query generation options for CRUD DTO factory.
287
+ */
288
+ export interface MetalCrudQueryOptions<T = Record<string, unknown>>
289
+ extends PaginationConfig {
290
+ /** Filters in one place: schema + mapping + operator */
291
+ filters?: Record<string, MetalCrudQueryFilterDef<T>>;
292
+ /** Allowed sortBy values mapped to entity fields */
293
+ sortableColumns?: MetalCrudSortableColumns<T>;
294
+ /** Query key for sort field (default: "sortBy") */
295
+ sortByKey?: string;
296
+ /** Query key for sort direction (default: "sortDirection") */
297
+ sortDirectionKey?: string;
298
+ /** Default sort field key */
299
+ defaultSortBy?: string;
300
+ /** Default sort direction */
301
+ defaultSortDirection?: SortDirection;
302
+ /** Options endpoint query generation options */
303
+ options?: MetalCrudOptionsQueryOptions<T>;
304
+ }
305
+
306
+ /**
307
+ * Standard CRUD errors generation options.
308
+ */
309
+ export interface MetalCrudStandardErrorsOptions {
310
+ /** Enable generation (default: false) */
311
+ enabled?: boolean;
312
+ /** Reuse a specific error DTO schema */
313
+ schema?: DtoConstructor;
314
+ /** Generate default schema with details */
315
+ withDetails?: boolean;
316
+ /** Include traceId in generated schema */
317
+ includeTraceId?: boolean;
318
+ /** 400 invalid id error config (set false to disable) */
319
+ invalidId?: false | ErrorResponseOptions;
320
+ /** 404 not found error config (set false to disable) */
321
+ notFound?: false | ErrorResponseOptions;
322
+ }
213
323
 
214
324
  /**
215
325
  * Options for paged filter query DTOs.
@@ -225,7 +335,7 @@ export interface PagedFilterQueryDtoOptions extends PagedQueryDtoOptions {
225
335
  /**
226
336
  * Options for Metal CRUD DTOs.
227
337
  */
228
- export interface MetalCrudDtoOptions {
338
+ export interface MetalCrudDtoOptions<T = Record<string, unknown>> {
229
339
  /** Field overrides */
230
340
  overrides?: Record<string, FieldOverride>;
231
341
  /** Response DTO options */
@@ -244,6 +354,10 @@ export interface MetalCrudDtoOptions {
244
354
  paramsInclude?: string[];
245
355
  /** Immutable fields */
246
356
  immutable?: string[];
357
+ /** Query/options/paged artifact generation */
358
+ query?: MetalCrudQueryOptions<T>;
359
+ /** Standard CRUD errors generation */
360
+ errors?: boolean | MetalCrudStandardErrorsOptions;
247
361
  /** Whether to throw errors instead of warnings for invalid metadata (default: false) */
248
362
  strict?: boolean;
249
363
  }
@@ -267,13 +381,32 @@ export interface MetalCrudDtoDecorators {
267
381
  /**
268
382
  * Metal CRUD DTO class names.
269
383
  */
270
- export type MetalCrudDtoClassNames = Partial<Record<keyof MetalCrudDtoDecorators, string>>;
384
+ export type RouteErrorsDecorator = (
385
+ value: unknown,
386
+ context: ClassMethodDecoratorContext
387
+ ) => void;
388
+
389
+ /**
390
+ * Metal CRUD generated class names.
391
+ */
392
+ export type MetalCrudDtoClassNameKey =
393
+ keyof MetalCrudDtoDecorators
394
+ | "queryDto"
395
+ | "optionsQueryDto"
396
+ | "pagedResponseDto"
397
+ | "optionDto"
398
+ | "optionsDto";
399
+
400
+ /**
401
+ * Metal CRUD DTO class names.
402
+ */
403
+ export type MetalCrudDtoClassNames = Partial<Record<MetalCrudDtoClassNameKey, string>>;
271
404
 
272
405
  /**
273
406
  * Options for Metal CRUD DTO classes.
274
407
  * @extends MetalCrudDtoOptions
275
408
  */
276
- export interface MetalCrudDtoClassOptions extends MetalCrudDtoOptions {
409
+ export interface MetalCrudDtoClassOptions<T = Record<string, unknown>> extends MetalCrudDtoOptions<T> {
277
410
  /** Base name for DTO classes */
278
411
  baseName?: string;
279
412
  /** Custom class names */
@@ -285,7 +418,7 @@ export interface MetalCrudDtoClassOptions extends MetalCrudDtoOptions {
285
418
  /**
286
419
  * Metal CRUD DTO classes.
287
420
  */
288
- export interface MetalCrudDtoClasses {
421
+ export interface MetalCrudDtoClasses<T = Record<string, unknown>> {
289
422
  /** Response DTO class */
290
423
  response: DtoConstructor;
291
424
  /** Create DTO class */
@@ -296,6 +429,22 @@ export interface MetalCrudDtoClasses {
296
429
  update: DtoConstructor;
297
430
  /** Params DTO class */
298
431
  params: DtoConstructor;
432
+ /** Query DTO class (paged + filters + sort) */
433
+ queryDto: DtoConstructor;
434
+ /** Options query DTO class */
435
+ optionsQueryDto: DtoConstructor;
436
+ /** Paged response DTO class for main list endpoints */
437
+ pagedResponseDto: DtoConstructor;
438
+ /** Option DTO class (value + label fields) */
439
+ optionDto: DtoConstructor;
440
+ /** Paged response DTO class for options endpoints */
441
+ optionsDto: DtoConstructor;
442
+ /** Prebuilt CRUD error decorator (400 invalid id, 404 not found) */
443
+ errors?: RouteErrorsDecorator;
444
+ /** Execution-ready filter mappings for parseFilter */
445
+ filterMappings: Record<string, FilterMapping<T>>;
446
+ /** Execution-ready sortable column mappings */
447
+ sortableColumns: MetalCrudSortableColumns<T>;
299
448
  }
300
449
 
301
450
  /**
@@ -8,8 +8,6 @@ import {
8
8
  Query,
9
9
  buildOpenApi,
10
10
  createMetalCrudDtoClasses,
11
- createPagedResponseDtoClass,
12
- createPagedFilterQueryDtoClass,
13
11
  t,
14
12
  type RequestContext
15
13
  } from "../../src/index";
@@ -40,29 +38,34 @@ class NotaVersao {
40
38
  }
41
39
 
42
40
  const notaVersaoCrud = createMetalCrudDtoClasses(NotaVersao, {
43
- mutationExclude: ["id", "data_exclusao", "data_inativacao"]
41
+ mutationExclude: ["id", "data_exclusao", "data_inativacao"],
42
+ query: {
43
+ filters: {
44
+ sprint: { schema: t.integer({ minimum: 1 }), field: "sprint", operator: "equals" },
45
+ ativo: { schema: t.boolean(), field: "ativo", operator: "equals" },
46
+ mensagemContains: { schema: t.string({ minLength: 1 }), field: "mensagem", operator: "contains" }
47
+ },
48
+ sortableColumns: {
49
+ id: "id",
50
+ sprint: "sprint",
51
+ data: "data"
52
+ },
53
+ options: {
54
+ labelField: "mensagem"
55
+ }
56
+ },
57
+ errors: true
44
58
  });
45
59
 
46
60
  const {
47
61
  response: NotaVersaoDto,
48
- create: CreateNotaVersaoDto
62
+ create: CreateNotaVersaoDto,
63
+ queryDto: NotaVersaoQueryDtoClass,
64
+ pagedResponseDto: NotaVersaoPagedResponseDto,
65
+ filterMappings: NotaVersaoFilterMappings,
66
+ sortableColumns: NotaVersaoSortableColumns
49
67
  } = notaVersaoCrud;
50
68
 
51
- const NotaVersaoQueryDtoClass = createPagedFilterQueryDtoClass({
52
- name: "NotaVersaoQueryDto",
53
- filters: {
54
- sprint: { schema: t.integer({ minimum: 1 }), operator: "equals" },
55
- ativo: { schema: t.boolean(), operator: "equals" },
56
- mensagemContains: { schema: t.string({ minLength: 1 }), operator: "contains" }
57
- }
58
- });
59
-
60
- const NotaVersaoPagedResponseDto = createPagedResponseDtoClass({
61
- name: "NotaVersaoPagedResponseDto",
62
- itemDto: NotaVersaoDto,
63
- description: "Lista paginada de notas de versão."
64
- });
65
-
66
69
  @Controller({ path: "/nota-versao", tags: ["Nota Versão"] })
67
70
  class NotaVersaoController {
68
71
  @Get("/")
@@ -81,6 +84,20 @@ class NotaVersaoController {
81
84
  }
82
85
 
83
86
  describe("e2e metal-orm CRUD DTOs to OpenAPI", () => {
87
+ it("provides execution-ready query metadata", () => {
88
+ expect(NotaVersaoFilterMappings).toEqual({
89
+ sprint: { field: "sprint", operator: "equals" },
90
+ ativo: { field: "ativo", operator: "equals" },
91
+ mensagemContains: { field: "mensagem", operator: "contains" },
92
+ search: { field: "mensagem", operator: "contains" }
93
+ });
94
+ expect(NotaVersaoSortableColumns).toEqual({
95
+ id: "id",
96
+ sprint: "sprint",
97
+ data: "data"
98
+ });
99
+ });
100
+
84
101
  it("generates OpenAPI schemas with properties from createMetalCrudDtoClasses", () => {
85
102
  const doc = buildOpenApi({
86
103
  info: { title: "Test API", version: "1.0.0" },
@@ -1,10 +1,12 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- createMetalCrudDtos,
4
- createMetalCrudDtoClasses
5
- } from "../../src/adapter/metal-orm/index";
6
- import { getDtoMeta } from "../../src/core/metadata";
7
- import { Alphanumeric, Column, Email, Entity, Length, Pattern, PrimaryKey, col } from "metal-orm";
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createMetalCrudDtos,
4
+ createMetalCrudDtoClasses
5
+ } from "../../src/adapter/metal-orm/index";
6
+ import type { FilterMapping } from "../../src/adapter/metal-orm/index";
7
+ import { getDtoMeta } from "../../src/core/metadata";
8
+ import { t } from "../../src/core/schema";
9
+ import { Alphanumeric, Column, Email, Entity, Length, Pattern, PrimaryKey, col } from "metal-orm";
8
10
 
9
11
  describe("createMetalCrudDtos", () => {
10
12
  @Entity({ tableName: "crud_dto_entities" })
@@ -97,20 +99,34 @@ describe("createMetalCrudDtoClasses", () => {
97
99
  nickname?: string | null;
98
100
  }
99
101
 
100
- it("builds ready-to-export DTO classes", () => {
101
- const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
102
- mutationExclude: ["id"]
103
- });
104
-
105
- const responseMeta = getDtoMeta(classes.response);
106
- const createMeta = getDtoMeta(classes.create);
107
- const paramsMeta = getDtoMeta(classes.params);
108
-
109
- expect(classes.response.name).toBe("CrudDtoClassEntityDto");
110
- expect(responseMeta?.fields.id).toBeDefined();
111
- expect(createMeta?.fields.id).toBeUndefined();
112
- expect(paramsMeta?.fields).toEqual({ id: expect.any(Object) });
113
- });
102
+ it("builds ready-to-export DTO classes", () => {
103
+ const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
104
+ mutationExclude: ["id"]
105
+ });
106
+
107
+ const responseMeta = getDtoMeta(classes.response);
108
+ const createMeta = getDtoMeta(classes.create);
109
+ const paramsMeta = getDtoMeta(classes.params);
110
+ const pagedResponseMeta = getDtoMeta(classes.pagedResponseDto);
111
+ const optionsMeta = getDtoMeta(classes.optionsDto);
112
+
113
+ expect(classes.response.name).toBe("CrudDtoClassEntityDto");
114
+ expect(classes.queryDto.name).toBe("CrudDtoClassEntityQueryDto");
115
+ expect(classes.optionsQueryDto.name).toBe("CrudDtoClassEntityOptionsQueryDto");
116
+ expect(classes.optionDto.name).toBe("CrudDtoClassEntityOptionDto");
117
+ expect(pagedResponseMeta?.name).toBe("CrudDtoClassEntityPagedResponseDto");
118
+ expect(optionsMeta?.name).toBe("CrudDtoClassEntityOptionsDto");
119
+ expect(responseMeta?.fields.id).toBeDefined();
120
+ expect(createMeta?.fields.id).toBeUndefined();
121
+ expect(paramsMeta?.fields).toEqual({ id: expect.any(Object) });
122
+ expect(classes.filterMappings).toEqual({
123
+ search: {
124
+ field: "nome",
125
+ operator: "contains"
126
+ }
127
+ });
128
+ expect(classes.sortableColumns).toEqual({});
129
+ });
114
130
 
115
131
  it("applies custom name overrides", () => {
116
132
  const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
@@ -121,8 +137,64 @@ describe("createMetalCrudDtoClasses", () => {
121
137
  }
122
138
  });
123
139
 
124
- expect(classes.response.name).toBe("PersonDto");
125
- expect(classes.params.name).toBe("PersonIdDto");
126
- expect(classes.create.name).toBe("CreatePersonDto");
127
- });
128
- });
140
+ expect(classes.response.name).toBe("PersonDto");
141
+ expect(classes.params.name).toBe("PersonIdDto");
142
+ expect(classes.create.name).toBe("CreatePersonDto");
143
+ });
144
+
145
+ it("generates query/options artifacts and execution-ready metadata from one config", () => {
146
+ const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
147
+ query: {
148
+ filters: {
149
+ nameContains: {
150
+ schema: t.string({ minLength: 1 }),
151
+ field: "name",
152
+ operator: "contains"
153
+ },
154
+ nickname: {
155
+ schema: t.string({ minLength: 1 }),
156
+ field: "nickname"
157
+ }
158
+ },
159
+ sortableColumns: {
160
+ name: "name",
161
+ nickname: "nickname"
162
+ },
163
+ options: {
164
+ labelField: "name",
165
+ searchKey: "labelContains"
166
+ }
167
+ },
168
+ errors: true
169
+ });
170
+
171
+ const queryMeta = getDtoMeta(classes.queryDto);
172
+ const optionsQueryMeta = getDtoMeta(classes.optionsQueryDto);
173
+ const optionMeta = getDtoMeta(classes.optionDto);
174
+
175
+ expect(queryMeta?.fields.page).toBeDefined();
176
+ expect(queryMeta?.fields.pageSize).toBeDefined();
177
+ expect(queryMeta?.fields.nameContains).toBeDefined();
178
+ expect(queryMeta?.fields.nickname).toBeDefined();
179
+ expect(queryMeta?.fields.sortBy).toBeDefined();
180
+ expect(queryMeta?.fields.sortDirection).toBeDefined();
181
+
182
+ expect(optionsQueryMeta?.fields.labelContains).toBeDefined();
183
+ expect(optionMeta?.fields.id).toBeDefined();
184
+ expect(optionMeta?.fields.name).toBeDefined();
185
+
186
+ expect(classes.filterMappings).toEqual({
187
+ nameContains: { field: "name", operator: "contains" },
188
+ nickname: { field: "nickname", operator: "equals" },
189
+ labelContains: { field: "name", operator: "contains" }
190
+ });
191
+
192
+ const typedMappings: Record<string, FilterMapping<CrudDtoClassEntity>> = classes.filterMappings;
193
+ expect(typedMappings.nameContains.field).toBe("name");
194
+ expect(classes.sortableColumns).toEqual({
195
+ name: "name",
196
+ nickname: "nickname"
197
+ });
198
+ expect(typeof classes.errors).toBe("function");
199
+ });
200
+ });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { parseFilter } from "../../src/adapter/metal-orm/index";
3
- import type { FilterMapping } from "../../src/adapter/metal-orm/types";
3
+ import type { FilterMapping } from "../../src/adapter/metal-orm/index";
4
4
 
5
5
  describe("parseFilter", () => {
6
6
  it("returns undefined when query is undefined", () => {
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseSort } from "../../src/adapter/metal-orm/index";
3
+
4
+ describe("parseSort", () => {
5
+ it("returns undefined when sortBy is missing", () => {
6
+ const result = parseSort({
7
+ query: { page: 1 },
8
+ sortableColumns: { name: "name" }
9
+ });
10
+ expect(result).toBeUndefined();
11
+ });
12
+
13
+ it("returns undefined when sortBy is not allowed", () => {
14
+ const result = parseSort({
15
+ query: { sortBy: "email" },
16
+ sortableColumns: { name: "name" }
17
+ });
18
+ expect(result).toBeUndefined();
19
+ });
20
+
21
+ it("parses valid sort and resolves mapped field", () => {
22
+ const result = parseSort({
23
+ query: { sortBy: "userName", sortDirection: "desc" },
24
+ sortableColumns: { userName: "name" }
25
+ });
26
+ expect(result).toEqual({
27
+ sortBy: "userName",
28
+ sortDirection: "desc",
29
+ field: "name"
30
+ });
31
+ });
32
+
33
+ it("supports custom sort keys and defaults", () => {
34
+ const result = parseSort({
35
+ query: { direction: "desc" },
36
+ sortableColumns: { createdAt: "createdAt" },
37
+ sortByKey: "orderBy",
38
+ sortDirectionKey: "direction",
39
+ defaultSortBy: "createdAt",
40
+ defaultSortDirection: "asc"
41
+ });
42
+ expect(result).toEqual({
43
+ sortBy: "createdAt",
44
+ sortDirection: "desc",
45
+ field: "createdAt"
46
+ });
47
+ });
48
+ });