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.
@@ -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
- if (raw === "desc") {
77
+ const lower = raw?.toLowerCase();
78
+ if (lower === "desc") {
76
79
  return "desc";
77
80
  }
78
- if (raw === "asc") {
81
+ if (lower === "asc") {
79
82
  return "asc";
80
83
  }
81
84
  return fallback;