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 +126 -17
- package/dist/adapter/metal-orm/crud-dtos.d.ts +5 -4
- package/dist/adapter/metal-orm/crud-dtos.js +199 -6
- package/dist/adapter/metal-orm/filters.d.ts +1 -1
- package/dist/adapter/metal-orm/index.d.ts +2 -1
- package/dist/adapter/metal-orm/index.js +3 -1
- package/dist/adapter/metal-orm/pagination.d.ts +1 -1
- package/dist/adapter/metal-orm/pagination.js +3 -2
- package/dist/adapter/metal-orm/sort.d.ts +7 -0
- package/dist/adapter/metal-orm/sort.js +54 -0
- package/dist/adapter/metal-orm/types.d.ts +132 -5
- package/package.json +1 -1
- package/src/adapter/metal-orm/crud-dtos.ts +376 -106
- package/src/adapter/metal-orm/filters.ts +9 -9
- package/src/adapter/metal-orm/index.ts +25 -11
- package/src/adapter/metal-orm/pagination.ts +29 -28
- package/src/adapter/metal-orm/sort.ts +82 -0
- package/src/adapter/metal-orm/types.ts +160 -11
- package/tests/e2e/metal-crud-openapi.e2e.test.ts +36 -19
- package/tests/unit/crud-dtos.test.ts +98 -26
- package/tests/unit/parse-filter.test.ts +1 -1
- package/tests/unit/parse-sort.test.ts +48 -0
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
|
-
|
|
345
|
-
CreateUserDto,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
410
|
+
UserDto,
|
|
376
411
|
CreateUserDto,
|
|
377
|
-
UpdateUserDto,
|
|
378
412
|
ReplaceUserDto,
|
|
413
|
+
UpdateUserDto,
|
|
414
|
+
UserParamsDto,
|
|
379
415
|
UserQueryDto,
|
|
380
|
-
|
|
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
|
|
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
|
|
394
|
-
User.select().orderBy(User.id,
|
|
440
|
+
const ormQuery = applyFilter(
|
|
441
|
+
User.select().orderBy(User.id, direction),
|
|
395
442
|
User,
|
|
396
|
-
|
|
443
|
+
filters
|
|
397
444
|
);
|
|
398
445
|
|
|
399
|
-
const paged = await
|
|
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(
|
|
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 {
|
|
3
|
-
export declare function createMetalCrudDtos(target:
|
|
4
|
-
export declare function createMetalCrudDtoClasses(target:
|
|
5
|
-
export declare function createNestedCreateDtoClass(target:
|
|
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
|
|
92
|
+
const crudClasses = {};
|
|
83
93
|
for (const key of Object.keys(decorators)) {
|
|
84
94
|
const name = names?.[key] ?? defaultNames[key];
|
|
85
|
-
|
|
95
|
+
crudClasses[key] = buildDtoClass(name, decorators[key]);
|
|
86
96
|
}
|
|
87
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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
|
+
}
|