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 CHANGED
@@ -353,7 +353,8 @@ export const {
353
353
  optionsDto: UserOptionsDto,
354
354
  errors: UserErrors,
355
355
  filterMappings: USER_FILTER_MAPPINGS,
356
- sortableColumns: USER_SORTABLE_COLUMNS
356
+ sortableColumns: USER_SORTABLE_COLUMNS,
357
+ listConfig: USER_LIST_CONFIG
357
358
  } = createMetalCrudDtoClasses(User, {
358
359
  mutationExclude: ["id", "createdAt"],
359
360
  query: {
@@ -490,6 +491,80 @@ export class UserController {
490
491
 
491
492
  ### Migration Guide (Breaking)
492
493
 
494
+ ### CRUD Controller Factory (`createCrudController`)
495
+
496
+ When your controller only wires DTOs + service calls, you can generate the full CRUD controller and remove decorator boilerplate.
497
+
498
+ ```typescript
499
+ // user.controller.ts
500
+ import { createCrudController } from "adorn-api";
501
+ import { userCrudDtos } from "./user.dtos";
502
+ import { UserCrudService } from "./user.service";
503
+
504
+ export const UserController = createCrudController({
505
+ path: "/users",
506
+ service: UserCrudService, // class or instance
507
+ dtos: userCrudDtos, // result of createMetalCrudDtoClasses(...)
508
+ entityName: "User", // used by parseIdOrThrow messages
509
+ withOptionsRoute: true,
510
+ withReplace: true,
511
+ withPatch: true,
512
+ withDelete: true
513
+ });
514
+ ```
515
+
516
+ Generated routes:
517
+ - `GET /`
518
+ - `GET /options` (optional)
519
+ - `GET /:id`
520
+ - `POST /`
521
+ - `PUT /:id` (optional)
522
+ - `PATCH /:id` (optional)
523
+ - `DELETE /:id` (optional)
524
+
525
+ The factory applies the correct `@Query/@Body/@Params/@Returns` schemas and also propagates `dtos.errors` to all `/:id` routes.
526
+
527
+ Before (manual, repeated decorators/status/schema wiring):
528
+
529
+ ```typescript
530
+ @Controller("/users")
531
+ class UserController {
532
+ @Get("/")
533
+ @Query(UserQueryDto)
534
+ @Returns(UserPagedResponseDto)
535
+ async list(ctx: RequestContext<unknown, UserQueryDto>) { ... }
536
+
537
+ @Get("/:id")
538
+ @Params(UserParamsDto)
539
+ @Returns(UserDto)
540
+ @UserErrors
541
+ async getById(ctx: RequestContext<unknown, undefined, UserParamsDto>) { ... }
542
+
543
+ @Post("/")
544
+ @Body(CreateUserDto)
545
+ @Returns({ status: 201, schema: UserDto })
546
+ async create(ctx: RequestContext<CreateUserDto>) { ... }
547
+
548
+ // put/patch/delete/options...
549
+ }
550
+ ```
551
+
552
+ After (factory + service):
553
+
554
+ ```typescript
555
+ export const UserController = createCrudController({
556
+ path: "/users",
557
+ service: new UserCrudService(),
558
+ dtos: userCrudDtos,
559
+ entityName: "User"
560
+ });
561
+ ```
562
+
563
+ When to use factory vs manual controller:
564
+ - Use `createCrudController` when routes follow standard CRUD and behavior lives in a service.
565
+ - Use a manual controller when route contracts diverge (custom status/body shape, non-standard params, upload/stream/raw endpoints, or route-level auth/doc decorators not shared by all CRUD routes).
566
+ - For extra endpoints, keep the generated CRUD controller and add a second manual controller for custom routes on the same base path.
567
+
493
568
  Before (duplicated config):
494
569
 
495
570
  ```typescript
@@ -530,6 +605,82 @@ Breaking changes summary:
530
605
  - Generated outputs now include `queryDto`, `optionsQueryDto`, `pagedResponseDto`, `optionDto`, `optionsDto`, `errors`, `filterMappings`, and `sortableColumns`.
531
606
  - Consumers no longer need internal `dist/...` imports for query/filter metadata types; all relevant types/utilities are publicly exported from `adorn-api`.
532
607
 
608
+ ### Using `listConfig` (Zero-Duplication Service Layer)
609
+
610
+ `createMetalCrudDtoClasses` now exposes a `listConfig` object that bundles all filter/sort/pagination config needed by your service layer. No more re-declaring mappings in your repository:
611
+
612
+ ```typescript
613
+ // user.controller.ts — using listConfig directly
614
+ import {
615
+ Controller, Get, Query, Returns,
616
+ parseFilter, parsePagination, parseSort,
617
+ type RequestContext
618
+ } from "adorn-api";
619
+ import { applyFilter, toPagedResponse } from "metal-orm";
620
+ import { createSession } from "./db";
621
+ import { User } from "./user.entity";
622
+ import {
623
+ UserQueryDto,
624
+ UserPagedResponseDto,
625
+ USER_LIST_CONFIG
626
+ } from "./user.dtos";
627
+
628
+ @Controller("/users")
629
+ export class UserController {
630
+ @Get("/")
631
+ @Query(UserQueryDto)
632
+ @Returns(UserPagedResponseDto)
633
+ async list(ctx: RequestContext<unknown, UserQueryDto>) {
634
+ const query = (ctx.query ?? {}) as Record<string, unknown>;
635
+ const { page, pageSize } = parsePagination(query, USER_LIST_CONFIG);
636
+ const filters = parseFilter(query, USER_LIST_CONFIG.filterMappings);
637
+ const sort = parseSort(query, USER_LIST_CONFIG.sortableColumns, {
638
+ defaultSortBy: USER_LIST_CONFIG.defaultSortBy,
639
+ defaultSortDirection: USER_LIST_CONFIG.defaultSortDirection
640
+ });
641
+ const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
642
+
643
+ const session = createSession();
644
+ try {
645
+ const ormQuery = applyFilter(
646
+ User.select().orderBy(User.id, direction),
647
+ User,
648
+ filters
649
+ );
650
+ const paged = await ormQuery.executePaged(session, { page, pageSize });
651
+ return toPagedResponse(paged);
652
+ } finally {
653
+ await session.dispose();
654
+ }
655
+ }
656
+ }
657
+ ```
658
+
659
+ The `listConfig` object contains: `filterMappings`, `sortableColumns`, `defaultSortBy`, `defaultSortDirection`, `defaultPageSize`, `maxPageSize`, `sortByKey`, and `sortDirectionKey`.
660
+
661
+ ### Sort Order Compatibility (`sortOrder` / `sortDirection`)
662
+
663
+ `parseSort` now accepts both `sortDirection` (lowercase `asc`/`desc`) and `sortOrder` (uppercase `ASC`/`DESC`). This avoids the need for a custom helper when integrating with clients that send uppercase sort orders.
664
+
665
+ **Precedence**: `sortDirection` > `sortOrder` > default. Direction values are case-insensitive.
666
+
667
+ ```typescript
668
+ // Client sends: ?sortBy=name&sortOrder=DESC
669
+ const sort = parseSort(query, sortableColumns);
670
+ // → { sortBy: "name", sortDirection: "desc", field: "name" }
671
+
672
+ // Client sends both: ?sortBy=name&sortDirection=asc&sortOrder=DESC
673
+ const sort2 = parseSort(query, sortableColumns);
674
+ // → { sortBy: "name", sortDirection: "asc", field: "name" } (sortDirection wins)
675
+
676
+ // Custom sortOrder key:
677
+ const sort3 = parseSort({
678
+ query,
679
+ sortableColumns,
680
+ sortOrderKey: "order" // reads from query.order instead of query.sortOrder
681
+ });
682
+ ```
683
+
533
684
  ### Deep Relation Filters
534
685
 
535
686
  `parseFilter` now accepts nested relation paths, so you can filter deep chains like Alpha → Bravo → Charlie → Delta. If
@@ -0,0 +1,16 @@
1
+ import type { DtoConstructor } from "../../core/types";
2
+ import type { RequestContext } from "../express/types";
3
+ import type { CreateCrudControllerOptions, MetalCrudDtoClasses } from "./types";
4
+ type DtoInstance<TDto extends DtoConstructor> = InstanceType<TDto>;
5
+ export declare function createCrudController<TDtos extends MetalCrudDtoClasses<any>>(options: CreateCrudControllerOptions<TDtos>): {
6
+ new (): {
7
+ list(ctx: RequestContext<unknown, DtoInstance<TDtos["queryDto"]>>): Promise<DtoInstance<TDtos["pagedResponseDto"]>>;
8
+ options(ctx: RequestContext<unknown, DtoInstance<TDtos["optionsQueryDto"]>>): Promise<DtoInstance<TDtos["optionsDto"]>>;
9
+ getById(ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
10
+ create(ctx: RequestContext<DtoInstance<TDtos["create"]>>): Promise<DtoInstance<TDtos["response"]>>;
11
+ replace(ctx: RequestContext<DtoInstance<TDtos["replace"]>, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
12
+ update(ctx: RequestContext<DtoInstance<TDtos["update"]>, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
13
+ delete(ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>): Promise<void>;
14
+ };
15
+ };
16
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.createCrudController = createCrudController;
38
+ const decorators_1 = require("../../core/decorators");
39
+ const utils_1 = require("./utils");
40
+ function createCrudController(options) {
41
+ const withOptionsRoute = options.withOptionsRoute ?? true;
42
+ const withReplace = options.withReplace ?? true;
43
+ const withPatch = options.withPatch ?? true;
44
+ const withDelete = options.withDelete ?? true;
45
+ const service = resolveService(options.service);
46
+ const routeErrorsDecorator = options.dtos.errors;
47
+ let GeneratedCrudController = (() => {
48
+ let _classDecorators = [(0, decorators_1.Controller)({ path: options.path, tags: options.tags })];
49
+ let _classDescriptor;
50
+ let _classExtraInitializers = [];
51
+ let _classThis;
52
+ let _instanceExtraInitializers = [];
53
+ let _list_decorators;
54
+ let _options_decorators;
55
+ let _getById_decorators;
56
+ let _create_decorators;
57
+ let _replace_decorators;
58
+ let _update_decorators;
59
+ let _delete_decorators;
60
+ var GeneratedCrudController = class {
61
+ static { _classThis = this; }
62
+ static {
63
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
64
+ _list_decorators = [(0, decorators_1.Get)("/"), (0, decorators_1.Query)(options.dtos.queryDto), (0, decorators_1.Returns)(options.dtos.pagedResponseDto)];
65
+ _options_decorators = [when(withOptionsRoute, (0, decorators_1.Get)("/options")), when(withOptionsRoute, (0, decorators_1.Query)(options.dtos.optionsQueryDto)), when(withOptionsRoute, (0, decorators_1.Returns)(options.dtos.optionsDto))];
66
+ _getById_decorators = [(0, decorators_1.Get)("/:id"), (0, decorators_1.Params)(options.dtos.params), (0, decorators_1.Returns)(options.dtos.response), applyRouteErrors(routeErrorsDecorator)];
67
+ _create_decorators = [(0, decorators_1.Post)("/"), (0, decorators_1.Body)(options.dtos.create), (0, decorators_1.Returns)({ status: 201, schema: options.dtos.response })];
68
+ _replace_decorators = [when(withReplace, (0, decorators_1.Put)("/:id")), when(withReplace, (0, decorators_1.Params)(options.dtos.params)), when(withReplace, (0, decorators_1.Body)(options.dtos.replace)), when(withReplace, (0, decorators_1.Returns)(options.dtos.response)), when(withReplace, applyRouteErrors(routeErrorsDecorator))];
69
+ _update_decorators = [when(withPatch, (0, decorators_1.Patch)("/:id")), when(withPatch, (0, decorators_1.Params)(options.dtos.params)), when(withPatch, (0, decorators_1.Body)(options.dtos.update)), when(withPatch, (0, decorators_1.Returns)(options.dtos.response)), when(withPatch, applyRouteErrors(routeErrorsDecorator))];
70
+ _delete_decorators = [when(withDelete, (0, decorators_1.Delete)("/:id")), when(withDelete, (0, decorators_1.Params)(options.dtos.params)), when(withDelete, (0, decorators_1.Returns)({ status: 204, description: "No Content" })), when(withDelete, applyRouteErrors(routeErrorsDecorator))];
71
+ __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
72
+ __esDecorate(this, null, _options_decorators, { kind: "method", name: "options", static: false, private: false, access: { has: obj => "options" in obj, get: obj => obj.options }, metadata: _metadata }, null, _instanceExtraInitializers);
73
+ __esDecorate(this, null, _getById_decorators, { kind: "method", name: "getById", static: false, private: false, access: { has: obj => "getById" in obj, get: obj => obj.getById }, metadata: _metadata }, null, _instanceExtraInitializers);
74
+ __esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers);
75
+ __esDecorate(this, null, _replace_decorators, { kind: "method", name: "replace", static: false, private: false, access: { has: obj => "replace" in obj, get: obj => obj.replace }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _update_decorators, { kind: "method", name: "update", static: false, private: false, access: { has: obj => "update" in obj, get: obj => obj.update }, metadata: _metadata }, null, _instanceExtraInitializers);
77
+ __esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
78
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
79
+ GeneratedCrudController = _classThis = _classDescriptor.value;
80
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
81
+ __runInitializers(_classThis, _classExtraInitializers);
82
+ }
83
+ async list(ctx) {
84
+ return await service.list(ctx);
85
+ }
86
+ async options(ctx) {
87
+ assertServiceMethod(service, "options");
88
+ return await service.options(ctx);
89
+ }
90
+ async getById(ctx) {
91
+ const id = parseContextId(ctx, options.entityName);
92
+ return await service.getById(id, ctx);
93
+ }
94
+ async create(ctx) {
95
+ return await service.create(ctx.body, ctx);
96
+ }
97
+ async replace(ctx) {
98
+ assertServiceMethod(service, "replace");
99
+ const id = parseContextId(ctx, options.entityName);
100
+ return await service.replace(id, ctx.body, ctx);
101
+ }
102
+ async update(ctx) {
103
+ assertServiceMethod(service, "update");
104
+ const id = parseContextId(ctx, options.entityName);
105
+ return await service.update(id, ctx.body, ctx);
106
+ }
107
+ async delete(ctx) {
108
+ assertServiceMethod(service, "delete");
109
+ const id = parseContextId(ctx, options.entityName);
110
+ await service.delete(id, ctx);
111
+ }
112
+ constructor() {
113
+ __runInitializers(this, _instanceExtraInitializers);
114
+ }
115
+ };
116
+ return GeneratedCrudController = _classThis;
117
+ })();
118
+ Object.defineProperty(GeneratedCrudController, "name", {
119
+ value: `${options.entityName}CrudController`,
120
+ configurable: true
121
+ });
122
+ return GeneratedCrudController;
123
+ }
124
+ function resolveService(input) {
125
+ if (typeof input === "function") {
126
+ return new input();
127
+ }
128
+ return input;
129
+ }
130
+ function when(enabled, decorator) {
131
+ return (value, context) => {
132
+ if (enabled) {
133
+ decorator(value, context);
134
+ }
135
+ };
136
+ }
137
+ function applyRouteErrors(decorator) {
138
+ return (value, context) => {
139
+ decorator?.(value, context);
140
+ };
141
+ }
142
+ function parseContextId(ctx, entityName) {
143
+ const params = (ctx.params ?? {});
144
+ return (0, utils_1.parseIdOrThrow)(toIdValue(params.id), entityName);
145
+ }
146
+ function toIdValue(value) {
147
+ if (typeof value === "string" || typeof value === "number") {
148
+ return value;
149
+ }
150
+ return "";
151
+ }
152
+ function assertServiceMethod(service, method) {
153
+ if (typeof service[method] !== "function") {
154
+ throw new Error(`CRUD service is missing "${method}" method required by enabled route.`);
155
+ }
156
+ }
@@ -161,6 +161,16 @@ function createMetalCrudDtoClasses(target, options = {}) {
161
161
  description: `${entityName} options response.`
162
162
  });
163
163
  const errors = buildStandardCrudErrors(entityName, errorOptions);
164
+ const listConfig = {
165
+ filterMappings,
166
+ sortableColumns,
167
+ defaultSortBy: queryOptions.defaultSortBy,
168
+ defaultSortDirection: queryOptions.defaultSortDirection ?? "asc",
169
+ defaultPageSize,
170
+ maxPageSize,
171
+ sortByKey,
172
+ sortDirectionKey
173
+ };
164
174
  return {
165
175
  response: crudClasses.response,
166
176
  create: crudClasses.create,
@@ -174,7 +184,8 @@ function createMetalCrudDtoClasses(target, options = {}) {
174
184
  optionsDto,
175
185
  errors,
176
186
  filterMappings,
177
- sortableColumns
187
+ sortableColumns,
188
+ listConfig
178
189
  };
179
190
  }
180
191
  function createNestedCreateDtoClass(target, overrides, options) {
@@ -4,9 +4,10 @@ export { parseFilter, createFilterMappings } from "./filters";
4
4
  export { parseSort } from "./sort";
5
5
  export { createPagedQueryDtoClass, createPagedResponseDtoClass, createPagedFilterQueryDtoClass } from "./paged-dtos";
6
6
  export { createMetalCrudDtos, createMetalCrudDtoClasses, createNestedCreateDtoClass } from "./crud-dtos";
7
+ export { createCrudController } from "./crud-controller";
7
8
  export { createMetalTreeDtoClasses } from "./tree-dtos";
8
9
  export { createMetalDtoOverrides, type CreateMetalDtoOverridesOptions } from "./convention-overrides";
9
10
  export { createErrorDtoClass, StandardErrorDto, SimpleErrorDto, BasicErrorDto } from "./error-dtos";
10
11
  export { withSession, parseIdOrThrow, compactUpdates, applyInput, getEntityOrThrow } from "./utils";
11
12
  export { validateEntityMetadata, hasValidEntityMetadata } from "./field-builder";
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";
13
+ export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, ParseSortOptions, ParsedSort, SortDirection, ListConfig, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudQueryFilterDef, MetalCrudSortableColumns, MetalCrudOptionsQueryOptions, MetalCrudQueryOptions, MetalCrudStandardErrorsOptions, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNameKey, MetalCrudDtoClassNames, CrudControllerService, CrudControllerServiceInput, CreateCrudControllerOptions, 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.parseSort = 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.createCrudController = 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) {
@@ -23,6 +23,8 @@ var crud_dtos_1 = require("./crud-dtos");
23
23
  Object.defineProperty(exports, "createMetalCrudDtos", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtos; } });
24
24
  Object.defineProperty(exports, "createMetalCrudDtoClasses", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtoClasses; } });
25
25
  Object.defineProperty(exports, "createNestedCreateDtoClass", { enumerable: true, get: function () { return crud_dtos_1.createNestedCreateDtoClass; } });
26
+ var crud_controller_1 = require("./crud-controller");
27
+ Object.defineProperty(exports, "createCrudController", { enumerable: true, get: function () { return crud_controller_1.createCrudController; } });
26
28
  var tree_dtos_1 = require("./tree-dtos");
27
29
  Object.defineProperty(exports, "createMetalTreeDtoClasses", { enumerable: true, get: function () { return tree_dtos_1.createMetalTreeDtoClasses; } });
28
30
  var convention_overrides_1 = require("./convention-overrides");
@@ -16,6 +16,7 @@ function parseSort(queryOrOptions, sortableColumns, options) {
16
16
  }
17
17
  const sortByKey = resolved.sortByKey ?? "sortBy";
18
18
  const sortDirectionKey = resolved.sortDirectionKey ?? "sortDirection";
19
+ const sortOrderKey = resolved.sortOrderKey ?? "sortOrder";
19
20
  const defaultSortBy = resolved.defaultSortBy;
20
21
  const defaultDirection = resolved.defaultSortDirection ?? "asc";
21
22
  const requestedSortBy = toTrimmedString(query[sortByKey]);
@@ -23,7 +24,8 @@ function parseSort(queryOrOptions, sortableColumns, options) {
23
24
  if (!selectedSortBy) {
24
25
  return undefined;
25
26
  }
26
- const requestedDirection = toTrimmedString(query[sortDirectionKey]);
27
+ const requestedDirection = toTrimmedString(query[sortDirectionKey])
28
+ ?? toTrimmedString(query[sortOrderKey]);
27
29
  const sortDirection = normalizeDirection(requestedDirection, defaultDirection);
28
30
  return {
29
31
  sortBy: selectedSortBy,
@@ -44,10 +46,11 @@ function selectSortBy(requestedSortBy, defaultSortBy, allowed) {
44
46
  return undefined;
45
47
  }
46
48
  function normalizeDirection(raw, fallback) {
47
- if (raw === "desc") {
49
+ const lower = raw?.toLowerCase();
50
+ if (lower === "desc") {
48
51
  return "desc";
49
52
  }
50
- if (raw === "asc") {
53
+ if (lower === "asc") {
51
54
  return "asc";
52
55
  }
53
56
  return fallback;
@@ -2,6 +2,7 @@ import type { BelongsToReference, HasManyCollection, HasOneReference, ManyToMany
2
2
  import type { DtoOptions, ErrorResponseOptions, FieldOverride } from "../../core/decorators";
3
3
  import type { SchemaNode } from "../../core/schema";
4
4
  import type { DtoConstructor } from "../../core/types";
5
+ import type { RequestContext } from "../express/types";
5
6
  /**
6
7
  * Metal ORM DTO modes.
7
8
  */
@@ -119,6 +120,8 @@ export interface ParseSortOptions<T = Record<string, unknown>> {
119
120
  sortByKey?: string;
120
121
  /** Sort direction query key */
121
122
  sortDirectionKey?: string;
123
+ /** Query key for legacy sort order param (default: "sortOrder") */
124
+ sortOrderKey?: string;
122
125
  /** Default sort field */
123
126
  defaultSortBy?: string;
124
127
  /** Default sort direction */
@@ -135,6 +138,29 @@ export interface ParsedSort<T = Record<string, unknown>> {
135
138
  /** Resolved entity field */
136
139
  field?: FilterFieldInput<T>;
137
140
  }
141
+ /**
142
+ * Ready-to-use list/query configuration extracted from CRUD DTO class generation.
143
+ * Eliminates the need for consumers to reassemble filter/sort/pagination config
144
+ * in their service or repository layer.
145
+ */
146
+ export interface ListConfig<T = Record<string, unknown>> {
147
+ /** Execution-ready filter mappings for parseFilter */
148
+ filterMappings: Record<string, FilterMapping<T>>;
149
+ /** Execution-ready sortable column mappings for parseSort */
150
+ sortableColumns: MetalCrudSortableColumns<T>;
151
+ /** Default sort field key */
152
+ defaultSortBy?: string;
153
+ /** Default sort direction */
154
+ defaultSortDirection: SortDirection;
155
+ /** Default page size */
156
+ defaultPageSize: number;
157
+ /** Maximum page size */
158
+ maxPageSize: number;
159
+ /** Sort field query key */
160
+ sortByKey: string;
161
+ /** Sort direction query key */
162
+ sortDirectionKey: string;
163
+ }
138
164
  /**
139
165
  * Filter operator.
140
166
  */
@@ -364,6 +390,51 @@ export interface MetalCrudDtoClasses<T = Record<string, unknown>> {
364
390
  filterMappings: Record<string, FilterMapping<T>>;
365
391
  /** Execution-ready sortable column mappings */
366
392
  sortableColumns: MetalCrudSortableColumns<T>;
393
+ /** Ready-to-use config for list/query endpoints (combines filters, sort, pagination defaults) */
394
+ listConfig: ListConfig<T>;
395
+ }
396
+ /**
397
+ * Awaitable helper for CRUD service methods.
398
+ */
399
+ export type Awaitable<T> = T | Promise<T>;
400
+ /**
401
+ * Input for CRUD controller service: class or ready instance.
402
+ */
403
+ export type CrudControllerServiceInput<TDtos extends MetalCrudDtoClasses<any>> = CrudControllerService<TDtos> | (new () => CrudControllerService<TDtos>);
404
+ /**
405
+ * CRUD controller service contract used by createCrudController.
406
+ */
407
+ export interface CrudControllerService<TDtos extends MetalCrudDtoClasses<any>> {
408
+ list(ctx: RequestContext<unknown, InstanceType<TDtos["queryDto"]>>): Awaitable<InstanceType<TDtos["pagedResponseDto"]>>;
409
+ options?(ctx: RequestContext<unknown, InstanceType<TDtos["optionsQueryDto"]>>): Awaitable<InstanceType<TDtos["optionsDto"]>>;
410
+ getById(id: number, ctx: RequestContext<unknown, undefined, InstanceType<TDtos["params"]>>): Awaitable<InstanceType<TDtos["response"]>>;
411
+ create(body: InstanceType<TDtos["create"]>, ctx: RequestContext<InstanceType<TDtos["create"]>>): Awaitable<InstanceType<TDtos["response"]>>;
412
+ replace?(id: number, body: InstanceType<TDtos["replace"]>, ctx: RequestContext<InstanceType<TDtos["replace"]>, undefined, InstanceType<TDtos["params"]>>): Awaitable<InstanceType<TDtos["response"]>>;
413
+ update?(id: number, body: InstanceType<TDtos["update"]>, ctx: RequestContext<InstanceType<TDtos["update"]>, undefined, InstanceType<TDtos["params"]>>): Awaitable<InstanceType<TDtos["response"]>>;
414
+ delete?(id: number, ctx: RequestContext<unknown, undefined, InstanceType<TDtos["params"]>>): Awaitable<void>;
415
+ }
416
+ /**
417
+ * createCrudController options.
418
+ */
419
+ export interface CreateCrudControllerOptions<TDtos extends MetalCrudDtoClasses<any>> {
420
+ /** Controller path. */
421
+ path: string;
422
+ /** Service instance or class (new () => service). */
423
+ service: CrudControllerServiceInput<TDtos>;
424
+ /** DTO bundle produced by createMetalCrudDtoClasses. */
425
+ dtos: TDtos;
426
+ /** Entity label used by parseIdOrThrow messages. */
427
+ entityName: string;
428
+ /** Generate GET /options route (default: true). */
429
+ withOptionsRoute?: boolean;
430
+ /** Generate PUT /:id route (default: true). */
431
+ withReplace?: boolean;
432
+ /** Generate PATCH /:id route (default: true). */
433
+ withPatch?: boolean;
434
+ /** Generate DELETE /:id route (default: true). */
435
+ withDelete?: boolean;
436
+ /** Optional OpenAPI tags for generated controller. */
437
+ tags?: string[];
367
438
  }
368
439
  /**
369
440
  * Metal Tree DTO class names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",