adorn-api 1.1.4 → 1.1.6
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 +152 -1
- package/dist/adapter/metal-orm/crud-controller.d.ts +16 -0
- package/dist/adapter/metal-orm/crud-controller.js +156 -0
- package/dist/adapter/metal-orm/crud-dtos.js +12 -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/sort.js +6 -3
- package/dist/adapter/metal-orm/types.d.ts +71 -0
- package/package.json +1 -1
- package/src/adapter/metal-orm/crud-controller.ts +188 -0
- package/src/adapter/metal-orm/crud-dtos.ts +13 -1
- package/src/adapter/metal-orm/index.ts +61 -53
- package/src/adapter/metal-orm/sort.ts +6 -3
- package/src/adapter/metal-orm/types.ts +256 -147
- package/tests/unit/crud-controller-factory.test.ts +222 -0
- package/tests/unit/crud-dtos.test.ts +150 -102
- package/tests/unit/parse-sort.test.ts +111 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
Get,
|
|
6
|
+
Params,
|
|
7
|
+
Patch,
|
|
8
|
+
Post,
|
|
9
|
+
Put,
|
|
10
|
+
Query,
|
|
11
|
+
Returns
|
|
12
|
+
} from "../../core/decorators";
|
|
13
|
+
import type { DtoConstructor } from "../../core/types";
|
|
14
|
+
import type { RequestContext } from "../express/types";
|
|
15
|
+
import type {
|
|
16
|
+
CreateCrudControllerOptions,
|
|
17
|
+
CrudControllerService,
|
|
18
|
+
CrudControllerServiceInput,
|
|
19
|
+
MetalCrudDtoClasses,
|
|
20
|
+
RouteErrorsDecorator
|
|
21
|
+
} from "./types";
|
|
22
|
+
import { parseIdOrThrow } from "./utils";
|
|
23
|
+
|
|
24
|
+
type DtoInstance<TDto extends DtoConstructor> = InstanceType<TDto>;
|
|
25
|
+
type MethodDecorator = (
|
|
26
|
+
value: unknown,
|
|
27
|
+
context: ClassMethodDecoratorContext
|
|
28
|
+
) => void;
|
|
29
|
+
|
|
30
|
+
export function createCrudController<TDtos extends MetalCrudDtoClasses<any>>(
|
|
31
|
+
options: CreateCrudControllerOptions<TDtos>
|
|
32
|
+
) {
|
|
33
|
+
const withOptionsRoute = options.withOptionsRoute ?? true;
|
|
34
|
+
const withReplace = options.withReplace ?? true;
|
|
35
|
+
const withPatch = options.withPatch ?? true;
|
|
36
|
+
const withDelete = options.withDelete ?? true;
|
|
37
|
+
const service = resolveService(options.service);
|
|
38
|
+
const routeErrorsDecorator = options.dtos.errors;
|
|
39
|
+
|
|
40
|
+
@Controller({ path: options.path, tags: options.tags })
|
|
41
|
+
class GeneratedCrudController {
|
|
42
|
+
@Get("/")
|
|
43
|
+
@Query(options.dtos.queryDto)
|
|
44
|
+
@Returns(options.dtos.pagedResponseDto)
|
|
45
|
+
async list(
|
|
46
|
+
ctx: RequestContext<unknown, DtoInstance<TDtos["queryDto"]>>
|
|
47
|
+
): Promise<DtoInstance<TDtos["pagedResponseDto"]>> {
|
|
48
|
+
return await service.list(ctx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@when(withOptionsRoute, Get("/options"))
|
|
52
|
+
@when(withOptionsRoute, Query(options.dtos.optionsQueryDto))
|
|
53
|
+
@when(withOptionsRoute, Returns(options.dtos.optionsDto))
|
|
54
|
+
async options(
|
|
55
|
+
ctx: RequestContext<unknown, DtoInstance<TDtos["optionsQueryDto"]>>
|
|
56
|
+
): Promise<DtoInstance<TDtos["optionsDto"]>> {
|
|
57
|
+
assertServiceMethod(service, "options");
|
|
58
|
+
return await service.options(ctx);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Get("/:id")
|
|
62
|
+
@Params(options.dtos.params)
|
|
63
|
+
@Returns(options.dtos.response)
|
|
64
|
+
@applyRouteErrors(routeErrorsDecorator)
|
|
65
|
+
async getById(
|
|
66
|
+
ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>
|
|
67
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
68
|
+
const id = parseContextId(ctx, options.entityName);
|
|
69
|
+
return await service.getById(id, ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Post("/")
|
|
73
|
+
@Body(options.dtos.create)
|
|
74
|
+
@Returns({ status: 201, schema: options.dtos.response })
|
|
75
|
+
async create(
|
|
76
|
+
ctx: RequestContext<DtoInstance<TDtos["create"]>>
|
|
77
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
78
|
+
return await service.create(ctx.body, ctx);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@when(withReplace, Put("/:id"))
|
|
82
|
+
@when(withReplace, Params(options.dtos.params))
|
|
83
|
+
@when(withReplace, Body(options.dtos.replace))
|
|
84
|
+
@when(withReplace, Returns(options.dtos.response))
|
|
85
|
+
@when(withReplace, applyRouteErrors(routeErrorsDecorator))
|
|
86
|
+
async replace(
|
|
87
|
+
ctx: RequestContext<
|
|
88
|
+
DtoInstance<TDtos["replace"]>,
|
|
89
|
+
undefined,
|
|
90
|
+
DtoInstance<TDtos["params"]>
|
|
91
|
+
>
|
|
92
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
93
|
+
assertServiceMethod(service, "replace");
|
|
94
|
+
const id = parseContextId(ctx, options.entityName);
|
|
95
|
+
return await service.replace(id, ctx.body, ctx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@when(withPatch, Patch("/:id"))
|
|
99
|
+
@when(withPatch, Params(options.dtos.params))
|
|
100
|
+
@when(withPatch, Body(options.dtos.update))
|
|
101
|
+
@when(withPatch, Returns(options.dtos.response))
|
|
102
|
+
@when(withPatch, applyRouteErrors(routeErrorsDecorator))
|
|
103
|
+
async update(
|
|
104
|
+
ctx: RequestContext<
|
|
105
|
+
DtoInstance<TDtos["update"]>,
|
|
106
|
+
undefined,
|
|
107
|
+
DtoInstance<TDtos["params"]>
|
|
108
|
+
>
|
|
109
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
110
|
+
assertServiceMethod(service, "update");
|
|
111
|
+
const id = parseContextId(ctx, options.entityName);
|
|
112
|
+
return await service.update(id, ctx.body, ctx);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@when(withDelete, Delete("/:id"))
|
|
116
|
+
@when(withDelete, Params(options.dtos.params))
|
|
117
|
+
@when(withDelete, Returns({ status: 204, description: "No Content" }))
|
|
118
|
+
@when(withDelete, applyRouteErrors(routeErrorsDecorator))
|
|
119
|
+
async delete(
|
|
120
|
+
ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
assertServiceMethod(service, "delete");
|
|
123
|
+
const id = parseContextId(ctx, options.entityName);
|
|
124
|
+
await service.delete(id, ctx);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Object.defineProperty(GeneratedCrudController, "name", {
|
|
129
|
+
value: `${options.entityName}CrudController`,
|
|
130
|
+
configurable: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return GeneratedCrudController;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveService<TDtos extends MetalCrudDtoClasses<any>>(
|
|
137
|
+
input: CrudControllerServiceInput<TDtos>
|
|
138
|
+
): CrudControllerService<TDtos> {
|
|
139
|
+
if (typeof input === "function") {
|
|
140
|
+
return new input();
|
|
141
|
+
}
|
|
142
|
+
return input;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function when(enabled: boolean, decorator: MethodDecorator): MethodDecorator {
|
|
146
|
+
return (value, context) => {
|
|
147
|
+
if (enabled) {
|
|
148
|
+
decorator(value, context);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyRouteErrors(
|
|
154
|
+
decorator: RouteErrorsDecorator | undefined
|
|
155
|
+
): MethodDecorator {
|
|
156
|
+
return (value, context) => {
|
|
157
|
+
decorator?.(value, context);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseContextId(
|
|
162
|
+
ctx: RequestContext<unknown, undefined, object | undefined>,
|
|
163
|
+
entityName: string
|
|
164
|
+
): number {
|
|
165
|
+
const params = (ctx.params ?? {}) as Record<string, unknown>;
|
|
166
|
+
return parseIdOrThrow(toIdValue(params.id), entityName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function toIdValue(value: unknown): string | number {
|
|
170
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertServiceMethod<
|
|
177
|
+
TDtos extends MetalCrudDtoClasses<any>,
|
|
178
|
+
TMethod extends "options" | "replace" | "update" | "delete"
|
|
179
|
+
>(
|
|
180
|
+
service: CrudControllerService<TDtos>,
|
|
181
|
+
method: TMethod
|
|
182
|
+
): asserts service is CrudControllerService<TDtos> & Record<TMethod, NonNullable<CrudControllerService<TDtos>[TMethod]>> {
|
|
183
|
+
if (typeof service[method] !== "function") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`CRUD service is missing "${method}" method required by enabled route.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -166,6 +166,17 @@ export function createMetalCrudDtoClasses<TEntity extends Record<string, unknown
|
|
|
166
166
|
|
|
167
167
|
const errors = buildStandardCrudErrors(entityName, errorOptions);
|
|
168
168
|
|
|
169
|
+
const listConfig = {
|
|
170
|
+
filterMappings,
|
|
171
|
+
sortableColumns,
|
|
172
|
+
defaultSortBy: queryOptions.defaultSortBy,
|
|
173
|
+
defaultSortDirection: queryOptions.defaultSortDirection ?? "asc" as SortDirection,
|
|
174
|
+
defaultPageSize,
|
|
175
|
+
maxPageSize,
|
|
176
|
+
sortByKey,
|
|
177
|
+
sortDirectionKey
|
|
178
|
+
};
|
|
179
|
+
|
|
169
180
|
return {
|
|
170
181
|
response: crudClasses.response as DtoConstructor,
|
|
171
182
|
create: crudClasses.create as DtoConstructor,
|
|
@@ -179,7 +190,8 @@ export function createMetalCrudDtoClasses<TEntity extends Record<string, unknown
|
|
|
179
190
|
optionsDto,
|
|
180
191
|
errors,
|
|
181
192
|
filterMappings,
|
|
182
|
-
sortableColumns
|
|
193
|
+
sortableColumns,
|
|
194
|
+
listConfig
|
|
183
195
|
};
|
|
184
196
|
}
|
|
185
197
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// Ensure standard decorator metadata is available for metal-orm transformers.
|
|
2
|
-
const symbolMetadata = (Symbol as { metadata?: symbol }).metadata;
|
|
3
|
-
if (!symbolMetadata) {
|
|
4
|
-
(Symbol as { metadata?: symbol }).metadata = Symbol("Symbol.metadata");
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export {
|
|
8
|
-
MetalDto
|
|
9
|
-
} from "./dto";
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
parsePagination
|
|
13
|
-
} from "./pagination";
|
|
14
|
-
|
|
1
|
+
// Ensure standard decorator metadata is available for metal-orm transformers.
|
|
2
|
+
const symbolMetadata = (Symbol as { metadata?: symbol }).metadata;
|
|
3
|
+
if (!symbolMetadata) {
|
|
4
|
+
(Symbol as { metadata?: symbol }).metadata = Symbol("Symbol.metadata");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
MetalDto
|
|
9
|
+
} from "./dto";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
parsePagination
|
|
13
|
+
} from "./pagination";
|
|
14
|
+
|
|
15
15
|
export {
|
|
16
16
|
parseFilter,
|
|
17
17
|
createFilterMappings
|
|
@@ -20,55 +20,59 @@ export {
|
|
|
20
20
|
export {
|
|
21
21
|
parseSort
|
|
22
22
|
} from "./sort";
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
createPagedQueryDtoClass,
|
|
26
|
-
createPagedResponseDtoClass,
|
|
27
|
-
createPagedFilterQueryDtoClass
|
|
28
|
-
} from "./paged-dtos";
|
|
29
|
-
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
createPagedQueryDtoClass,
|
|
26
|
+
createPagedResponseDtoClass,
|
|
27
|
+
createPagedFilterQueryDtoClass
|
|
28
|
+
} from "./paged-dtos";
|
|
29
|
+
|
|
30
30
|
export {
|
|
31
31
|
createMetalCrudDtos,
|
|
32
32
|
createMetalCrudDtoClasses,
|
|
33
33
|
createNestedCreateDtoClass
|
|
34
34
|
} from "./crud-dtos";
|
|
35
35
|
|
|
36
|
+
export {
|
|
37
|
+
createCrudController
|
|
38
|
+
} from "./crud-controller";
|
|
39
|
+
|
|
36
40
|
export {
|
|
37
41
|
createMetalTreeDtoClasses
|
|
38
42
|
} from "./tree-dtos";
|
|
39
43
|
|
|
40
|
-
export {
|
|
41
|
-
createMetalDtoOverrides,
|
|
42
|
-
type CreateMetalDtoOverridesOptions
|
|
43
|
-
} from "./convention-overrides";
|
|
44
|
-
|
|
45
|
-
export {
|
|
46
|
-
createErrorDtoClass,
|
|
47
|
-
StandardErrorDto,
|
|
48
|
-
SimpleErrorDto,
|
|
49
|
-
BasicErrorDto
|
|
50
|
-
} from "./error-dtos";
|
|
51
|
-
|
|
52
|
-
export {
|
|
53
|
-
withSession,
|
|
54
|
-
parseIdOrThrow,
|
|
55
|
-
compactUpdates,
|
|
56
|
-
applyInput,
|
|
57
|
-
getEntityOrThrow
|
|
58
|
-
} from "./utils";
|
|
59
|
-
|
|
60
|
-
export {
|
|
61
|
-
validateEntityMetadata,
|
|
62
|
-
hasValidEntityMetadata
|
|
63
|
-
} from "./field-builder";
|
|
64
|
-
|
|
65
|
-
export type {
|
|
66
|
-
MetalDtoMode,
|
|
67
|
-
MetalDtoOptions,
|
|
68
|
-
MetalDtoTarget,
|
|
69
|
-
PaginationConfig,
|
|
70
|
-
PaginationOptions,
|
|
71
|
-
ParsedPagination,
|
|
44
|
+
export {
|
|
45
|
+
createMetalDtoOverrides,
|
|
46
|
+
type CreateMetalDtoOverridesOptions
|
|
47
|
+
} from "./convention-overrides";
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
createErrorDtoClass,
|
|
51
|
+
StandardErrorDto,
|
|
52
|
+
SimpleErrorDto,
|
|
53
|
+
BasicErrorDto
|
|
54
|
+
} from "./error-dtos";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
withSession,
|
|
58
|
+
parseIdOrThrow,
|
|
59
|
+
compactUpdates,
|
|
60
|
+
applyInput,
|
|
61
|
+
getEntityOrThrow
|
|
62
|
+
} from "./utils";
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
validateEntityMetadata,
|
|
66
|
+
hasValidEntityMetadata
|
|
67
|
+
} from "./field-builder";
|
|
68
|
+
|
|
69
|
+
export type {
|
|
70
|
+
MetalDtoMode,
|
|
71
|
+
MetalDtoOptions,
|
|
72
|
+
MetalDtoTarget,
|
|
73
|
+
PaginationConfig,
|
|
74
|
+
PaginationOptions,
|
|
75
|
+
ParsedPagination,
|
|
72
76
|
Filter,
|
|
73
77
|
FilterMapping,
|
|
74
78
|
FilterFieldMapping,
|
|
@@ -80,6 +84,7 @@ export type {
|
|
|
80
84
|
ParseSortOptions,
|
|
81
85
|
ParsedSort,
|
|
82
86
|
SortDirection,
|
|
87
|
+
ListConfig,
|
|
83
88
|
PagedQueryDtoOptions,
|
|
84
89
|
PagedResponseDtoOptions,
|
|
85
90
|
PagedFilterQueryDtoOptions,
|
|
@@ -95,6 +100,9 @@ export type {
|
|
|
95
100
|
MetalCrudDtoClasses,
|
|
96
101
|
MetalCrudDtoClassNameKey,
|
|
97
102
|
MetalCrudDtoClassNames,
|
|
103
|
+
CrudControllerService,
|
|
104
|
+
CrudControllerServiceInput,
|
|
105
|
+
CreateCrudControllerOptions,
|
|
98
106
|
RouteErrorsDecorator,
|
|
99
107
|
NestedCreateDtoOptions,
|
|
100
108
|
MetalTreeDtoClassOptions,
|
|
@@ -31,6 +31,7 @@ export function parseSort<T>(
|
|
|
31
31
|
|
|
32
32
|
const sortByKey = resolved.sortByKey ?? "sortBy";
|
|
33
33
|
const sortDirectionKey = resolved.sortDirectionKey ?? "sortDirection";
|
|
34
|
+
const sortOrderKey = resolved.sortOrderKey ?? "sortOrder";
|
|
34
35
|
const defaultSortBy = resolved.defaultSortBy;
|
|
35
36
|
const defaultDirection = resolved.defaultSortDirection ?? "asc";
|
|
36
37
|
|
|
@@ -40,7 +41,8 @@ export function parseSort<T>(
|
|
|
40
41
|
return undefined;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const requestedDirection = toTrimmedString(query[sortDirectionKey])
|
|
44
|
+
const requestedDirection = toTrimmedString(query[sortDirectionKey])
|
|
45
|
+
?? toTrimmedString(query[sortOrderKey]);
|
|
44
46
|
const sortDirection = normalizeDirection(requestedDirection, defaultDirection);
|
|
45
47
|
|
|
46
48
|
return {
|
|
@@ -72,10 +74,11 @@ function normalizeDirection(
|
|
|
72
74
|
raw: string | undefined,
|
|
73
75
|
fallback: SortDirection
|
|
74
76
|
): SortDirection {
|
|
75
|
-
|
|
77
|
+
const lower = raw?.toLowerCase();
|
|
78
|
+
if (lower === "desc") {
|
|
76
79
|
return "desc";
|
|
77
80
|
}
|
|
78
|
-
if (
|
|
81
|
+
if (lower === "asc") {
|
|
79
82
|
return "asc";
|
|
80
83
|
}
|
|
81
84
|
return fallback;
|