adorn-api 1.1.5 → 1.1.7
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 +138 -38
- 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/index.d.ts +3 -1
- package/dist/adapter/metal-orm/index.js +6 -1
- package/dist/adapter/metal-orm/list.d.ts +11 -0
- package/dist/adapter/metal-orm/list.js +112 -0
- package/dist/adapter/metal-orm/types.d.ts +88 -1
- package/package.json +5 -3
- package/src/adapter/metal-orm/crud-controller.ts +188 -0
- package/src/adapter/metal-orm/index.ts +16 -0
- package/src/adapter/metal-orm/list.ts +168 -0
- package/src/adapter/metal-orm/types.ts +149 -1
- package/tests/metal-orm-integration/run-paged-list.test.ts +169 -0
- package/tests/unit/crud-controller-factory.test.ts +222 -0
- package/tests/unit/run-paged-list.test.ts +109 -0
package/README.md
CHANGED
|
@@ -398,13 +398,10 @@ import {
|
|
|
398
398
|
Body,
|
|
399
399
|
Query,
|
|
400
400
|
Returns,
|
|
401
|
-
|
|
402
|
-
parsePagination,
|
|
403
|
-
parseSort,
|
|
401
|
+
runPagedList,
|
|
404
402
|
t,
|
|
405
403
|
type RequestContext
|
|
406
404
|
} from "adorn-api";
|
|
407
|
-
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
408
405
|
import { createSession } from "./db";
|
|
409
406
|
import { User } from "./user.entity";
|
|
410
407
|
import {
|
|
@@ -428,24 +425,18 @@ export class UserController {
|
|
|
428
425
|
@Query(UserQueryDto)
|
|
429
426
|
@Returns(UserPagedResponseDto)
|
|
430
427
|
async list(ctx: RequestContext<unknown, UserQueryDto>) {
|
|
431
|
-
const query = (ctx.query ?? {}) as Record<string, unknown>;
|
|
432
|
-
const { page, pageSize } = parsePagination(query);
|
|
433
|
-
const filters = parseFilter(query, USER_FILTER_MAPPINGS);
|
|
434
|
-
const sort = parseSort(query, USER_SORTABLE_COLUMNS, {
|
|
435
|
-
defaultSortBy: "id"
|
|
436
|
-
});
|
|
437
|
-
const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
|
|
438
428
|
const session = createSession();
|
|
439
|
-
|
|
429
|
+
|
|
440
430
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
User,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
431
|
+
return await runPagedList({
|
|
432
|
+
query: (ctx.query ?? {}) as Record<string, unknown>,
|
|
433
|
+
target: User,
|
|
434
|
+
qb: () => User.select(),
|
|
435
|
+
session,
|
|
436
|
+
filterMappings: USER_FILTER_MAPPINGS,
|
|
437
|
+
sortableColumns: USER_SORTABLE_COLUMNS,
|
|
438
|
+
defaultSortBy: "id"
|
|
439
|
+
});
|
|
449
440
|
} finally {
|
|
450
441
|
await session.dispose();
|
|
451
442
|
}
|
|
@@ -491,6 +482,80 @@ export class UserController {
|
|
|
491
482
|
|
|
492
483
|
### Migration Guide (Breaking)
|
|
493
484
|
|
|
485
|
+
### CRUD Controller Factory (`createCrudController`)
|
|
486
|
+
|
|
487
|
+
When your controller only wires DTOs + service calls, you can generate the full CRUD controller and remove decorator boilerplate.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// user.controller.ts
|
|
491
|
+
import { createCrudController } from "adorn-api";
|
|
492
|
+
import { userCrudDtos } from "./user.dtos";
|
|
493
|
+
import { UserCrudService } from "./user.service";
|
|
494
|
+
|
|
495
|
+
export const UserController = createCrudController({
|
|
496
|
+
path: "/users",
|
|
497
|
+
service: UserCrudService, // class or instance
|
|
498
|
+
dtos: userCrudDtos, // result of createMetalCrudDtoClasses(...)
|
|
499
|
+
entityName: "User", // used by parseIdOrThrow messages
|
|
500
|
+
withOptionsRoute: true,
|
|
501
|
+
withReplace: true,
|
|
502
|
+
withPatch: true,
|
|
503
|
+
withDelete: true
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Generated routes:
|
|
508
|
+
- `GET /`
|
|
509
|
+
- `GET /options` (optional)
|
|
510
|
+
- `GET /:id`
|
|
511
|
+
- `POST /`
|
|
512
|
+
- `PUT /:id` (optional)
|
|
513
|
+
- `PATCH /:id` (optional)
|
|
514
|
+
- `DELETE /:id` (optional)
|
|
515
|
+
|
|
516
|
+
The factory applies the correct `@Query/@Body/@Params/@Returns` schemas and also propagates `dtos.errors` to all `/:id` routes.
|
|
517
|
+
|
|
518
|
+
Before (manual, repeated decorators/status/schema wiring):
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
@Controller("/users")
|
|
522
|
+
class UserController {
|
|
523
|
+
@Get("/")
|
|
524
|
+
@Query(UserQueryDto)
|
|
525
|
+
@Returns(UserPagedResponseDto)
|
|
526
|
+
async list(ctx: RequestContext<unknown, UserQueryDto>) { ... }
|
|
527
|
+
|
|
528
|
+
@Get("/:id")
|
|
529
|
+
@Params(UserParamsDto)
|
|
530
|
+
@Returns(UserDto)
|
|
531
|
+
@UserErrors
|
|
532
|
+
async getById(ctx: RequestContext<unknown, undefined, UserParamsDto>) { ... }
|
|
533
|
+
|
|
534
|
+
@Post("/")
|
|
535
|
+
@Body(CreateUserDto)
|
|
536
|
+
@Returns({ status: 201, schema: UserDto })
|
|
537
|
+
async create(ctx: RequestContext<CreateUserDto>) { ... }
|
|
538
|
+
|
|
539
|
+
// put/patch/delete/options...
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
After (factory + service):
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
export const UserController = createCrudController({
|
|
547
|
+
path: "/users",
|
|
548
|
+
service: new UserCrudService(),
|
|
549
|
+
dtos: userCrudDtos,
|
|
550
|
+
entityName: "User"
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
When to use factory vs manual controller:
|
|
555
|
+
- Use `createCrudController` when routes follow standard CRUD and behavior lives in a service.
|
|
556
|
+
- 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).
|
|
557
|
+
- For extra endpoints, keep the generated CRUD controller and add a second manual controller for custom routes on the same base path.
|
|
558
|
+
|
|
494
559
|
Before (duplicated config):
|
|
495
560
|
|
|
496
561
|
```typescript
|
|
@@ -539,10 +604,9 @@ Breaking changes summary:
|
|
|
539
604
|
// user.controller.ts — using listConfig directly
|
|
540
605
|
import {
|
|
541
606
|
Controller, Get, Query, Returns,
|
|
542
|
-
|
|
607
|
+
runPagedList,
|
|
543
608
|
type RequestContext
|
|
544
609
|
} from "adorn-api";
|
|
545
|
-
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
546
610
|
import { createSession } from "./db";
|
|
547
611
|
import { User } from "./user.entity";
|
|
548
612
|
import {
|
|
@@ -557,24 +621,15 @@ export class UserController {
|
|
|
557
621
|
@Query(UserQueryDto)
|
|
558
622
|
@Returns(UserPagedResponseDto)
|
|
559
623
|
async list(ctx: RequestContext<unknown, UserQueryDto>) {
|
|
560
|
-
const query = (ctx.query ?? {}) as Record<string, unknown>;
|
|
561
|
-
const { page, pageSize } = parsePagination(query, USER_LIST_CONFIG);
|
|
562
|
-
const filters = parseFilter(query, USER_LIST_CONFIG.filterMappings);
|
|
563
|
-
const sort = parseSort(query, USER_LIST_CONFIG.sortableColumns, {
|
|
564
|
-
defaultSortBy: USER_LIST_CONFIG.defaultSortBy,
|
|
565
|
-
defaultSortDirection: USER_LIST_CONFIG.defaultSortDirection
|
|
566
|
-
});
|
|
567
|
-
const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
|
|
568
|
-
|
|
569
624
|
const session = createSession();
|
|
570
625
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
User,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
626
|
+
return await runPagedList({
|
|
627
|
+
query: (ctx.query ?? {}) as Record<string, unknown>,
|
|
628
|
+
target: User,
|
|
629
|
+
qb: () => User.select(),
|
|
630
|
+
session,
|
|
631
|
+
...USER_LIST_CONFIG
|
|
632
|
+
});
|
|
578
633
|
} finally {
|
|
579
634
|
await session.dispose();
|
|
580
635
|
}
|
|
@@ -584,6 +639,51 @@ export class UserController {
|
|
|
584
639
|
|
|
585
640
|
The `listConfig` object contains: `filterMappings`, `sortableColumns`, `defaultSortBy`, `defaultSortDirection`, `defaultPageSize`, `maxPageSize`, `sortByKey`, and `sortDirectionKey`.
|
|
586
641
|
|
|
642
|
+
### `BaseService.list` Before/After (Boilerplate Reduction)
|
|
643
|
+
|
|
644
|
+
Before:
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
async list(query: Record<string, unknown>) {
|
|
648
|
+
const { page, pageSize } = parsePagination(query, this.listConfig);
|
|
649
|
+
const filters = parseFilter(query, this.listConfig.filterMappings);
|
|
650
|
+
const sort = parseSort(query, this.listConfig.sortableColumns, {
|
|
651
|
+
defaultSortBy: this.listConfig.defaultSortBy,
|
|
652
|
+
defaultSortDirection: this.listConfig.defaultSortDirection,
|
|
653
|
+
sortByKey: this.listConfig.sortByKey,
|
|
654
|
+
sortDirectionKey: this.listConfig.sortDirectionKey
|
|
655
|
+
});
|
|
656
|
+
const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
|
|
657
|
+
|
|
658
|
+
return withSession(this.createSession, async (session) => {
|
|
659
|
+
const qb = applyFilter(this.baseQuery().orderBy(this.ref.id, direction), this.entity, filters);
|
|
660
|
+
const paged = await qb.executePaged(session, { page, pageSize });
|
|
661
|
+
return toPagedResponse(paged);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
After:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
async list(query: Record<string, unknown>) {
|
|
670
|
+
return withSession(this.createSession, async (session) =>
|
|
671
|
+
runPagedList({
|
|
672
|
+
query,
|
|
673
|
+
target: this.entity,
|
|
674
|
+
qb: () => this.baseQuery(),
|
|
675
|
+
session,
|
|
676
|
+
...this.listConfig
|
|
677
|
+
})
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Migration note:
|
|
683
|
+
- Existing `parsePagination`, `parseFilter`, and `parseSort` remain unchanged and can still be used manually.
|
|
684
|
+
- `runPagedList`/`executeCrudList` is additive and optional; no breaking API changes.
|
|
685
|
+
- For sortable fields that are not direct columns of the base table, pass `allowedSortColumns` with explicit metal-orm sort terms.
|
|
686
|
+
|
|
587
687
|
### Sort Order Compatibility (`sortOrder` / `sortDirection`)
|
|
588
688
|
|
|
589
689
|
`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.
|
|
@@ -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
|
+
}
|
|
@@ -2,11 +2,13 @@ export { MetalDto } from "./dto";
|
|
|
2
2
|
export { parsePagination } from "./pagination";
|
|
3
3
|
export { parseFilter, createFilterMappings } from "./filters";
|
|
4
4
|
export { parseSort } from "./sort";
|
|
5
|
+
export { runPagedList, executeCrudList } from "./list";
|
|
5
6
|
export { createPagedQueryDtoClass, createPagedResponseDtoClass, createPagedFilterQueryDtoClass } from "./paged-dtos";
|
|
6
7
|
export { createMetalCrudDtos, createMetalCrudDtoClasses, createNestedCreateDtoClass } from "./crud-dtos";
|
|
8
|
+
export { createCrudController } from "./crud-controller";
|
|
7
9
|
export { createMetalTreeDtoClasses } from "./tree-dtos";
|
|
8
10
|
export { createMetalDtoOverrides, type CreateMetalDtoOverridesOptions } from "./convention-overrides";
|
|
9
11
|
export { createErrorDtoClass, StandardErrorDto, SimpleErrorDto, BasicErrorDto } from "./error-dtos";
|
|
10
12
|
export { withSession, parseIdOrThrow, compactUpdates, applyInput, getEntityOrThrow } from "./utils";
|
|
11
13
|
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, ListConfig, 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";
|
|
14
|
+
export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, ParseSortOptions, ParsedSort, SortDirection, CrudListSortTerm, RunPagedListOptions, ExecuteCrudListOptions, CrudPagedResponse, 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.executeCrudList = exports.runPagedList = 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) {
|
|
@@ -15,6 +15,9 @@ Object.defineProperty(exports, "parseFilter", { enumerable: true, get: function
|
|
|
15
15
|
Object.defineProperty(exports, "createFilterMappings", { enumerable: true, get: function () { return filters_1.createFilterMappings; } });
|
|
16
16
|
var sort_1 = require("./sort");
|
|
17
17
|
Object.defineProperty(exports, "parseSort", { enumerable: true, get: function () { return sort_1.parseSort; } });
|
|
18
|
+
var list_1 = require("./list");
|
|
19
|
+
Object.defineProperty(exports, "runPagedList", { enumerable: true, get: function () { return list_1.runPagedList; } });
|
|
20
|
+
Object.defineProperty(exports, "executeCrudList", { enumerable: true, get: function () { return list_1.executeCrudList; } });
|
|
18
21
|
var paged_dtos_1 = require("./paged-dtos");
|
|
19
22
|
Object.defineProperty(exports, "createPagedQueryDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedQueryDtoClass; } });
|
|
20
23
|
Object.defineProperty(exports, "createPagedResponseDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedResponseDtoClass; } });
|
|
@@ -23,6 +26,8 @@ var crud_dtos_1 = require("./crud-dtos");
|
|
|
23
26
|
Object.defineProperty(exports, "createMetalCrudDtos", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtos; } });
|
|
24
27
|
Object.defineProperty(exports, "createMetalCrudDtoClasses", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtoClasses; } });
|
|
25
28
|
Object.defineProperty(exports, "createNestedCreateDtoClass", { enumerable: true, get: function () { return crud_dtos_1.createNestedCreateDtoClass; } });
|
|
29
|
+
var crud_controller_1 = require("./crud-controller");
|
|
30
|
+
Object.defineProperty(exports, "createCrudController", { enumerable: true, get: function () { return crud_controller_1.createCrudController; } });
|
|
26
31
|
var tree_dtos_1 = require("./tree-dtos");
|
|
27
32
|
Object.defineProperty(exports, "createMetalTreeDtoClasses", { enumerable: true, get: function () { return tree_dtos_1.createMetalTreeDtoClasses; } });
|
|
28
33
|
var convention_overrides_1 = require("./convention-overrides");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PagedResponse, type TableDef } from "metal-orm";
|
|
2
|
+
import type { ExecuteCrudListOptions, RunPagedListOptions } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Runs a unified filtered/sorted/paginated list query for metal-orm.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runPagedList<TResult, TTable extends TableDef = TableDef, TTarget = unknown, TFilterTarget = Record<string, unknown>>(options: RunPagedListOptions<TResult, TTable, TTarget, TFilterTarget>): Promise<PagedResponse<TResult>>;
|
|
7
|
+
/**
|
|
8
|
+
* Alias for runPagedList.
|
|
9
|
+
*/
|
|
10
|
+
export declare const executeCrudList: typeof runPagedList;
|
|
11
|
+
export type { ExecuteCrudListOptions, RunPagedListOptions };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeCrudList = void 0;
|
|
4
|
+
exports.runPagedList = runPagedList;
|
|
5
|
+
const metal_orm_1 = require("metal-orm");
|
|
6
|
+
const filters_1 = require("./filters");
|
|
7
|
+
const pagination_1 = require("./pagination");
|
|
8
|
+
const sort_1 = require("./sort");
|
|
9
|
+
/**
|
|
10
|
+
* Runs a unified filtered/sorted/paginated list query for metal-orm.
|
|
11
|
+
*/
|
|
12
|
+
async function runPagedList(options) {
|
|
13
|
+
const query = options.query ?? {};
|
|
14
|
+
const qb = resolveQueryBuilder(options.qb);
|
|
15
|
+
const { page, pageSize } = (0, pagination_1.parsePagination)(query, {
|
|
16
|
+
defaultPageSize: options.defaultPageSize,
|
|
17
|
+
maxPageSize: options.maxPageSize
|
|
18
|
+
});
|
|
19
|
+
const filters = (0, filters_1.parseFilter)(query, options.filterMappings);
|
|
20
|
+
const parsedSort = (0, sort_1.parseSort)(query, options.sortableColumns, {
|
|
21
|
+
defaultSortBy: options.defaultSortBy,
|
|
22
|
+
defaultSortDirection: options.defaultSortDirection,
|
|
23
|
+
sortByKey: options.sortByKey,
|
|
24
|
+
sortDirectionKey: options.sortDirectionKey,
|
|
25
|
+
sortOrderKey: options.sortOrderKey
|
|
26
|
+
});
|
|
27
|
+
const inferredSortColumns = inferAllowedSortColumns(qb.getTable(), options.sortableColumns);
|
|
28
|
+
const allowedSortColumns = {
|
|
29
|
+
...inferredSortColumns,
|
|
30
|
+
...(options.allowedSortColumns ?? {})
|
|
31
|
+
};
|
|
32
|
+
const sortBy = parsedSort?.sortBy && parsedSort.sortBy in allowedSortColumns
|
|
33
|
+
? parsedSort.sortBy
|
|
34
|
+
: undefined;
|
|
35
|
+
const defaultSortBy = !sortBy && options.defaultSortBy && options.defaultSortBy in allowedSortColumns
|
|
36
|
+
? options.defaultSortBy
|
|
37
|
+
: undefined;
|
|
38
|
+
return (0, metal_orm_1.executeFilteredPaged)({
|
|
39
|
+
qb,
|
|
40
|
+
tableOrEntity: options.target,
|
|
41
|
+
session: options.session,
|
|
42
|
+
page,
|
|
43
|
+
pageSize,
|
|
44
|
+
filters: filters,
|
|
45
|
+
sortBy,
|
|
46
|
+
sortDirection: toOrderDirection(parsedSort?.sortDirection),
|
|
47
|
+
allowedSortColumns: Object.keys(allowedSortColumns).length
|
|
48
|
+
? allowedSortColumns
|
|
49
|
+
: undefined,
|
|
50
|
+
defaultSortBy,
|
|
51
|
+
defaultSortDirection: toOrderDirection(options.defaultSortDirection),
|
|
52
|
+
tieBreakerColumn: options.tieBreakerColumn
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Alias for runPagedList.
|
|
57
|
+
*/
|
|
58
|
+
exports.executeCrudList = runPagedList;
|
|
59
|
+
function resolveQueryBuilder(qbOrFactory) {
|
|
60
|
+
return typeof qbOrFactory === "function" ? qbOrFactory() : qbOrFactory;
|
|
61
|
+
}
|
|
62
|
+
function inferAllowedSortColumns(table, sortableColumns) {
|
|
63
|
+
const output = {};
|
|
64
|
+
for (const [queryKey, field] of Object.entries(sortableColumns)) {
|
|
65
|
+
const columnName = toSortableColumnName(field);
|
|
66
|
+
if (!columnName) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const column = resolveTableColumn(table, columnName);
|
|
70
|
+
if (!column) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
output[queryKey] = column;
|
|
74
|
+
}
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
function toSortableColumnName(value) {
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
if (value.length !== 1) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const segment = String(value[0]).trim();
|
|
83
|
+
return segment.length > 0 ? segment : undefined;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value !== "string") {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const path = value
|
|
89
|
+
.split(".")
|
|
90
|
+
.map((segment) => segment.trim())
|
|
91
|
+
.filter((segment) => segment.length > 0);
|
|
92
|
+
if (path.length !== 1) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return path[0];
|
|
96
|
+
}
|
|
97
|
+
function resolveTableColumn(table, name) {
|
|
98
|
+
const byKey = table.columns[name];
|
|
99
|
+
if (byKey) {
|
|
100
|
+
return byKey;
|
|
101
|
+
}
|
|
102
|
+
return Object.values(table.columns).find((column) => column.name === name);
|
|
103
|
+
}
|
|
104
|
+
function toOrderDirection(direction) {
|
|
105
|
+
if (direction === "desc") {
|
|
106
|
+
return "DESC";
|
|
107
|
+
}
|
|
108
|
+
if (direction === "asc") {
|
|
109
|
+
return "ASC";
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|