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
|
@@ -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
|
-
|
|
10
|
+
FilterMapping,
|
|
11
|
+
MetalCrudDtoClassNameKey,
|
|
6
12
|
MetalCrudDtoClassOptions,
|
|
7
|
-
MetalCrudDtoDecorators,
|
|
8
13
|
MetalCrudDtoClasses,
|
|
9
|
-
|
|
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:
|
|
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:
|
|
51
|
-
options: MetalCrudDtoClassOptions = {}
|
|
52
|
-
): MetalCrudDtoClasses {
|
|
53
|
-
const { baseName, names, ...crudOptions } = options;
|
|
54
|
-
const decorators = createMetalCrudDtos(target, crudOptions);
|
|
55
|
-
const entityName = baseName ?? getTargetName(target);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
12
|
-
config: PaginationConfig = {}
|
|
13
|
-
): ParsedPagination {
|
|
14
|
-
const { defaultPageSize = 25, maxPageSize = 100 } = config;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|