From 977d35da9a71cd71dc2eeeb3757a49ced5d5eea6 Mon Sep 17 00:00:00 2001 From: Marshall Sorenson Date: Thu, 19 Dec 2024 16:27:49 -0500 Subject: [PATCH] feat: configurable crud builder --- packages/nestjs-crud/package.json | 2 +- .../app-ccb-custom.module.fixture.ts | 14 + .../app-ccb-ext.module.fixture.ts | 14 + .../app-ccb-sub.module.fixture.ts | 14 + .../__fixtures__/app-ccb.module.fixture.ts | 14 + .../photo-ccb-custom.controller.fixture.ts | 136 ++++++++ .../photo-ccb-custom.module.fixture.ts | 22 ++ .../photo-ccb-ext.controller.fixture.ts | 54 +++ .../photo-ccb-ext.module.fixture.ts | 28 ++ .../photo-ccb-sub.controller.fixture.ts | 122 +++++++ .../photo-ccb-sub.module.fixture.ts | 22 ++ .../photo-ccb/photo-ccb.controller.fixture.ts | 55 +++ .../photo-ccb/photo-ccb.module.fixture.ts | 21 ++ .../photo/dto/photo-create.dto.fixture.ts | 15 +- .../photo/dto/photo-update.dto.fixture.ts | 17 +- .../photo-creatable.interface.fixture.ts | 7 + .../photo-updatable.interface.fixture.ts | 7 + .../controllers/abstract-crud.controller.ts | 78 +++++ .../actions/crud-create-many.decorator.ts | 8 +- .../actions/crud-get-many.decorator.ts | 7 + .../actions/crud-get-one.decorator.ts | 7 + .../actions/crud-replace-one.decorator.ts | 8 +- .../actions/crud-update-one.decorator.ts | 8 +- .../crud-method-not-implemented.exception.ts | 23 ++ packages/nestjs-crud/src/index.ts | 9 + .../crud-serialize.interceptor.ts | 7 +- .../interfaces/crud-controller.interface.ts | 36 +- .../crud-extra-decorators.interface.ts | 5 + .../crud-result-paginated.interface.ts | 6 - .../crud-route-options.interface.ts | 17 +- .../src/services/typeorm-crud.service.ts | 11 +- .../configurable-crud.builder.e2e-spec.ts | 172 +++++++++ .../src/util/configurable-crud.builder.ts | 326 ++++++++++++++++++ .../src/util/crud-is-paginated.helper.ts | 14 + .../configurable-crud-decorators.interface.ts | 13 + .../configurable-crud-host.interface.ts | 20 ++ .../configurable-crud-options.interface.ts | 30 ++ 37 files changed, 1314 insertions(+), 55 deletions(-) create mode 100644 packages/nestjs-crud/src/__fixtures__/app-ccb-custom.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/app-ccb-ext.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/app-ccb-sub.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/app-ccb.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.controller.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.controller.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.controller.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.controller.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.module.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-creatable.interface.fixture.ts create mode 100644 packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-updatable.interface.fixture.ts create mode 100644 packages/nestjs-crud/src/controllers/abstract-crud.controller.ts create mode 100644 packages/nestjs-crud/src/decorators/actions/crud-get-many.decorator.ts create mode 100644 packages/nestjs-crud/src/decorators/actions/crud-get-one.decorator.ts create mode 100644 packages/nestjs-crud/src/exceptions/crud-method-not-implemented.exception.ts create mode 100644 packages/nestjs-crud/src/interfaces/crud-extra-decorators.interface.ts delete mode 100644 packages/nestjs-crud/src/interfaces/crud-result-paginated.interface.ts create mode 100644 packages/nestjs-crud/src/util/configurable-crud.builder.e2e-spec.ts create mode 100644 packages/nestjs-crud/src/util/configurable-crud.builder.ts create mode 100644 packages/nestjs-crud/src/util/crud-is-paginated.helper.ts create mode 100644 packages/nestjs-crud/src/util/interfaces/configurable-crud-decorators.interface.ts create mode 100644 packages/nestjs-crud/src/util/interfaces/configurable-crud-host.interface.ts create mode 100644 packages/nestjs-crud/src/util/interfaces/configurable-crud-options.interface.ts diff --git a/packages/nestjs-crud/package.json b/packages/nestjs-crud/package.json index fc7403925..9409492bd 100644 --- a/packages/nestjs-crud/package.json +++ b/packages/nestjs-crud/package.json @@ -14,6 +14,7 @@ "dependencies": { "@concepta/nestjs-common": "^5.1.0", "@concepta/nestjs-exception": "^5.1.0", + "@concepta/nestjs-typeorm-ext": "^5.1.0", "@concepta/typeorm-common": "^5.1.0", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", @@ -24,7 +25,6 @@ "@nestjsx/crud-typeorm": "^5.0.0-alpha.3" }, "devDependencies": { - "@concepta/nestjs-typeorm-ext": "^5.1.0", "@concepta/typeorm-seeding": "^4.0.0", "@faker-js/faker": "^8.4.1", "@nestjs/testing": "^10.4.1", diff --git a/packages/nestjs-crud/src/__fixtures__/app-ccb-custom.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/app-ccb-custom.module.fixture.ts new file mode 100644 index 000000000..18e8ca1fe --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/app-ccb-custom.module.fixture.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { default as ormConfig } from './ormconfig.fixture'; +import { CrudModule } from '../crud.module'; +import { PhotoCcbCustomModuleFixture } from './photo-ccb-custom/photo-ccb-custom.module.fixture'; + +@Module({ + imports: [ + TypeOrmModule.forRoot(ormConfig), + CrudModule.forRoot({}), + PhotoCcbCustomModuleFixture, + ], +}) +export class AppCcbCustomModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/app-ccb-ext.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/app-ccb-ext.module.fixture.ts new file mode 100644 index 000000000..6758e0863 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/app-ccb-ext.module.fixture.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { default as ormConfig } from './ormconfig.fixture'; +import { CrudModule } from '../crud.module'; +import { PhotoCcbExtModuleFixture } from './photo-ccb-ext/photo-ccb-ext.module.fixture'; + +@Module({ + imports: [ + TypeOrmExtModule.forRoot(ormConfig), + CrudModule.forRoot({}), + PhotoCcbExtModuleFixture, + ], +}) +export class AppCcbExtModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/app-ccb-sub.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/app-ccb-sub.module.fixture.ts new file mode 100644 index 000000000..241641483 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/app-ccb-sub.module.fixture.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { default as ormConfig } from './ormconfig.fixture'; +import { CrudModule } from '../crud.module'; +import { PhotoCcbSubModuleFixture } from './photo-ccb-sub/photo-ccb-sub.module.fixture'; + +@Module({ + imports: [ + TypeOrmModule.forRoot(ormConfig), + CrudModule.forRoot({}), + PhotoCcbSubModuleFixture, + ], +}) +export class AppCcbSubModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/app-ccb.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/app-ccb.module.fixture.ts new file mode 100644 index 000000000..c260555ba --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/app-ccb.module.fixture.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { default as ormConfig } from './ormconfig.fixture'; +import { CrudModule } from '../crud.module'; +import { PhotoCcbModuleFixture } from './photo-ccb/photo-ccb.module.fixture'; + +@Module({ + imports: [ + TypeOrmModule.forRoot(ormConfig), + CrudModule.forRoot({}), + PhotoCcbModuleFixture, + ], +}) +export class AppCcbModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.controller.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.controller.fixture.ts new file mode 100644 index 000000000..dcf8792cc --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.controller.fixture.ts @@ -0,0 +1,136 @@ +import { Inject } from '@nestjs/common'; +import { PhotoEntityInterfaceFixture } from '../photo/interfaces/photo-entity.interface.fixture'; +import { PhotoCreatableInterfaceFixture } from '../photo/interfaces/photo-creatable.interface.fixture'; +import { PhotoUpdatableInterfaceFixture } from '../photo/interfaces/photo-updatable.interface.fixture'; +import { PhotoFixture } from '../photo/photo.entity.fixture'; +import { PhotoDtoFixture } from '../photo/dto/photo.dto.fixture'; +import { PhotoPaginatedDtoFixture } from '../photo/dto/photo-paginated.dto.fixture'; +import { PhotoCreateDtoFixture } from '../photo/dto/photo-create.dto.fixture'; +import { PhotoCreateManyDtoFixture } from '../photo/dto/photo-create-many.dto.fixture'; +import { PhotoUpdateDtoFixture } from '../photo/dto/photo-update.dto.fixture'; +import { ConfigurableCrudBuilder } from '../../util/configurable-crud.builder'; +import { CrudSoftDelete } from '../../decorators/routes/crud-soft-delete.decorator'; +import { AbstractCrudController } from '../../controllers/abstract-crud.controller'; +import { CrudRequest } from '../../decorators/params/crud-request.decorator'; +import { CrudRequestInterface } from '../../interfaces/crud-request.interface'; +import { TypeOrmCrudService } from '../../services/typeorm-crud.service'; +import { CrudBody } from '../../decorators/params/crud-body.decorator'; + +export const PHOTO_CRUD_SERVICE_TOKEN = Symbol('__PHOTO_CRUD_SERVICE_TOKEN__'); + +const crudBuilder = new ConfigurableCrudBuilder< + PhotoEntityInterfaceFixture, + PhotoCreatableInterfaceFixture, + PhotoUpdatableInterfaceFixture +>(); + +const { + ConfigurableServiceClass, + CrudController, + CrudGetMany, + CrudGetOne, + CrudCreateMany, + CrudCreateOne, + CrudUpdateOne, + CrudReplaceOne, + CrudDeleteOne, + CrudRecoverOne, +} = crudBuilder.build({ + service: { + entity: PhotoFixture, + injectionToken: PHOTO_CRUD_SERVICE_TOKEN, + }, + controller: { + path: 'photo', + model: { + type: PhotoDtoFixture, + paginatedType: PhotoPaginatedDtoFixture, + }, + }, + getMany: {}, + getOne: {}, + createMany: { + dto: PhotoCreateManyDtoFixture, + }, + createOne: { + dto: PhotoCreateDtoFixture, + }, + updateOne: { + dto: PhotoUpdateDtoFixture, + }, + replaceOne: { + dto: PhotoUpdateDtoFixture, + }, + deleteOne: { + extraDecorators: [CrudSoftDelete(true)], + }, + recoverOne: { path: 'recover/:id' }, +}); + +export class PhotoCcbCustomCrudServiceFixture extends ConfigurableServiceClass {} + +@CrudController +export class PhotoCcbCustomControllerFixture extends AbstractCrudController< + PhotoEntityInterfaceFixture, + PhotoCreatableInterfaceFixture, + PhotoUpdatableInterfaceFixture +> { + constructor( + @Inject(PHOTO_CRUD_SERVICE_TOKEN) + protected crudService: TypeOrmCrudService, + ) { + super(crudService); + } + + @CrudGetMany + async getMany(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.crudService.getMany(crudRequest); + } + + @CrudGetOne + async getOne(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.crudService.getOne(crudRequest); + } + + @CrudCreateMany + async createMany( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() dto: PhotoCreateManyDtoFixture, + ) { + return this.crudService.createMany(crudRequest, dto); + } + + @CrudCreateOne + async createOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() dto: PhotoCreateDtoFixture, + ) { + return this.crudService.createOne(crudRequest, dto); + } + + @CrudUpdateOne + async updateOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() dto: PhotoUpdateDtoFixture, + ) { + return this.crudService.createOne(crudRequest, dto); + } + + @CrudReplaceOne + async replaceOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() dto: PhotoUpdateDtoFixture, + ) { + return this.crudService.replaceOne(crudRequest, dto); + } + + @CrudDeleteOne + async deleteOne(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.crudService.deleteOne(crudRequest); + } + + @CrudRecoverOne + async recoverOne(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.crudService.recoverOne(crudRequest); + } +} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.module.fixture.ts new file mode 100644 index 000000000..e4f859656 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-custom/photo-ccb-custom.module.fixture.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PhotoFixture } from '../photo/photo.entity.fixture'; + +import { + PhotoCcbCustomControllerFixture, + PhotoCcbCustomCrudServiceFixture, + PHOTO_CRUD_SERVICE_TOKEN, +} from './photo-ccb-custom.controller.fixture'; + +@Module({ + imports: [TypeOrmModule.forFeature([PhotoFixture])], + providers: [ + { + provide: PHOTO_CRUD_SERVICE_TOKEN, + useClass: PhotoCcbCustomCrudServiceFixture, + }, + ], + controllers: [PhotoCcbCustomControllerFixture], +}) +export class PhotoCcbCustomModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.controller.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.controller.fixture.ts new file mode 100644 index 000000000..731d8998d --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.controller.fixture.ts @@ -0,0 +1,54 @@ +import { PhotoEntityInterfaceFixture } from '../photo/interfaces/photo-entity.interface.fixture'; +import { PhotoCreatableInterfaceFixture } from '../photo/interfaces/photo-creatable.interface.fixture'; +import { PhotoUpdatableInterfaceFixture } from '../photo/interfaces/photo-updatable.interface.fixture'; +import { PhotoDtoFixture } from '../photo/dto/photo.dto.fixture'; +import { PhotoPaginatedDtoFixture } from '../photo/dto/photo-paginated.dto.fixture'; +import { PhotoCreateDtoFixture } from '../photo/dto/photo-create.dto.fixture'; +import { PhotoCreateManyDtoFixture } from '../photo/dto/photo-create-many.dto.fixture'; +import { PhotoUpdateDtoFixture } from '../photo/dto/photo-update.dto.fixture'; +import { ConfigurableCrudBuilder } from '../../util/configurable-crud.builder'; +import { CrudSoftDelete } from '../../decorators/routes/crud-soft-delete.decorator'; + +export const PHOTO_CRUD_SERVICE_TOKEN = Symbol('__PHOTO_CRUD_SERVICE_TOKEN__'); + +const crudBuilder = new ConfigurableCrudBuilder< + PhotoEntityInterfaceFixture, + PhotoCreatableInterfaceFixture, + PhotoUpdatableInterfaceFixture +>(); + +const { ConfigurableControllerClass, ConfigurableServiceClass } = + crudBuilder.build({ + service: { + entityKey: 'photo', + injectionToken: PHOTO_CRUD_SERVICE_TOKEN, + }, + controller: { + path: 'photo', + model: { + type: PhotoDtoFixture, + paginatedType: PhotoPaginatedDtoFixture, + }, + }, + getMany: {}, + getOne: {}, + createMany: { + dto: PhotoCreateManyDtoFixture, + }, + createOne: { + dto: PhotoCreateDtoFixture, + }, + updateOne: { + dto: PhotoUpdateDtoFixture, + }, + replaceOne: { + dto: PhotoUpdateDtoFixture, + }, + deleteOne: { + extraDecorators: [CrudSoftDelete(true)], + }, + recoverOne: { path: 'recover/:id' }, + }); + +export class PhotoCcbExtCrudServiceFixture extends ConfigurableServiceClass {} +export class PhotoCcbExtControllerFixture extends ConfigurableControllerClass {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.module.fixture.ts new file mode 100644 index 000000000..b31d933b6 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-ext/photo-ccb-ext.module.fixture.ts @@ -0,0 +1,28 @@ +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { Module } from '@nestjs/common'; + +import { PhotoFixture } from '../photo/photo.entity.fixture'; + +import { + PhotoCcbExtControllerFixture, + PhotoCcbExtCrudServiceFixture, + PHOTO_CRUD_SERVICE_TOKEN, +} from './photo-ccb-ext.controller.fixture'; + +@Module({ + imports: [ + TypeOrmExtModule.forFeature({ + photo: { + entity: PhotoFixture, + }, + }), + ], + providers: [ + { + provide: PHOTO_CRUD_SERVICE_TOKEN, + useClass: PhotoCcbExtCrudServiceFixture, + }, + ], + controllers: [PhotoCcbExtControllerFixture], +}) +export class PhotoCcbExtModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.controller.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.controller.fixture.ts new file mode 100644 index 000000000..ad8bf1c10 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.controller.fixture.ts @@ -0,0 +1,122 @@ +import { PhotoEntityInterfaceFixture } from '../photo/interfaces/photo-entity.interface.fixture'; +import { PhotoCreatableInterfaceFixture } from '../photo/interfaces/photo-creatable.interface.fixture'; +import { PhotoUpdatableInterfaceFixture } from '../photo/interfaces/photo-updatable.interface.fixture'; +import { PhotoFixture } from '../photo/photo.entity.fixture'; +import { PhotoDtoFixture } from '../photo/dto/photo.dto.fixture'; +import { PhotoPaginatedDtoFixture } from '../photo/dto/photo-paginated.dto.fixture'; +import { PhotoCreateDtoFixture } from '../photo/dto/photo-create.dto.fixture'; +import { PhotoCreateManyDtoFixture } from '../photo/dto/photo-create-many.dto.fixture'; +import { PhotoUpdateDtoFixture } from '../photo/dto/photo-update.dto.fixture'; +import { ConfigurableCrudBuilder } from '../../util/configurable-crud.builder'; +import { CrudCreateManyInterface } from '../../interfaces/crud-create-many.interface'; +import { CrudSoftDelete } from '../../decorators/routes/crud-soft-delete.decorator'; +import { CrudRequestInterface } from '../../interfaces/crud-request.interface'; + +export const PHOTO_CRUD_SERVICE_TOKEN = Symbol('__PHOTO_CRUD_SERVICE_TOKEN__'); + +const crudBuilder = new ConfigurableCrudBuilder< + PhotoEntityInterfaceFixture, + PhotoCreatableInterfaceFixture, + PhotoUpdatableInterfaceFixture +>(); + +const { + ConfigurableServiceClass, + ConfigurableControllerClass, + CrudController, + CrudGetMany, + CrudGetOne, + CrudCreateMany, + CrudCreateOne, + CrudUpdateOne, + CrudReplaceOne, + CrudDeleteOne, + CrudRecoverOne, +} = crudBuilder.build({ + service: { + entity: PhotoFixture, + injectionToken: PHOTO_CRUD_SERVICE_TOKEN, + }, + controller: { + path: 'photo', + model: { + type: PhotoDtoFixture, + paginatedType: PhotoPaginatedDtoFixture, + }, + }, + getMany: {}, + getOne: {}, + createMany: { + dto: PhotoCreateManyDtoFixture, + }, + createOne: { + dto: PhotoCreateDtoFixture, + }, + updateOne: { + dto: PhotoUpdateDtoFixture, + }, + replaceOne: { + dto: PhotoUpdateDtoFixture, + }, + deleteOne: { + extraDecorators: [CrudSoftDelete(true)], + }, + recoverOne: { path: 'recover/:id' }, +}); + +export class PhotoCcbSubCrudServiceFixture extends ConfigurableServiceClass {} + +@CrudController +export class PhotoCcbSubControllerFixture extends ConfigurableControllerClass { + @CrudGetMany + async getMany(crudRequest: CrudRequestInterface) { + return super.getMany(crudRequest); + } + + @CrudGetOne + async getOne(crudRequest: CrudRequestInterface) { + return super.getOne(crudRequest); + } + + @CrudCreateMany + async createMany( + crudRequest: CrudRequestInterface, + dto: CrudCreateManyInterface, + ) { + return super.createMany(crudRequest, dto); + } + + @CrudCreateOne + async createOne( + crudRequest: CrudRequestInterface, + dto: PhotoCreatableInterfaceFixture, + ) { + return super.createOne(crudRequest, dto); + } + + @CrudUpdateOne + async updateOne( + crudRequest: CrudRequestInterface, + dto: PhotoUpdatableInterfaceFixture, + ) { + return super.createOne(crudRequest, dto); + } + + @CrudReplaceOne + async replaceOne( + crudRequest: CrudRequestInterface, + dto: PhotoUpdatableInterfaceFixture, + ) { + return super.replaceOne(crudRequest, dto); + } + + @CrudDeleteOne + async deleteOne(crudRequest: CrudRequestInterface) { + return super.deleteOne(crudRequest); + } + + @CrudRecoverOne + async recoverOne(crudRequest: CrudRequestInterface) { + return super.recoverOne(crudRequest); + } +} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.module.fixture.ts new file mode 100644 index 000000000..a883cbf53 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb-sub/photo-ccb-sub.module.fixture.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PhotoFixture } from '../photo/photo.entity.fixture'; + +import { + PhotoCcbSubControllerFixture, + PhotoCcbSubCrudServiceFixture, + PHOTO_CRUD_SERVICE_TOKEN, +} from './photo-ccb-sub.controller.fixture'; + +@Module({ + imports: [TypeOrmModule.forFeature([PhotoFixture])], + providers: [ + { + provide: PHOTO_CRUD_SERVICE_TOKEN, + useClass: PhotoCcbSubCrudServiceFixture, + }, + ], + controllers: [PhotoCcbSubControllerFixture], +}) +export class PhotoCcbSubModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.controller.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.controller.fixture.ts new file mode 100644 index 000000000..fbec5fdb7 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.controller.fixture.ts @@ -0,0 +1,55 @@ +import { PhotoEntityInterfaceFixture } from '../photo/interfaces/photo-entity.interface.fixture'; +import { PhotoCreatableInterfaceFixture } from '../photo/interfaces/photo-creatable.interface.fixture'; +import { PhotoUpdatableInterfaceFixture } from '../photo/interfaces/photo-updatable.interface.fixture'; +import { PhotoFixture } from '../photo/photo.entity.fixture'; +import { PhotoDtoFixture } from '../photo/dto/photo.dto.fixture'; +import { PhotoPaginatedDtoFixture } from '../photo/dto/photo-paginated.dto.fixture'; +import { PhotoCreateDtoFixture } from '../photo/dto/photo-create.dto.fixture'; +import { PhotoCreateManyDtoFixture } from '../photo/dto/photo-create-many.dto.fixture'; +import { PhotoUpdateDtoFixture } from '../photo/dto/photo-update.dto.fixture'; +import { ConfigurableCrudBuilder } from '../../util/configurable-crud.builder'; +import { CrudSoftDelete } from '../../decorators/routes/crud-soft-delete.decorator'; + +export const PHOTO_CRUD_SERVICE_TOKEN = Symbol('__PHOTO_CRUD_SERVICE_TOKEN__'); + +const crudBuilder = new ConfigurableCrudBuilder< + PhotoEntityInterfaceFixture, + PhotoCreatableInterfaceFixture, + PhotoUpdatableInterfaceFixture +>(); + +const { ConfigurableControllerClass, ConfigurableServiceClass } = + crudBuilder.build({ + service: { + entity: PhotoFixture, + injectionToken: PHOTO_CRUD_SERVICE_TOKEN, + }, + controller: { + path: 'photo', + model: { + type: PhotoDtoFixture, + paginatedType: PhotoPaginatedDtoFixture, + }, + }, + getMany: {}, + getOne: {}, + createMany: { + dto: PhotoCreateManyDtoFixture, + }, + createOne: { + dto: PhotoCreateDtoFixture, + }, + updateOne: { + dto: PhotoUpdateDtoFixture, + }, + replaceOne: { + dto: PhotoUpdateDtoFixture, + }, + deleteOne: { + extraDecorators: [CrudSoftDelete(true)], + }, + recoverOne: { path: 'recover/:id' }, + }); + +export class PhotoCcbCrudServiceFixture extends ConfigurableServiceClass {} +export class PhotoCcbControllerFixture extends ConfigurableControllerClass {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.module.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.module.fixture.ts new file mode 100644 index 000000000..76feec590 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo-ccb/photo-ccb.module.fixture.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PhotoFixture } from '../photo/photo.entity.fixture'; +import { + PhotoCcbControllerFixture, + PhotoCcbCrudServiceFixture, + PHOTO_CRUD_SERVICE_TOKEN, +} from './photo-ccb.controller.fixture'; + +@Module({ + imports: [TypeOrmModule.forFeature([PhotoFixture])], + providers: [ + { + provide: PHOTO_CRUD_SERVICE_TOKEN, + useClass: PhotoCcbCrudServiceFixture, + }, + ], + controllers: [PhotoCcbControllerFixture], +}) +export class PhotoCcbModuleFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-create.dto.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-create.dto.fixture.ts index 9ecdba88d..aeeb3649f 100644 --- a/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-create.dto.fixture.ts +++ b/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-create.dto.fixture.ts @@ -1,11 +1,14 @@ import { PickType } from '@nestjs/swagger'; import { Exclude } from 'class-transformer'; +import { PhotoCreatableInterfaceFixture } from '../interfaces/photo-creatable.interface.fixture'; import { PhotoDtoFixture } from './photo.dto.fixture'; @Exclude() -export class PhotoCreateDtoFixture extends PickType(PhotoDtoFixture, [ - 'name', - 'description', - 'filename', - 'isPublished', -] as const) {} +export class PhotoCreateDtoFixture + extends PickType(PhotoDtoFixture, [ + 'name', + 'description', + 'filename', + 'isPublished', + ] as const) + implements PhotoCreatableInterfaceFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-update.dto.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-update.dto.fixture.ts index 41c0b89cd..337811c3f 100644 --- a/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-update.dto.fixture.ts +++ b/packages/nestjs-crud/src/__fixtures__/photo/dto/photo-update.dto.fixture.ts @@ -1,12 +1,15 @@ import { PickType } from '@nestjs/swagger'; import { Exclude } from 'class-transformer'; +import { PhotoUpdatableInterfaceFixture } from '../interfaces/photo-updatable.interface.fixture'; import { PhotoDtoFixture } from './photo.dto.fixture'; @Exclude() -export class PhotoUpdateDtoFixture extends PickType(PhotoDtoFixture, [ - 'name', - 'description', - 'filename', - 'isPublished', - 'views', -]) {} +export class PhotoUpdateDtoFixture + extends PickType(PhotoDtoFixture, [ + 'name', + 'description', + 'filename', + 'isPublished', + 'views', + ] as const) + implements PhotoUpdatableInterfaceFixture {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-creatable.interface.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-creatable.interface.fixture.ts new file mode 100644 index 000000000..fbbaf97d6 --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-creatable.interface.fixture.ts @@ -0,0 +1,7 @@ +import { PhotoEntityInterfaceFixture } from './photo-entity.interface.fixture'; + +export interface PhotoCreatableInterfaceFixture + extends Pick< + PhotoEntityInterfaceFixture, + 'name' | 'description' | 'filename' | 'isPublished' + > {} diff --git a/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-updatable.interface.fixture.ts b/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-updatable.interface.fixture.ts new file mode 100644 index 000000000..99a04e51e --- /dev/null +++ b/packages/nestjs-crud/src/__fixtures__/photo/interfaces/photo-updatable.interface.fixture.ts @@ -0,0 +1,7 @@ +import { PhotoEntityInterfaceFixture } from './photo-entity.interface.fixture'; + +export interface PhotoUpdatableInterfaceFixture + extends Pick< + PhotoEntityInterfaceFixture, + 'name' | 'description' | 'filename' | 'isPublished' | 'views' + > {} diff --git a/packages/nestjs-crud/src/controllers/abstract-crud.controller.ts b/packages/nestjs-crud/src/controllers/abstract-crud.controller.ts new file mode 100644 index 000000000..cfa48bcee --- /dev/null +++ b/packages/nestjs-crud/src/controllers/abstract-crud.controller.ts @@ -0,0 +1,78 @@ +import { DeepPartial, ObjectLiteral } from 'typeorm'; +import { AdditionalCrudMethodArgs } from '../crud.types'; +import { CrudMethodNotImplementedException } from '../exceptions/crud-method-not-implemented.exception'; +import { CrudControllerInterface } from '../interfaces/crud-controller.interface'; +import { CrudRequestInterface } from '../interfaces/crud-request.interface'; +import { CrudCreateManyInterface } from '../interfaces/crud-create-many.interface'; +import { CrudResponsePaginatedInterface } from '../interfaces/crud-response-paginated.interface'; +import { TypeOrmCrudService } from '../services/typeorm-crud.service'; + +export abstract class AbstractCrudController< + Entity extends ObjectLiteral, + Creatable extends DeepPartial, + Updatable extends DeepPartial, + Replaceable extends Creatable = Creatable, +> implements CrudControllerInterface +{ + constructor(protected crudService: TypeOrmCrudService) {} + + getMany( + _crudRequest: CrudRequestInterface, + ..._rest: AdditionalCrudMethodArgs + ): Promise | Entity[]> { + throw new CrudMethodNotImplementedException(this, this.getMany); + } + + getOne( + _crudRequest: CrudRequestInterface, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.getOne); + } + + createOne( + _crudRequest: CrudRequestInterface, + _dto: Creatable, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.createOne); + } + + createMany( + _crudRequest: CrudRequestInterface, + _dto: CrudCreateManyInterface, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.createMany); + } + + updateOne( + _crudRequest: CrudRequestInterface, + _dto: Updatable, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.updateOne); + } + + replaceOne( + _crudRequest: CrudRequestInterface, + _dto: Replaceable, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.replaceOne); + } + + deleteOne( + _crudRequest: CrudRequestInterface, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.deleteOne); + } + + recoverOne( + _crudRequest: CrudRequestInterface, + ..._rest: AdditionalCrudMethodArgs + ): Promise { + throw new CrudMethodNotImplementedException(this, this.recoverOne); + } +} diff --git a/packages/nestjs-crud/src/decorators/actions/crud-create-many.decorator.ts b/packages/nestjs-crud/src/decorators/actions/crud-create-many.decorator.ts index a2eb355fd..8cbb78750 100644 --- a/packages/nestjs-crud/src/decorators/actions/crud-create-many.decorator.ts +++ b/packages/nestjs-crud/src/decorators/actions/crud-create-many.decorator.ts @@ -1,5 +1,6 @@ import { applyDecorators, Post } from '@nestjs/common'; import { CrudCreateManyOptionsInterface } from '../../interfaces/crud-route-options.interface'; +import { CrudValidationOptions } from '../../crud.types'; import { CrudActions } from '../../crud.enums'; import { CrudAction } from '../routes/crud-action.decorator'; import { CRUD_MODULE_ROUTE_CREATE_MANY_DEFAULT_PATH } from '../../crud.constants'; @@ -16,15 +17,20 @@ export const CrudCreateMany = ( ) => { const { path = CRUD_MODULE_ROUTE_CREATE_MANY_DEFAULT_PATH, + dto, validation, serialization, api, } = { ...options }; + const validationMerged: CrudValidationOptions = dto + ? { expectedType: dto, ...validation } + : validation; + return applyDecorators( Post(path), CrudAction(CrudActions.CreateMany), - CrudValidate(validation), + CrudValidate(validationMerged), CrudSerialize(serialization), CrudApiOperation(api?.operation), CrudApiResponse(CrudActions.CreateMany, api?.response), diff --git a/packages/nestjs-crud/src/decorators/actions/crud-get-many.decorator.ts b/packages/nestjs-crud/src/decorators/actions/crud-get-many.decorator.ts new file mode 100644 index 000000000..92f1abf8d --- /dev/null +++ b/packages/nestjs-crud/src/decorators/actions/crud-get-many.decorator.ts @@ -0,0 +1,7 @@ +import { CrudReadAll } from './crud-read-all.decorator'; + +/** + * CRUD Get Many route decorator (alias for Read All) + */ +export const CrudGetMany = (...args: Parameters) => + CrudReadAll(...args); diff --git a/packages/nestjs-crud/src/decorators/actions/crud-get-one.decorator.ts b/packages/nestjs-crud/src/decorators/actions/crud-get-one.decorator.ts new file mode 100644 index 000000000..434f032d1 --- /dev/null +++ b/packages/nestjs-crud/src/decorators/actions/crud-get-one.decorator.ts @@ -0,0 +1,7 @@ +import { CrudReadOne } from './crud-read-one.decorator'; + +/** + * CRUD Get One route decorator (alias for Read One) + */ +export const CrudGetOne = (...args: Parameters) => + CrudReadOne(...args); diff --git a/packages/nestjs-crud/src/decorators/actions/crud-replace-one.decorator.ts b/packages/nestjs-crud/src/decorators/actions/crud-replace-one.decorator.ts index ec5e435ba..6bb2857cf 100644 --- a/packages/nestjs-crud/src/decorators/actions/crud-replace-one.decorator.ts +++ b/packages/nestjs-crud/src/decorators/actions/crud-replace-one.decorator.ts @@ -1,5 +1,6 @@ import { applyDecorators, Put, SetMetadata } from '@nestjs/common'; import { CrudReplaceOneOptionsInterface } from '../../interfaces/crud-route-options.interface'; +import { CrudValidationOptions } from '../../crud.types'; import { CrudActions } from '../../crud.enums'; import { CrudAction } from '../routes/crud-action.decorator'; import { @@ -20,17 +21,22 @@ export const CrudReplaceOne = ( ) => { const { path = CRUD_MODULE_ROUTE_ID_DEFAULT_PATH, + dto, validation, serialization, api, ...rest } = { ...options }; + const validationMerged: CrudValidationOptions = dto + ? { expectedType: dto, ...validation } + : validation; + return applyDecorators( Put(path), CrudAction(CrudActions.ReplaceOne), SetMetadata(CRUD_MODULE_ROUTE_REPLACE_ONE_METADATA, rest), - CrudValidate(validation), + CrudValidate(validationMerged), CrudSerialize(serialization), CrudApiOperation(api?.operation), CrudApiParam(api?.params), diff --git a/packages/nestjs-crud/src/decorators/actions/crud-update-one.decorator.ts b/packages/nestjs-crud/src/decorators/actions/crud-update-one.decorator.ts index cdb6129be..037cdb8f5 100644 --- a/packages/nestjs-crud/src/decorators/actions/crud-update-one.decorator.ts +++ b/packages/nestjs-crud/src/decorators/actions/crud-update-one.decorator.ts @@ -1,5 +1,6 @@ import { applyDecorators, Patch, SetMetadata } from '@nestjs/common'; import { CrudUpdateOneOptionsInterface } from '../../interfaces/crud-route-options.interface'; +import { CrudValidationOptions } from '../../crud.types'; import { CrudActions } from '../../crud.enums'; import { CrudAction } from '../routes/crud-action.decorator'; import { @@ -18,17 +19,22 @@ import { CrudApiResponse } from '../openapi/crud-api-response.decorator'; export const CrudUpdateOne = (options: CrudUpdateOneOptionsInterface = {}) => { const { path = CRUD_MODULE_ROUTE_ID_DEFAULT_PATH, + dto, validation, serialization, api, ...rest } = { ...options }; + const validationMerged: CrudValidationOptions = dto + ? { expectedType: dto, ...validation } + : validation; + return applyDecorators( Patch(path), CrudAction(CrudActions.UpdateOne), SetMetadata(CRUD_MODULE_ROUTE_UPDATE_ONE_METADATA, rest), - CrudValidate(validation), + CrudValidate(validationMerged), CrudSerialize(serialization), CrudApiOperation(api?.operation), CrudApiParam(api?.params), diff --git a/packages/nestjs-crud/src/exceptions/crud-method-not-implemented.exception.ts b/packages/nestjs-crud/src/exceptions/crud-method-not-implemented.exception.ts new file mode 100644 index 000000000..380a83597 --- /dev/null +++ b/packages/nestjs-crud/src/exceptions/crud-method-not-implemented.exception.ts @@ -0,0 +1,23 @@ +import { Type } from '@nestjs/common'; +import { RuntimeExceptionOptions } from '@concepta/nestjs-exception'; +import { CrudException } from './crud.exception'; + +/** + * Crud method not implemented exception. + */ +export class CrudMethodNotImplementedException< + T extends Type = Type, +> extends CrudException { + constructor( + instance: InstanceType, + method: Function, + options?: RuntimeExceptionOptions, + ) { + super({ + message: `CRUD controller "%s" method "%s" not implemented`, + messageParams: [instance.constructor.name, method.name], + ...options, + }); + this.errorCode = 'CRUD_METHOD_NOT_IMPLEMENTED_ERROR'; + } +} diff --git a/packages/nestjs-crud/src/index.ts b/packages/nestjs-crud/src/index.ts index 29b34ab7d..053712022 100644 --- a/packages/nestjs-crud/src/index.ts +++ b/packages/nestjs-crud/src/index.ts @@ -13,7 +13,9 @@ export { CrudController } from './decorators/controller/crud-controller.decorato // route decorators export { CrudReadAll } from './decorators/actions/crud-read-all.decorator'; export { CrudReadMany } from './decorators/actions/crud-read-many.decorator'; +export { CrudGetMany } from './decorators/actions/crud-get-many.decorator'; export { CrudReadOne } from './decorators/actions/crud-read-one.decorator'; +export { CrudGetOne } from './decorators/actions/crud-get-one.decorator'; export { CrudCreateOne } from './decorators/actions/crud-create-one.decorator'; export { CrudCreateMany } from './decorators/actions/crud-create-many.decorator'; export { CrudUpdateOne } from './decorators/actions/crud-update-one.decorator'; @@ -45,6 +47,7 @@ export { CrudBody } from './decorators/params/crud-body.decorator'; // classes export { CrudQueryHelper } from './util/crud-query.helper'; export { TypeOrmCrudService } from './services/typeorm-crud.service'; +export { AbstractCrudController } from './controllers/abstract-crud.controller'; // dto export { CrudResponsePaginatedDto } from './dto/crud-response-paginated.dto'; @@ -52,5 +55,11 @@ export { CrudCreateManyDto } from './dto/crud-create-many.dto'; // exceptions export { CrudException } from './exceptions/crud.exception'; +export { CrudMethodNotImplementedException } from './exceptions/crud-method-not-implemented.exception'; export { CrudRequestException } from './exceptions/crud-request.exception'; export { CrudQueryException } from './exceptions/crud-query.exception'; + +// configurable crud builder +export { ConfigurableCrudHost } from './util/interfaces/configurable-crud-host.interface'; +export { ConfigurableCrudOptions } from './util/interfaces/configurable-crud-options.interface'; +export { ConfigurableCrudBuilder } from './util/configurable-crud.builder'; diff --git a/packages/nestjs-crud/src/interceptors/crud-serialize.interceptor.ts b/packages/nestjs-crud/src/interceptors/crud-serialize.interceptor.ts index 008603455..e563d48e9 100644 --- a/packages/nestjs-crud/src/interceptors/crud-serialize.interceptor.ts +++ b/packages/nestjs-crud/src/interceptors/crud-serialize.interceptor.ts @@ -18,14 +18,15 @@ import { LiteralObject } from '@concepta/nestjs-common'; import { CrudInvalidResponseDto } from '../dto/crud-invalid-response.dto'; import { CrudResponsePaginatedDto } from '../dto/crud-response-paginated.dto'; import { CrudSerializationOptionsInterface } from '../interfaces/crud-serialization-options.interface'; -import { CrudResultPaginatedInterface } from '../interfaces/crud-result-paginated.interface'; +import { CrudResponsePaginatedInterface } from '../interfaces/crud-response-paginated.interface'; import { CrudSettingsInterface } from '../interfaces/crud-settings.interface'; import { CrudReflectionService } from '../services/crud-reflection.service'; import { CRUD_MODULE_SETTINGS_TOKEN } from '../crud.constants'; import { CrudException } from '../exceptions/crud.exception'; +import { crudIsPaginatedHelper } from '../util/crud-is-paginated.helper'; type ResponseType = - | (LiteralObject & CrudResultPaginatedInterface) + | (LiteralObject & CrudResponsePaginatedInterface) | Array; export class CrudSerializeInterceptor implements NestInterceptor { @@ -62,7 +63,7 @@ export class CrudSerializeInterceptor implements NestInterceptor { // determine the type to use const type = - !Array.isArray(response) && response?.__isPaginated === true + !Array.isArray(response) && crudIsPaginatedHelper(response) === true ? options?.paginatedType : options?.type; diff --git a/packages/nestjs-crud/src/interfaces/crud-controller.interface.ts b/packages/nestjs-crud/src/interfaces/crud-controller.interface.ts index cb046a9f7..d9f253a2a 100644 --- a/packages/nestjs-crud/src/interfaces/crud-controller.interface.ts +++ b/packages/nestjs-crud/src/interfaces/crud-controller.interface.ts @@ -1,5 +1,5 @@ import { DeepPartial } from 'typeorm'; -import { CreateManyDto } from '@nestjsx/crud'; +import { CrudCreateManyInterface } from './crud-create-many.interface'; import { CrudRequestInterface } from '../interfaces/crud-request.interface'; import { CrudResponsePaginatedInterface } from './crud-response-paginated.interface'; import { AdditionalCrudMethodArgs } from '../crud.types'; @@ -10,47 +10,47 @@ export interface CrudControllerInterface< Updatable extends DeepPartial, Replaceable extends Creatable = Creatable, > { - getMany?: ( + getMany?( crudRequest: CrudRequestInterface, ...rest: AdditionalCrudMethodArgs - ) => Promise | Entity[]>; + ): Promise | Entity[]>; - getOne?: ( + getOne?( crudRequest: CrudRequestInterface, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - createOne?: ( + createOne?( crudRequest: CrudRequestInterface, dto: Creatable, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - createMany?: ( + createMany?( crudRequest: CrudRequestInterface, - dto: CreateManyDto, + dto: CrudCreateManyInterface, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - updateOne?: ( + updateOne?( crudRequest: CrudRequestInterface, dto: Updatable, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - replaceOne?: ( + replaceOne?( crudRequest: CrudRequestInterface, dto: Replaceable, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - deleteOne?: ( + deleteOne?( crudRequest: CrudRequestInterface, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; - recoverOne?: ( + recoverOne?( crudRequest: CrudRequestInterface, ...rest: AdditionalCrudMethodArgs - ) => Promise; + ): Promise; } diff --git a/packages/nestjs-crud/src/interfaces/crud-extra-decorators.interface.ts b/packages/nestjs-crud/src/interfaces/crud-extra-decorators.interface.ts new file mode 100644 index 000000000..3660485c6 --- /dev/null +++ b/packages/nestjs-crud/src/interfaces/crud-extra-decorators.interface.ts @@ -0,0 +1,5 @@ +import { applyDecorators } from '@nestjs/common'; + +export interface CrudExtraDecoratorsInterface { + extraDecorators?: ReturnType[]; +} diff --git a/packages/nestjs-crud/src/interfaces/crud-result-paginated.interface.ts b/packages/nestjs-crud/src/interfaces/crud-result-paginated.interface.ts deleted file mode 100644 index 7e26e34b7..000000000 --- a/packages/nestjs-crud/src/interfaces/crud-result-paginated.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CrudResponsePaginatedInterface } from './crud-response-paginated.interface'; - -export interface CrudResultPaginatedInterface - extends CrudResponsePaginatedInterface { - __isPaginated?: boolean; -} diff --git a/packages/nestjs-crud/src/interfaces/crud-route-options.interface.ts b/packages/nestjs-crud/src/interfaces/crud-route-options.interface.ts index f797f5031..cea9cbf53 100644 --- a/packages/nestjs-crud/src/interfaces/crud-route-options.interface.ts +++ b/packages/nestjs-crud/src/interfaces/crud-route-options.interface.ts @@ -1,3 +1,4 @@ +import { Type } from '@nestjs/common'; import { ApiOperationOptions, ApiParamOptions, @@ -26,12 +27,18 @@ export interface CrudRouteOptionsInterface { }; } +export interface CrudRouteDtoOptionsInterface { + dto?: Type; +} + export interface CrudCreateManyOptionsInterface - extends CrudRouteOptionsInterface {} + extends CrudRouteOptionsInterface, + CrudRouteDtoOptionsInterface {} export interface CrudCreateOneOptionsInterface extends CrudRouteOptionsInterface, - Pick {} + Pick, + CrudRouteDtoOptionsInterface {} export interface CrudReadAllOptionsInterface extends CrudRouteOptionsInterface {} @@ -41,11 +48,13 @@ export interface CrudReadOneOptionsInterface export interface CrudUpdateOneOptionsInterface extends CrudRouteOptionsInterface, - Pick {} + Pick, + CrudRouteDtoOptionsInterface {} export interface CrudReplaceOneOptionsInterface extends CrudRouteOptionsInterface, - Pick {} + Pick, + CrudRouteDtoOptionsInterface {} export interface CrudDeleteOneOptionsInterface extends CrudRouteOptionsInterface, diff --git a/packages/nestjs-crud/src/services/typeorm-crud.service.ts b/packages/nestjs-crud/src/services/typeorm-crud.service.ts index 7dfdfc1f1..e5ff7ca65 100644 --- a/packages/nestjs-crud/src/services/typeorm-crud.service.ts +++ b/packages/nestjs-crud/src/services/typeorm-crud.service.ts @@ -9,7 +9,7 @@ import { import { CrudQueryHelper } from '../util/crud-query.helper'; import { CrudQueryOptionsInterface } from '../interfaces/crud-query-options.interface'; -import { CrudResultPaginatedInterface } from '../interfaces/crud-result-paginated.interface'; +import { CrudResponsePaginatedInterface } from '../interfaces/crud-response-paginated.interface'; import { CrudQueryException } from '../exceptions/crud-query.exception'; import { ParsedRequestParams, QueryJoin } from '@nestjsx/crud-request'; @@ -26,7 +26,7 @@ export class TypeOrmCrudService< async getMany( req: CrudRequest, queryOptions?: CrudQueryOptionsInterface, - ): Promise> { + ): Promise> { // apply options this.crudQueryHelper.modifyRequest(req, queryOptions); @@ -47,11 +47,8 @@ export class TypeOrmCrudService< // yes, just return return result; } else { - // not an array, add pagination hint - return { - ...result, - __isPaginated: this.decidePagination(req.parsed, req.options), - }; + // not an array, return as is + return result; } } diff --git a/packages/nestjs-crud/src/util/configurable-crud.builder.e2e-spec.ts b/packages/nestjs-crud/src/util/configurable-crud.builder.e2e-spec.ts new file mode 100644 index 000000000..24fce5c9a --- /dev/null +++ b/packages/nestjs-crud/src/util/configurable-crud.builder.e2e-spec.ts @@ -0,0 +1,172 @@ +import supertest from 'supertest'; + +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { SeedingSource } from '@concepta/typeorm-seeding'; + +import { AppCcbModuleFixture } from '../__fixtures__/app-ccb.module.fixture'; +import { AppCcbExtModuleFixture } from '../__fixtures__/app-ccb-ext.module.fixture'; +import { AppCcbSubModuleFixture } from '../__fixtures__/app-ccb-sub.module.fixture'; +import { AppCcbCustomModuleFixture } from '../__fixtures__/app-ccb-custom.module.fixture'; + +import { PhotoFixture } from '../__fixtures__/photo/photo.entity.fixture'; +import { PhotoSeederFixture } from '../__fixtures__/photo/photo.seeder.fixture'; +import { PhotoFactoryFixture } from '../__fixtures__/photo/photo.factory.fixture'; + +describe.each([ + { testModule: AppCcbModuleFixture }, + { testModule: AppCcbExtModuleFixture }, + { testModule: AppCcbCustomModuleFixture }, + { testModule: AppCcbSubModuleFixture }, +])('Configurable Crud Builder (e2e)', ({ testModule }) => { + let app: INestApplication; + let seedingSource: SeedingSource; + + let photoFactory: PhotoFactoryFixture; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [testModule], + }).compile(); + app = moduleFixture.createNestApplication(); + await app.init(); + + const dataSource = app.get(getDataSourceToken()); + seedingSource = new SeedingSource({ dataSource }); + await seedingSource.initialize(); + photoFactory = new PhotoFactoryFixture({ seedingSource }); + await seedingSource.run.one(PhotoSeederFixture); + }); + + afterEach(async () => { + jest.clearAllMocks(); + return app ? await app.close() : undefined; + }); + + it('GET /photo?limit=10', async () => { + const response = await supertest(app.getHttpServer()) + .get('/photo?limit=10') + .expect(200); + + expect(response.body).toBeInstanceOf(Array); + expect(response.body.length).toEqual(10); + + expect(response.body).toBeInstanceOf(Object); + }); + + it('GET /photo?limit=10&page=1', async () => { + const response = await supertest(app.getHttpServer()) + .get('/photo?limit=10&page=1') + .expect(200); + + expect(response.body).toBeInstanceOf(Object); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.data.length).toEqual(10); + expect(response.body.page).toEqual(1); + expect(response.body.pageCount).toEqual(2); + expect(response.body.count).toEqual(10); + expect(response.body.total).toEqual(15); + expect(typeof response.body.data[0].id).toEqual('string'); + }); + + it('GET /photo/:id', async () => { + const photo = await photoFactory.create(); + expect(photo).toBeInstanceOf(PhotoFixture); + + const response = await supertest(app.getHttpServer()) + .get(`/photo/${photo.id}`) + .expect(200); + + expect(response.body).toBeInstanceOf(Object); + }); + + it('POST /photo', async () => { + const photo = await photoFactory.make(); + + const newPhoto: Partial> & + Omit = photo; + + delete newPhoto.id; + + const response = await supertest(app.getHttpServer()) + .post('/photo') + .send(newPhoto) + .expect(201); + + expect(response.body).toBeInstanceOf(Object); + expect(typeof response.body.id).toEqual('string'); + }); + + it('POST /photo/bulk', async () => { + const photos = await photoFactory.createMany(5); + + const response = await supertest(app.getHttpServer()) + .post('/photo/bulk') + .send({ + bulk: photos, + }) + .expect(201); + + expect(response.body).toBeInstanceOf(Array); + expect(response.body.length).toEqual(5); + }); + + it('PATCH /photo/:id', async () => { + const photo = await photoFactory.create(); + expect(photo).toBeInstanceOf(PhotoFixture); + photo.views = 37; + + const { id, ...rest } = { ...photo }; + + const response = await supertest(app.getHttpServer()) + .patch(`/photo/${id}`) + .send(rest) + .expect(200); + + expect(response.body).toMatchObject(photo); + expect(response.body.views).toEqual(37); + }); + + it('PUT /photo/:id', async () => { + const photo = await photoFactory.create(); + expect(photo).toBeInstanceOf(PhotoFixture); + + const { id, ...rest } = { ...photo }; + + const response = await supertest(app.getHttpServer()) + .put(`/photo/${id}`) + .send(rest) + .expect(200); + + expect(response.body).toMatchObject(photo); + }); + + it('DELETE /photo/1', async () => { + const photo = await photoFactory.create(); + expect(photo).toBeInstanceOf(PhotoFixture); + + await supertest(app.getHttpServer()) + .delete(`/photo/${photo.id}`) + .expect(200); + + await supertest(app.getHttpServer()).get(`/photo/${photo.id}`).expect(404); + }); + + it('PATCH /photo/recover/1', async () => { + const photo = await photoFactory.create(); + expect(photo).toBeInstanceOf(PhotoFixture); + + await supertest(app.getHttpServer()) + .delete(`/photo/${photo.id}`) + .expect(200); + + await supertest(app.getHttpServer()).get(`/photo/${photo.id}`).expect(404); + + await supertest(app.getHttpServer()) + .patch(`/photo/recover/${photo.id}`) + .expect(200); + + await supertest(app.getHttpServer()).get(`/photo/${photo.id}`).expect(200); + }); +}); diff --git a/packages/nestjs-crud/src/util/configurable-crud.builder.ts b/packages/nestjs-crud/src/util/configurable-crud.builder.ts new file mode 100644 index 000000000..e0d77c0e0 --- /dev/null +++ b/packages/nestjs-crud/src/util/configurable-crud.builder.ts @@ -0,0 +1,326 @@ +import { DeepPartial, ObjectLiteral, Repository } from 'typeorm'; +import { applyDecorators, Inject, Type } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { CrudCreateMany } from '../decorators/actions/crud-create-many.decorator'; +import { CrudCreateOne } from '../decorators/actions/crud-create-one.decorator'; +import { CrudDeleteOne } from '../decorators/actions/crud-delete-one.decorator'; +import { CrudGetMany } from '../decorators/actions/crud-get-many.decorator'; +import { CrudGetOne } from '../decorators/actions/crud-get-one.decorator'; +import { CrudRecoverOne } from '../decorators/actions/crud-recover-one.decorator'; +import { CrudReplaceOne } from '../decorators/actions/crud-replace-one.decorator'; +import { CrudUpdateOne } from '../decorators/actions/crud-update-one.decorator'; +import { CrudController } from '../decorators/controller/crud-controller.decorator'; +import { CrudBody } from '../decorators/params/crud-body.decorator'; +import { CrudRequest } from '../decorators/params/crud-request.decorator'; +import { CrudRequestInterface } from '../interfaces/crud-request.interface'; +import { CrudCreateManyInterface } from '../interfaces/crud-create-many.interface'; +import { TypeOrmCrudService } from '../services/typeorm-crud.service'; +import { AbstractCrudController } from '../controllers/abstract-crud.controller'; +import { ConfigurableCrudDecorators } from './interfaces/configurable-crud-decorators.interface'; +import { ConfigurableCrudHost } from './interfaces/configurable-crud-host.interface'; +import { ConfigurableCrudOptions } from './interfaces/configurable-crud-options.interface'; + +export class ConfigurableCrudBuilder< + Entity extends ObjectLiteral, + Creatable extends DeepPartial, + Updatable extends DeepPartial, + Replaceable extends Creatable = Creatable, +> { + build( + options: O, + ): ConfigurableCrudHost { + const decorators = this.generateDecorators(options); + const ConfigurableServiceClass = this.generateService( + options.service, + ); + const ConfigurableControllerClass = this.generateClass( + options, + decorators, + ); + + return { + ConfigurableServiceClass, + ConfigurableControllerClass, + ...decorators, + }; + } + + generateDecorators( + options: O, + ): ConfigurableCrudDecorators { + const { + controller, + getMany, + getOne, + createMany, + createOne, + updateOne, + replaceOne, + deleteOne, + recoverOne, + } = options; + + return { + CrudController: applyDecorators( + CrudController(controller), + ...(controller?.extraDecorators ?? []), + ), + CrudGetMany: applyDecorators( + CrudGetMany(getMany), + ...(getMany?.extraDecorators ?? []), + ), + CrudGetOne: applyDecorators( + CrudGetOne(getOne), + ...(getOne?.extraDecorators ?? []), + ), + CrudCreateMany: applyDecorators( + CrudCreateMany(createMany), + ...(createMany?.extraDecorators ?? []), + ), + CrudCreateOne: applyDecorators( + CrudCreateOne(createOne), + ...(createOne?.extraDecorators ?? []), + ), + CrudUpdateOne: applyDecorators( + CrudUpdateOne(updateOne), + ...(updateOne?.extraDecorators ?? []), + ), + CrudReplaceOne: applyDecorators( + CrudReplaceOne(replaceOne), + ...(replaceOne?.extraDecorators ?? []), + ), + CrudDeleteOne: applyDecorators( + CrudDeleteOne(deleteOne), + ...(deleteOne?.extraDecorators ?? []), + ), + CrudRecoverOne: applyDecorators( + CrudRecoverOne(recoverOne), + ...(recoverOne?.extraDecorators ?? []), + ), + }; + } + + generateClass( + options: O, + decorators: ConfigurableCrudDecorators, + ): typeof AbstractCrudController { + const { + CrudController, + CrudGetMany, + CrudGetOne, + CrudCreateMany, + CrudCreateOne, + CrudUpdateOne, + CrudReplaceOne, + CrudDeleteOne, + CrudRecoverOne, + } = decorators; + + class InternalCrudClass extends AbstractCrudController< + Entity, + Creatable, + Updatable, + Replaceable + > { + constructor( + @Inject(options.service.injectionToken) + protected crudService: TypeOrmCrudService, + ) { + super(crudService); + } + } + + if (options?.getMany) { + InternalCrudClass.prototype.getMany = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + ) { + return this.crudService.getMany(crudRequest); + }; + + CrudGetMany( + InternalCrudClass.prototype, + 'getMany', + Object.getOwnPropertyDescriptor(InternalCrudClass.prototype, 'getMany'), + ); + CrudRequest()(InternalCrudClass.prototype, 'getMany', 0); + } + + if (options?.getOne) { + InternalCrudClass.prototype.getOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + ) { + return this.crudService.getOne(crudRequest); + }; + + CrudGetOne( + InternalCrudClass.prototype, + 'getOne', + Object.getOwnPropertyDescriptor(InternalCrudClass.prototype, 'getOne'), + ); + CrudRequest()(InternalCrudClass.prototype, 'getOne', 0); + } + + if (options?.createMany) { + InternalCrudClass.prototype.createMany = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + createManyDto: CrudCreateManyInterface, + ) { + return this.crudService.createMany(crudRequest, createManyDto); + }; + + CrudCreateMany( + InternalCrudClass.prototype, + 'createMany', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'createMany', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'createMany', 0); + CrudBody()(InternalCrudClass.prototype, 'createMany', 1); + } + + if (options?.createOne) { + InternalCrudClass.prototype.createOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + createDto: Creatable, + ) { + return this.crudService.createOne(crudRequest, createDto); + }; + + CrudCreateOne( + InternalCrudClass.prototype, + 'createOne', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'createOne', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'createOne', 0); + CrudBody()(InternalCrudClass.prototype, 'createOne', 1); + } + + if (options?.updateOne) { + InternalCrudClass.prototype.updateOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + updateDto: Updatable, + ) { + return this.crudService.updateOne(crudRequest, updateDto); + }; + + CrudUpdateOne( + InternalCrudClass.prototype, + 'updateOne', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'updateOne', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'updateOne', 0); + CrudBody()(InternalCrudClass.prototype, 'updateOne', 1); + } + + if (options?.replaceOne) { + InternalCrudClass.prototype.replaceOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + replaceDto: Replaceable, + ) { + return this.crudService.replaceOne(crudRequest, replaceDto); + }; + + CrudReplaceOne( + InternalCrudClass.prototype, + 'replaceOne', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'replaceOne', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'replaceOne', 0); + CrudBody()(InternalCrudClass.prototype, 'replaceOne', 1); + } + + if (options?.deleteOne) { + InternalCrudClass.prototype.deleteOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + ) { + return this.crudService.deleteOne(crudRequest); + }; + + CrudDeleteOne( + InternalCrudClass.prototype, + 'deleteOne', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'deleteOne', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'deleteOne', 0); + } + + if (options?.recoverOne) { + InternalCrudClass.prototype.recoverOne = async function ( + this: InternalCrudClass, + crudRequest: CrudRequestInterface, + ) { + return this.crudService.recoverOne(crudRequest); + }; + + CrudRecoverOne( + InternalCrudClass.prototype, + 'recoverOne', + Object.getOwnPropertyDescriptor( + InternalCrudClass.prototype, + 'recoverOne', + ), + ); + CrudRequest()(InternalCrudClass.prototype, 'recoverOne', 0); + } + + CrudController(InternalCrudClass); + + return InternalCrudClass; + } + + generateService( + options: ConfigurableCrudOptions['service'], + ): Type> { + // standard repository injection style + if ('entity' in options && options.entity) { + const { entity } = options; + + class InternalServiceClass extends TypeOrmCrudService { + constructor( + @InjectRepository(entity) + protected readonly repo: Repository, + ) { + super(repo); + } + } + + return InternalServiceClass; + } else { + // EXT repository injection style + const { entityKey } = options; + + class InternalServiceClass extends TypeOrmCrudService { + constructor( + @InjectDynamicRepository(entityKey) + protected readonly repo: Repository, + ) { + super(repo); + } + } + + return InternalServiceClass; + } + } +} diff --git a/packages/nestjs-crud/src/util/crud-is-paginated.helper.ts b/packages/nestjs-crud/src/util/crud-is-paginated.helper.ts new file mode 100644 index 000000000..0a39fa153 --- /dev/null +++ b/packages/nestjs-crud/src/util/crud-is-paginated.helper.ts @@ -0,0 +1,14 @@ +import { CrudResponsePaginatedInterface } from '../interfaces/crud-response-paginated.interface'; + +export function crudIsPaginatedHelper( + response: object, +): response is CrudResponsePaginatedInterface { + return ( + 'data' in response && + Array.isArray(response.data) === true && + 'count' in response && + 'total' in response && + 'page' in response && + 'pageCount' in response + ); +} diff --git a/packages/nestjs-crud/src/util/interfaces/configurable-crud-decorators.interface.ts b/packages/nestjs-crud/src/util/interfaces/configurable-crud-decorators.interface.ts new file mode 100644 index 000000000..205cc2d8d --- /dev/null +++ b/packages/nestjs-crud/src/util/interfaces/configurable-crud-decorators.interface.ts @@ -0,0 +1,13 @@ +import { applyDecorators } from '@nestjs/common'; + +export interface ConfigurableCrudDecorators { + CrudController: ReturnType; + CrudGetMany: ReturnType; + CrudGetOne: ReturnType; + CrudCreateMany: ReturnType; + CrudCreateOne: ReturnType; + CrudUpdateOne: ReturnType; + CrudReplaceOne: ReturnType; + CrudDeleteOne: ReturnType; + CrudRecoverOne: ReturnType; +} diff --git a/packages/nestjs-crud/src/util/interfaces/configurable-crud-host.interface.ts b/packages/nestjs-crud/src/util/interfaces/configurable-crud-host.interface.ts new file mode 100644 index 000000000..b6e114787 --- /dev/null +++ b/packages/nestjs-crud/src/util/interfaces/configurable-crud-host.interface.ts @@ -0,0 +1,20 @@ +import { Type } from '@nestjs/common'; +import { DeepPartial, ObjectLiteral } from 'typeorm'; +import { AbstractCrudController } from '../../controllers/abstract-crud.controller'; +import { TypeOrmCrudService } from '../../services/typeorm-crud.service'; +import { ConfigurableCrudDecorators } from './configurable-crud-decorators.interface'; + +export interface ConfigurableCrudHost< + Entity extends ObjectLiteral, + Creatable extends DeepPartial, + Updatable extends DeepPartial, + Replaceable extends Creatable = Creatable, +> extends ConfigurableCrudDecorators { + ConfigurableControllerClass: typeof AbstractCrudController< + Entity, + Creatable, + Updatable, + Replaceable + >; + ConfigurableServiceClass: Type>; +} diff --git a/packages/nestjs-crud/src/util/interfaces/configurable-crud-options.interface.ts b/packages/nestjs-crud/src/util/interfaces/configurable-crud-options.interface.ts new file mode 100644 index 000000000..dc414007e --- /dev/null +++ b/packages/nestjs-crud/src/util/interfaces/configurable-crud-options.interface.ts @@ -0,0 +1,30 @@ +import { InjectionToken } from '@nestjs/common'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { CrudControllerOptionsInterface } from '../../interfaces/crud-controller-options.interface'; +import { CrudExtraDecoratorsInterface } from '../../interfaces/crud-extra-decorators.interface'; +import { + CrudCreateManyOptionsInterface, + CrudCreateOneOptionsInterface, + CrudDeleteOneOptionsInterface, + CrudReadAllOptionsInterface, + CrudReadOneOptionsInterface, + CrudRecoverOneOptionsInterface, + CrudReplaceOneOptionsInterface, + CrudUpdateOneOptionsInterface, +} from '../../interfaces/crud-route-options.interface'; + +export interface ConfigurableCrudOptions { + service: { injectionToken: InjectionToken } & ( + | { entity: EntityClassOrSchema; entityKey?: never } + | { entityKey: string; entity?: never } + ); + controller: CrudControllerOptionsInterface & CrudExtraDecoratorsInterface; + getMany?: CrudReadAllOptionsInterface & CrudExtraDecoratorsInterface; + getOne?: CrudReadOneOptionsInterface & CrudExtraDecoratorsInterface; + createMany?: CrudCreateManyOptionsInterface & CrudExtraDecoratorsInterface; + createOne?: CrudCreateOneOptionsInterface & CrudExtraDecoratorsInterface; + updateOne?: CrudUpdateOneOptionsInterface & CrudExtraDecoratorsInterface; + replaceOne?: CrudReplaceOneOptionsInterface & CrudExtraDecoratorsInterface; + deleteOne?: CrudDeleteOneOptionsInterface & CrudExtraDecoratorsInterface; + recoverOne?: CrudRecoverOneOptionsInterface & CrudExtraDecoratorsInterface; +}