adorn-api 1.1.5 → 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 +74 -0
- 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 +2 -1
- package/dist/adapter/metal-orm/index.js +3 -1
- package/dist/adapter/metal-orm/types.d.ts +44 -0
- package/package.json +1 -1
- package/src/adapter/metal-orm/crud-controller.ts +188 -0
- package/src/adapter/metal-orm/index.ts +7 -0
- package/src/adapter/metal-orm/types.ts +81 -0
- package/tests/unit/crud-controller-factory.test.ts +222 -0
package/README.md
CHANGED
|
@@ -491,6 +491,80 @@ export class UserController {
|
|
|
491
491
|
|
|
492
492
|
### Migration Guide (Breaking)
|
|
493
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
|
+
|
|
494
568
|
Before (duplicated config):
|
|
495
569
|
|
|
496
570
|
```typescript
|
|
@@ -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
|
+
}
|
|
@@ -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, 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";
|
|
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");
|
|
@@ -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
|
*/
|
|
@@ -392,6 +393,49 @@ export interface MetalCrudDtoClasses<T = Record<string, unknown>> {
|
|
|
392
393
|
/** Ready-to-use config for list/query endpoints (combines filters, sort, pagination defaults) */
|
|
393
394
|
listConfig: ListConfig<T>;
|
|
394
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[];
|
|
438
|
+
}
|
|
395
439
|
/**
|
|
396
440
|
* Metal Tree DTO class names.
|
|
397
441
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
Get,
|
|
6
|
+
Params,
|
|
7
|
+
Patch,
|
|
8
|
+
Post,
|
|
9
|
+
Put,
|
|
10
|
+
Query,
|
|
11
|
+
Returns
|
|
12
|
+
} from "../../core/decorators";
|
|
13
|
+
import type { DtoConstructor } from "../../core/types";
|
|
14
|
+
import type { RequestContext } from "../express/types";
|
|
15
|
+
import type {
|
|
16
|
+
CreateCrudControllerOptions,
|
|
17
|
+
CrudControllerService,
|
|
18
|
+
CrudControllerServiceInput,
|
|
19
|
+
MetalCrudDtoClasses,
|
|
20
|
+
RouteErrorsDecorator
|
|
21
|
+
} from "./types";
|
|
22
|
+
import { parseIdOrThrow } from "./utils";
|
|
23
|
+
|
|
24
|
+
type DtoInstance<TDto extends DtoConstructor> = InstanceType<TDto>;
|
|
25
|
+
type MethodDecorator = (
|
|
26
|
+
value: unknown,
|
|
27
|
+
context: ClassMethodDecoratorContext
|
|
28
|
+
) => void;
|
|
29
|
+
|
|
30
|
+
export function createCrudController<TDtos extends MetalCrudDtoClasses<any>>(
|
|
31
|
+
options: CreateCrudControllerOptions<TDtos>
|
|
32
|
+
) {
|
|
33
|
+
const withOptionsRoute = options.withOptionsRoute ?? true;
|
|
34
|
+
const withReplace = options.withReplace ?? true;
|
|
35
|
+
const withPatch = options.withPatch ?? true;
|
|
36
|
+
const withDelete = options.withDelete ?? true;
|
|
37
|
+
const service = resolveService(options.service);
|
|
38
|
+
const routeErrorsDecorator = options.dtos.errors;
|
|
39
|
+
|
|
40
|
+
@Controller({ path: options.path, tags: options.tags })
|
|
41
|
+
class GeneratedCrudController {
|
|
42
|
+
@Get("/")
|
|
43
|
+
@Query(options.dtos.queryDto)
|
|
44
|
+
@Returns(options.dtos.pagedResponseDto)
|
|
45
|
+
async list(
|
|
46
|
+
ctx: RequestContext<unknown, DtoInstance<TDtos["queryDto"]>>
|
|
47
|
+
): Promise<DtoInstance<TDtos["pagedResponseDto"]>> {
|
|
48
|
+
return await service.list(ctx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@when(withOptionsRoute, Get("/options"))
|
|
52
|
+
@when(withOptionsRoute, Query(options.dtos.optionsQueryDto))
|
|
53
|
+
@when(withOptionsRoute, Returns(options.dtos.optionsDto))
|
|
54
|
+
async options(
|
|
55
|
+
ctx: RequestContext<unknown, DtoInstance<TDtos["optionsQueryDto"]>>
|
|
56
|
+
): Promise<DtoInstance<TDtos["optionsDto"]>> {
|
|
57
|
+
assertServiceMethod(service, "options");
|
|
58
|
+
return await service.options(ctx);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Get("/:id")
|
|
62
|
+
@Params(options.dtos.params)
|
|
63
|
+
@Returns(options.dtos.response)
|
|
64
|
+
@applyRouteErrors(routeErrorsDecorator)
|
|
65
|
+
async getById(
|
|
66
|
+
ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>
|
|
67
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
68
|
+
const id = parseContextId(ctx, options.entityName);
|
|
69
|
+
return await service.getById(id, ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Post("/")
|
|
73
|
+
@Body(options.dtos.create)
|
|
74
|
+
@Returns({ status: 201, schema: options.dtos.response })
|
|
75
|
+
async create(
|
|
76
|
+
ctx: RequestContext<DtoInstance<TDtos["create"]>>
|
|
77
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
78
|
+
return await service.create(ctx.body, ctx);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@when(withReplace, Put("/:id"))
|
|
82
|
+
@when(withReplace, Params(options.dtos.params))
|
|
83
|
+
@when(withReplace, Body(options.dtos.replace))
|
|
84
|
+
@when(withReplace, Returns(options.dtos.response))
|
|
85
|
+
@when(withReplace, applyRouteErrors(routeErrorsDecorator))
|
|
86
|
+
async replace(
|
|
87
|
+
ctx: RequestContext<
|
|
88
|
+
DtoInstance<TDtos["replace"]>,
|
|
89
|
+
undefined,
|
|
90
|
+
DtoInstance<TDtos["params"]>
|
|
91
|
+
>
|
|
92
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
93
|
+
assertServiceMethod(service, "replace");
|
|
94
|
+
const id = parseContextId(ctx, options.entityName);
|
|
95
|
+
return await service.replace(id, ctx.body, ctx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@when(withPatch, Patch("/:id"))
|
|
99
|
+
@when(withPatch, Params(options.dtos.params))
|
|
100
|
+
@when(withPatch, Body(options.dtos.update))
|
|
101
|
+
@when(withPatch, Returns(options.dtos.response))
|
|
102
|
+
@when(withPatch, applyRouteErrors(routeErrorsDecorator))
|
|
103
|
+
async update(
|
|
104
|
+
ctx: RequestContext<
|
|
105
|
+
DtoInstance<TDtos["update"]>,
|
|
106
|
+
undefined,
|
|
107
|
+
DtoInstance<TDtos["params"]>
|
|
108
|
+
>
|
|
109
|
+
): Promise<DtoInstance<TDtos["response"]>> {
|
|
110
|
+
assertServiceMethod(service, "update");
|
|
111
|
+
const id = parseContextId(ctx, options.entityName);
|
|
112
|
+
return await service.update(id, ctx.body, ctx);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@when(withDelete, Delete("/:id"))
|
|
116
|
+
@when(withDelete, Params(options.dtos.params))
|
|
117
|
+
@when(withDelete, Returns({ status: 204, description: "No Content" }))
|
|
118
|
+
@when(withDelete, applyRouteErrors(routeErrorsDecorator))
|
|
119
|
+
async delete(
|
|
120
|
+
ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
assertServiceMethod(service, "delete");
|
|
123
|
+
const id = parseContextId(ctx, options.entityName);
|
|
124
|
+
await service.delete(id, ctx);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Object.defineProperty(GeneratedCrudController, "name", {
|
|
129
|
+
value: `${options.entityName}CrudController`,
|
|
130
|
+
configurable: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return GeneratedCrudController;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveService<TDtos extends MetalCrudDtoClasses<any>>(
|
|
137
|
+
input: CrudControllerServiceInput<TDtos>
|
|
138
|
+
): CrudControllerService<TDtos> {
|
|
139
|
+
if (typeof input === "function") {
|
|
140
|
+
return new input();
|
|
141
|
+
}
|
|
142
|
+
return input;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function when(enabled: boolean, decorator: MethodDecorator): MethodDecorator {
|
|
146
|
+
return (value, context) => {
|
|
147
|
+
if (enabled) {
|
|
148
|
+
decorator(value, context);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyRouteErrors(
|
|
154
|
+
decorator: RouteErrorsDecorator | undefined
|
|
155
|
+
): MethodDecorator {
|
|
156
|
+
return (value, context) => {
|
|
157
|
+
decorator?.(value, context);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseContextId(
|
|
162
|
+
ctx: RequestContext<unknown, undefined, object | undefined>,
|
|
163
|
+
entityName: string
|
|
164
|
+
): number {
|
|
165
|
+
const params = (ctx.params ?? {}) as Record<string, unknown>;
|
|
166
|
+
return parseIdOrThrow(toIdValue(params.id), entityName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function toIdValue(value: unknown): string | number {
|
|
170
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertServiceMethod<
|
|
177
|
+
TDtos extends MetalCrudDtoClasses<any>,
|
|
178
|
+
TMethod extends "options" | "replace" | "update" | "delete"
|
|
179
|
+
>(
|
|
180
|
+
service: CrudControllerService<TDtos>,
|
|
181
|
+
method: TMethod
|
|
182
|
+
): asserts service is CrudControllerService<TDtos> & Record<TMethod, NonNullable<CrudControllerService<TDtos>[TMethod]>> {
|
|
183
|
+
if (typeof service[method] !== "function") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`CRUD service is missing "${method}" method required by enabled route.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -33,6 +33,10 @@ export {
|
|
|
33
33
|
createNestedCreateDtoClass
|
|
34
34
|
} from "./crud-dtos";
|
|
35
35
|
|
|
36
|
+
export {
|
|
37
|
+
createCrudController
|
|
38
|
+
} from "./crud-controller";
|
|
39
|
+
|
|
36
40
|
export {
|
|
37
41
|
createMetalTreeDtoClasses
|
|
38
42
|
} from "./tree-dtos";
|
|
@@ -96,6 +100,9 @@ export type {
|
|
|
96
100
|
MetalCrudDtoClasses,
|
|
97
101
|
MetalCrudDtoClassNameKey,
|
|
98
102
|
MetalCrudDtoClassNames,
|
|
103
|
+
CrudControllerService,
|
|
104
|
+
CrudControllerServiceInput,
|
|
105
|
+
CreateCrudControllerOptions,
|
|
99
106
|
RouteErrorsDecorator,
|
|
100
107
|
NestedCreateDtoOptions,
|
|
101
108
|
MetalTreeDtoClassOptions,
|
|
@@ -3,6 +3,7 @@ import type { BelongsToReference, HasManyCollection, HasOneReference, ManyToMany
|
|
|
3
3
|
import type { DtoOptions, ErrorResponseOptions, FieldOverride } from "../../core/decorators";
|
|
4
4
|
import type { SchemaNode } from "../../core/schema";
|
|
5
5
|
import type { DtoConstructor } from "../../core/types";
|
|
6
|
+
import type { RequestContext } from "../express/types";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Metal ORM DTO modes.
|
|
@@ -475,6 +476,86 @@ export interface MetalCrudDtoClasses<T = Record<string, unknown>> {
|
|
|
475
476
|
listConfig: ListConfig<T>;
|
|
476
477
|
}
|
|
477
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Awaitable helper for CRUD service methods.
|
|
481
|
+
*/
|
|
482
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Input for CRUD controller service: class or ready instance.
|
|
486
|
+
*/
|
|
487
|
+
export type CrudControllerServiceInput<TDtos extends MetalCrudDtoClasses<any>> =
|
|
488
|
+
CrudControllerService<TDtos>
|
|
489
|
+
| (new () => CrudControllerService<TDtos>);
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* CRUD controller service contract used by createCrudController.
|
|
493
|
+
*/
|
|
494
|
+
export interface CrudControllerService<TDtos extends MetalCrudDtoClasses<any>> {
|
|
495
|
+
list(
|
|
496
|
+
ctx: RequestContext<unknown, InstanceType<TDtos["queryDto"]>>
|
|
497
|
+
): Awaitable<InstanceType<TDtos["pagedResponseDto"]>>;
|
|
498
|
+
options?(
|
|
499
|
+
ctx: RequestContext<unknown, InstanceType<TDtos["optionsQueryDto"]>>
|
|
500
|
+
): Awaitable<InstanceType<TDtos["optionsDto"]>>;
|
|
501
|
+
getById(
|
|
502
|
+
id: number,
|
|
503
|
+
ctx: RequestContext<unknown, undefined, InstanceType<TDtos["params"]>>
|
|
504
|
+
): Awaitable<InstanceType<TDtos["response"]>>;
|
|
505
|
+
create(
|
|
506
|
+
body: InstanceType<TDtos["create"]>,
|
|
507
|
+
ctx: RequestContext<InstanceType<TDtos["create"]>>
|
|
508
|
+
): Awaitable<InstanceType<TDtos["response"]>>;
|
|
509
|
+
replace?(
|
|
510
|
+
id: number,
|
|
511
|
+
body: InstanceType<TDtos["replace"]>,
|
|
512
|
+
ctx: RequestContext<
|
|
513
|
+
InstanceType<TDtos["replace"]>,
|
|
514
|
+
undefined,
|
|
515
|
+
InstanceType<TDtos["params"]>
|
|
516
|
+
>
|
|
517
|
+
): Awaitable<InstanceType<TDtos["response"]>>;
|
|
518
|
+
update?(
|
|
519
|
+
id: number,
|
|
520
|
+
body: InstanceType<TDtos["update"]>,
|
|
521
|
+
ctx: RequestContext<
|
|
522
|
+
InstanceType<TDtos["update"]>,
|
|
523
|
+
undefined,
|
|
524
|
+
InstanceType<TDtos["params"]>
|
|
525
|
+
>
|
|
526
|
+
): Awaitable<InstanceType<TDtos["response"]>>;
|
|
527
|
+
delete?(
|
|
528
|
+
id: number,
|
|
529
|
+
ctx: RequestContext<unknown, undefined, InstanceType<TDtos["params"]>>
|
|
530
|
+
): Awaitable<void>;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* createCrudController options.
|
|
535
|
+
*/
|
|
536
|
+
export interface CreateCrudControllerOptions<
|
|
537
|
+
TDtos extends MetalCrudDtoClasses<any>
|
|
538
|
+
> {
|
|
539
|
+
/** Controller path. */
|
|
540
|
+
path: string;
|
|
541
|
+
/** Service instance or class (new () => service). */
|
|
542
|
+
service: CrudControllerServiceInput<TDtos>;
|
|
543
|
+
/** DTO bundle produced by createMetalCrudDtoClasses. */
|
|
544
|
+
dtos: TDtos;
|
|
545
|
+
/** Entity label used by parseIdOrThrow messages. */
|
|
546
|
+
entityName: string;
|
|
547
|
+
/** Generate GET /options route (default: true). */
|
|
548
|
+
withOptionsRoute?: boolean;
|
|
549
|
+
/** Generate PUT /:id route (default: true). */
|
|
550
|
+
withReplace?: boolean;
|
|
551
|
+
/** Generate PATCH /:id route (default: true). */
|
|
552
|
+
withPatch?: boolean;
|
|
553
|
+
/** Generate DELETE /:id route (default: true). */
|
|
554
|
+
withDelete?: boolean;
|
|
555
|
+
/** Optional OpenAPI tags for generated controller. */
|
|
556
|
+
tags?: string[];
|
|
557
|
+
}
|
|
558
|
+
|
|
478
559
|
/**
|
|
479
560
|
* Metal Tree DTO class names.
|
|
480
561
|
*/
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildOpenApi,
|
|
4
|
+
createCrudController,
|
|
5
|
+
createMetalCrudDtoClasses,
|
|
6
|
+
t,
|
|
7
|
+
type RequestContext
|
|
8
|
+
} from "../../src/index";
|
|
9
|
+
import { getControllerMeta } from "../../src/core/metadata";
|
|
10
|
+
import { Column, Entity, PrimaryKey, col } from "metal-orm";
|
|
11
|
+
|
|
12
|
+
@Entity({ tableName: "crud_factory_entities" })
|
|
13
|
+
class CrudFactoryEntity {
|
|
14
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
15
|
+
id!: number;
|
|
16
|
+
|
|
17
|
+
@Column(col.notNull(col.text()))
|
|
18
|
+
nome!: string;
|
|
19
|
+
|
|
20
|
+
@Column(col.notNull(col.boolean()))
|
|
21
|
+
ativo!: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const crudDtos = createMetalCrudDtoClasses(CrudFactoryEntity, {
|
|
25
|
+
mutationExclude: ["id"],
|
|
26
|
+
query: {
|
|
27
|
+
filters: {
|
|
28
|
+
nomeContains: {
|
|
29
|
+
schema: t.string({ minLength: 1 }),
|
|
30
|
+
field: "nome",
|
|
31
|
+
operator: "contains"
|
|
32
|
+
},
|
|
33
|
+
ativo: {
|
|
34
|
+
schema: t.boolean(),
|
|
35
|
+
field: "ativo",
|
|
36
|
+
operator: "equals"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
sortableColumns: {
|
|
40
|
+
id: "id",
|
|
41
|
+
nome: "nome"
|
|
42
|
+
},
|
|
43
|
+
options: {
|
|
44
|
+
labelField: "nome"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
errors: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
class CrudFactoryService {
|
|
51
|
+
async list(_ctx: RequestContext<unknown, InstanceType<typeof crudDtos.queryDto>>) {
|
|
52
|
+
return { items: [], total: 0, page: 1, pageSize: 25 } as InstanceType<typeof crudDtos.pagedResponseDto>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async options(_ctx: RequestContext<unknown, InstanceType<typeof crudDtos.optionsQueryDto>>) {
|
|
56
|
+
return { items: [], total: 0, page: 1, pageSize: 25 } as InstanceType<typeof crudDtos.optionsDto>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getById(
|
|
60
|
+
id: number,
|
|
61
|
+
_ctx: RequestContext<unknown, undefined, InstanceType<typeof crudDtos.params>>
|
|
62
|
+
) {
|
|
63
|
+
return { id, nome: "Teste", ativo: true } as InstanceType<typeof crudDtos.response>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async create(
|
|
67
|
+
body: InstanceType<typeof crudDtos.create>,
|
|
68
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.create>>
|
|
69
|
+
) {
|
|
70
|
+
return { id: 1, ...body } as InstanceType<typeof crudDtos.response>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async replace(
|
|
74
|
+
id: number,
|
|
75
|
+
body: InstanceType<typeof crudDtos.replace>,
|
|
76
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.replace>, undefined, InstanceType<typeof crudDtos.params>>
|
|
77
|
+
) {
|
|
78
|
+
return { id, ...body } as InstanceType<typeof crudDtos.response>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async update(
|
|
82
|
+
id: number,
|
|
83
|
+
body: InstanceType<typeof crudDtos.update>,
|
|
84
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.update>, undefined, InstanceType<typeof crudDtos.params>>
|
|
85
|
+
) {
|
|
86
|
+
return { id, ...body } as InstanceType<typeof crudDtos.response>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(
|
|
90
|
+
_id: number,
|
|
91
|
+
_ctx: RequestContext<unknown, undefined, InstanceType<typeof crudDtos.params>>
|
|
92
|
+
) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CrudFactoryController = createCrudController({
|
|
96
|
+
path: "/crud-factory",
|
|
97
|
+
service: CrudFactoryService,
|
|
98
|
+
dtos: crudDtos,
|
|
99
|
+
entityName: "CrudFactoryEntity"
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("createCrudController", () => {
|
|
103
|
+
it("registers CRUD routes with expected schemas and statuses", () => {
|
|
104
|
+
const meta = getControllerMeta(CrudFactoryController);
|
|
105
|
+
expect(meta).toBeDefined();
|
|
106
|
+
expect(meta?.basePath).toBe("/crud-factory");
|
|
107
|
+
|
|
108
|
+
const routeKeys = (meta?.routes ?? [])
|
|
109
|
+
.map((route) => `${route.httpMethod} ${route.path}`)
|
|
110
|
+
.sort();
|
|
111
|
+
|
|
112
|
+
expect(routeKeys).toEqual([
|
|
113
|
+
"delete /:id",
|
|
114
|
+
"get /",
|
|
115
|
+
"get /:id",
|
|
116
|
+
"get /options",
|
|
117
|
+
"patch /:id",
|
|
118
|
+
"post /",
|
|
119
|
+
"put /:id"
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const byKey = new Map(
|
|
123
|
+
(meta?.routes ?? []).map((route) => [`${route.httpMethod} ${route.path}`, route] as const)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const listRoute = byKey.get("get /");
|
|
127
|
+
expect(listRoute?.query?.schema).toBe(crudDtos.queryDto);
|
|
128
|
+
expect(listRoute?.responses).toEqual(
|
|
129
|
+
expect.arrayContaining([
|
|
130
|
+
expect.objectContaining({ status: 200, schema: crudDtos.pagedResponseDto })
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const optionsRoute = byKey.get("get /options");
|
|
135
|
+
expect(optionsRoute?.query?.schema).toBe(crudDtos.optionsQueryDto);
|
|
136
|
+
expect(optionsRoute?.responses).toEqual(
|
|
137
|
+
expect.arrayContaining([
|
|
138
|
+
expect.objectContaining({ status: 200, schema: crudDtos.optionsDto })
|
|
139
|
+
])
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const createRoute = byKey.get("post /");
|
|
143
|
+
expect(createRoute?.body?.schema).toBe(crudDtos.create);
|
|
144
|
+
expect(createRoute?.responses).toEqual(
|
|
145
|
+
expect.arrayContaining([
|
|
146
|
+
expect.objectContaining({ status: 201, schema: crudDtos.response })
|
|
147
|
+
])
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
for (const key of ["get /:id", "put /:id", "patch /:id", "delete /:id"]) {
|
|
151
|
+
const route = byKey.get(key);
|
|
152
|
+
expect(route?.params?.schema).toBe(crudDtos.params);
|
|
153
|
+
expect(route?.responses).toEqual(
|
|
154
|
+
expect.arrayContaining([
|
|
155
|
+
expect.objectContaining({ status: 400, error: true }),
|
|
156
|
+
expect.objectContaining({ status: 404, error: true })
|
|
157
|
+
])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
expect(byKey.get("put /:id")?.body?.schema).toBe(crudDtos.replace);
|
|
162
|
+
expect(byKey.get("patch /:id")?.body?.schema).toBe(crudDtos.update);
|
|
163
|
+
expect(byKey.get("delete /:id")?.responses).toEqual(
|
|
164
|
+
expect.arrayContaining([expect.objectContaining({ status: 204 })])
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("exposes matching schemas/status in OpenAPI", () => {
|
|
169
|
+
const doc = buildOpenApi({
|
|
170
|
+
info: { title: "Test API", version: "1.0.0" },
|
|
171
|
+
controllers: [CrudFactoryController]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const listResponses = (doc.paths["/crud-factory"]?.get as Record<string, any>)?.responses;
|
|
175
|
+
const createResponses = (doc.paths["/crud-factory"]?.post as Record<string, any>)?.responses;
|
|
176
|
+
const getByIdResponses = (doc.paths["/crud-factory/{id}"]?.get as Record<string, any>)?.responses;
|
|
177
|
+
const deleteResponses = (doc.paths["/crud-factory/{id}"]?.delete as Record<string, any>)?.responses;
|
|
178
|
+
|
|
179
|
+
expect(listResponses?.["200"]).toBeDefined();
|
|
180
|
+
expect(createResponses?.["201"]).toBeDefined();
|
|
181
|
+
expect(getByIdResponses?.["200"]).toBeDefined();
|
|
182
|
+
expect(getByIdResponses?.["400"]).toBeDefined();
|
|
183
|
+
expect(getByIdResponses?.["404"]).toBeDefined();
|
|
184
|
+
expect(deleteResponses?.["204"]).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("supports disabling optional routes with flags", () => {
|
|
188
|
+
const MinimalController = createCrudController({
|
|
189
|
+
path: "/crud-factory-minimal",
|
|
190
|
+
service: new CrudFactoryService(),
|
|
191
|
+
dtos: crudDtos,
|
|
192
|
+
entityName: "CrudFactoryEntity",
|
|
193
|
+
withOptionsRoute: false,
|
|
194
|
+
withReplace: false,
|
|
195
|
+
withPatch: false,
|
|
196
|
+
withDelete: false
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const meta = getControllerMeta(MinimalController);
|
|
200
|
+
const routeKeys = (meta?.routes ?? [])
|
|
201
|
+
.map((route) => `${route.httpMethod} ${route.path}`)
|
|
202
|
+
.sort();
|
|
203
|
+
|
|
204
|
+
expect(routeKeys).toEqual([
|
|
205
|
+
"get /",
|
|
206
|
+
"get /:id",
|
|
207
|
+
"post /"
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("uses entityName in parseIdOrThrow messages", async () => {
|
|
212
|
+
const controller = new CrudFactoryController();
|
|
213
|
+
await expect(
|
|
214
|
+
controller.getById({
|
|
215
|
+
params: { id: "invalid" }
|
|
216
|
+
} as any)
|
|
217
|
+
).rejects.toMatchObject({
|
|
218
|
+
status: 400,
|
|
219
|
+
message: "Invalid CrudFactoryEntity id."
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|