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
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
|
-
|
|
49
|
+
const lower = raw?.toLowerCase();
|
|
50
|
+
if (lower === "desc") {
|
|
48
51
|
return "desc";
|
|
49
52
|
}
|
|
50
|
-
if (
|
|
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.
|