├── packages ├── crud-typeorm │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── test │ │ ├── __fixture__ │ │ │ ├── notes.service.ts │ │ │ ├── devices.service.ts │ │ │ ├── companies.service.ts │ │ │ ├── projects.service.ts │ │ │ └── users.service.ts │ │ ├── d.crud-auth.spec.ts │ │ └── a.params-options.spec.ts │ ├── package.json │ └── README.md ├── crud │ ├── src │ │ ├── module │ │ │ ├── index.ts │ │ │ └── crud-config.service.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ └── crud-service.abstract.ts │ │ ├── interfaces │ │ │ ├── model-options.interface.ts │ │ │ ├── create-many-dto.interface.ts │ │ │ ├── dto-options.interface.ts │ │ │ ├── get-many-default-response.interface.ts │ │ │ ├── crud-request.interface.ts │ │ │ ├── base-route.interface.ts │ │ │ ├── operators-options.interface.ts │ │ │ ├── serialize-options.interface.ts │ │ │ ├── params-options.interface.ts │ │ │ ├── auth-options.interface.ts │ │ │ ├── index.ts │ │ │ ├── crud-controller.interface.ts │ │ │ ├── query-options.interface.ts │ │ │ ├── crud-global-config.interface.ts │ │ │ ├── crud-options.interface.ts │ │ │ └── routes-options.interface.ts │ │ ├── enums │ │ │ ├── index.ts │ │ │ ├── crud-validation-groups.enum.ts │ │ │ └── crud-actions.enum.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── base-route-name.type.ts │ │ │ └── query-filter-option.type.ts │ │ ├── interceptors │ │ │ ├── index.ts │ │ │ ├── crud-base.interceptor.ts │ │ │ ├── crud-response.interceptor.ts │ │ │ └── crud-request.interceptor.ts │ │ ├── crud │ │ │ ├── index.ts │ │ │ ├── serialize.helper.ts │ │ │ ├── validation.helper.ts │ │ │ └── reflection.helper.ts │ │ ├── decorators │ │ │ ├── parsed-body.decorator.ts │ │ │ ├── crud-auth.decorator.ts │ │ │ ├── index.ts │ │ │ ├── crud.decorator.ts │ │ │ ├── parsed-request.decorator.ts │ │ │ ├── override.decorator.ts │ │ │ └── feature-action.decorator.ts │ │ ├── index.ts │ │ ├── util.ts │ │ └── constants.ts │ ├── test │ │ ├── __fixture__ │ │ │ ├── dto │ │ │ │ ├── index.ts │ │ │ │ ├── test-create.dto.ts │ │ │ │ └── test-update.dto.ts │ │ │ ├── services │ │ │ │ ├── index.ts │ │ │ │ ├── test.service.ts │ │ │ │ └── test-serialize.service.ts │ │ │ ├── models │ │ │ │ ├── index.ts │ │ │ │ ├── test-serialize.model.ts │ │ │ │ ├── test-serialize-2.model.ts │ │ │ │ └── test.model.ts │ │ │ ├── response │ │ │ │ ├── delete-model-response.dto.ts │ │ │ │ ├── index.ts │ │ │ │ ├── recover-model-response.dto.ts │ │ │ │ ├── get-model-response.dto.ts │ │ │ │ └── get-many-model-response.dto.ts │ │ │ └── exception.filter.ts │ │ ├── feature-action.decorator.spec.ts │ │ ├── crud-service.abstract.spec.ts │ │ ├── crud.decorator.soft.spec.ts │ │ ├── crud.decorator.exclude.spec.ts │ │ ├── crud.dto.options.spec.ts │ │ ├── crud.decorator.options.spec.ts │ │ ├── crud-config.service.spec.ts │ │ ├── crud-config.service.global.spec.ts │ │ ├── crud.decorator.override.spec.ts │ │ └── crud.decorator.base.spec.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── crud-request │ ├── src │ │ ├── exceptions │ │ │ ├── index.ts │ │ │ └── request-query.exception.ts │ │ ├── types │ │ │ ├── request-param.types.ts │ │ │ ├── index.ts │ │ │ └── request-query.types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── operators-options.interface.ts │ │ │ ├── index.ts │ │ │ ├── params-options.interface.ts │ │ │ ├── parsed-request.interface.ts │ │ │ ├── request-query-builder-options.interface.ts │ │ │ └── create-query-params.interface.ts │ │ └── request-query.validator.ts │ ├── tsconfig.json │ ├── package.json │ ├── test │ │ └── request.query.validator.spec.ts │ └── README.md └── util │ ├── src │ ├── types │ │ ├── class.type.ts │ │ ├── index.ts │ │ └── object-literal.type.ts │ ├── index.ts │ ├── obj.util.ts │ └── checks.util.ts │ ├── tsconfig.json │ ├── test │ └── obj.util.spec.ts │ ├── package.json │ └── README.md ├── integration ├── crud-typeorm │ ├── constants.ts │ ├── users-profiles │ │ ├── index.ts │ │ └── user-profile.entity.ts │ ├── notes │ │ ├── index.ts │ │ ├── requests │ │ │ ├── index.ts │ │ │ └── create-note.dto.ts │ │ ├── responses │ │ │ ├── index.ts │ │ │ └── get-note-response.dto.ts │ │ ├── note.entity.ts │ │ ├── notes.service.ts │ │ ├── notes.module.ts │ │ └── notes.controller.ts │ ├── users │ │ ├── index.ts │ │ ├── users.service.ts │ │ ├── users.module.ts │ │ ├── me.controller.ts │ │ ├── users.controller.ts │ │ └── user.entity.ts │ ├── devices │ │ ├── index.ts │ │ ├── response │ │ │ ├── index.ts │ │ │ └── delete-device-response.dto.ts │ │ ├── devices.service.ts │ │ ├── devices.module.ts │ │ ├── device.entity.ts │ │ └── devices.controller.ts │ ├── companies │ │ ├── index.ts │ │ ├── requests │ │ │ ├── index.ts │ │ │ └── create-company.dto.ts │ │ ├── responses │ │ │ ├── index.ts │ │ │ └── get-company-response.dto.ts │ │ ├── companies.service.ts │ │ ├── companies.module.ts │ │ ├── companies.controller.ts │ │ └── company.entity.ts │ ├── users-licenses │ │ ├── index.ts │ │ ├── license.entity.ts │ │ └── user-license.entity.ts │ ├── projects │ │ ├── index.ts │ │ ├── projects.service.ts │ │ ├── user-projects.service.ts │ │ ├── projects.module.ts │ │ ├── user-project.entity.ts │ │ ├── projects.controller.ts │ │ ├── my-projects.controller.ts │ │ └── project.entity.ts │ ├── base-entity.ts │ ├── auth.guard.ts │ ├── orm.yaml │ ├── orm.config.ts │ ├── app.module.ts │ └── main.ts └── shared │ └── https-exception.filter.ts ├── img ├── crud-usage2.png ├── nestjsx-logo.png ├── awesome-nest.svg └── awesome-rest.svg ├── tsconfig.jest.json ├── .prettierrc.json ├── lerna.json ├── .gitignore ├── tslint.json ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── docker-compose.yml ├── tsconfig.json ├── LICENSE ├── jest.config.js ├── package-scripts.js ├── CODE_OF_CONDUCT.md └── package.json /packages/crud-typeorm/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typeorm-crud.service'; 2 | -------------------------------------------------------------------------------- /packages/crud/src/module/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-config.service'; 2 | -------------------------------------------------------------------------------- /integration/crud-typeorm/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_REQUEST_KEY = 'user'; 2 | -------------------------------------------------------------------------------- /packages/crud/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-service.abstract'; 2 | -------------------------------------------------------------------------------- /img/crud-usage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rewiko/crud/HEAD/img/crud-usage2.png -------------------------------------------------------------------------------- /img/nestjsx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rewiko/crud/HEAD/img/nestjsx-logo.png -------------------------------------------------------------------------------- /integration/crud-typeorm/users-profiles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-profile.entity'; 2 | -------------------------------------------------------------------------------- /packages/crud-request/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request-query.exception'; 2 | -------------------------------------------------------------------------------- /packages/util/src/types/class.type.ts: -------------------------------------------------------------------------------- 1 | export type ClassType = { 2 | new (...args: any[]): T; 3 | }; 4 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './note.entity'; 2 | export * from './notes.service'; 3 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.entity'; 2 | export * from './users.service'; 3 | -------------------------------------------------------------------------------- /packages/util/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './class.type'; 2 | export * from './object-literal.type'; 3 | -------------------------------------------------------------------------------- /packages/util/src/types/object-literal.type.ts: -------------------------------------------------------------------------------- 1 | export type ObjectLiteral = { 2 | [key: string]: any; 3 | }; 4 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/index.ts: -------------------------------------------------------------------------------- 1 | export * from './device.entity'; 2 | export * from './devices.service'; 3 | -------------------------------------------------------------------------------- /packages/crud-request/src/types/request-param.types.ts: -------------------------------------------------------------------------------- 1 | export type ParamOptionType = 'number' | 'string' | 'uuid'; 2 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/model-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ModelOptions { 2 | type: any; 3 | } 4 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './company.entity'; 2 | export * from './companies.service'; 3 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test-create.dto'; 2 | export * from './test-update.dto'; 3 | -------------------------------------------------------------------------------- /packages/util/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checks.util'; 2 | export * from './obj.util'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/crud-request/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request-query.types'; 2 | export * from './request-param.types'; 3 | -------------------------------------------------------------------------------- /packages/crud/src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-actions.enum'; 2 | export * from './crud-validation-groups.enum'; 3 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/create-many-dto.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateManyDto { 2 | bulk: T[]; 3 | } 4 | -------------------------------------------------------------------------------- /packages/crud/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-route-name.type'; 2 | export * from './query-filter-option.type'; 3 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users-licenses/index.ts: -------------------------------------------------------------------------------- 1 | export * from './license.entity'; 2 | export * from './user-license.entity'; 3 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test.service'; 2 | export * from './test-serialize.service'; 3 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "removeComments": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/crud/src/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-request.interceptor'; 2 | export * from './crud-response.interceptor'; 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/dto-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DtoOptions { 2 | create?: any; 3 | update?: any; 4 | replace?: any; 5 | } 6 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project.entity'; 2 | export * from './user-project.entity'; 3 | export * from './projects.service'; 4 | -------------------------------------------------------------------------------- /packages/crud/src/enums/crud-validation-groups.enum.ts: -------------------------------------------------------------------------------- 1 | export enum CrudValidationGroups { 2 | CREATE = 'CRUD-CREATE', 3 | UPDATE = 'CRUD-UPDATE', 4 | } 5 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/requests/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateNoteDto } from './create-note.dto'; 2 | 3 | export const dto = { 4 | create: CreateNoteDto, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test.model'; 2 | export * from './test-serialize.model'; 3 | export * from './test-serialize-2.model'; 4 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/requests/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateCompanyDto } from './create-company.dto'; 2 | 3 | export const dto = { 4 | create: CreateCompanyDto, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/crud-request/src/exceptions/request-query.exception.ts: -------------------------------------------------------------------------------- 1 | export class RequestQueryException extends Error { 2 | constructor(msg: string) { 3 | super(msg); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/responses/index.ts: -------------------------------------------------------------------------------- 1 | import { GetNoteResponseDto } from './get-note-response.dto'; 2 | 3 | export const serialize = { 4 | get: GetNoteResponseDto, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/util/src/obj.util.ts: -------------------------------------------------------------------------------- 1 | export const objKeys = (val: any): string[] => Object.keys(val); 2 | export const getOwnPropNames = (val: any): string[] => Object.getOwnPropertyNames(val); 3 | -------------------------------------------------------------------------------- /packages/crud/src/crud/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-routes.factory'; 2 | export * from './reflection.helper'; 3 | export * from './swagger.helper'; 4 | export * from './validation.helper'; 5 | -------------------------------------------------------------------------------- /packages/crud-request/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exceptions'; 2 | export * from './request-query.builder'; 3 | export * from './request-query.parser'; 4 | export * from './interfaces'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/get-many-default-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface GetManyDefaultResponse { 2 | data: T[]; 3 | count: number; 4 | total: number; 5 | page: number; 6 | pageCount: number; 7 | } 8 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/response/delete-model-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | 3 | @Exclude() 4 | export class DeleteModelResponseDto { 5 | @Expose() 6 | id: number; 7 | } 8 | -------------------------------------------------------------------------------- /packages/util/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/operators-options.interface.ts: -------------------------------------------------------------------------------- 1 | export type CustomOperatorQuery = (field: string, param: string) => string; 2 | 3 | export interface CustomOperators { 4 | [key: string]: { isArray?: boolean }; 5 | } 6 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-many-model-response.dto'; 2 | export * from './get-model-response.dto'; 3 | export * from './delete-model-response.dto'; 4 | export * from './recover-model-response.dto'; 5 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/parsed-body.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PARSED_BODY_METADATA } from '../constants'; 2 | 3 | export const ParsedBody = () => (target, key, index) => { 4 | Reflect.defineMetadata(PARSED_BODY_METADATA, { index }, target[key]); 5 | }; 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "5.1.12", 8 | "command": { 9 | "publish": { 10 | "message": "chore: release" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/responses/index.ts: -------------------------------------------------------------------------------- 1 | import { SerializeOptions } from '@rewiko/crud'; 2 | import { GetCompanyResponseDto } from './get-company-response.dto'; 3 | 4 | export const serialize: SerializeOptions = { 5 | get: GetCompanyResponseDto, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/response/recover-model-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | 3 | export class RecoverModelResponseDto { 4 | id: number; 5 | name: string; 6 | email: string; 7 | 8 | isActive: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/response/index.ts: -------------------------------------------------------------------------------- 1 | import { SerializeOptions } from '@rewiko/crud'; 2 | import { DeleteDeviceResponseDto } from './delete-device-response.dto'; 3 | 4 | export const serialize: SerializeOptions = { 5 | delete: DeleteDeviceResponseDto, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/crud-request/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib"], 9 | "references": [{ "path": "../util" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/crud/src/types/base-route-name.type.ts: -------------------------------------------------------------------------------- 1 | export type BaseRouteName = 2 | | 'getManyBase' 3 | | 'getOneBase' 4 | | 'createOneBase' 5 | | 'createManyBase' 6 | | 'updateOneBase' 7 | | 'replaceOneBase' 8 | | 'deleteOneBase' 9 | | 'recoverOneBase'; 10 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/response/get-model-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | 3 | export class GetModelResponseDto { 4 | id: number; 5 | name: string; 6 | @Exclude() 7 | email: string; 8 | 9 | isActive: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/requests/create-note.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNumber } from 'class-validator'; 3 | 4 | export class CreateNoteDto { 5 | @ApiProperty({ type: 'number' }) 6 | @IsNumber() 7 | revisionId: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/crud-auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { R } from '../crud/reflection.helper'; 2 | import { AuthOptions } from '../interfaces'; 3 | 4 | export const CrudAuth = (options: AuthOptions) => (target: Object) => { 5 | R.setCrudAuthOptions(options, target); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/crud/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud/crud-routes.factory'; 2 | export * from './decorators'; 3 | export * from './enums'; 4 | export * from './interfaces'; 5 | export * from './types'; 6 | export * from './module'; 7 | export * from './interceptors'; 8 | export * from './services'; 9 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/crud-request.interface.ts: -------------------------------------------------------------------------------- 1 | import { ParsedRequestParams } from '@rewiko/crud-request'; 2 | 3 | import { CrudRequestOptions } from '../interfaces'; 4 | 5 | export interface CrudRequest { 6 | parsed: ParsedRequestParams; 7 | options: CrudRequestOptions; 8 | } 9 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud.decorator'; 2 | export * from './crud-auth.decorator'; 3 | export * from './override.decorator'; 4 | export * from './parsed-request.decorator'; 5 | export * from './parsed-body.decorator'; 6 | export * from './feature-action.decorator'; 7 | -------------------------------------------------------------------------------- /packages/crud/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib"], 9 | "references": [{ "path": "../util" }, { "path": "../crud-request" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request-query-builder-options.interface'; 2 | export * from './params-options.interface'; 3 | export * from './parsed-request.interface'; 4 | export * from './create-query-params.interface'; 5 | export * from './operators-options.interface'; 6 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/models/test-serialize.model.ts: -------------------------------------------------------------------------------- 1 | export class TestSerializeModel { 2 | id: number; 3 | name: string; 4 | email: string; 5 | isActive: boolean; 6 | 7 | constructor(partial: Partial) { 8 | Object.assign(this, partial); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/response/get-many-model-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | 3 | import { GetModelResponseDto } from './get-model-response.dto'; 4 | 5 | export class GetManyModelResponseDto { 6 | @Type(() => GetModelResponseDto) 7 | items: GetModelResponseDto[]; 8 | } 9 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/note.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity('notes') 4 | export class Note { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column({ name: 'revision_id', nullable: false }) 9 | revisionId: number; 10 | } 11 | -------------------------------------------------------------------------------- /packages/crud/src/util.ts: -------------------------------------------------------------------------------- 1 | export function safeRequire(path: string, loader?: () => T): T | null { 2 | try { 3 | /* istanbul ignore next */ 4 | const pack = loader ? loader() : require(path); 5 | return pack; 6 | } catch (_) { 7 | /* istanbul ignore next */ 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /**/node_modules 3 | yarn-error.log 4 | npm-debug.log 5 | 6 | # IDE 7 | /.idea 8 | /.awcache 9 | /.vscode 10 | 11 | # misc 12 | .DS_Store 13 | lerna-debug.log 14 | /.npmrc 15 | 16 | # tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # dist 21 | packages/**/lib 22 | packages/**/tsconfig.tsbuildinfo 23 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/response/delete-device-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Exclude } from 'class-transformer'; 3 | 4 | export class DeleteDeviceResponseDto { 5 | @ApiProperty({ type: 'string' }) 6 | deviceKey: string; 7 | 8 | @Exclude() 9 | description?: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/params-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ParamOptionType } from '../types'; 2 | 3 | export interface ParamsOptions { 4 | [key: string]: ParamOption; 5 | } 6 | 7 | export interface ParamOption { 8 | field?: string; 9 | type?: ParamOptionType; 10 | primary?: boolean; 11 | disabled?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/base-route.interface.ts: -------------------------------------------------------------------------------- 1 | import { RequestMethod } from '@nestjs/common'; 2 | 3 | import { BaseRouteName } from '../types'; 4 | 5 | export interface BaseRoute { 6 | name: BaseRouteName; 7 | path: string; 8 | method: RequestMethod; 9 | enable: boolean; 10 | override: boolean; 11 | withParams: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud-typeorm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib"], 9 | "references": [ 10 | { "path": "../util" }, 11 | { "path": "../crud-request" }, 12 | { "path": "../crud" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/crud/src/enums/crud-actions.enum.ts: -------------------------------------------------------------------------------- 1 | export enum CrudActions { 2 | ReadAll = 'Read-All', 3 | ReadOne = 'Read-One', 4 | CreateOne = 'Create-One', 5 | CreateMany = 'Create-Many', 6 | UpdateOne = 'Update-One', 7 | ReplaceOne = 'Replace-One', 8 | DeleteOne = 'Delete-One', 9 | DeleteAll = 'Delete-All', 10 | RecoverOne = 'Recover-One', 11 | } 12 | -------------------------------------------------------------------------------- /integration/crud-typeorm/base-entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export class BaseEntity { 4 | @PrimaryGeneratedColumn() 5 | id?: number; 6 | 7 | @CreateDateColumn({ nullable: true }) 8 | createdAt?: Date; 9 | 10 | @UpdateDateColumn({ nullable: true }) 11 | updatedAt?: Date; 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/crud.decorator.ts: -------------------------------------------------------------------------------- 1 | import { CrudRoutesFactory } from '../crud'; 2 | import { CrudOptions } from '../interfaces'; 3 | 4 | export const Crud = (options: CrudOptions) => (target: Object) => { 5 | const factoryMethod = options.routesFactory || CrudRoutesFactory; 6 | let factory = new factoryMethod(target, options); 7 | factory = undefined; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/crud/src/types/query-filter-option.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryFilter, 3 | SCondition, 4 | } from '@rewiko/crud-request/lib/types/request-query.types'; 5 | 6 | export type QueryFilterFunction = ( 7 | search?: SCondition, 8 | getMany?: boolean, 9 | ) => SCondition | void; 10 | export type QueryFilterOption = QueryFilter[] | SCondition | QueryFilterFunction; 11 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/responses/get-note-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNumber } from 'class-validator'; 3 | 4 | export class GetNoteResponseDto { 5 | @ApiProperty({ type: 'number' }) 6 | @IsNumber() 7 | id: string; 8 | 9 | @ApiProperty({ type: 'number' }) 10 | @IsNumber() 11 | revisionId: string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/parsed-request.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | import { PARSED_CRUD_REQUEST_KEY } from '../constants'; 4 | import { R } from '../crud/reflection.helper'; 5 | 6 | export const ParsedRequest = createParamDecorator( 7 | (_, ctx): ParameterDecorator => { 8 | return R.getContextRequest(ctx)[PARSED_CRUD_REQUEST_KEY]; 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/operators-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface OperatorsOptions { 2 | custom?: CustomOperators; 3 | } 4 | 5 | export type CustomOperatorQuery = (field: string, param: string) => string; 6 | 7 | export interface CustomOperators { 8 | [key: string]: { 9 | query: CustomOperatorQuery; 10 | params?: { [field: string]: any }; 11 | isArray?: boolean; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/serialize-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | export interface SerializeOptions { 4 | getMany?: Type | false; 5 | get?: Type | false; 6 | create?: Type | false; 7 | createMany?: Type | false; 8 | update?: Type | false; 9 | replace?: Type | false; 10 | delete?: Type | false; 11 | recover?: Type | false; 12 | } 13 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@rewiko/crud-typeorm'; 4 | 5 | import { User } from './user.entity'; 6 | 7 | @Injectable() 8 | export class UsersService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(User) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/override.decorator.ts: -------------------------------------------------------------------------------- 1 | import { BaseRouteName } from '../types/base-route-name.type'; 2 | import { OVERRIDE_METHOD_METADATA } from '../constants'; 3 | 4 | export const Override = (name?: BaseRouteName) => ( 5 | target, 6 | key, 7 | descriptor: PropertyDescriptor, 8 | ) => { 9 | Reflect.defineMetadata(OVERRIDE_METHOD_METADATA, name || `${key}Base`, target[key]); 10 | return descriptor; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/dto/test-create.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | IsEmail, 4 | IsNumber, 5 | IsOptional, 6 | IsNotEmpty, 7 | IsEmpty, 8 | } from 'class-validator'; 9 | 10 | export class TestCreateDto { 11 | @IsString() 12 | firstName: string; 13 | 14 | @IsString() 15 | lastName: string; 16 | 17 | @IsEmail({ require_tld: false }) 18 | email: string; 19 | 20 | @IsNumber() 21 | age: number; 22 | } 23 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/devices.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@rewiko/crud-typeorm'; 4 | 5 | import { Device } from './device.entity'; 6 | 7 | @Injectable() 8 | export class DevicesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Device) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/notes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '../../../packages/crud-typeorm/src'; 4 | 5 | import { Note } from './note.entity'; 6 | 7 | @Injectable() 8 | export class NotesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Note) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "object-literal-sort-keys": false, 7 | "member-access": false, 8 | "no-implicit-dependencies": false, 9 | "member-ordering": false, 10 | "prefer-for-of": false, 11 | "no-submodule-imports": false, 12 | "interface-name": false 13 | }, 14 | "rulesDirectory": [] 15 | } 16 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/companies.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@rewiko/crud-typeorm'; 4 | 5 | import { Company } from './company.entity'; 6 | 7 | @Injectable() 8 | export class CompaniesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Company) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@rewiko/crud-typeorm'; 4 | 5 | import { Project } from './project.entity'; 6 | 7 | @Injectable() 8 | export class ProjectsService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Project) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/params-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { SwaggerEnumType } from '@nestjs/swagger/dist/types/swagger-enum.type'; 2 | import { ParamOptionType } from '@rewiko/crud-request'; 3 | 4 | export interface ParamsOptions { 5 | [key: string]: ParamOption; 6 | } 7 | 8 | export interface ParamOption { 9 | field?: string; 10 | type?: ParamOptionType; 11 | enum?: SwaggerEnumType; 12 | primary?: boolean; 13 | disabled?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/auth-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { SCondition } from '@rewiko/crud-request/lib/types/request-query.types'; 2 | import { ObjectLiteral } from '@rewiko/util'; 3 | 4 | export interface AuthGlobalOptions { 5 | property?: string; 6 | } 7 | 8 | export interface AuthOptions { 9 | property?: string; 10 | filter?: (req: any) => SCondition | void; 11 | or?: (req: any) => SCondition | void; 12 | persist?: (req: any) => ObjectLiteral; 13 | } 14 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/user-projects.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@rewiko/crud-typeorm'; 4 | 5 | import { UserProject } from './user-project.entity'; 6 | 7 | @Injectable() 8 | export class UserProjectsService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(UserProject) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/__fixture__/notes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service'; 5 | import { Note } from '../../../../integration/crud-typeorm/notes'; 6 | 7 | @Injectable() 8 | export class NotesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Note) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/models/test-serialize-2.model.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | 3 | import { TestSerializeModel } from './test-serialize.model'; 4 | 5 | export class TestSerialize2Model extends TestSerializeModel { 6 | id: number; 7 | name: string; 8 | email: string; 9 | 10 | @Exclude() 11 | isActive: boolean; 12 | 13 | constructor(partial: Partial) { 14 | super(partial); 15 | Object.assign(this, partial); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/__fixture__/devices.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service'; 5 | import { Device } from '../../../../integration/crud-typeorm/devices'; 6 | 7 | @Injectable() 8 | export class DevicesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Device) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/notes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Note } from './note.entity'; 5 | import { NotesService } from './notes.service'; 6 | import { NotesController } from './notes.controller'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Note])], 10 | providers: [NotesService], 11 | exports: [NotesService], 12 | controllers: [NotesController], 13 | }) 14 | export class NotesModule {} 15 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users-licenses/license.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../base-entity'; 3 | import { IsOptional, IsString, MaxLength } from 'class-validator'; 4 | 5 | @Entity('licenses') 6 | export class License extends BaseEntity { 7 | @IsOptional({ always: true }) 8 | @IsString({ always: true }) 9 | @MaxLength(32, { always: true }) 10 | @Column({ type: 'varchar', length: 32, nullable: true, default: null }) 11 | name: string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/__fixture__/companies.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service'; 5 | import { Company } from '../../../../integration/crud-typeorm/companies'; 6 | 7 | @Injectable() 8 | export class CompaniesService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Company) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/__fixture__/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service'; 5 | import { Project } from '../../../../integration/crud-typeorm/projects'; 6 | 7 | @Injectable() 8 | export class ProjectsService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(Project) repo) { 10 | super(repo); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/dto/test-update.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | IsEmail, 4 | IsNumber, 5 | IsOptional, 6 | IsNotEmpty, 7 | IsEmpty, 8 | } from 'class-validator'; 9 | 10 | export class TestUpdateDto { 11 | @IsOptional() 12 | @IsString() 13 | firstName?: string; 14 | 15 | @IsOptional() 16 | @IsString() 17 | lastName?: string; 18 | 19 | @IsOptional() 20 | @IsEmail({ require_tld: false }) 21 | email?: string; 22 | 23 | @IsOptional() 24 | @IsNumber() 25 | age?: number; 26 | } 27 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/devices.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Device } from './device.entity'; 5 | import { DevicesService } from './devices.service'; 6 | import { DevicesController } from './devices.controller'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Device])], 10 | providers: [DevicesService], 11 | exports: [DevicesService], 12 | controllers: [DevicesController], 13 | }) 14 | export class DevicesModule {} 15 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/requests/create-company.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString, MaxLength } from 'class-validator'; 3 | 4 | export class CreateCompanyDto { 5 | @ApiProperty({ type: 'string' }) 6 | @IsString() 7 | @MaxLength(100) 8 | name: string; 9 | 10 | @ApiProperty({ type: 'string' }) 11 | @IsString() 12 | @MaxLength(100) 13 | domain: string; 14 | 15 | @ApiProperty({ type: 'string' }) 16 | @IsString() 17 | @MaxLength(100) 18 | description: string; 19 | } 20 | -------------------------------------------------------------------------------- /packages/crud/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const FEAUTURE_NAME_METADATA = 'NESTJSX_FEAUTURE_NAME_METADATA'; 2 | export const ACTION_NAME_METADATA = 'NESTJSX_ACTION_NAME_METADATA'; 3 | export const OVERRIDE_METHOD_METADATA = 'NESTJSX_OVERRIDE_METHOD_METADATA'; 4 | export const PARSED_BODY_METADATA = 'NESTJSX_PARSED_BODY_METADATA'; 5 | export const PARSED_CRUD_REQUEST_KEY = 'NESTJSX_PARSED_CRUD_REQUEST_KEY'; 6 | export const CRUD_OPTIONS_METADATA = 'NESTJSX_CRUD_OPTIONS_METADATA'; 7 | export const CRUD_AUTH_OPTIONS_METADATA = 'NESTJSX_CRUD_AUTH_OPTIONS_METADATA'; 8 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/companies.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Company } from './company.entity'; 5 | import { CompaniesService } from './companies.service'; 6 | import { CompaniesController } from './companies.controller'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Company])], 10 | providers: [CompaniesService], 11 | exports: [CompaniesService], 12 | controllers: [CompaniesController], 13 | }) 14 | export class CompaniesModule {} 15 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/responses/get-company-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Exclude } from 'class-transformer'; 3 | 4 | export class GetCompanyResponseDto { 5 | @ApiProperty({ type: 'number' }) 6 | id: string; 7 | 8 | @ApiProperty({ type: 'string' }) 9 | name: string; 10 | 11 | @ApiProperty({ type: 'string' }) 12 | domain: string; 13 | 14 | @ApiProperty({ type: 'string' }) 15 | description: string; 16 | 17 | @Exclude() 18 | createdAt: any; 19 | 20 | @Exclude() 21 | updatedAt: any; 22 | } 23 | -------------------------------------------------------------------------------- /integration/crud-typeorm/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | 3 | import { UsersService } from './users'; 4 | import { USER_REQUEST_KEY } from './constants'; 5 | 6 | @Injectable() 7 | export class AuthGuard implements CanActivate { 8 | constructor(private usersService: UsersService) {} 9 | 10 | async canActivate(ctx: ExecutionContext): Promise { 11 | const req = ctx.switchToHttp().getRequest(); 12 | req[USER_REQUEST_KEY] = await this.usersService.findOne(1); 13 | 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integration/crud-typeorm/orm.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | type: postgres 3 | host: 127.0.0.1 4 | port: 5455 5 | username: root 6 | password: root 7 | database: nestjsx_crud 8 | entities: 9 | - ./**/*.entity.ts 10 | migrationsTableName: orm_migrations 11 | migrations: 12 | - ./seeds.ts 13 | mysql: 14 | type: mysql 15 | host: 127.0.0.1 16 | port: 3316 17 | username: nestjsx_crud 18 | password: nestjsx_crud 19 | database: nestjsx_crud 20 | entities: 21 | - ./**/*.entity.ts 22 | migrationsTableName: orm_migrations 23 | migrations: 24 | - ./seeds.ts 25 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/parsed-request.interface.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral } from '@rewiko/util'; 2 | import { QueryFields, QueryFilter, QueryJoin, QuerySort, SCondition } from '../types'; 3 | 4 | export interface ParsedRequestParams { 5 | fields: QueryFields; 6 | paramsFilter: QueryFilter[]; 7 | authPersist: ObjectLiteral; 8 | search: SCondition; 9 | filter: QueryFilter[]; 10 | or: QueryFilter[]; 11 | join: QueryJoin[]; 12 | sort: QuerySort[]; 13 | limit: number; 14 | offset: number; 15 | page: number; 16 | cache: number; 17 | includeDeleted: number; 18 | } 19 | -------------------------------------------------------------------------------- /packages/crud/src/decorators/feature-action.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata, Type } from '@nestjs/common'; 2 | 3 | import { ACTION_NAME_METADATA, FEAUTURE_NAME_METADATA } from '../constants'; 4 | 5 | export const Feature = (name: string) => SetMetadata(FEAUTURE_NAME_METADATA, name); 6 | export const Action = (name: string) => SetMetadata(ACTION_NAME_METADATA, name); 7 | 8 | export const getFeature = (target: Type) => 9 | Reflect.getMetadata(FEAUTURE_NAME_METADATA, target); 10 | export const getAction = (target: Function) => 11 | Reflect.getMetadata(ACTION_NAME_METADATA, target); 12 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/request-query-builder-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RequestQueryBuilderOptions { 2 | delim?: string; 3 | delimStr?: string; 4 | paramNamesMap?: { 5 | fields?: string | string[]; 6 | search?: string | string[]; 7 | filter?: string | string[]; 8 | or?: string | string[]; 9 | join?: string | string[]; 10 | sort?: string | string[]; 11 | limit?: string | string[]; 12 | offset?: string | string[]; 13 | page?: string | string[]; 14 | cache?: string | string[]; 15 | includeDeleted?: string | string[]; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: nestjsx 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: npm/@rewiko/crud 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/device.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { IsOptional, IsString, IsUUID } from 'class-validator'; 3 | import { CrudValidationGroups } from '@rewiko/crud'; 4 | 5 | const { CREATE, UPDATE } = CrudValidationGroups; 6 | 7 | @Entity('devices') 8 | export class Device { 9 | @IsOptional({ always: true }) 10 | @IsUUID('4', { always: true }) 11 | @PrimaryGeneratedColumn('uuid') 12 | deviceKey: string; 13 | 14 | @IsOptional({ always: true }) 15 | @IsString({ always: true }) 16 | @Column({ type: 'text', nullable: true }) 17 | description?: string; 18 | } 19 | -------------------------------------------------------------------------------- /integration/crud-typeorm/notes/notes.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Crud } from '@rewiko/crud'; 4 | 5 | import { Note } from './note.entity'; 6 | import { NotesService } from './notes.service'; 7 | import { dto } from './requests'; 8 | import { serialize } from './responses'; 9 | 10 | @Crud({ 11 | model: { type: Note }, 12 | dto, 13 | serialize, 14 | query: { 15 | alwaysPaginate: true, 16 | }, 17 | }) 18 | @ApiTags('notes') 19 | @Controller('/notes') 20 | export class NotesController { 21 | constructor(public service: NotesService) {} 22 | } 23 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { User } from './user.entity'; 5 | import { UserProfile } from '../users-profiles/user-profile.entity'; 6 | import { UsersService } from './users.service'; 7 | import { UsersController } from './users.controller'; 8 | import { MeController } from './me.controller'; 9 | 10 | @Module({ 11 | imports: [TypeOrmModule.forFeature([User, UserProfile])], 12 | providers: [UsersService], 13 | exports: [UsersService], 14 | controllers: [UsersController, MeController], 15 | }) 16 | export class UsersModule {} 17 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users-licenses/user-license.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import { User } from '../users/user.entity'; 3 | import { Type } from 'class-transformer'; 4 | import { License } from './license.entity'; 5 | 6 | @Entity('user_licenses') 7 | export class UserLicense { 8 | @PrimaryColumn() 9 | userId: number; 10 | 11 | @PrimaryColumn() 12 | licenseId: number; 13 | 14 | @ManyToOne((type) => User) 15 | @Type((t) => User) 16 | user: User; 17 | 18 | @ManyToOne((type) => License) 19 | @Type((t) => License) 20 | license: License; 21 | 22 | @Column() 23 | yearsActive: number; 24 | } 25 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/__fixture__/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service'; 5 | import { User } from '../../../../integration/crud-typeorm/users'; 6 | 7 | @Injectable() 8 | export class UsersService extends TypeOrmCrudService { 9 | constructor(@InjectRepository(User) repo) { 10 | super(repo); 11 | } 12 | } 13 | 14 | @Injectable() 15 | export class UsersService2 extends TypeOrmCrudService { 16 | constructor(@InjectRepository(User) repo) { 17 | super(repo); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; 2 | import { RequestQueryException } from '@rewiko/crud-request'; 3 | import { Response } from 'express'; 4 | 5 | @Catch(RequestQueryException) 6 | export class HttpExceptionFilter implements ExceptionFilter { 7 | catch(exception: RequestQueryException, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | 11 | response.status(HttpStatus.BAD_REQUEST).json({ 12 | statusCode: HttpStatus.BAD_REQUEST, 13 | message: exception.message, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/util/test/obj.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { objKeys, getOwnPropNames } from '../src'; 2 | 3 | describe('#util', () => { 4 | describe('#objKeys', () => { 5 | it('should return array of strings', () => { 6 | const obj = { foo: 1, bar: 1 }; 7 | const keys = ['foo', 'bar']; 8 | expect(objKeys(obj)).toMatchObject(keys); 9 | }); 10 | }); 11 | 12 | describe('#getOwnPropNames', () => { 13 | it('should return own properties', () => { 14 | class Parent { 15 | foo = 1; 16 | } 17 | class Child extends Parent { 18 | bar = 1; 19 | } 20 | const expected = ['foo', 'bar']; 21 | expect(getOwnPropNames(new Child())).toMatchObject(expected); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud-controller.interface'; 2 | export * from './crud-options.interface'; 3 | export * from './auth-options.interface'; 4 | export * from './operators-options.interface'; 5 | export * from './params-options.interface'; 6 | export * from './query-options.interface'; 7 | export * from './routes-options.interface'; 8 | export * from './base-route.interface'; 9 | export * from './crud-request.interface'; 10 | export * from './model-options.interface'; 11 | export * from './create-many-dto.interface'; 12 | export * from './get-many-default-response.interface'; 13 | export * from './crud-global-config.interface'; 14 | export * from './dto-options.interface'; 15 | export * from './serialize-options.interface'; 16 | -------------------------------------------------------------------------------- /packages/crud/test/feature-action.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Feature, Action, getFeature, getAction } from '../src/decorators'; 2 | 3 | describe('#crud', () => { 4 | const feature = 'feature'; 5 | const action = 'action'; 6 | 7 | @Feature(feature) 8 | class TestClass { 9 | @Action(action) 10 | root() {} 11 | } 12 | 13 | describe('#feature decorator', () => { 14 | it('should save metadata', () => { 15 | const metadata = getFeature(TestClass); 16 | expect(metadata).toBe(feature); 17 | }); 18 | }); 19 | describe('#action decorator', () => { 20 | it('should save metadata', () => { 21 | const metadata = getAction(TestClass.prototype.root); 22 | expect(metadata).toBe(action); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/crud-controller.interface.ts: -------------------------------------------------------------------------------- 1 | import { CreateManyDto, CrudRequest, GetManyDefaultResponse } from '../interfaces'; 2 | import { CrudService } from '../services'; 3 | 4 | export interface CrudController { 5 | service: CrudService; 6 | getManyBase?(req: CrudRequest): Promise | T[]>; 7 | getOneBase?(req: CrudRequest): Promise; 8 | createOneBase?(req: CrudRequest, dto: T): Promise; 9 | createManyBase?(req: CrudRequest, dto: CreateManyDto): Promise; 10 | updateOneBase?(req: CrudRequest, dto: Partial): Promise; 11 | replaceOneBase?(req: CrudRequest, dto: T): Promise; 12 | deleteOneBase?(req: CrudRequest): Promise; 13 | recoverOneBase?(req: CrudRequest): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /integration/crud-typeorm/devices/devices.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Crud } from '@rewiko/crud'; 4 | 5 | import { Device } from './device.entity'; 6 | import { DevicesService } from './devices.service'; 7 | import { serialize } from './response'; 8 | 9 | @Crud({ 10 | model: { type: Device }, 11 | serialize, 12 | params: { 13 | deviceKey: { 14 | field: 'deviceKey', 15 | type: 'uuid', 16 | primary: true, 17 | }, 18 | }, 19 | routes: { 20 | deleteOneBase: { 21 | returnDeleted: true, 22 | }, 23 | }, 24 | }) 25 | @ApiTags('devices') 26 | @Controller('/devices') 27 | export class DevicesController { 28 | constructor(public service: DevicesService) {} 29 | } 30 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users-profiles/user-profile.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, OneToOne, DeleteDateColumn } from 'typeorm'; 2 | import { IsOptional, IsString, MaxLength } from 'class-validator'; 3 | 4 | import { BaseEntity } from '../base-entity'; 5 | import { User } from '../users/user.entity'; 6 | 7 | @Entity('user_profiles') 8 | export class UserProfile extends BaseEntity { 9 | @IsOptional({ always: true }) 10 | @IsString({ always: true }) 11 | @MaxLength(32, { always: true }) 12 | @Column({ type: 'varchar', length: 32, nullable: true, default: null }) 13 | name: string; 14 | 15 | @DeleteDateColumn({ nullable: true }) 16 | deletedAt?: Date; 17 | 18 | /** 19 | * Relations 20 | */ 21 | 22 | @OneToOne((type) => User, (u) => u.profile) 23 | user?: User; 24 | } 25 | -------------------------------------------------------------------------------- /packages/crud-request/src/interfaces/create-query-params.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryFields, 3 | QueryFilter, 4 | QueryFilterArr, 5 | QueryJoin, 6 | QueryJoinArr, 7 | QuerySort, 8 | QuerySortArr, 9 | SCondition, 10 | } from '../types'; 11 | 12 | export interface CreateQueryParams { 13 | fields?: QueryFields; 14 | search?: SCondition; 15 | filter?: QueryFilter | QueryFilterArr | Array; 16 | or?: QueryFilter | QueryFilterArr | Array; 17 | join?: QueryJoin | QueryJoinArr | Array; 18 | sort?: QuerySort | QuerySortArr | Array; 19 | limit?: number; 20 | offset?: number; 21 | page?: number; 22 | resetCache?: boolean; 23 | includeDeleted?: number; 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | nestjsx_crud: 5 | 6 | services: 7 | postgres: 8 | # TypeORM fails with Postgres v.12 9 | image: postgres:11.5 10 | ports: 11 | - 5455:5432 12 | environment: 13 | POSTGRES_USER: root 14 | POSTGRES_PASSWORD: root 15 | POSTGRES_DB: nestjsx_crud 16 | networks: 17 | - nestjsx_crud 18 | 19 | mysql: 20 | image: mysql:5.7 21 | ports: 22 | - 3316:3306 23 | environment: 24 | MYSQL_DATABASE: nestjsx_crud 25 | MYSQL_USER: nestjsx_crud 26 | MYSQL_PASSWORD: nestjsx_crud 27 | MYSQL_ROOT_PASSWORD: nestjsx_crud 28 | 29 | redis: 30 | image: redis:alpine 31 | ports: 32 | - 6399:6379 33 | command: redis-server 34 | networks: 35 | - nestjsx_crud 36 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/projects.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Project } from './project.entity'; 5 | import { UserProject } from './user-project.entity'; 6 | import { ProjectsService } from './projects.service'; 7 | import { UserProjectsService } from './user-projects.service'; 8 | import { ProjectsController } from './projects.controller'; 9 | import { MyProjectsController } from './my-projects.controller'; 10 | 11 | @Module({ 12 | imports: [TypeOrmModule.forFeature([Project, UserProject])], 13 | providers: [ProjectsService, UserProjectsService], 14 | exports: [ProjectsService, UserProjectsService], 15 | controllers: [ProjectsController, MyProjectsController], 16 | }) 17 | export class ProjectsModule {} 18 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/user-project.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | 3 | import { User } from '../users/user.entity'; 4 | import { Project } from './project.entity'; 5 | 6 | @Entity('user_projects') 7 | export class UserProject { 8 | @PrimaryColumn() 9 | public projectId!: number; 10 | 11 | @PrimaryColumn() 12 | public userId!: number; 13 | 14 | @Column({ nullable: true }) 15 | public review!: string; 16 | 17 | @ManyToOne((type) => Project, (el) => el.userProjects, { 18 | primary: true, 19 | persistence: false, 20 | onDelete: 'CASCADE', 21 | }) 22 | public project: Project; 23 | 24 | @ManyToOne((type) => User, (el) => el.userProjects, { 25 | primary: true, 26 | persistence: false, 27 | }) 28 | public user: User; 29 | } 30 | -------------------------------------------------------------------------------- /integration/crud-typeorm/orm.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | import { isNil } from '@rewiko/util'; 4 | 5 | const type = (process.env.TYPEORM_CONNECTION as any) || 'postgres'; 6 | 7 | export const withCache: TypeOrmModuleOptions = { 8 | type, 9 | host: '127.0.0.1', 10 | port: type === 'postgres' ? 5455 : 3316, 11 | username: type === 'mysql' ? 'nestjsx_crud' : 'root', 12 | password: type === 'mysql' ? 'nestjsx_crud' : 'root', 13 | database: 'nestjsx_crud', 14 | synchronize: false, 15 | logging: !isNil(process.env.TYPEORM_LOGGING) 16 | ? !!parseInt(process.env.TYPEORM_LOGGING, 10) 17 | : true, 18 | cache: { 19 | type: 'redis', 20 | options: { 21 | host: '127.0.0.1', 22 | port: 6399, 23 | }, 24 | }, 25 | entities: [join(__dirname, './**/*.entity{.ts,.js}')], 26 | }; 27 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/query-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryFields, 3 | QuerySort, 4 | } from '@rewiko/crud-request/lib/types/request-query.types'; 5 | 6 | import { QueryFilterOption } from '../types'; 7 | 8 | export interface QueryOptions { 9 | allow?: QueryFields; 10 | exclude?: QueryFields; 11 | persist?: QueryFields; 12 | filter?: QueryFilterOption; 13 | join?: JoinOptions; 14 | sort?: QuerySort[]; 15 | limit?: number; 16 | maxLimit?: number; 17 | cache?: number | false; 18 | alwaysPaginate?: boolean; 19 | softDelete?: boolean; 20 | } 21 | 22 | export interface JoinOptions { 23 | [key: string]: JoinOption; 24 | } 25 | 26 | export interface JoinOption { 27 | alias?: string; 28 | allow?: QueryFields; 29 | eager?: boolean; 30 | exclude?: QueryFields; 31 | persist?: QueryFields; 32 | select?: boolean; 33 | required?: boolean; 34 | } 35 | -------------------------------------------------------------------------------- /integration/crud-typeorm/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | 5 | import { AuthGuard } from './auth.guard'; 6 | import { withCache } from './orm.config'; 7 | import { CompaniesModule } from './companies/companies.module'; 8 | import { ProjectsModule } from './projects/projects.module'; 9 | import { UsersModule } from './users/users.module'; 10 | import { DevicesModule } from './devices/devices.module'; 11 | import { NotesModule } from './notes/notes.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forRoot(withCache), 16 | CompaniesModule, 17 | ProjectsModule, 18 | UsersModule, 19 | DevicesModule, 20 | NotesModule, 21 | ], 22 | providers: [ 23 | { 24 | provide: APP_GUARD, 25 | useClass: AuthGuard, 26 | }, 27 | ], 28 | }) 29 | export class AppModule {} 30 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/me.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Crud, CrudAuth } from '@rewiko/crud'; 4 | 5 | import { User } from './user.entity'; 6 | import { UsersService } from './users.service'; 7 | 8 | @Crud({ 9 | model: { 10 | type: User, 11 | }, 12 | routes: { 13 | only: ['getOneBase', 'updateOneBase'], 14 | }, 15 | params: { 16 | id: { 17 | primary: true, 18 | disabled: true, 19 | }, 20 | }, 21 | query: { 22 | join: { 23 | company: { 24 | eager: true, 25 | }, 26 | profile: { 27 | eager: true, 28 | }, 29 | }, 30 | }, 31 | }) 32 | @CrudAuth({ 33 | property: "user", 34 | filter: (user: User) => ({ 35 | id: user.id, 36 | }), 37 | }) 38 | @ApiTags('me') 39 | @Controller('me') 40 | export class MeController { 41 | constructor(public service: UsersService) {} 42 | } 43 | -------------------------------------------------------------------------------- /packages/crud/src/interceptors/crud-base.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '@nestjs/common'; 2 | import { R } from '../crud/reflection.helper'; 3 | import { CrudActions } from '../enums'; 4 | import { MergedCrudOptions } from '../interfaces'; 5 | 6 | export class CrudBaseInterceptor { 7 | protected getCrudInfo( 8 | context: ExecutionContext, 9 | ): { 10 | ctrlOptions: MergedCrudOptions; 11 | crudOptions: Partial; 12 | action: CrudActions; 13 | } { 14 | const ctrl = context.getClass(); 15 | const handler = context.getHandler(); 16 | const ctrlOptions = R.getCrudOptions(ctrl); 17 | const crudOptions = ctrlOptions 18 | ? ctrlOptions 19 | : { 20 | query: {}, 21 | routes: {}, 22 | params: {}, 23 | operators: {}, 24 | }; 25 | const action = R.getAction(handler); 26 | 27 | return { ctrlOptions, crudOptions, action }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/projects.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | 4 | import { Crud, OperatorsOptions, CustomOperators } from '@rewiko/crud'; 5 | 6 | import { Project } from './project.entity'; 7 | import { ProjectsService } from './projects.service'; 8 | 9 | @Crud({ 10 | model: { 11 | type: Project, 12 | }, 13 | params: { 14 | companyId: { 15 | field: 'companyId', 16 | type: 'number', 17 | }, 18 | id: { 19 | field: 'id', 20 | type: 'number', 21 | primary: true, 22 | }, 23 | }, 24 | query: { 25 | join: { 26 | users: {}, 27 | }, 28 | }, 29 | operators: { 30 | custom: { custom: {query: (field: string, param: string) => `${field} = :${param}`}} 31 | } 32 | }) 33 | @ApiTags('projects') 34 | @Controller('/companies/:companyId/projects') 35 | export class ProjectsController { 36 | constructor(public service: ProjectsService) {} 37 | } 38 | -------------------------------------------------------------------------------- /packages/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rewiko/util", 3 | "description": "NestJs CRUD for RESTful APIs - util", 4 | "version": "5.1.12", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "publishConfig": { 9 | "access": "public", 10 | "registry": "https://registry.npmjs.org/" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rewiko/crud.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rewiko/crud/issues" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "typeorm", 25 | "nest", 26 | "nestjs", 27 | "rest", 28 | "restful", 29 | "api", 30 | "crud", 31 | "crud-generator" 32 | ], 33 | "author": { 34 | "name": "Michael Yali", 35 | "email": "mihon4ik@gmail.com" 36 | }, 37 | "scripts": { 38 | "build": "npx tsc -b" 39 | }, 40 | "gitHead": "4ced7579a2a4474611088fcc5f3c0a7200c73c09" 41 | } 42 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/crud-global-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { RequestQueryBuilderOptions } from '@rewiko/crud-request'; 2 | 3 | import { AuthGlobalOptions } from './auth-options.interface'; 4 | import { OperatorsOptions } from './operators-options.interface'; 5 | import { ParamsOptions } from './params-options.interface'; 6 | import { RoutesOptions } from './routes-options.interface'; 7 | 8 | export interface CrudGlobalConfig { 9 | queryParser?: RequestQueryBuilderOptions; 10 | auth?: AuthGlobalOptions; 11 | routes?: RoutesOptions; 12 | params?: ParamsOptions; 13 | operators?: OperatorsOptions; 14 | query?: { 15 | limit?: number; 16 | maxLimit?: number; 17 | cache?: number | false; 18 | alwaysPaginate?: boolean; 19 | softDelete?: boolean; 20 | }; 21 | serialize?: { 22 | getMany?: false; 23 | get?: false; 24 | create?: false; 25 | createMany?: false; 26 | update?: false; 27 | replace?: false; 28 | delete?: false; 29 | recover?: false; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/my-projects.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Crud, CrudAuth } from '@rewiko/crud'; 4 | 5 | import { User } from '../users/user.entity'; 6 | import { UserProject } from './user-project.entity'; 7 | import { UserProjectsService } from './user-projects.service'; 8 | 9 | @Crud({ 10 | model: { 11 | type: UserProject, 12 | }, 13 | params: { 14 | projectId: { 15 | field: 'projectId', 16 | type: 'number', 17 | primary: true, 18 | }, 19 | }, 20 | query: { 21 | join: { 22 | project: { 23 | eager: true, 24 | }, 25 | }, 26 | }, 27 | }) 28 | @CrudAuth({ 29 | filter: (user: User) => ({ 30 | userId: user.id, 31 | }), 32 | persist: (user: User) => ({ 33 | userId: user.id, 34 | }), 35 | }) 36 | @ApiTags('my-projects') 37 | @Controller('my-projects') 38 | export class MyProjectsController { 39 | constructor(public service: UserProjectsService) {} 40 | } 41 | -------------------------------------------------------------------------------- /integration/shared/https-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { HttpException, InternalServerErrorException } from '@nestjs/common'; 3 | 4 | @Catch() 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | catch(exception: HttpException, host: ArgumentsHost) { 7 | const ctx = host.switchToHttp(); 8 | const response = ctx.getResponse(); 9 | const { status, json } = this.prepareException(exception); 10 | 11 | response.status(status).send(json); 12 | } 13 | 14 | prepareException(exc: any): { status: number; json: object } { 15 | if (process.env.NODE_ENV !== 'test') { 16 | console.log(exc); 17 | } 18 | 19 | const error = 20 | exc instanceof HttpException ? exc : new InternalServerErrorException(exc.message); 21 | const status = error.getStatus(); 22 | const response = error.getResponse(); 23 | const json = typeof response === 'string' ? { error: response } : response; 24 | 25 | return { status, json }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/models/test.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | IsEmail, 4 | IsNumber, 5 | IsOptional, 6 | IsNotEmpty, 7 | IsEmpty, 8 | } from 'class-validator'; 9 | 10 | import { CrudValidationGroups } from '../../../src'; 11 | 12 | const { CREATE, UPDATE } = CrudValidationGroups; 13 | 14 | export class TestModel { 15 | @IsEmpty({ groups: [CREATE] }) 16 | @IsNumber({}, { groups: [UPDATE] }) 17 | id?: number; 18 | 19 | @IsOptional({ groups: [UPDATE] }) 20 | @IsNotEmpty({ groups: [CREATE] }) 21 | @IsString({ always: true }) 22 | firstName?: string; 23 | 24 | @IsOptional({ groups: [UPDATE] }) 25 | @IsNotEmpty({ groups: [CREATE] }) 26 | @IsString({ always: true }) 27 | lastName?: string; 28 | 29 | @IsOptional({ groups: [UPDATE] }) 30 | @IsNotEmpty({ groups: [CREATE] }) 31 | @IsEmail({ require_tld: false }, { always: true }) 32 | email?: string; 33 | 34 | @IsOptional({ groups: [UPDATE] }) 35 | @IsNotEmpty({ groups: [CREATE] }) 36 | @IsNumber({}, { always: true }) 37 | age?: number; 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "noUnusedLocals": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es2018", 12 | "sourceMap": true, 13 | "allowJs": false, 14 | "skipLibCheck": false, 15 | "composite": true, 16 | "baseUrl": "./packages", 17 | "paths": { 18 | "@rewiko/crud": ["crud/src"], 19 | "@rewiko/crud-typeorm": ["crud-typeorm/src"], 20 | "@rewiko/crud-request": ["crud-request/src"], 21 | "@rewiko/util": ["util/src"], 22 | "@rewiko/crud/*": ["crud/src/*"], 23 | "@rewiko/crud-typeorm/*": ["crud-typeorm/src/*"], 24 | "@rewiko/crud-request/*": ["crud-request/src/*"], 25 | "@rewiko/util/*": ["util/src/*"] 26 | } 27 | }, 28 | "references": [ 29 | { "path": "crud" }, 30 | { "path": "crud-typeorm" }, 31 | { "path": "crud-request" }, 32 | { "path": "util" } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/crud-typeorm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rewiko/crud-typeorm", 3 | "description": "NestJs CRUD for RESTful APIs - TypeORM", 4 | "version": "5.1.12", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "publishConfig": { 9 | "access": "public", 10 | "registry": "https://registry.npmjs.org/" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rewiko/crud.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rewiko/crud/issues" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "typeorm", 25 | "nest", 26 | "nestjs", 27 | "rest", 28 | "restful", 29 | "api", 30 | "crud", 31 | "crud-generator", 32 | "backend", 33 | "frameworks" 34 | ], 35 | "author": { 36 | "name": "Michael Yali", 37 | "email": "mihon4ik@gmail.com" 38 | }, 39 | "scripts": { 40 | "build": "npx tsc -b" 41 | }, 42 | "dependencies": { 43 | "@zmotivat0r/o0": "^1.0.2" 44 | }, 45 | "gitHead": "4ced7579a2a4474611088fcc5f3c0a7200c73c09" 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2018-Present Michael Yali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /integration/crud-typeorm/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 3 | import { CrudConfigService } from '@rewiko/crud'; 4 | import { USER_REQUEST_KEY } from './constants'; 5 | 6 | // Important: load config before (!!!) you import AppModule 7 | // https://github.com/rewiko/crud/wiki/Controllers#global-options 8 | CrudConfigService.load({ 9 | auth: { 10 | property: USER_REQUEST_KEY, 11 | }, 12 | routes: { 13 | // exclude: ['createManyBase'], 14 | }, 15 | }); 16 | 17 | import { HttpExceptionFilter } from '../shared/https-exception.filter'; 18 | import { AppModule } from './app.module'; 19 | 20 | async function bootstrap() { 21 | const app = await NestFactory.create(AppModule); 22 | 23 | app.useGlobalFilters(new HttpExceptionFilter()); 24 | 25 | const options = new DocumentBuilder() 26 | .setTitle('@rewiko/crud-typeorm') 27 | .setDescription('@rewiko/crud-typeorm') 28 | .setVersion('1.0') 29 | .build(); 30 | const document = SwaggerModule.createDocument(app, options); 31 | SwaggerModule.setup('docs', app, document); 32 | 33 | await app.listen(process.env.PORT || 3000); 34 | } 35 | 36 | bootstrap(); 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const tsconfig = require('tsconfig-extends'); 2 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 3 | const compilerOptions = tsconfig.load_file_sync('./tsconfig.jest.json', __dirname); 4 | 5 | module.exports = { 6 | setupFilesAfterEnv: ['jest-extended'], 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '/packages/', 9 | }), 10 | moduleFileExtensions: ['ts', 'js'], 11 | testRegex: '\\.spec.ts$', 12 | rootDir: '.', 13 | transform: { 14 | '^.+\\.ts$': 'ts-jest', 15 | }, 16 | globals: { 17 | 'ts-jest': { 18 | tsConfig: 'tsconfig.jest.json', 19 | }, 20 | }, 21 | coverageReporters: ['json', 'lcov', 'text-summary'], 22 | coverageDirectory: 'coverage', 23 | collectCoverageFrom: [ 24 | 'packages/**/*.ts', 25 | '!packages/**/*.d.ts', 26 | '!packages/**/index.ts', 27 | '!packages/**/*.interface.ts', 28 | '!**/node_modules/**', 29 | '!**/__stubs__/**', 30 | '!**/__fixture__/**', 31 | '!integration/*', 32 | ], 33 | coverageThreshold: { 34 | global: { 35 | branches: 97, 36 | functions: 97, 37 | lines: 98, 38 | statements: 98, 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/companies.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Crud } from '@rewiko/crud'; 4 | 5 | import { Company } from './company.entity'; 6 | import { CompaniesService } from './companies.service'; 7 | import { serialize } from './responses'; 8 | 9 | @Crud({ 10 | model: { 11 | type: Company 12 | }, 13 | serialize, 14 | routes: { 15 | deleteOneBase: { 16 | returnDeleted: false, 17 | }, 18 | }, 19 | query: { 20 | alwaysPaginate: false, 21 | softDelete: true, 22 | allow: ['name'], 23 | join: { 24 | users: { 25 | alias: 'companyUsers', 26 | exclude: ['email'], 27 | eager: true, 28 | }, 29 | 'users.projects': { 30 | eager: true, 31 | alias: 'usersProjects', 32 | allow: ['name'], 33 | }, 34 | 'users.projects.company': { 35 | eager: true, 36 | alias: 'usersProjectsCompany', 37 | }, 38 | projects: { 39 | eager: true, 40 | select: false, 41 | }, 42 | }, 43 | }, 44 | }) 45 | @ApiTags('companies') 46 | @Controller('companies') 47 | export class CompaniesController { 48 | constructor(public service: CompaniesService) {} 49 | } 50 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/crud-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipeOptions } from '@nestjs/common'; 2 | 3 | import { AuthOptions } from './auth-options.interface'; 4 | import { DtoOptions } from './dto-options.interface'; 5 | import { CrudRoutesFactory } from '../crud'; 6 | import { ModelOptions } from './model-options.interface'; 7 | import { OperatorsOptions } from './operators-options.interface'; 8 | import { ParamsOptions } from './params-options.interface'; 9 | import { QueryOptions } from './query-options.interface'; 10 | import { RoutesOptions } from './routes-options.interface'; 11 | import { SerializeOptions } from './serialize-options.interface'; 12 | 13 | export interface CrudRequestOptions { 14 | query?: QueryOptions; 15 | routes?: RoutesOptions; 16 | params?: ParamsOptions; 17 | operators?: OperatorsOptions; 18 | } 19 | 20 | export interface CrudOptions { 21 | model: ModelOptions; 22 | dto?: DtoOptions; 23 | serialize?: SerializeOptions; 24 | query?: QueryOptions; 25 | routes?: RoutesOptions; 26 | routesFactory?: typeof CrudRoutesFactory; 27 | params?: ParamsOptions; 28 | validation?: ValidationPipeOptions | false; 29 | operators?: OperatorsOptions; 30 | } 31 | 32 | export interface MergedCrudOptions extends CrudOptions { 33 | auth?: AuthOptions; 34 | } 35 | -------------------------------------------------------------------------------- /packages/crud/src/crud/serialize.helper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { GetManyDefaultResponse } from '../interfaces'; 3 | import { ApiProperty } from './swagger.helper'; 4 | 5 | export class SerializeHelper { 6 | static createGetManyDto(dto: any, resourceName: string): any { 7 | class GetManyResponseDto implements GetManyDefaultResponse { 8 | @ApiProperty({ type: dto, isArray: true }) 9 | @Type(() => dto) 10 | data: any[]; 11 | 12 | @ApiProperty({ type: 'number' }) 13 | count: number; 14 | 15 | @ApiProperty({ type: 'number' }) 16 | total: number; 17 | 18 | @ApiProperty({ type: 'number' }) 19 | page: number; 20 | 21 | @ApiProperty({ type: 'number' }) 22 | pageCount: number; 23 | } 24 | 25 | Object.defineProperty(GetManyResponseDto, 'name', { 26 | writable: false, 27 | value: `GetMany${resourceName}ResponseDto`, 28 | }); 29 | 30 | return GetManyResponseDto; 31 | } 32 | 33 | static createGetOneResponseDto(resourceName: string): any { 34 | class GetOneResponseDto {} 35 | 36 | Object.defineProperty(GetOneResponseDto, 'name', { 37 | writable: false, 38 | value: `${resourceName}ResponseDto`, 39 | }); 40 | 41 | return GetOneResponseDto; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/crud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rewiko/crud", 3 | "description": "NestJs CRUD for RESTful APIs", 4 | "version": "5.1.12", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "publishConfig": { 9 | "access": "public", 10 | "registry": "https://registry.npmjs.org/" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rewiko/crud.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rewiko/crud/issues" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "typeorm", 25 | "nest", 26 | "nestjs", 27 | "rest", 28 | "restful", 29 | "api", 30 | "crud", 31 | "crud-generator", 32 | "backend", 33 | "frameworks" 34 | ], 35 | "author": { 36 | "name": "Michael Yali", 37 | "email": "mihon4ik@gmail.com" 38 | }, 39 | "scripts": { 40 | "build": "npx tsc -b" 41 | }, 42 | "dependencies": { 43 | "@rewiko/crud-request": "^5.1.12", 44 | "@rewiko/util": "^5.1.12", 45 | "deepmerge": "^3.2.0", 46 | "pluralize": "^8.0.0" 47 | }, 48 | "peerDependencies": { 49 | "class-transformer": "*", 50 | "class-validator": "*" 51 | }, 52 | "gitHead": "4ced7579a2a4474611088fcc5f3c0a7200c73c09" 53 | } 54 | -------------------------------------------------------------------------------- /packages/crud-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rewiko/crud-request", 3 | "description": "NestJs CRUD for RESTful APIs - request query builder", 4 | "version": "5.1.12", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "publishConfig": { 9 | "access": "public", 10 | "registry": "https://registry.npmjs.org/" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rewiko/crud.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rewiko/crud/issues" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "typeorm", 25 | "nest", 26 | "nestjs", 27 | "rest", 28 | "restful", 29 | "api", 30 | "crud", 31 | "crud-generator", 32 | "http", 33 | "request", 34 | "request-query", 35 | "requestquery", 36 | "get", 37 | "query", 38 | "query-string", 39 | "querystring", 40 | "query-builder", 41 | "querybuilder" 42 | ], 43 | "author": { 44 | "name": "Michael Yali", 45 | "email": "mihon4ik@gmail.com" 46 | }, 47 | "scripts": { 48 | "build": "npx tsc -b" 49 | }, 50 | "dependencies": { 51 | "@rewiko/util": "^5.1.12", 52 | "qs": "^6.8.0" 53 | }, 54 | "gitHead": "4ced7579a2a4474611088fcc5f3c0a7200c73c09" 55 | } 56 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/services/test.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ParsedRequestParams } from '@rewiko/crud-request'; 3 | import { CrudRequestOptions } from '../../../src/interfaces'; 4 | 5 | import { CreateManyDto, CrudRequest } from '../../../src/interfaces'; 6 | import { CrudService } from '../../../src/services'; 7 | 8 | @Injectable() 9 | export class TestService extends CrudService { 10 | async getMany(req: CrudRequest): Promise { 11 | return { req }; 12 | } 13 | async getOne(req: CrudRequest): Promise { 14 | return { req }; 15 | } 16 | async createOne(req: CrudRequest, dto: T): Promise { 17 | return { req, dto }; 18 | } 19 | async createMany(req: CrudRequest, dto: CreateManyDto): Promise { 20 | return { req, dto }; 21 | } 22 | async updateOne(req: CrudRequest, dto: T): Promise { 23 | return { req, dto }; 24 | } 25 | async replaceOne(req: CrudRequest, dto: T): Promise { 26 | return { req, dto }; 27 | } 28 | async deleteOne(req: CrudRequest): Promise { 29 | return { req }; 30 | } 31 | async recoverOne(req: CrudRequest): Promise { 32 | return { req }; 33 | } 34 | decidePagination(parsed: ParsedRequestParams, options: CrudRequestOptions): boolean { 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { 4 | Crud, 5 | CrudController, 6 | CrudRequest, 7 | ParsedRequest, 8 | Override, 9 | } from '@rewiko/crud'; 10 | 11 | import { User } from './user.entity'; 12 | import { UsersService } from './users.service'; 13 | 14 | @Crud({ 15 | model: { 16 | type: User, 17 | }, 18 | params: { 19 | companyId: { 20 | field: 'companyId', 21 | type: 'number', 22 | }, 23 | id: { 24 | field: 'id', 25 | type: 'number', 26 | primary: true, 27 | }, 28 | }, 29 | query: { 30 | softDelete: true, 31 | join: { 32 | company: { 33 | exclude: ['description'], 34 | }, 35 | 'company.projects': { 36 | alias: 'pr', 37 | exclude: ['description'], 38 | }, 39 | profile: { 40 | eager: true, 41 | exclude: ['updatedAt'], 42 | }, 43 | }, 44 | }, 45 | }) 46 | @ApiTags('users') 47 | @Controller('/companies/:companyId/users') 48 | export class UsersController implements CrudController { 49 | constructor(public service: UsersService) {} 50 | 51 | get base(): CrudController { 52 | return this; 53 | } 54 | 55 | @Override('getManyBase') 56 | getAll(@ParsedRequest() req: CrudRequest) { 57 | return this.base.getManyBase(req); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | const utils = require('nps-utils'); 2 | 3 | const getSeries = (args) => utils.series.nps(...args); 4 | const names = ['util', 'crud-request', 'crud', 'crud-typeorm']; 5 | 6 | const getBuildCmd = (pkg) => { 7 | const str = 'npx lerna run build'; 8 | const scoped = (name) => `--scope @rewiko/${name}`; 9 | return pkg ? `${str} ${scoped(pkg)}` : getSeries(names.map((name) => `build.${name}`)); 10 | }; 11 | 12 | const getCleanCmd = (pkg) => { 13 | const cmd = `npx rimraf ./packages/${pkg}/lib && npx rimraf ./packages/${pkg}/tsconfig.tsbuildinfo`; 14 | return pkg ? cmd : getSeries(names.map((name) => `clean.${name}`)); 15 | }; 16 | 17 | const getTestCmd = (pkg, coverage) => 18 | `npx jest --runInBand -c=jest.config.js packages/${pkg ? pkg + '/' : ''} ${ 19 | coverage ? '--coverage' : '' 20 | } --verbose`; 21 | 22 | const setBuild = () => 23 | names.reduce((a, c) => ({ ...a, [c]: getBuildCmd(c) }), { 24 | default: getBuildCmd(), 25 | }); 26 | 27 | const setClean = () => 28 | names.reduce((a, c) => ({ ...a, [c]: getCleanCmd(c) }), { 29 | default: getCleanCmd(), 30 | }); 31 | 32 | const setTest = () => 33 | names.reduce((a, c) => ({ ...a, [c]: getTestCmd(c) }), { 34 | default: getTestCmd(false, true), 35 | coveralls: getTestCmd(false, true) + ' --coverageReporters=text-lcov | coveralls', 36 | }); 37 | 38 | module.exports = { 39 | scripts: { 40 | test: setTest(), 41 | build: setBuild(), 42 | clean: setClean(), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/crud/src/interfaces/routes-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { BaseRouteName } from '../types'; 2 | 3 | export interface RoutesOptions { 4 | exclude?: BaseRouteName[]; 5 | only?: BaseRouteName[]; 6 | getManyBase?: GetManyRouteOptions; 7 | getOneBase?: GetOneRouteOptions; 8 | createOneBase?: CreateOneRouteOptions; 9 | createManyBase?: CreateManyRouteOptions; 10 | updateOneBase?: UpdateOneRouteOptions; 11 | replaceOneBase?: ReplaceOneRouteOptions; 12 | deleteOneBase?: DeleteOneRouteOptions; 13 | recoverOneBase?: RecoverOneRouteOptions; 14 | } 15 | 16 | export interface BaseRouteOptions { 17 | interceptors?: any[]; 18 | decorators?: (PropertyDecorator | MethodDecorator)[]; 19 | } 20 | 21 | export interface GetManyRouteOptions extends BaseRouteOptions {} 22 | 23 | export interface GetOneRouteOptions extends BaseRouteOptions {} 24 | 25 | export interface CreateOneRouteOptions extends BaseRouteOptions { 26 | returnShallow?: boolean; 27 | } 28 | 29 | export interface CreateManyRouteOptions extends BaseRouteOptions {} 30 | 31 | export interface ReplaceOneRouteOptions extends BaseRouteOptions { 32 | allowParamsOverride?: boolean; 33 | returnShallow?: boolean; 34 | } 35 | 36 | export interface UpdateOneRouteOptions extends BaseRouteOptions { 37 | allowParamsOverride?: boolean; 38 | returnShallow?: boolean; 39 | } 40 | 41 | export interface DeleteOneRouteOptions extends BaseRouteOptions { 42 | returnDeleted?: boolean; 43 | } 44 | 45 | export interface RecoverOneRouteOptions extends BaseRouteOptions { 46 | returnRecovered?: boolean; 47 | } 48 | -------------------------------------------------------------------------------- /packages/crud/test/crud-service.abstract.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, NotFoundException } from '@nestjs/common'; 2 | 3 | import { TestService } from './__fixture__/services'; 4 | 5 | describe('#crud', () => { 6 | describe('#CrudService', () => { 7 | let service: TestService; 8 | 9 | beforeAll(() => { 10 | service = new TestService(); 11 | }); 12 | 13 | describe('#throwBadRequestException', () => { 14 | it('should throw BadRequestException', () => { 15 | expect(service.throwBadRequestException.bind(service, '')).toThrowError( 16 | BadRequestException, 17 | ); 18 | }); 19 | }); 20 | 21 | describe('#throwNotFoundException', () => { 22 | it('should throw NotFoundException', () => { 23 | expect(service.throwNotFoundException.bind(service, '')).toThrowError( 24 | NotFoundException, 25 | ); 26 | }); 27 | }); 28 | 29 | describe('#createPageInfo', () => { 30 | it('should return an object', () => { 31 | const expected = { 32 | count: 0, 33 | data: [], 34 | page: 2, 35 | pageCount: 10, 36 | total: 100, 37 | }; 38 | expect(service.createPageInfo([], 100, 10, 10)).toMatchObject(expected); 39 | }); 40 | 41 | it('should return an object when limit and offset undefined', () => { 42 | const expected = { 43 | count: 0, 44 | data: [], 45 | page: 1, 46 | pageCount: 1, 47 | total: 100, 48 | }; 49 | expect(service.createPageInfo([], 100, undefined, undefined)).toMatchObject( 50 | expected, 51 | ); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/crud-request/test/request.query.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequestQueryException } from '@rewiko/crud-request'; 2 | import { validateComparisonOperator, validateUUID } from '../src/request-query.validator'; 3 | 4 | describe('#request-query', () => { 5 | describe('#validator', () => { 6 | describe('#validateUUID', () => { 7 | const uuid = 'cf0917fc-af7d-11e9-a2a3-2a2ae2dbcce4'; 8 | const uuidV4 = '6650aad9-29bd-4601-b9b1-543a7a2d2d54'; 9 | const invalid = 'invalid-uuid'; 10 | 11 | it('should throw an error', () => { 12 | expect(validateUUID.bind(validateUUID, invalid)).toThrow(); 13 | }); 14 | it('should pass, 1', () => { 15 | expect(validateUUID(uuid, '')).toBeUndefined(); 16 | }); 17 | it('should pass, 2', () => { 18 | expect(validateUUID(uuidV4, '')).toBeUndefined(); 19 | }); 20 | }); 21 | 22 | describe('#validateComparisonOperator', () => { 23 | it('should pass with common validator', () => { 24 | const withCustom = validateComparisonOperator('gt', {}); 25 | const withoutCustom = validateComparisonOperator('gt'); 26 | expect(withCustom).toBeUndefined(); 27 | expect(withoutCustom).toBeUndefined(); 28 | }); 29 | it('should pass with defined custom validator', () => { 30 | const withCustom = validateComparisonOperator('definedCustom', { 31 | definedCustom: {}, 32 | }); 33 | expect(withCustom).toBeUndefined(); 34 | }); 35 | it('should not pass with undefined custom validator', () => { 36 | expect(validateComparisonOperator.bind(this, 'undefinedCustom')).toThrowError( 37 | RequestQueryException, 38 | ); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /img/awesome-nest.svg: -------------------------------------------------------------------------------- 1 | awesome-nestawesome-nest 2 | -------------------------------------------------------------------------------- /img/awesome-rest.svg: -------------------------------------------------------------------------------- 1 | awesome-restawesome-rest 2 | -------------------------------------------------------------------------------- /integration/crud-typeorm/companies/company.entity.ts: -------------------------------------------------------------------------------- 1 | import { CrudValidationGroups } from '@rewiko/crud'; 2 | import { Entity, Column, OneToMany, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'; 3 | import { 4 | IsOptional, 5 | IsString, 6 | MaxLength, 7 | IsNotEmpty, 8 | IsNumber, 9 | IsEmpty, 10 | } from 'class-validator'; 11 | import { Type } from 'class-transformer'; 12 | 13 | import { BaseEntity } from '../base-entity'; 14 | import { User } from '../users/user.entity'; 15 | import { Project } from '../projects/project.entity'; 16 | 17 | const { CREATE, UPDATE } = CrudValidationGroups; 18 | 19 | @Entity('companies') 20 | export class Company extends BaseEntity { 21 | @IsOptional({ groups: [UPDATE] }) 22 | @IsEmpty({ groups: [CREATE] }) 23 | @IsNumber({}, { groups: [UPDATE] }) 24 | @PrimaryGeneratedColumn() 25 | id?: number; 26 | 27 | @IsOptional({ groups: [UPDATE] }) 28 | @IsNotEmpty({ groups: [CREATE] }) 29 | @IsString({ always: true }) 30 | @MaxLength(100, { always: true }) 31 | @Column({ type: 'varchar', length: 100, nullable: false }) 32 | name: string; 33 | 34 | @IsOptional({ groups: [UPDATE] }) 35 | @IsNotEmpty({ groups: [CREATE] }) 36 | @IsString({ groups: [CREATE, UPDATE] }) 37 | @MaxLength(100, { groups: [CREATE, UPDATE] }) 38 | @Column({ type: 'varchar', length: 100, nullable: false, unique: true }) 39 | domain: string; 40 | 41 | @IsOptional({ always: true }) 42 | @IsString({ always: true }) 43 | @Column({ type: 'text', nullable: true, default: null }) 44 | description: string; 45 | 46 | @DeleteDateColumn({ nullable: true }) 47 | deletedAt?: Date; 48 | 49 | /** 50 | * Relations 51 | */ 52 | 53 | @OneToMany((type) => User, (u) => u.company) 54 | @Type((t) => User) 55 | users: User[]; 56 | 57 | @OneToMany((type) => Project, (p) => p.company) 58 | projects: Project[]; 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Tests 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | test: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | env: 23 | COMPOSE_FILE: ./docker-compose.yml 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v2 29 | 30 | # Runs a single command using the runners shell 31 | - name: build docker db 32 | run: docker-compose up -d 33 | 34 | - name: install 35 | run: yarn bootstrap 36 | 37 | - name: build 38 | run: yarn build 39 | 40 | - name: check docker 41 | run: docker-compose up -d 42 | 43 | # Runs a set of commands using the runners shell 44 | - name: tests 45 | run: yarn test:coverage 46 | 47 | - name: Coveralls Parallel 48 | uses: coverallsapp/github-action@master 49 | with: 50 | github-token: ${{ secrets.github_token }} 51 | flag-name: run-${{ matrix.test_number }} 52 | parallel: true 53 | 54 | coverage: 55 | needs: test 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Coveralls coverage 59 | uses: coverallsapp/github-action@master 60 | with: 61 | github-token: ${{ secrets.github_token }} 62 | parallel-finished: true 63 | -------------------------------------------------------------------------------- /integration/crud-typeorm/projects/project.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, ManyToMany, JoinTable, OneToMany } from 'typeorm'; 2 | import { 3 | IsOptional, 4 | IsString, 5 | IsNumber, 6 | MaxLength, 7 | IsDefined, 8 | IsBoolean, 9 | } from 'class-validator'; 10 | import { CrudValidationGroups } from '@rewiko/crud'; 11 | 12 | import { BaseEntity } from '../base-entity'; 13 | import { Company } from '../companies/company.entity'; 14 | import { User } from '../users/user.entity'; 15 | import { UserProject } from './user-project.entity'; 16 | 17 | const { CREATE, UPDATE } = CrudValidationGroups; 18 | 19 | @Entity('projects') 20 | export class Project extends BaseEntity { 21 | @IsOptional({ groups: [UPDATE] }) 22 | @IsDefined({ groups: [CREATE] }) 23 | @IsString({ always: true }) 24 | @MaxLength(100, { always: true }) 25 | @Column({ type: 'varchar', length: 100, nullable: false, unique: true }) 26 | name?: string; 27 | 28 | @IsOptional({ always: true }) 29 | @Column({ type: 'text', nullable: true }) 30 | description?: string; 31 | 32 | @IsOptional({ always: true }) 33 | @IsBoolean({ always: true }) 34 | @Column({ type: 'boolean', default: true }) 35 | isActive?: boolean; 36 | 37 | @IsOptional({ always: true }) 38 | @IsNumber({}, { always: true }) 39 | @Column({ nullable: false }) 40 | companyId?: number; 41 | 42 | /** 43 | * Relations 44 | */ 45 | 46 | @ManyToOne((type) => Company, (c) => c.projects) 47 | company?: Company; 48 | 49 | @ManyToMany((type) => User, (u) => u.projects, { cascade: true }) 50 | @JoinTable({ 51 | name: 'user_projects', 52 | joinColumn: { 53 | name: 'projectId', 54 | referencedColumnName: 'id', 55 | }, 56 | inverseJoinColumn: { 57 | name: 'userId', 58 | referencedColumnName: 'id', 59 | }, 60 | }) 61 | users?: User[]; 62 | 63 | @OneToMany((type) => UserProject, (el) => el.project, { 64 | persistence: false, 65 | onDelete: 'CASCADE', 66 | }) 67 | userProjects!: UserProject[]; 68 | } 69 | -------------------------------------------------------------------------------- /packages/util/src/checks.util.ts: -------------------------------------------------------------------------------- 1 | import { objKeys } from './obj.util'; 2 | 3 | export const isUndefined = (val: any): boolean => typeof val === 'undefined'; 4 | export const isNull = (val: any): boolean => val === null; 5 | export const isNil = (val: any): boolean => isUndefined(val) || isNull(val); 6 | export const isString = (val: any): boolean => typeof val === 'string'; 7 | export const hasLength = (val: any): boolean => val.length > 0; 8 | export const isStringFull = (val: any): boolean => isString(val) && hasLength(val); 9 | export const isArrayFull = (val: any): boolean => Array.isArray(val) && hasLength(val); 10 | export const isArrayStrings = (val: any): boolean => 11 | isArrayFull(val) && (val as string[]).every((v) => isStringFull(v)); 12 | export const isObject = (val: any): boolean => typeof val === 'object' && !isNull(val); 13 | export const isObjectFull = (val: any) => isObject(val) && hasLength(objKeys(val)); 14 | export const isNumber = (val: any): boolean => 15 | typeof val === 'number' && !Number.isNaN(val) && Number.isFinite(val); 16 | export const isEqual = (val: any, eq: any): boolean => val === eq; 17 | export const isFalse = (val: any): boolean => val === false; 18 | export const isTrue = (val: any): boolean => val === true; 19 | export const isIn = (val: any, arr: any[] = []): boolean => 20 | arr.some((o) => isEqual(val, o)); 21 | export const isBoolean = (val: any): boolean => typeof val === 'boolean'; 22 | export const isNumeric = (val: any): boolean => /^[+-]?([0-9]*[.])?[0-9]+$/.test(val); 23 | export const isDateString = (val: any): boolean => 24 | isStringFull(val) && 25 | /^\d{4}-[01]\d-[0-3]\d(?:T[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[-+][0-2]\d(?::?[0-5]\d)?)?)?$/g.test( 26 | val, 27 | ); 28 | export const isDate = (val: any): val is Date => val instanceof Date; 29 | export const isValue = (val: any): boolean => 30 | isStringFull(val) || isNumber(val) || isBoolean(val) || isDate(val); 31 | export const hasValue = (val: any): boolean => 32 | isArrayFull(val) ? (val as any[]).every((o) => isValue(o)) : isValue(val); 33 | export const isFunction = (val: any): boolean => typeof val === 'function'; 34 | -------------------------------------------------------------------------------- /packages/crud/src/module/crud-config.service.ts: -------------------------------------------------------------------------------- 1 | import { RequestQueryBuilder } from '@rewiko/crud-request'; 2 | import { isObjectFull } from '@rewiko/util'; 3 | import * as deepmerge from 'deepmerge'; 4 | 5 | import { CrudGlobalConfig } from '../interfaces'; 6 | 7 | export class CrudConfigService { 8 | static config: CrudGlobalConfig = { 9 | auth: {}, 10 | query: { 11 | alwaysPaginate: false, 12 | }, 13 | operators: {}, 14 | routes: { 15 | getManyBase: { interceptors: [], decorators: [] }, 16 | getOneBase: { interceptors: [], decorators: [] }, 17 | createOneBase: { interceptors: [], decorators: [], returnShallow: false }, 18 | createManyBase: { interceptors: [], decorators: [] }, 19 | updateOneBase: { 20 | interceptors: [], 21 | decorators: [], 22 | allowParamsOverride: false, 23 | returnShallow: false, 24 | }, 25 | replaceOneBase: { 26 | interceptors: [], 27 | decorators: [], 28 | allowParamsOverride: false, 29 | returnShallow: false, 30 | }, 31 | deleteOneBase: { interceptors: [], decorators: [], returnDeleted: false }, 32 | recoverOneBase: { interceptors: [], decorators: [], returnRecovered: false }, 33 | }, 34 | params: {}, 35 | }; 36 | 37 | static load(config: CrudGlobalConfig = {}) { 38 | const auth = isObjectFull(config.auth) ? config.auth : {}; 39 | const query = isObjectFull(config.query) ? config.query : {}; 40 | const routes = isObjectFull(config.routes) ? config.routes : {}; 41 | const operators = isObjectFull(config.operators) ? config.operators : {}; 42 | const params = isObjectFull(config.params) ? config.params : {}; 43 | const serialize = isObjectFull(config.serialize) ? config.serialize : {}; 44 | 45 | if (isObjectFull(config.queryParser)) { 46 | RequestQueryBuilder.setOptions({ ...config.queryParser }); 47 | } 48 | 49 | CrudConfigService.config = deepmerge( 50 | CrudConfigService.config, 51 | { auth, query, routes, operators, params, serialize }, 52 | { arrayMerge: (a, b, c) => b }, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/crud/test/__fixture__/services/test-serialize.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | 3 | import { 4 | CreateManyDto, 5 | CrudRequest, 6 | GetManyDefaultResponse, 7 | } from '../../../src/interfaces'; 8 | import { CrudService } from '../../../src/services'; 9 | import { TestSerializeModel } from '../models'; 10 | 11 | @Injectable() 12 | export class TestSerializeService extends CrudService { 13 | private store: T[] = []; 14 | 15 | constructor(private Model: Type) { 16 | super(); 17 | this.store = [ 18 | new this.Model({ id: 1, name: 'name', email: 'email1', isActive: true }), 19 | new this.Model({ id: 2, name: 'name2', email: 'email2', isActive: false }), 20 | new this.Model({ id: 3, name: 'name3', email: 'email3', isActive: true }), 21 | new this.Model({ id: 4, name: 'name4', email: 'email4', isActive: false }), 22 | new this.Model({ id: 5, name: 'name5', email: 'email5', isActive: true }), 23 | ]; 24 | } 25 | 26 | async getMany(req: CrudRequest): Promise | T[]> { 27 | const total = this.store.length; 28 | const limit = this.getTake(req.parsed, req.options.query); 29 | const offset = this.getSkip(req.parsed, limit); 30 | 31 | return this.decidePagination(req.parsed, req.options) 32 | ? this.createPageInfo(this.store, total, limit || total, offset || 0) 33 | : this.store; 34 | } 35 | async getOne(req: CrudRequest): Promise { 36 | return this.store[0]; 37 | } 38 | async createOne(req: CrudRequest, dto: T): Promise {} 39 | async createMany(req: CrudRequest, dto: CreateManyDto): Promise {} 40 | async updateOne(req: CrudRequest, dto: T): Promise {} 41 | async replaceOne(req: CrudRequest, dto: T): Promise {} 42 | 43 | async deleteOne(req: CrudRequest): Promise { 44 | return req.options.routes.deleteOneBase.returnDeleted ? this.store[0] : undefined; 45 | } 46 | 47 | async recoverOne(req: CrudRequest): Promise { 48 | return req.options.routes.recoverOneBase.returnRecovered ? this.store[0] : undefined; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/crud/src/crud/validation.helper.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { isFalse, isNil } from '@rewiko/util'; 3 | import { CrudValidationGroups } from '../enums'; 4 | import { CreateManyDto, CrudOptions, MergedCrudOptions } from '../interfaces'; 5 | import { safeRequire } from '../util'; 6 | import { ApiProperty } from './swagger.helper'; 7 | 8 | const validator = safeRequire('class-validator', () => require('class-validator')); 9 | const transformer = safeRequire('class-transformer', () => require('class-transformer')); 10 | 11 | class BulkDto implements CreateManyDto { 12 | bulk: T[]; 13 | } 14 | 15 | export class Validation { 16 | static getValidationPipe( 17 | options: CrudOptions, 18 | group?: CrudValidationGroups, 19 | ): ValidationPipe { 20 | return validator && !isFalse(options.validation) 21 | ? new ValidationPipe({ 22 | ...(options.validation || {}), 23 | groups: group ? [group] : undefined, 24 | }) 25 | : /* istanbul ignore next */ undefined; 26 | } 27 | 28 | static createBulkDto(options: MergedCrudOptions): any { 29 | /* istanbul ignore else */ 30 | if (validator && transformer && !isFalse(options.validation)) { 31 | const { IsArray, ArrayNotEmpty, ValidateNested } = validator; 32 | const { Type } = transformer; 33 | const hasDto = !isNil(options.dto.create); 34 | const groups = !hasDto ? [CrudValidationGroups.CREATE] : undefined; 35 | const always = hasDto ? true : undefined; 36 | const Model = hasDto ? options.dto.create : options.model.type; 37 | 38 | // tslint:disable-next-line:max-classes-per-file 39 | class BulkDtoImpl implements CreateManyDto { 40 | @ApiProperty({ type: Model, isArray: true }) 41 | @IsArray({ groups, always }) 42 | @ArrayNotEmpty({ groups, always }) 43 | @ValidateNested({ each: true, groups, always }) 44 | @Type(() => Model) 45 | bulk: T[]; 46 | } 47 | 48 | Object.defineProperty(BulkDtoImpl, 'name', { 49 | writable: false, 50 | value: `CreateMany${options.model.type.name}Dto`, 51 | }); 52 | 53 | return BulkDtoImpl; 54 | } else { 55 | return BulkDto; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/crud/src/interceptors/crud-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { isFalse, isObject, isFunction } from '@rewiko/util'; 8 | import { classToPlain, classToPlainFromExist } from 'class-transformer'; 9 | import { Observable } from 'rxjs'; 10 | import { map } from 'rxjs/operators'; 11 | import { CrudActions } from '../enums'; 12 | import { SerializeOptions } from '../interfaces'; 13 | import { CrudBaseInterceptor } from './crud-base.interceptor'; 14 | 15 | const actionToDtoNameMap: { 16 | [key in CrudActions]: keyof SerializeOptions; 17 | } = { 18 | [CrudActions.ReadAll]: 'getMany', 19 | [CrudActions.ReadOne]: 'get', 20 | [CrudActions.CreateMany]: 'createMany', 21 | [CrudActions.CreateOne]: 'create', 22 | [CrudActions.UpdateOne]: 'update', 23 | [CrudActions.ReplaceOne]: 'replace', 24 | [CrudActions.DeleteAll]: 'delete', 25 | [CrudActions.DeleteOne]: 'delete', 26 | [CrudActions.RecoverOne]: 'recover', 27 | }; 28 | 29 | @Injectable() 30 | export class CrudResponseInterceptor extends CrudBaseInterceptor 31 | implements NestInterceptor { 32 | intercept(context: ExecutionContext, next: CallHandler): Observable { 33 | return next.handle().pipe(map((data) => this.serialize(context, data))); 34 | } 35 | 36 | protected transform(dto: any, data: any) { 37 | if (!isObject(data) || isFalse(dto)) { 38 | return data; 39 | } 40 | 41 | if (!isFunction(dto)) { 42 | return data.constructor !== Object ? classToPlain(data) : data; 43 | } 44 | 45 | return data instanceof dto 46 | ? classToPlain(data) 47 | : classToPlain(classToPlainFromExist(data, new dto())); 48 | } 49 | 50 | protected serialize(context: ExecutionContext, data: any): any { 51 | const { crudOptions, action } = this.getCrudInfo(context); 52 | const { serialize } = crudOptions; 53 | const dto = serialize[actionToDtoNameMap[action]]; 54 | const isArray = Array.isArray(data); 55 | 56 | switch (action) { 57 | case CrudActions.ReadAll: 58 | return isArray 59 | ? (data as any[]).map((item) => this.transform(serialize.get, item)) 60 | : this.transform(dto, data); 61 | case CrudActions.CreateMany: 62 | return isArray 63 | ? (data as any[]).map((item) => this.transform(dto, item)) 64 | : this.transform(dto, data); 65 | default: 66 | return this.transform(dto, data); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/crud/test/crud.decorator.soft.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | 6 | import { Crud } from '../src/decorators'; 7 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 8 | import { TestModel } from './__fixture__/models'; 9 | import { TestService } from './__fixture__/services'; 10 | 11 | describe('#crud', () => { 12 | describe('#soft delete disabled', () => { 13 | let app: INestApplication; 14 | let server: any; 15 | 16 | @Crud({ 17 | model: { type: TestModel }, 18 | }) 19 | @Controller('test') 20 | class TestController { 21 | constructor(public service: TestService) {} 22 | } 23 | 24 | beforeAll(async () => { 25 | const fixture = await Test.createTestingModule({ 26 | controllers: [TestController], 27 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 28 | }).compile(); 29 | 30 | app = fixture.createNestApplication(); 31 | 32 | await app.init(); 33 | server = app.getHttpServer(); 34 | }); 35 | 36 | afterAll(async () => { 37 | app.close(); 38 | }); 39 | 40 | describe('#recoverOneBase', () => { 41 | it('should return status 404 if controller does not have soft delete', () => { 42 | return request(server) 43 | .patch('/test/1/recover') 44 | .expect(404); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('#soft delete enabled', () => { 50 | let app: INestApplication; 51 | let server: any; 52 | 53 | @Crud({ 54 | model: { type: TestModel }, 55 | query: { 56 | softDelete: true, 57 | }, 58 | }) 59 | @Controller('test') 60 | class TestController { 61 | constructor(public service: TestService) {} 62 | } 63 | 64 | beforeAll(async () => { 65 | const fixture = await Test.createTestingModule({ 66 | controllers: [TestController], 67 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 68 | }).compile(); 69 | 70 | app = fixture.createNestApplication(); 71 | 72 | await app.init(); 73 | server = app.getHttpServer(); 74 | }); 75 | 76 | afterAll(async () => { 77 | app.close(); 78 | }); 79 | 80 | describe('#recoverOneBase', () => { 81 | it('should return status 200 if controller has soft delete', () => { 82 | return request(server) 83 | .patch('/test/1/recover') 84 | .expect(200); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /integration/crud-typeorm/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | JoinColumn, 5 | OneToOne, 6 | OneToMany, 7 | ManyToOne, 8 | ManyToMany, 9 | DeleteDateColumn, 10 | } from 'typeorm'; 11 | import { 12 | IsOptional, 13 | IsString, 14 | MaxLength, 15 | IsNotEmpty, 16 | IsEmail, 17 | IsBoolean, 18 | ValidateNested, 19 | } from 'class-validator'; 20 | import { Type } from 'class-transformer'; 21 | import { CrudValidationGroups } from '@rewiko/crud'; 22 | 23 | import { BaseEntity } from '../base-entity'; 24 | import { UserProfile } from '../users-profiles/user-profile.entity'; 25 | import { UserLicense } from '../users-licenses/user-license.entity'; 26 | import { Company } from '../companies/company.entity'; 27 | import { Project } from '../projects/project.entity'; 28 | import { UserProject } from '../projects/user-project.entity'; 29 | 30 | const { CREATE, UPDATE } = CrudValidationGroups; 31 | 32 | export class Name { 33 | @IsString({ always: true }) 34 | @Column({ nullable: true }) 35 | first: string; 36 | 37 | @IsString({ always: true }) 38 | @Column({ nullable: true }) 39 | last: string; 40 | } 41 | 42 | // tslint:disable-next-line:max-classes-per-file 43 | @Entity('users') 44 | export class User extends BaseEntity { 45 | @IsOptional({ groups: [UPDATE] }) 46 | @IsNotEmpty({ groups: [CREATE] }) 47 | @IsString({ always: true }) 48 | @MaxLength(255, { always: true }) 49 | @IsEmail({ require_tld: false }, { always: true }) 50 | @Column({ type: 'varchar', length: 255, nullable: false, unique: true }) 51 | email: string; 52 | 53 | @IsOptional({ groups: [UPDATE] }) 54 | @IsNotEmpty({ groups: [CREATE] }) 55 | @IsBoolean({ always: true }) 56 | @Column({ type: 'boolean', default: true }) 57 | isActive: boolean; 58 | 59 | @Type((t) => Name) 60 | @Column((type) => Name) 61 | name: Name; 62 | 63 | @Column({ nullable: true }) 64 | profileId?: number; 65 | 66 | @Column({ nullable: false }) 67 | companyId?: number; 68 | 69 | @DeleteDateColumn({ nullable: true }) 70 | deletedAt?: Date; 71 | 72 | /** 73 | * Relations 74 | */ 75 | 76 | @IsOptional({ groups: [UPDATE] }) 77 | @IsNotEmpty({ groups: [CREATE] }) 78 | @ValidateNested({ always: true }) 79 | @Type((t) => UserProfile) 80 | @OneToOne((type) => UserProfile, (p) => p.user, { cascade: true }) 81 | @JoinColumn() 82 | profile?: UserProfile; 83 | 84 | @ManyToOne((type) => Company, (c) => c.users) 85 | company?: Company; 86 | 87 | @ManyToMany((type) => Project, (c) => c.users) 88 | projects?: Project[]; 89 | 90 | @OneToMany((type) => UserProject, (el) => el.user, { 91 | persistence: false, 92 | onDelete: 'CASCADE', 93 | }) 94 | userProjects?: UserProject[]; 95 | 96 | @OneToMany((type) => UserLicense, (ul) => ul.user) 97 | @Type((t) => UserLicense) 98 | @JoinColumn() 99 | userLicenses?: UserLicense[]; 100 | } 101 | -------------------------------------------------------------------------------- /packages/crud/test/crud.decorator.exclude.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | 6 | import { Crud } from '../src/decorators'; 7 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 8 | import { TestModel } from './__fixture__/models'; 9 | import { TestService } from './__fixture__/services'; 10 | 11 | describe('#crud', () => { 12 | describe('#exclude routes', () => { 13 | let app: INestApplication; 14 | let server: any; 15 | 16 | @Crud({ 17 | model: { type: TestModel }, 18 | routes: { 19 | exclude: ['getManyBase'], 20 | }, 21 | }) 22 | @Controller('test') 23 | class TestController { 24 | constructor(public service: TestService) {} 25 | } 26 | 27 | beforeAll(async () => { 28 | const fixture = await Test.createTestingModule({ 29 | controllers: [TestController], 30 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 31 | }).compile(); 32 | 33 | app = fixture.createNestApplication(); 34 | 35 | await app.init(); 36 | server = app.getHttpServer(); 37 | }); 38 | 39 | afterAll(async () => { 40 | app.close(); 41 | }); 42 | 43 | describe('#getManyBase excluded', () => { 44 | it('should return status 404', () => { 45 | return request(server) 46 | .get('/test') 47 | .expect(404); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('#only routes', () => { 53 | let app: INestApplication; 54 | let server: any; 55 | 56 | @Crud({ 57 | model: { type: TestModel }, 58 | routes: { 59 | only: ['getManyBase'], 60 | }, 61 | }) 62 | @Controller('test') 63 | class TestController { 64 | constructor(public service: TestService) {} 65 | } 66 | 67 | beforeAll(async () => { 68 | const fixture = await Test.createTestingModule({ 69 | controllers: [TestController], 70 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 71 | }).compile(); 72 | 73 | app = fixture.createNestApplication(); 74 | 75 | await app.init(); 76 | server = app.getHttpServer(); 77 | }); 78 | 79 | afterAll(async () => { 80 | app.close(); 81 | }); 82 | 83 | describe('#getManyBase only', () => { 84 | it('should return status 200', () => { 85 | return request(server) 86 | .get('/test') 87 | .expect(200); 88 | }); 89 | }); 90 | 91 | describe('#getOneBase excluded', () => { 92 | it('should return status 404', () => { 93 | return request(server) 94 | .get('/test/1') 95 | .expect(404); 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/crud-request/src/types/request-query.types.ts: -------------------------------------------------------------------------------- 1 | export type QueryFields = string[]; 2 | 3 | export interface QueryFilter { 4 | field: string; 5 | operator: ComparisonOperator; 6 | value?: any; 7 | } 8 | 9 | export type QueryFilterArr = [string, ComparisonOperator, any?]; 10 | 11 | export interface QueryJoin { 12 | field: string; 13 | select?: QueryFields; 14 | } 15 | 16 | export type QueryJoinArr = [string, QueryFields?]; 17 | 18 | export interface QuerySort { 19 | field: string; 20 | order: QuerySortOperator; 21 | } 22 | 23 | export type QuerySortArr = [string, QuerySortOperator]; 24 | 25 | export type QuerySortOperator = 'ASC' | 'DESC'; 26 | 27 | type DeprecatedCondOperator = 28 | | 'eq' 29 | | 'ne' 30 | | 'gt' 31 | | 'lt' 32 | | 'gte' 33 | | 'lte' 34 | | 'starts' 35 | | 'ends' 36 | | 'cont' 37 | | 'excl' 38 | | 'in' 39 | | 'notin' 40 | | 'isnull' 41 | | 'notnull' 42 | | 'between'; 43 | 44 | export enum CondOperator { 45 | EQUALS = '$eq', 46 | NOT_EQUALS = '$ne', 47 | GREATER_THAN = '$gt', 48 | LOWER_THAN = '$lt', 49 | GREATER_THAN_EQUALS = '$gte', 50 | LOWER_THAN_EQUALS = '$lte', 51 | STARTS = '$starts', 52 | ENDS = '$ends', 53 | CONTAINS = '$cont', 54 | EXCLUDES = '$excl', 55 | IN = '$in', 56 | NOT_IN = '$notin', 57 | IS_NULL = '$isnull', 58 | NOT_NULL = '$notnull', 59 | BETWEEN = '$between', 60 | EQUALS_LOW = '$eqL', 61 | NOT_EQUALS_LOW = '$neL', 62 | STARTS_LOW = '$startsL', 63 | ENDS_LOW = '$endsL', 64 | CONTAINS_LOW = '$contL', 65 | EXCLUDES_LOW = '$exclL', 66 | IN_LOW = '$inL', 67 | NOT_IN_LOW = '$notinL', 68 | } 69 | 70 | export type ComparisonOperator = DeprecatedCondOperator | keyof SFieldOperator | string; 71 | 72 | // new search 73 | export type SPrimitivesVal = string | number | boolean; 74 | 75 | export type SFiledValues = SPrimitivesVal | SPrimitivesVal[]; 76 | 77 | export interface SFieldOperator { 78 | $eq?: SFiledValues; 79 | $ne?: SFiledValues; 80 | $gt?: SFiledValues; 81 | $lt?: SFiledValues; 82 | $gte?: SFiledValues; 83 | $lte?: SFiledValues; 84 | $starts?: SFiledValues; 85 | $ends?: SFiledValues; 86 | $cont?: SFiledValues; 87 | $excl?: SFiledValues; 88 | $in?: SFiledValues; 89 | $notin?: SFiledValues; 90 | $between?: SFiledValues; 91 | $isnull?: SFiledValues; 92 | $notnull?: SFiledValues; 93 | $eqL?: SFiledValues; 94 | $neL?: SFiledValues; 95 | $startsL?: SFiledValues; 96 | $endsL?: SFiledValues; 97 | $contL?: SFiledValues; 98 | $exclL?: SFiledValues; 99 | $inL?: SFiledValues; 100 | $notinL?: SFiledValues; 101 | $or?: SFieldOperator; 102 | $and?: never; 103 | } 104 | 105 | export type SField = 106 | | SPrimitivesVal 107 | | SFieldOperator 108 | | { [$custom: string]: SFiledValues }; 109 | 110 | export interface SFields { 111 | [key: string]: SField | Array | undefined; 112 | $or?: Array; 113 | $and?: never; 114 | } 115 | 116 | export interface SConditionAND { 117 | $and?: Array; 118 | $or?: never; 119 | } 120 | 121 | export type SConditionKey = '$and' | '$or'; 122 | 123 | export type SCondition = SFields | SConditionAND; 124 | -------------------------------------------------------------------------------- /packages/crud/test/crud.dto.options.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | 6 | import { Crud } from '../src/decorators/crud.decorator'; 7 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 8 | import { TestModel } from './__fixture__/models'; 9 | import { TestCreateDto, TestUpdateDto } from './__fixture__/dto'; 10 | import { TestService } from './__fixture__/services'; 11 | 12 | describe('#crud', () => { 13 | describe('#dto options', () => { 14 | let app: INestApplication; 15 | let server: any; 16 | 17 | @Crud({ 18 | model: { 19 | type: TestModel, 20 | }, 21 | dto: { 22 | create: TestCreateDto, 23 | update: TestUpdateDto, 24 | }, 25 | }) 26 | @Controller('test') 27 | class TestController { 28 | constructor(public service: TestService) {} 29 | } 30 | 31 | beforeAll(async () => { 32 | const fixture = await Test.createTestingModule({ 33 | controllers: [TestController], 34 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 35 | }).compile(); 36 | 37 | app = fixture.createNestApplication(); 38 | 39 | await app.init(); 40 | server = app.getHttpServer(); 41 | }); 42 | 43 | afterAll(async () => { 44 | await app.close(); 45 | }); 46 | 47 | describe('#createOneBase', () => { 48 | it('should return status 201', () => { 49 | const send: TestCreateDto = { 50 | firstName: 'firstName', 51 | lastName: 'lastName', 52 | email: 'test@test.com', 53 | age: 15, 54 | }; 55 | return request(server) 56 | .post('/test') 57 | .send(send) 58 | .expect(201); 59 | }); 60 | it('should return status 400', (done) => { 61 | const send: TestModel = { 62 | firstName: 'firstName', 63 | lastName: 'lastName', 64 | email: 'test@test.com', 65 | }; 66 | return request(server) 67 | .post('/test') 68 | .send(send) 69 | .end((_, res) => { 70 | expect(res.status).toEqual(400); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('#updateOneBase', () => { 77 | it('should return status 200', () => { 78 | const send: TestModel = { 79 | id: 1, 80 | firstName: 'firstName', 81 | lastName: 'lastName', 82 | email: 'test@test.com', 83 | age: 15, 84 | }; 85 | return request(server) 86 | .patch('/test/1') 87 | .send(send) 88 | .expect(200); 89 | }); 90 | it('should return status 400', (done) => { 91 | const send: TestModel = { 92 | firstName: 'firstName', 93 | lastName: 'lastName', 94 | email: 'foo', 95 | }; 96 | return request(server) 97 | .patch('/test/1') 98 | .send(send) 99 | .end((_, res) => { 100 | expect(res.status).toEqual(400); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mihon4ik@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /packages/crud/src/services/crud-service.abstract.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, NotFoundException } from '@nestjs/common'; 2 | import { ParsedRequestParams } from '@rewiko/crud-request'; 3 | import { objKeys } from '@rewiko/util'; 4 | 5 | import { 6 | CreateManyDto, 7 | CrudRequest, 8 | CrudRequestOptions, 9 | GetManyDefaultResponse, 10 | QueryOptions, 11 | } from '../interfaces'; 12 | 13 | export abstract class CrudService { 14 | abstract getMany(req: CrudRequest): Promise | T[]>; 15 | 16 | abstract getOne(req: CrudRequest): Promise; 17 | 18 | abstract createOne(req: CrudRequest, dto: DTO): Promise; 19 | 20 | abstract createMany(req: CrudRequest, dto: CreateManyDto): Promise; 21 | 22 | abstract updateOne(req: CrudRequest, dto: DTO): Promise; 23 | 24 | abstract replaceOne(req: CrudRequest, dto: DTO): Promise; 25 | 26 | abstract deleteOne(req: CrudRequest): Promise; 27 | 28 | abstract recoverOne(req: CrudRequest): Promise; 29 | 30 | throwBadRequestException(msg?: any): BadRequestException { 31 | throw new BadRequestException(msg); 32 | } 33 | 34 | throwNotFoundException(name: string): NotFoundException { 35 | throw new NotFoundException(`${name} not found`); 36 | } 37 | 38 | /** 39 | * Wrap page into page-info 40 | * override this method to create custom page-info response 41 | * or set custom `serialize.getMany` dto in the controller's CrudOption 42 | * @param data 43 | * @param total 44 | * @param limit 45 | * @param offset 46 | */ 47 | createPageInfo( 48 | data: T[], 49 | total: number, 50 | limit: number, 51 | offset: number, 52 | ): GetManyDefaultResponse { 53 | return { 54 | data, 55 | count: data.length, 56 | total, 57 | page: limit ? Math.floor(offset / limit) + 1 : 1, 58 | pageCount: limit && total ? Math.ceil(total / limit) : 1, 59 | }; 60 | } 61 | 62 | /** 63 | * Determine if need paging 64 | * @param parsed 65 | * @param options 66 | */ 67 | decidePagination(parsed: ParsedRequestParams, options: CrudRequestOptions): boolean { 68 | return ( 69 | options.query.alwaysPaginate || 70 | ((Number.isFinite(parsed.page) || Number.isFinite(parsed.offset)) && 71 | /* istanbul ignore next */ !!this.getTake(parsed, options.query)) 72 | ); 73 | } 74 | 75 | /** 76 | * Get number of resources to be fetched 77 | * @param query 78 | * @param options 79 | */ 80 | getTake(query: ParsedRequestParams, options: QueryOptions): number | null { 81 | if (query.limit) { 82 | return options.maxLimit 83 | ? query.limit <= options.maxLimit 84 | ? query.limit 85 | : options.maxLimit 86 | : query.limit; 87 | } 88 | /* istanbul ignore if */ 89 | if (options.limit) { 90 | return options.maxLimit 91 | ? options.limit <= options.maxLimit 92 | ? options.limit 93 | : options.maxLimit 94 | : options.limit; 95 | } 96 | 97 | return options.maxLimit ? options.maxLimit : null; 98 | } 99 | 100 | /** 101 | * Get number of resources to be skipped 102 | * @param query 103 | * @param take 104 | */ 105 | getSkip(query: ParsedRequestParams, take: number): number | null { 106 | return query.page && take 107 | ? take * (query.page - 1) 108 | : query.offset 109 | ? query.offset 110 | : null; 111 | } 112 | 113 | /** 114 | * Get primary param name from CrudOptions 115 | * @param options 116 | */ 117 | getPrimaryParams(options: CrudRequestOptions): string[] { 118 | const params = objKeys(options.params).filter( 119 | (n) => options.params[n] && options.params[n].primary, 120 | ); 121 | 122 | return params.map((p) => options.params[p].field); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/crud/test/crud.decorator.options.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | import { CrudRoutesFactory } from '../src/crud/crud-routes.factory'; 6 | import { Swagger } from '../src/crud/swagger.helper'; 7 | import { Crud } from '../src/decorators'; 8 | import { CrudOptions } from '../src/interfaces'; 9 | import { BaseRouteName } from '../src/types'; 10 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 11 | import { TestModel } from './__fixture__/models'; 12 | import { TestService } from './__fixture__/services'; 13 | 14 | describe('#crud', () => { 15 | describe('#options', () => { 16 | let app: INestApplication; 17 | let server: any; 18 | 19 | class CustomSwaggerRoutesFactory extends CrudRoutesFactory { 20 | protected setSwaggerOperation(name: BaseRouteName) { 21 | const summary = Swagger.operationsMap(this.modelName)[name]; 22 | const operationId = '_' + name + this.modelName; 23 | Swagger.setOperation({ summary, operationId }, this.targetProto[name]); 24 | } 25 | } 26 | 27 | const options: CrudOptions = { 28 | model: { type: TestModel }, 29 | params: { 30 | id: { 31 | field: 'id', 32 | type: 'uuid', 33 | primary: true, 34 | }, 35 | }, 36 | query: { 37 | limit: 10, 38 | }, 39 | routes: { 40 | getManyBase: { 41 | interceptors: [], 42 | decorators: [], 43 | }, 44 | getOneBase: { 45 | interceptors: [], 46 | decorators: [], 47 | }, 48 | createOneBase: { 49 | interceptors: [], 50 | decorators: [], 51 | }, 52 | createManyBase: { 53 | interceptors: [], 54 | decorators: [], 55 | }, 56 | updateOneBase: { 57 | interceptors: [], 58 | decorators: [], 59 | allowParamsOverride: true, 60 | }, 61 | replaceOneBase: { 62 | interceptors: [], 63 | decorators: [], 64 | allowParamsOverride: true, 65 | }, 66 | deleteOneBase: { 67 | interceptors: [], 68 | decorators: [], 69 | returnDeleted: true, 70 | }, 71 | }, 72 | routesFactory: CustomSwaggerRoutesFactory, 73 | }; 74 | 75 | @Crud(options) 76 | @Controller('test') 77 | class TestController { 78 | constructor(public service: TestService) {} 79 | } 80 | 81 | beforeAll(async () => { 82 | const fixture = await Test.createTestingModule({ 83 | controllers: [TestController], 84 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 85 | }).compile(); 86 | 87 | app = fixture.createNestApplication(); 88 | 89 | await app.init(); 90 | server = app.getHttpServer(); 91 | }); 92 | 93 | afterAll(async () => { 94 | app.close(); 95 | }); 96 | 97 | it('should return options in ParsedRequest', (done) => { 98 | return request(server) 99 | .get('/test') 100 | .expect(200) 101 | .end((_, res) => { 102 | const opt = res.body.req.options; 103 | expect(opt.query).toMatchObject(options.query); 104 | expect(opt.routes).toMatchObject(options.routes); 105 | expect(opt.params).toMatchObject(options.params); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('should use crudRoutesFactory override', () => { 111 | const testController = app.get('TestController'); 112 | const { operationId } = Swagger.getOperation(testController.replaceOneBase); 113 | expect(operationId).toEqual('_replaceOneBaseTestModel'); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /packages/crud-request/src/request-query.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isArrayStrings, 3 | isEqual, 4 | isNil, 5 | isNumber, 6 | isObject, 7 | isStringFull, 8 | isUndefined, 9 | objKeys, 10 | } from '@rewiko/util'; 11 | 12 | import { RequestQueryException } from './exceptions'; 13 | import { CustomOperators, ParamOption, ParamsOptions } from './interfaces'; 14 | import { 15 | ComparisonOperator, 16 | CondOperator, 17 | QueryFields, 18 | QueryFilter, 19 | QueryJoin, 20 | QuerySort, 21 | } from './types'; 22 | 23 | export const deprecatedComparisonOperatorsList = [ 24 | 'eq', 25 | 'ne', 26 | 'gt', 27 | 'lt', 28 | 'gte', 29 | 'lte', 30 | 'starts', 31 | 'ends', 32 | 'cont', 33 | 'excl', 34 | 'in', 35 | 'notin', 36 | 'isnull', 37 | 'notnull', 38 | 'between', 39 | ]; 40 | export const comparisonOperatorsList = [ 41 | ...deprecatedComparisonOperatorsList, 42 | ...objKeys(CondOperator).map((n) => CondOperator[n]), 43 | ]; 44 | 45 | export const sortOrdersList = ['ASC', 'DESC']; 46 | 47 | const sortOrdersListStr = sortOrdersList.join(); 48 | 49 | export function validateFields(fields: QueryFields): void { 50 | if (!isArrayStrings(fields)) { 51 | throw new RequestQueryException('Invalid fields. Array of strings expected'); 52 | } 53 | } 54 | 55 | export function validateCondition( 56 | val: QueryFilter, 57 | cond: 'filter' | 'or' | 'search', 58 | customOperators: CustomOperators, 59 | ): void { 60 | if (!isObject(val) || !isStringFull(val.field)) { 61 | throw new RequestQueryException( 62 | `Invalid field type in ${cond} condition. String expected`, 63 | ); 64 | } 65 | validateComparisonOperator(val.operator, customOperators); 66 | } 67 | 68 | export function validateComparisonOperator( 69 | operator: ComparisonOperator, 70 | customOperators: CustomOperators = {}, 71 | ): void { 72 | const extendedComparisonOperatorsList = [ 73 | ...comparisonOperatorsList, 74 | ...Object.keys(customOperators), 75 | ]; 76 | if (!extendedComparisonOperatorsList.includes(operator)) { 77 | throw new RequestQueryException( 78 | `Invalid comparison operator. ${extendedComparisonOperatorsList.join()} expected`, 79 | ); 80 | } 81 | } 82 | 83 | export function validateJoin(join: QueryJoin): void { 84 | if (!isObject(join) || !isStringFull(join.field)) { 85 | throw new RequestQueryException('Invalid join field. String expected'); 86 | } 87 | if (!isUndefined(join.select) && !isArrayStrings(join.select)) { 88 | throw new RequestQueryException('Invalid join select. Array of strings expected'); 89 | } 90 | } 91 | 92 | export function validateSort(sort: QuerySort): void { 93 | if (!isObject(sort) || !isStringFull(sort.field)) { 94 | throw new RequestQueryException('Invalid sort field. String expected'); 95 | } 96 | if ( 97 | !isEqual(sort.order, sortOrdersList[0]) && 98 | !isEqual(sort.order, sortOrdersList[1]) 99 | ) { 100 | throw new RequestQueryException(`Invalid sort order. ${sortOrdersListStr} expected`); 101 | } 102 | } 103 | 104 | export function validateNumeric( 105 | val: number, 106 | num: 'limit' | 'offset' | 'page' | 'cache' | 'include_deleted' | string, 107 | ): void { 108 | if (!isNumber(val)) { 109 | throw new RequestQueryException(`Invalid ${num}. Number expected`); 110 | } 111 | } 112 | 113 | export function validateParamOption(options: ParamsOptions, name: string) { 114 | if (!isObject(options)) { 115 | throw new RequestQueryException(`Invalid param ${name}. Invalid crud options`); 116 | } 117 | const option = options[name]; 118 | if (option && option.disabled) { 119 | return; 120 | } 121 | if (!isObject(option) || isNil(option.field) || isNil(option.type)) { 122 | throw new RequestQueryException(`Invalid param option in Crud`); 123 | } 124 | } 125 | 126 | export function validateUUID(str: string, name: string) { 127 | const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 128 | const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 129 | if (!uuidV4.test(str) && !uuid.test(str)) { 130 | throw new RequestQueryException(`Invalid param ${name}. UUID string expected`); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/crud/test/crud-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequestQueryBuilder } from '@rewiko/crud-request'; 2 | import { CrudGlobalConfig } from '../src/interfaces'; 3 | import { CrudConfigService } from '../src/module/crud-config.service'; 4 | 5 | describe('#crud', () => { 6 | describe('#CrudConfigService', () => { 7 | const defaultConfig = { ...CrudConfigService.config }; 8 | 9 | beforeEach(() => { 10 | CrudConfigService.config = { ...defaultConfig }; 11 | }); 12 | 13 | it('should set default config, 1', () => { 14 | const conf: CrudGlobalConfig = {}; 15 | const expected = { ...CrudConfigService.config }; 16 | CrudConfigService.load(conf); 17 | expect(CrudConfigService.config).toEqual(expect.objectContaining(expected)); 18 | }); 19 | it('should set default config, 2', () => { 20 | const expected = { ...CrudConfigService.config }; 21 | CrudConfigService.load(); 22 | expect(CrudConfigService.config).toEqual(expect.objectContaining(expected)); 23 | }); 24 | it('should set queryParser', () => { 25 | const requestOptions = { ...RequestQueryBuilder.getOptions() }; 26 | const conf: CrudGlobalConfig = { 27 | queryParser: { 28 | delim: '__', 29 | }, 30 | }; 31 | const expected = { ...CrudConfigService.config }; 32 | CrudConfigService.load(conf); 33 | expect(CrudConfigService.config).toEqual(expect.objectContaining(expected)); 34 | expect(RequestQueryBuilder.getOptions()).toEqual( 35 | expect.objectContaining({ ...requestOptions, delim: '__' }), 36 | ); 37 | }); 38 | it('should set query, routes, params', () => { 39 | const conf: CrudGlobalConfig = { 40 | auth: { 41 | property: 'user', 42 | }, 43 | query: { 44 | limit: 10, 45 | }, 46 | operators: { 47 | custom: {}, 48 | }, 49 | params: { 50 | id: { 51 | field: 'id', 52 | type: 'uuid', 53 | primary: true, 54 | }, 55 | }, 56 | routes: { 57 | updateOneBase: { 58 | allowParamsOverride: true, 59 | returnShallow: true, 60 | }, 61 | replaceOneBase: { 62 | allowParamsOverride: true, 63 | }, 64 | getManyBase: { 65 | interceptors: [() => {}], 66 | }, 67 | }, 68 | }; 69 | const expected = { 70 | auth: { 71 | property: 'user', 72 | }, 73 | query: { 74 | limit: 10, 75 | }, 76 | operators: { 77 | custom: {}, 78 | }, 79 | params: { 80 | id: { 81 | field: 'id', 82 | type: 'uuid', 83 | primary: true, 84 | }, 85 | }, 86 | routes: { 87 | getManyBase: { 88 | interceptors: [() => {}], 89 | decorators: [], 90 | }, 91 | getOneBase: { interceptors: [], decorators: [] }, 92 | createOneBase: { interceptors: [], decorators: [], returnShallow: false }, 93 | createManyBase: { interceptors: [], decorators: [] }, 94 | updateOneBase: { 95 | interceptors: [], 96 | decorators: [], 97 | allowParamsOverride: true, 98 | returnShallow: true, 99 | }, 100 | replaceOneBase: { 101 | interceptors: [], 102 | decorators: [], 103 | allowParamsOverride: true, 104 | returnShallow: false, 105 | }, 106 | deleteOneBase: { interceptors: [], decorators: [], returnDeleted: false }, 107 | recoverOneBase: { interceptors: [], decorators: [], returnRecovered: false }, 108 | }, 109 | }; 110 | CrudConfigService.load(conf); 111 | expect(CrudConfigService.config.params).toEqual( 112 | expect.objectContaining(expected.params), 113 | ); 114 | expect(CrudConfigService.config.query).toEqual( 115 | expect.objectContaining(expected.query), 116 | ); 117 | expect(CrudConfigService.config.operators).toEqual( 118 | expect.objectContaining(expected.operators), 119 | ); 120 | expect(JSON.stringify(CrudConfigService.config.routes)).toEqual( 121 | JSON.stringify(expected.routes), 122 | ); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rewiko/crud", 3 | "version": "4.0.0", 4 | "description": "Nest CRUD for RESTful APIs", 5 | "license": "MIT", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "private": true, 10 | "scripts": { 11 | "bootstrap": "npx lerna bootstrap", 12 | "s": "npx nps", 13 | "s:list": "yarn s --scripts", 14 | "rebuild": "yarn clean && yarn build", 15 | "build": "yarn s build", 16 | "clean": "yarn s clean", 17 | "test": "npx jest --runInBand -c=jest.config.js packages/ --verbose", 18 | "test:coverage": "yarn test:all --coverage", 19 | "test:coveralls": "yarn test:coverage --coverageReporters=text-lcov | coveralls", 20 | "test:all": "yarn test:mysql && yarn test:postgres", 21 | "test:postgres": "yarn db:prepare:typeorm && yarn test", 22 | "test:mysql": "yarn db:prepare:typeorm:mysql && TYPEORM_CONNECTION=mysql yarn test", 23 | "start:typeorm": "npx nodemon -w ./integration/crud-typeorm -e ts node_modules/ts-node/dist/bin.js integration/crud-typeorm/main.ts", 24 | "db:cli:typeorm": "cd ./integration/crud-typeorm && npx ts-node -r tsconfig-paths/register ../../node_modules/typeorm/cli.js", 25 | "db:sync:typeorm": "yarn db:cli:typeorm schema:sync -f=orm", 26 | "db:drop:typeorm": "yarn db:cli:typeorm schema:drop -f=orm", 27 | "db:seeds:typeorm": "yarn db:cli:typeorm migration:run -f=orm", 28 | "db:prepare:typeorm": "yarn db:drop:typeorm && yarn db:sync:typeorm && yarn db:seeds:typeorm", 29 | "db:prepare:typeorm:mysql": "yarn db:drop:typeorm -c=mysql && yarn db:sync:typeorm -c=mysql && yarn db:seeds:typeorm -c=mysql", 30 | "format": "npx pretty-quick --pattern \"packages/**/!(*.d).ts\"", 31 | "lint": "npx tslint 'packages/**/*.ts'", 32 | "cm": "npx git-cz", 33 | "postinstall": "npx opencollective", 34 | "pub": "npx lerna publish --force-publish --no-verify-access" 35 | }, 36 | "config": { 37 | "commitizen": { 38 | "path": "node_modules/cz-conventional-changelog" 39 | }, 40 | "validate-commit-msg": { 41 | "types": "conventional-commit-types", 42 | "helpMessage": "Use \"yarn commit\" instead, we use conventional-changelog format :) (https://github.com/commitizen/cz-cli)" 43 | } 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "yarn format --staged", 48 | "commit-msg": "npx validate-commit-msg" 49 | } 50 | }, 51 | "collective": { 52 | "type": "opencollective", 53 | "url": "https://opencollective.com/nestjsx", 54 | "donation": { 55 | "text": "Become a partner:" 56 | } 57 | }, 58 | "peerDependencies": {}, 59 | "optionalDependencies": {}, 60 | "dependencies": { 61 | "@nestjs/common": "7.6.18", 62 | "@nestjs/core": "7.6.18", 63 | "@nestjs/platform-express": "7.6.18", 64 | "@nestjs/swagger": "4.8.2", 65 | "@nestjs/testing": "7.6.18", 66 | "@nestjs/typeorm": "7.1.5", 67 | "@nuxtjs/opencollective": "0.3.2", 68 | "@types/jest": "26.0.24", 69 | "@types/node": "12.7.5", 70 | "@types/qs": "6.5.3", 71 | "@types/supertest": "2.0.8", 72 | "class-transformer": "0.5.1", 73 | "class-validator": "0.13.2", 74 | "commitizen": "4.2.4", 75 | "coveralls": "3.1.1", 76 | "cz-conventional-changelog": "3.3.0", 77 | "husky": "3.1.0", 78 | "jest": "26.6.3", 79 | "jest-extended": "0.11.5", 80 | "lerna": "3.22.1", 81 | "mysql": "2.18.1", 82 | "nodemon": "1.19.4", 83 | "npm-check": "5.9.2", 84 | "nps": "5.10.0", 85 | "nps-utils": "1.7.0", 86 | "pg": "8.7.3", 87 | "pluralize": "8.0.0", 88 | "prettier": "1.18.2", 89 | "pretty-quick": "1.11.1", 90 | "qs": "6.8.0", 91 | "redis": "3.1.2", 92 | "reflect-metadata": "0.1.13", 93 | "rimraf": "3.0.0", 94 | "rxjs": "6.5.3", 95 | "supertest": "4.0.2", 96 | "swagger-ui-express": "4.3.0", 97 | "ts-jest": "26.5.6", 98 | "ts-node": "9.1.1", 99 | "tsconfig-extends": "1.0.1", 100 | "tsconfig-paths": "3.9.0", 101 | "tslint": "5.20.0", 102 | "tslint-config-prettier": "1.18.0", 103 | "typeorm": "0.2.43", 104 | "typescript": "4.2.4", 105 | "validate-commit-msg": "2.14.0" 106 | }, 107 | "resolutions": { 108 | "axios": "0.21.2", 109 | "glob-parent": "5.1.2", 110 | "hosted-git-info": "3.0.8", 111 | "merge": "2.1.1", 112 | "json-schema": "0.4.0", 113 | "semver-regex": "3.1.3", 114 | "ansi-regex": "5.0.1", 115 | "trim-newlines": "3.0.1" 116 | }, 117 | "devDependencies": {}, 118 | "author": { 119 | "name": "Michael Yali", 120 | "email": "mihon4ik@gmail.com" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/crud/test/crud-config.service.global.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | 6 | import { CrudGlobalConfig } from '../src/interfaces'; 7 | import { CrudConfigService } from '../src/module/crud-config.service'; 8 | 9 | // IMPORTANT: 10 | // CrudConfigService.load() should be called before importing @Crud() controllers 11 | 12 | const conf: CrudGlobalConfig = { 13 | query: { 14 | limit: 10, 15 | }, 16 | params: { 17 | id: { 18 | field: 'id', 19 | type: 'uuid', 20 | primary: true, 21 | }, 22 | }, 23 | routes: { 24 | exclude: ['createManyBase'], 25 | updateOneBase: { 26 | allowParamsOverride: true, 27 | }, 28 | replaceOneBase: { 29 | allowParamsOverride: true, 30 | }, 31 | }, 32 | serialize: { 33 | get: false, 34 | }, 35 | }; 36 | 37 | // Important: load config before (!!!) you import AppModule 38 | // https://github.com/rewiko/crud/wiki/Controllers#global-options 39 | CrudConfigService.load(conf); 40 | 41 | import { Crud } from '../src/decorators/crud.decorator'; 42 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 43 | import { TestModel } from './__fixture__/models'; 44 | import { TestService } from './__fixture__/services'; 45 | 46 | describe('#crud', () => { 47 | describe('#CrudConfigService', () => { 48 | let app: INestApplication; 49 | let server: any; 50 | 51 | @Crud({ 52 | model: { type: TestModel }, 53 | }) 54 | @Controller('test') 55 | class GlobalTestController { 56 | constructor(public service: TestService) {} 57 | } 58 | 59 | @Crud({ 60 | model: { type: TestModel }, 61 | query: { 62 | limit: 12, 63 | }, 64 | params: { 65 | id: { 66 | field: 'id', 67 | type: 'number', 68 | primary: true, 69 | }, 70 | }, 71 | routes: { 72 | updateOneBase: { 73 | allowParamsOverride: false, 74 | }, 75 | replaceOneBase: { 76 | allowParamsOverride: false, 77 | }, 78 | deleteOneBase: { 79 | returnDeleted: true, 80 | }, 81 | }, 82 | }) 83 | @Controller('test2') 84 | class GlobalTestController2 { 85 | constructor(public service: TestService) {} 86 | } 87 | 88 | beforeAll(async () => { 89 | const fixture = await Test.createTestingModule({ 90 | controllers: [GlobalTestController, GlobalTestController2], 91 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 92 | }).compile(); 93 | 94 | app = fixture.createNestApplication(); 95 | 96 | await app.init(); 97 | server = app.getHttpServer(); 98 | }); 99 | 100 | afterAll(async () => { 101 | app.close(); 102 | }); 103 | 104 | it('should use global config', (done) => { 105 | return request(server) 106 | .get('/test') 107 | .end((_, res) => { 108 | expect(res.status).toBe(200); 109 | expect(res.body.req.options.query).toMatchObject(conf.query); 110 | expect(res.body.req.options.params).toMatchObject(conf.params); 111 | expect(res.body.req.options.routes.updateOneBase.allowParamsOverride).toBe( 112 | true, 113 | ); 114 | expect(res.body.req.options.routes.replaceOneBase.allowParamsOverride).toBe( 115 | true, 116 | ); 117 | done(); 118 | }); 119 | }); 120 | it('should use merged config', (done) => { 121 | return request(server) 122 | .get('/test2') 123 | .end((_, res) => { 124 | expect(res.status).toBe(200); 125 | expect(res.body.req.options.query).toMatchObject({ 126 | limit: 12, 127 | }); 128 | expect(res.body.req.options.params).toMatchObject({ 129 | id: { 130 | field: 'id', 131 | type: 'number', 132 | primary: true, 133 | }, 134 | }); 135 | expect(res.body.req.options.routes.updateOneBase.allowParamsOverride).toBe( 136 | false, 137 | ); 138 | expect(res.body.req.options.routes.replaceOneBase.allowParamsOverride).toBe( 139 | false, 140 | ); 141 | expect(res.body.req.options.routes.deleteOneBase.returnDeleted).toBe(true); 142 | done(); 143 | }); 144 | }); 145 | it('should exclude route, 1', (done) => { 146 | return request(server) 147 | .post('/test/bulk') 148 | .send({}) 149 | .end((_, res) => { 150 | expect(res.status).toBe(404); 151 | done(); 152 | }); 153 | }); 154 | it('should exclude route, 1', (done) => { 155 | return request(server) 156 | .post('/test2/bulk') 157 | .send({}) 158 | .end((_, res) => { 159 | expect(res.status).toBe(404); 160 | done(); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /packages/crud/src/crud/reflection.helper.ts: -------------------------------------------------------------------------------- 1 | import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; 2 | import { 3 | CUSTOM_ROUTE_AGRS_METADATA, 4 | INTERCEPTORS_METADATA, 5 | METHOD_METADATA, 6 | PARAMTYPES_METADATA, 7 | PATH_METADATA, 8 | ROUTE_ARGS_METADATA, 9 | } from '@nestjs/common/constants'; 10 | import { ArgumentsHost } from '@nestjs/common'; 11 | import { isFunction } from '@rewiko/util'; 12 | 13 | import { BaseRoute, MergedCrudOptions, AuthOptions } from '../interfaces'; 14 | import { BaseRouteName } from '../types'; 15 | import { 16 | CRUD_OPTIONS_METADATA, 17 | ACTION_NAME_METADATA, 18 | PARSED_CRUD_REQUEST_KEY, 19 | PARSED_BODY_METADATA, 20 | OVERRIDE_METHOD_METADATA, 21 | CRUD_AUTH_OPTIONS_METADATA, 22 | } from '../constants'; 23 | import { CrudActions } from '../enums'; 24 | 25 | export class R { 26 | static set( 27 | metadataKey: any, 28 | metadataValue: any, 29 | target: Object, 30 | propertyKey: string | symbol = undefined, 31 | ) { 32 | if (propertyKey) { 33 | Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); 34 | } else { 35 | Reflect.defineMetadata(metadataKey, metadataValue, target); 36 | } 37 | } 38 | 39 | static get( 40 | metadataKey: any, 41 | target: Object, 42 | propertyKey: string | symbol = undefined, 43 | ): T { 44 | return propertyKey 45 | ? Reflect.getMetadata(metadataKey, target, propertyKey) 46 | : Reflect.getMetadata(metadataKey, target); 47 | } 48 | 49 | static createCustomRouteArg( 50 | paramtype: string, 51 | index: number, 52 | /* istanbul ignore next */ 53 | pipes: any[] = [], 54 | data = undefined, 55 | ): any { 56 | return { 57 | [`${paramtype}${CUSTOM_ROUTE_AGRS_METADATA}:${index}`]: { 58 | index, 59 | factory: (_, ctx) => R.getContextRequest(ctx)[paramtype], 60 | data, 61 | pipes, 62 | }, 63 | }; 64 | } 65 | 66 | static createRouteArg( 67 | paramtype: RouteParamtypes, 68 | index: number, 69 | /* istanbul ignore next */ 70 | pipes: any[] = [], 71 | data = undefined, 72 | ): any { 73 | return { 74 | [`${paramtype}:${index}`]: { 75 | index, 76 | pipes, 77 | data, 78 | }, 79 | }; 80 | } 81 | 82 | static setDecorators( 83 | decorators: (PropertyDecorator | MethodDecorator)[], 84 | target: object, 85 | name: string, 86 | ) { 87 | // this makes metadata decorator works 88 | const decoratedDescriptor = Reflect.decorate( 89 | decorators, 90 | target, 91 | name, 92 | Reflect.getOwnPropertyDescriptor(target, name), 93 | ); 94 | 95 | // this makes proxy decorator works 96 | Reflect.defineProperty(target, name, decoratedDescriptor); 97 | } 98 | 99 | static setParsedRequestArg(index: number) { 100 | return R.createCustomRouteArg(PARSED_CRUD_REQUEST_KEY, index); 101 | } 102 | 103 | static setBodyArg(index: number, /* istanbul ignore next */ pipes: any[] = []) { 104 | return R.createRouteArg(RouteParamtypes.BODY, index, pipes); 105 | } 106 | 107 | static setCrudOptions(options: MergedCrudOptions, target: any) { 108 | R.set(CRUD_OPTIONS_METADATA, options, target); 109 | } 110 | 111 | static setRoute(route: BaseRoute, func: Function) { 112 | R.set(PATH_METADATA, route.path, func); 113 | R.set(METHOD_METADATA, route.method, func); 114 | } 115 | 116 | static setInterceptors(interceptors: any[], func: Function) { 117 | R.set(INTERCEPTORS_METADATA, interceptors, func); 118 | } 119 | 120 | static setRouteArgs(metadata: any, target: any, name: string) { 121 | R.set(ROUTE_ARGS_METADATA, metadata, target, name); 122 | } 123 | 124 | static setRouteArgsTypes(metadata: any, target: any, name: string) { 125 | R.set(PARAMTYPES_METADATA, metadata, target, name); 126 | } 127 | 128 | static setAction(action: CrudActions, func: Function) { 129 | R.set(ACTION_NAME_METADATA, action, func); 130 | } 131 | 132 | static setCrudAuthOptions(metadata: any, target: any) { 133 | R.set(CRUD_AUTH_OPTIONS_METADATA, metadata, target); 134 | } 135 | 136 | static getCrudAuthOptions(target: any): AuthOptions { 137 | return R.get(CRUD_AUTH_OPTIONS_METADATA, target); 138 | } 139 | 140 | static getCrudOptions(target: any): MergedCrudOptions { 141 | return R.get(CRUD_OPTIONS_METADATA, target); 142 | } 143 | 144 | static getAction(func: Function): CrudActions { 145 | return R.get(ACTION_NAME_METADATA, func); 146 | } 147 | 148 | static getOverrideRoute(func: Function): BaseRouteName { 149 | return R.get(OVERRIDE_METHOD_METADATA, func); 150 | } 151 | 152 | static getInterceptors(func: Function): any[] { 153 | return R.get(INTERCEPTORS_METADATA, func) || []; 154 | } 155 | 156 | static getRouteArgs(target: any, name: string): any { 157 | return R.get(ROUTE_ARGS_METADATA, target, name); 158 | } 159 | 160 | static getRouteArgsTypes(target: any, name: string): any[] { 161 | return R.get(PARAMTYPES_METADATA, target, name) || /* istanbul ignore next */ []; 162 | } 163 | 164 | static getParsedBody(func: Function): any { 165 | return R.get(PARSED_BODY_METADATA, func); 166 | } 167 | 168 | static getContextRequest(ctx: ArgumentsHost): any { 169 | return isFunction(ctx.switchToHttp) 170 | ? ctx.switchToHttp().getRequest() 171 | : /* istanbul ignore next */ ctx; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/d.crud-auth.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | INestApplication, 4 | Injectable, 5 | CanActivate, 6 | ExecutionContext, 7 | } from '@nestjs/common'; 8 | import { APP_FILTER, APP_GUARD } from '@nestjs/core'; 9 | import { Test } from '@nestjs/testing'; 10 | import { TypeOrmModule } from '@nestjs/typeorm'; 11 | 12 | import { Crud, CrudAuth } from '@rewiko/crud'; 13 | import * as request from 'supertest'; 14 | import { withCache } from '../../../integration/crud-typeorm/orm.config'; 15 | import { User } from '../../../integration/crud-typeorm/users'; 16 | import { UserProfile } from '../../../integration/crud-typeorm/users-profiles'; 17 | import { Project } from '../../../integration/crud-typeorm/projects'; 18 | import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter'; 19 | import { UsersService } from './__fixture__/users.service'; 20 | import { ProjectsService } from './__fixture__/projects.service'; 21 | 22 | describe('#crud-typeorm', () => { 23 | describe('#CrudAuth', () => { 24 | const USER_REQUEST_KEY = 'user'; 25 | let app: INestApplication; 26 | let server: request.SuperTest; 27 | 28 | @Injectable() 29 | class AuthGuard implements CanActivate { 30 | constructor(private usersService: UsersService) {} 31 | 32 | async canActivate(ctx: ExecutionContext): Promise { 33 | const req = ctx.switchToHttp().getRequest(); 34 | req[USER_REQUEST_KEY] = await this.usersService.findOne(1); 35 | 36 | return true; 37 | } 38 | } 39 | 40 | @Crud({ 41 | model: { 42 | type: User, 43 | }, 44 | routes: { 45 | only: ['getOneBase', 'updateOneBase'], 46 | }, 47 | params: { 48 | id: { 49 | primary: true, 50 | disabled: true, 51 | }, 52 | }, 53 | }) 54 | @CrudAuth({ 55 | property: USER_REQUEST_KEY, 56 | filter: (user: User) => ({ 57 | id: user.id, 58 | }), 59 | persist: (user: User) => ({ 60 | email: user.email, 61 | }), 62 | }) 63 | @Controller('me') 64 | class MeController { 65 | constructor(public service: UsersService) {} 66 | } 67 | 68 | @Crud({ 69 | model: { 70 | type: Project, 71 | }, 72 | routes: { 73 | only: ['createOneBase', 'deleteOneBase'], 74 | }, 75 | }) 76 | @CrudAuth({ 77 | property: USER_REQUEST_KEY, 78 | filter: (user: User) => ({ 79 | companyId: user.companyId, 80 | }), 81 | persist: (user: User) => ({ 82 | companyId: user.companyId, 83 | }), 84 | }) 85 | @Controller('projects') 86 | class ProjectsController { 87 | constructor(public service: ProjectsService) {} 88 | } 89 | 90 | beforeAll(async () => { 91 | const fixture = await Test.createTestingModule({ 92 | imports: [ 93 | TypeOrmModule.forRoot({ ...withCache, logging: false }), 94 | TypeOrmModule.forFeature([User, UserProfile, Project]), 95 | ], 96 | controllers: [MeController, ProjectsController], 97 | providers: [ 98 | { 99 | provide: APP_GUARD, 100 | useClass: AuthGuard, 101 | }, 102 | { 103 | provide: APP_FILTER, 104 | useClass: HttpExceptionFilter, 105 | }, 106 | UsersService, 107 | ProjectsService, 108 | ], 109 | }).compile(); 110 | 111 | app = fixture.createNestApplication(); 112 | 113 | await app.init(); 114 | server = request(app.getHttpServer()); 115 | }); 116 | 117 | afterAll(async () => { 118 | await app.close(); 119 | }); 120 | 121 | describe('#getOneBase', () => { 122 | it('should return a user with id 1', async () => { 123 | const res = await server.get('/me').expect(200); 124 | expect(res.body.id).toBe(1); 125 | }); 126 | }); 127 | 128 | describe('#updateOneBase', () => { 129 | it('should update user with auth persist, 1', async () => { 130 | const res = await server 131 | .patch('/me') 132 | .send({ 133 | email: 'some@dot.com', 134 | isActive: false, 135 | }) 136 | .expect(200); 137 | expect(res.body.id).toBe(1); 138 | expect(res.body.email).toBe('1@email.com'); 139 | expect(res.body.isActive).toBe(false); 140 | }); 141 | it('should update user with auth persist, 1', async () => { 142 | const res = await server 143 | .patch('/me') 144 | .send({ 145 | email: 'some@dot.com', 146 | isActive: true, 147 | }) 148 | .expect(200); 149 | expect(res.body.id).toBe(1); 150 | expect(res.body.email).toBe('1@email.com'); 151 | expect(res.body.isActive).toBe(true); 152 | }); 153 | }); 154 | 155 | describe('#createOneBase', () => { 156 | it('should create an entity with auth persist', async () => { 157 | const res = await server 158 | .post('/projects') 159 | .send({ 160 | name: 'Test', 161 | description: 'foo', 162 | isActive: false, 163 | companyId: 10, 164 | }) 165 | .expect(201); 166 | expect(res.body.companyId).toBe(1); 167 | }); 168 | }); 169 | 170 | describe('#deleteOneBase', () => { 171 | it('should delete an entity with auth filter', async () => { 172 | const res = await server.delete('/projects/21').expect(200); 173 | }); 174 | it('should throw an error with auth filter', async () => { 175 | const res = await server.delete('/projects/20').expect(404); 176 | }); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /packages/crud-typeorm/test/a.params-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, INestApplication } from '@nestjs/common'; 2 | import { APP_FILTER } from '@nestjs/core'; 3 | import { Test } from '@nestjs/testing'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import 'jest-extended'; 6 | import * as request from 'supertest'; 7 | 8 | import { Company } from '../../../integration/crud-typeorm/companies'; 9 | import { withCache } from '../../../integration/crud-typeorm/orm.config'; 10 | import { Project } from '../../../integration/crud-typeorm/projects'; 11 | import { User } from '../../../integration/crud-typeorm/users'; 12 | import { UserProfile } from '../../../integration/crud-typeorm/users-profiles'; 13 | import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter'; 14 | import { Crud } from '../../crud/src/decorators/crud.decorator'; 15 | import { UsersService } from './__fixture__/users.service'; 16 | 17 | // tslint:disable:max-classes-per-file 18 | describe('#crud-typeorm', () => { 19 | describe('#params options', () => { 20 | let app: INestApplication; 21 | let server: any; 22 | 23 | @Crud({ 24 | model: { type: User }, 25 | params: { 26 | companyId: { 27 | field: 'companyId', 28 | type: 'number', 29 | }, 30 | id: { 31 | field: 'id', 32 | type: 'number', 33 | primary: true, 34 | }, 35 | }, 36 | routes: { 37 | updateOneBase: { 38 | allowParamsOverride: true, 39 | returnShallow: true, 40 | }, 41 | replaceOneBase: { 42 | allowParamsOverride: true, 43 | returnShallow: true, 44 | }, 45 | }, 46 | }) 47 | @Controller('/companiesA/:companyId/users') 48 | class UsersController1 { 49 | constructor(public service: UsersService) {} 50 | } 51 | 52 | @Crud({ 53 | model: { type: User }, 54 | params: { 55 | companyId: { 56 | field: 'companyId', 57 | type: 'number', 58 | }, 59 | id: { 60 | field: 'id', 61 | type: 'number', 62 | primary: true, 63 | }, 64 | }, 65 | query: { 66 | join: { 67 | company: { 68 | eager: true, 69 | }, 70 | }, 71 | }, 72 | }) 73 | @Controller('/companiesB/:companyId/users') 74 | class UsersController2 { 75 | constructor(public service: UsersService) {} 76 | } 77 | 78 | beforeAll(async () => { 79 | const fixture = await Test.createTestingModule({ 80 | imports: [ 81 | TypeOrmModule.forRoot({ ...withCache, logging: false }), 82 | TypeOrmModule.forFeature([Company, Project, User, UserProfile]), 83 | ], 84 | controllers: [UsersController1, UsersController2], 85 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, UsersService], 86 | }).compile(); 87 | 88 | app = fixture.createNestApplication(); 89 | 90 | await app.init(); 91 | server = app.getHttpServer(); 92 | }); 93 | 94 | afterAll(async () => { 95 | await app.close(); 96 | }); 97 | 98 | describe('#updateOneBase', () => { 99 | it('should override params', async () => { 100 | const dto = { isActive: false, companyId: 2 }; 101 | const res = await request(server) 102 | .patch('/companiesA/1/users/2') 103 | .send(dto) 104 | .expect(200); 105 | expect(res.body.companyId).toBe(2); 106 | }); 107 | it('should not override params', async () => { 108 | const dto = { isActive: false, companyId: 2 }; 109 | const res = await request(server) 110 | .patch('/companiesB/1/users/3') 111 | .send(dto) 112 | .expect(200); 113 | expect(res.body.companyId).toBe(1); 114 | }); 115 | it('should return full entity', async () => { 116 | const dto = { isActive: false }; 117 | const res = await request(server) 118 | .patch('/companiesB/2/users/2') 119 | .send(dto) 120 | .expect(200); 121 | expect(res.body.company.id).toBe(2); 122 | }); 123 | it('should return shallow entity', async () => { 124 | const dto = { isActive: false }; 125 | const res = await request(server) 126 | .patch('/companiesA/2/users/2') 127 | .send(dto) 128 | .expect(200); 129 | expect(res.body.company).toBeUndefined(); 130 | }); 131 | }); 132 | 133 | describe('#replaceOneBase', () => { 134 | it('should override params', async () => { 135 | const dto = { isActive: false, companyId: 2, email: '4@email.com' }; 136 | const res = await request(server) 137 | .put('/companiesA/1/users/4') 138 | .send(dto) 139 | .expect(200); 140 | expect(res.body.companyId).toBe(2); 141 | }); 142 | it('should not override params', async () => { 143 | const dto = { isActive: false, companyId: 1 }; 144 | const res = await request(server) 145 | .put('/companiesB/2/users/4') 146 | .send(dto) 147 | .expect(200); 148 | expect(res.body.companyId).toBe(2); 149 | }); 150 | it('should return full entity', async () => { 151 | const dto = { isActive: false }; 152 | const res = await request(server) 153 | .put('/companiesB/2/users/4') 154 | .send(dto) 155 | .expect(200); 156 | expect(res.body.company.id).toBe(2); 157 | }); 158 | it('should return shallow entity', async () => { 159 | const dto = { isActive: false }; 160 | const res = await request(server) 161 | .put('/companiesA/2/users/4') 162 | .send(dto) 163 | .expect(200); 164 | expect(res.body.company).toBeUndefined(); 165 | }); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /packages/crud/src/interceptors/crud-request.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | CallHandler, 4 | ExecutionContext, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { 9 | QueryFilter, 10 | RequestQueryException, 11 | RequestQueryParser, 12 | SCondition, 13 | } from '@rewiko/crud-request'; 14 | import { hasLength, isArrayFull, isFunction, isNil } from '@rewiko/util'; 15 | 16 | import { PARSED_CRUD_REQUEST_KEY } from '../constants'; 17 | import { CrudActions } from '../enums'; 18 | import { CrudRequest, MergedCrudOptions } from '../interfaces'; 19 | import { QueryFilterFunction } from '../types'; 20 | import { CrudBaseInterceptor } from './crud-base.interceptor'; 21 | 22 | @Injectable() 23 | export class CrudRequestInterceptor extends CrudBaseInterceptor 24 | implements NestInterceptor { 25 | intercept(context: ExecutionContext, next: CallHandler) { 26 | const req = context.switchToHttp().getRequest(); 27 | 28 | try { 29 | /* istanbul ignore else */ 30 | if (!req[PARSED_CRUD_REQUEST_KEY]) { 31 | const { ctrlOptions, crudOptions, action } = this.getCrudInfo(context); 32 | const parser = RequestQueryParser.create(); 33 | 34 | parser.parseQuery(req.query, crudOptions.operators.custom); 35 | 36 | if (!isNil(ctrlOptions)) { 37 | const search = this.getSearch(parser, crudOptions, action, req.params); 38 | const auth = this.getAuth(parser, crudOptions, req); 39 | parser.search = auth.or 40 | ? { $or: [auth.or, { $and: search }] } 41 | : { $and: [auth.filter, ...search] }; 42 | } else { 43 | parser.search = { $and: this.getSearch(parser, crudOptions, action) }; 44 | } 45 | 46 | req[PARSED_CRUD_REQUEST_KEY] = this.getCrudRequest(parser, crudOptions); 47 | } 48 | 49 | return next.handle(); 50 | } catch (error) { 51 | /* istanbul ignore next */ 52 | throw error instanceof RequestQueryException 53 | ? new BadRequestException(error.message) 54 | : error; 55 | } 56 | } 57 | 58 | getCrudRequest( 59 | parser: RequestQueryParser, 60 | crudOptions: Partial, 61 | ): CrudRequest { 62 | const parsed = parser.getParsed(); 63 | const { query, routes, params, operators } = crudOptions; 64 | 65 | return { 66 | parsed, 67 | options: { 68 | query, 69 | routes, 70 | params, 71 | operators, 72 | }, 73 | }; 74 | } 75 | 76 | getSearch( 77 | parser: RequestQueryParser, 78 | crudOptions: Partial, 79 | action: CrudActions, 80 | params?: any, 81 | ): SCondition[] { 82 | // params condition 83 | const paramsSearch = this.getParamsSearch(parser, crudOptions, params); 84 | 85 | // if `CrudOptions.query.filter` is a function then return transformed query search conditions 86 | if (isFunction(crudOptions.query.filter)) { 87 | const filterCond = 88 | (crudOptions.query.filter as QueryFilterFunction)( 89 | parser.search, 90 | action === CrudActions.ReadAll, 91 | ) || /* istanbul ignore next */ {}; 92 | 93 | return [...paramsSearch, filterCond]; 94 | } 95 | 96 | // if `CrudOptions.query.filter` is array or search condition type 97 | const optionsFilter = isArrayFull(crudOptions.query.filter) 98 | ? (crudOptions.query.filter as QueryFilter[]).map(parser.convertFilterToSearch) 99 | : [(crudOptions.query.filter as SCondition) || {}]; 100 | 101 | let search: SCondition[] = []; 102 | 103 | if (parser.search) { 104 | search = [parser.search]; 105 | } else if (hasLength(parser.filter) && hasLength(parser.or)) { 106 | search = 107 | parser.filter.length === 1 && parser.or.length === 1 108 | ? [ 109 | { 110 | $or: [ 111 | parser.convertFilterToSearch(parser.filter[0]), 112 | parser.convertFilterToSearch(parser.or[0]), 113 | ], 114 | }, 115 | ] 116 | : [ 117 | { 118 | $or: [ 119 | { $and: parser.filter.map(parser.convertFilterToSearch) }, 120 | { $and: parser.or.map(parser.convertFilterToSearch) }, 121 | ], 122 | }, 123 | ]; 124 | } else if (hasLength(parser.filter)) { 125 | search = parser.filter.map(parser.convertFilterToSearch); 126 | } else { 127 | if (hasLength(parser.or)) { 128 | search = 129 | parser.or.length === 1 130 | ? [parser.convertFilterToSearch(parser.or[0])] 131 | : /* istanbul ignore next */ [ 132 | { 133 | $or: parser.or.map(parser.convertFilterToSearch), 134 | }, 135 | ]; 136 | } 137 | } 138 | 139 | return [...paramsSearch, ...optionsFilter, ...search]; 140 | } 141 | 142 | getParamsSearch( 143 | parser: RequestQueryParser, 144 | crudOptions: Partial, 145 | params?: any, 146 | ): SCondition[] { 147 | if (params) { 148 | parser.parseParams(params, crudOptions.params); 149 | 150 | return isArrayFull(parser.paramsFilter) 151 | ? parser.paramsFilter.map(parser.convertFilterToSearch) 152 | : []; 153 | } 154 | 155 | return []; 156 | } 157 | 158 | getAuth( 159 | parser: RequestQueryParser, 160 | crudOptions: Partial, 161 | req: any, 162 | ): { filter?: any; or?: any } { 163 | const auth: any = {}; 164 | 165 | /* istanbul ignore else */ 166 | if (crudOptions.auth) { 167 | const userOrRequest = crudOptions.auth.property 168 | ? req[crudOptions.auth.property] 169 | : req; 170 | 171 | if (isFunction(crudOptions.auth.or)) { 172 | auth.or = crudOptions.auth.or(userOrRequest); 173 | } 174 | 175 | if (isFunction(crudOptions.auth.filter) && !auth.or) { 176 | auth.filter = 177 | crudOptions.auth.filter(userOrRequest) || /* istanbul ignore next */ {}; 178 | } 179 | 180 | if (isFunction(crudOptions.auth.persist)) { 181 | parser.setAuthPersist(crudOptions.auth.persist(userOrRequest)); 182 | } 183 | } 184 | 185 | return auth; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /packages/crud/test/crud.decorator.override.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | import { RequestQueryBuilder } from '@rewiko/crud-request'; 6 | 7 | import { Crud, Override, ParsedRequest, ParsedBody } from '../src/decorators'; 8 | import { CrudController, CrudRequest, CreateManyDto } from '../src/interfaces'; 9 | import { R, Swagger } from '../src/crud'; 10 | import { CrudActions } from '../src/enums'; 11 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 12 | import { TestModel } from './__fixture__/models'; 13 | import { TestService } from './__fixture__/services'; 14 | 15 | describe('#crud', () => { 16 | describe('#override methods', () => { 17 | let app: INestApplication; 18 | let server: any; 19 | let qb: RequestQueryBuilder; 20 | 21 | enum Field { 22 | ONE = 'one', 23 | } 24 | 25 | @Crud({ 26 | model: { type: TestModel }, 27 | params: { 28 | enumField: { 29 | field: 'enum_field', 30 | type: 'string', 31 | enum: Field, 32 | }, 33 | disabledField: { 34 | field: 'disabled_field', 35 | type: 'number', 36 | disabled: true, 37 | }, 38 | }, 39 | }) 40 | @Controller('test') 41 | class TestController implements CrudController { 42 | constructor(public service: TestService) {} 43 | 44 | get base(): CrudController { 45 | return this; 46 | } 47 | 48 | @Override() 49 | getMany(@ParsedRequest() req: CrudRequest) { 50 | return { foo: 'bar' }; 51 | } 52 | 53 | @Override('createManyBase') 54 | createBulk( 55 | @ParsedBody() dto: CreateManyDto, 56 | @ParsedRequest() req: CrudRequest, 57 | ) { 58 | return this.base.createManyBase(req, dto); 59 | } 60 | } 61 | 62 | beforeAll(async () => { 63 | const fixture = await Test.createTestingModule({ 64 | controllers: [TestController], 65 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 66 | }).compile(); 67 | 68 | app = fixture.createNestApplication(); 69 | 70 | await app.init(); 71 | server = app.getHttpServer(); 72 | }); 73 | 74 | beforeEach(() => { 75 | qb = RequestQueryBuilder.create(); 76 | }); 77 | 78 | afterAll(async () => { 79 | app.close(); 80 | }); 81 | 82 | describe('#override getMany', () => { 83 | it('should return status 200', (done) => { 84 | return request(server) 85 | .get('/test') 86 | .expect(200) 87 | .end((_, res) => { 88 | const expected = { foo: 'bar' }; 89 | expect(res.body).toMatchObject(expected); 90 | done(); 91 | }); 92 | }); 93 | it('should return status 400', (done) => { 94 | const query = qb.setFilter({ field: 'foo', operator: 'gt' }).query(); 95 | return request(server) 96 | .get('/test') 97 | .query(query) 98 | .end((_, res) => { 99 | const expected = { statusCode: 400, message: 'Invalid filter value' }; 100 | expect(res.status).toEqual(400); 101 | expect(res.body).toMatchObject(expected); 102 | done(); 103 | }); 104 | }); 105 | it('should have action metadata', () => { 106 | const action = R.getAction(TestController.prototype.getMany); 107 | expect(action).toBe(CrudActions.ReadAll); 108 | }); 109 | it('should return swagger operation', () => { 110 | const operation = Swagger.getOperation(TestController.prototype.getMany); 111 | const expected = { summary: 'Retrieve multiple TestModels' }; 112 | expect(operation).toMatchObject(expected); 113 | }); 114 | it('should return swagger params', () => { 115 | const params = Swagger.getParams(TestController.prototype.getMany); 116 | expect(Array.isArray(params)).toBe(true); 117 | expect(params.length > 0).toBe(true); 118 | 119 | const enumParam = params.find((param) => param.name === 'enumField'); 120 | expect(enumParam).toBeDefined(); 121 | expect(enumParam.enum).toEqual(['one']); 122 | }); 123 | it('should not return disabled fields in swagger', () => { 124 | const params = Swagger.getParams(TestController.prototype.getMany); 125 | expect(Array.isArray(params)).toBe(true); 126 | expect(params.length > 0).toBe(true); 127 | 128 | const disabledParam = params.find((param) => param.name === 'disabledField'); 129 | expect(disabledParam).toBeUndefined(); 130 | }); 131 | it('should return swagger response ok', () => { 132 | const response = Swagger.getResponseOk(TestController.prototype.getMany); 133 | const expected = { 134 | '200': { 135 | schema: { 136 | oneOf: [ 137 | { $ref: '#/components/schemas/GetManyTestModelResponseDto' }, 138 | { items: { $ref: '#/components/schemas/TestModel' }, type: 'array' }, 139 | ], 140 | }, 141 | }, 142 | }; 143 | expect(response).toMatchObject(expected); 144 | }); 145 | }); 146 | 147 | describe('#override createMany', () => { 148 | it('should still validate dto', (done) => { 149 | const send: CreateManyDto = { 150 | bulk: [], 151 | }; 152 | return request(server) 153 | .post('/test/bulk') 154 | .send(send) 155 | .end((_, res) => { 156 | expect(res.status).toEqual(400); 157 | done(); 158 | }); 159 | }); 160 | it('should return status 201', (done) => { 161 | const send: CreateManyDto = { 162 | bulk: [ 163 | { 164 | firstName: 'firstName', 165 | lastName: 'lastName', 166 | email: 'test@test.com', 167 | age: 15, 168 | }, 169 | { 170 | firstName: 'firstName', 171 | lastName: 'lastName', 172 | email: 'test@test.com', 173 | age: 15, 174 | }, 175 | ], 176 | }; 177 | return request(server) 178 | .post('/test/bulk') 179 | .send(send) 180 | .expect(201) 181 | .end((_, res) => { 182 | expect(res.body).toHaveProperty('req'); 183 | expect(res.body).toHaveProperty('dto'); 184 | expect(res.body.dto).toMatchObject(send); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /packages/crud/test/crud.decorator.base.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Controller, INestApplication } from '@nestjs/common'; 4 | import { APP_FILTER } from '@nestjs/core'; 5 | import { RequestQueryBuilder } from '@rewiko/crud-request'; 6 | 7 | import { Crud } from '../src/decorators/crud.decorator'; 8 | import { CreateManyDto } from '../src/interfaces'; 9 | import { HttpExceptionFilter } from './__fixture__/exception.filter'; 10 | import { TestModel } from './__fixture__/models'; 11 | import { TestService } from './__fixture__/services'; 12 | 13 | describe('#crud', () => { 14 | describe('#base methods', () => { 15 | let app: INestApplication; 16 | let server: any; 17 | let qb: RequestQueryBuilder; 18 | 19 | @Crud({ 20 | model: { type: TestModel }, 21 | }) 22 | @Controller('test') 23 | class TestController { 24 | constructor(public service: TestService) {} 25 | } 26 | 27 | beforeAll(async () => { 28 | const fixture = await Test.createTestingModule({ 29 | controllers: [TestController], 30 | providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService], 31 | }).compile(); 32 | 33 | app = fixture.createNestApplication(); 34 | 35 | await app.init(); 36 | server = app.getHttpServer(); 37 | }); 38 | 39 | beforeEach(() => { 40 | qb = RequestQueryBuilder.create(); 41 | }); 42 | 43 | afterAll(async () => { 44 | app.close(); 45 | }); 46 | 47 | describe('#getManyBase', () => { 48 | it('should return status 200', () => { 49 | return request(server) 50 | .get('/test') 51 | .expect(200); 52 | }); 53 | it('should return status 400', (done) => { 54 | const query = qb.setFilter({ field: 'foo', operator: 'gt' }).query(); 55 | return request(server) 56 | .get('/test') 57 | .query(query) 58 | .end((_, res) => { 59 | const expected = { statusCode: 400, message: 'Invalid filter value' }; 60 | expect(res.status).toEqual(400); 61 | expect(res.body).toMatchObject(expected); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('#getOneBase', () => { 68 | it('should return status 200', () => { 69 | return request(server) 70 | .get('/test/1') 71 | .expect(200); 72 | }); 73 | it('should return status 400', (done) => { 74 | return request(server) 75 | .get('/test/invalid') 76 | .end((_, res) => { 77 | const expected = { 78 | statusCode: 400, 79 | message: 'Invalid param id. Number expected', 80 | }; 81 | expect(res.status).toEqual(400); 82 | expect(res.body).toMatchObject(expected); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('#createOneBase', () => { 89 | it('should return status 201', () => { 90 | const send: TestModel = { 91 | firstName: 'firstName', 92 | lastName: 'lastName', 93 | email: 'test@test.com', 94 | age: 15, 95 | }; 96 | return request(server) 97 | .post('/test') 98 | .send(send) 99 | .expect(201); 100 | }); 101 | it('should return status 400', (done) => { 102 | const send: TestModel = { 103 | firstName: 'firstName', 104 | lastName: 'lastName', 105 | email: 'test@test.com', 106 | }; 107 | return request(server) 108 | .post('/test') 109 | .send(send) 110 | .end((_, res) => { 111 | expect(res.status).toEqual(400); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('#createMadyBase', () => { 118 | it('should return status 201', () => { 119 | const send: CreateManyDto = { 120 | bulk: [ 121 | { 122 | firstName: 'firstName', 123 | lastName: 'lastName', 124 | email: 'test@test.com', 125 | age: 15, 126 | }, 127 | { 128 | firstName: 'firstName', 129 | lastName: 'lastName', 130 | email: 'test@test.com', 131 | age: 15, 132 | }, 133 | ], 134 | }; 135 | return request(server) 136 | .post('/test/bulk') 137 | .send(send) 138 | .expect(201); 139 | }); 140 | it('should return status 400', (done) => { 141 | const send: CreateManyDto = { 142 | bulk: [], 143 | }; 144 | return request(server) 145 | .post('/test/bulk') 146 | .send(send) 147 | .end((_, res) => { 148 | expect(res.status).toEqual(400); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | 154 | describe('#replaceOneBase', () => { 155 | it('should return status 200', () => { 156 | const send: TestModel = { 157 | id: 1, 158 | firstName: 'firstName', 159 | lastName: 'lastName', 160 | email: 'test@test.com', 161 | age: 15, 162 | }; 163 | return request(server) 164 | .put('/test/1') 165 | .send(send) 166 | .expect(200); 167 | }); 168 | it('should return status 400', (done) => { 169 | const send: TestModel = { 170 | firstName: 'firstName', 171 | lastName: 'lastName', 172 | email: 'test@test.com', 173 | }; 174 | return request(server) 175 | .put('/test/1') 176 | .send(send) 177 | .end((_, res) => { 178 | expect(res.status).toEqual(400); 179 | done(); 180 | }); 181 | }); 182 | }); 183 | 184 | describe('#updateOneBase', () => { 185 | it('should return status 200', () => { 186 | const send: TestModel = { 187 | id: 1, 188 | firstName: 'firstName', 189 | lastName: 'lastName', 190 | email: 'test@test.com', 191 | age: 15, 192 | }; 193 | return request(server) 194 | .patch('/test/1') 195 | .send(send) 196 | .expect(200); 197 | }); 198 | it('should return status 400', (done) => { 199 | const send: TestModel = { 200 | firstName: 'firstName', 201 | lastName: 'lastName', 202 | email: 'test@test.com', 203 | }; 204 | return request(server) 205 | .patch('/test/1') 206 | .send(send) 207 | .end((_, res) => { 208 | expect(res.status).toEqual(400); 209 | done(); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('#deleteOneBase', () => { 215 | it('should return status 200', () => { 216 | return request(server) 217 | .delete('/test/1') 218 | .expect(200); 219 | }); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /packages/util/README.md: -------------------------------------------------------------------------------- 1 |
2 |

CRUD (@rewiko/util)

3 |
4 |
5 | for RESTful APIs built with NestJs 6 |
7 | 8 |
9 | 10 | 51 | 52 |
53 | Built by 54 | @MichaelYali and 55 | 56 | Contributors 57 | 58 |
59 | 60 |
61 | 62 | We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@rewiko/crud` microframework very useful. 63 | 64 | ## Features 65 | 66 | CRUD usage 67 | 68 | - Super easy to install and start using the full-featured controllers and services :point_right: 69 | 70 | - DB and service agnostic extendable CRUD controllers 71 | 72 | - Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc. 73 | 74 | - Framework agnostic package with query builder for a frontend usage 75 | 76 | - Query, path params and DTOs validation included 77 | 78 | - Overriding controller methods with ease 79 | 80 | - Tiny config (including globally) 81 | 82 | - Additional helper decorators 83 | 84 | - Swagger documentation 85 | 86 | ## Packages 87 | 88 | - [**@rewiko/crud**](https://www.npmjs.com/package/@rewiko/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/rewiko/crud/wiki/Controllers#description)) 89 | - [**@rewiko/crud-request**](https://www.npmjs.com/package/@rewiko/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/rewiko/crud/wiki/Requests#frontend-usage)) 90 | - [**@rewiko/crud-typeorm**](https://www.npmjs.com/package/@rewiko/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/rewiko/crud/wiki/ServiceTypeorm)) 91 | 92 | ## Documentation 93 | 94 | - [General Information](https://github.com/rewiko/crud/wiki#why) 95 | - [CRUD Controllers](https://github.com/rewiko/crud/wiki/Controllers#description) 96 | - [CRUD ORM Services](https://github.com/rewiko/crud/wiki/Services#description) 97 | - [Handling Requests](https://github.com/rewiko/crud/wiki/Requests#description) 98 | 99 | ## Support 100 | 101 | Any support is welcome. At least you can give us a star. 102 | 103 | ## Contributors 104 | 105 | ### Code Contributors 106 | 107 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 108 | 109 | 110 | ### Financial Contributors 111 | 112 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)] 113 | 114 | #### Individuals 115 | 116 | 117 | 118 | #### Organizations 119 | 120 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)] 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ## License 134 | 135 | [MIT](LICENSE) 136 | -------------------------------------------------------------------------------- /packages/crud-request/README.md: -------------------------------------------------------------------------------- 1 |
2 |

CRUD (@rewiko/crud-request)

3 |
4 |
5 | for RESTful APIs built with NestJs 6 |
7 | 8 |
9 | 10 | 51 | 52 |
53 | Built by 54 | @MichaelYali and 55 | 56 | Contributors 57 | 58 |
59 | 60 |
61 | 62 | We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@rewiko/crud` microframework very useful. 63 | 64 | ## Features 65 | 66 | CRUD usage 67 | 68 | - Super easy to install and start using the full-featured controllers and services :point_right: 69 | 70 | - DB and service agnostic extendable CRUD controllers 71 | 72 | - Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc. 73 | 74 | - Framework agnostic package with query builder for a frontend usage 75 | 76 | - Query, path params and DTOs validation included 77 | 78 | - Overriding controller methods with ease 79 | 80 | - Tiny config (including globally) 81 | 82 | - Additional helper decorators 83 | 84 | - Swagger documentation 85 | 86 | ## Install 87 | 88 | ```shell 89 | npm i @rewiko/crud-request 90 | ``` 91 | 92 | ## Packages 93 | 94 | - [**@rewiko/crud**](https://www.npmjs.com/package/@rewiko/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/rewiko/crud/wiki/Controllers#description)) 95 | - [**@rewiko/crud-request**](https://www.npmjs.com/package/@rewiko/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/rewiko/crud/wiki/Requests#frontend-usage)) 96 | - [**@rewiko/crud-typeorm**](https://www.npmjs.com/package/@rewiko/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/rewiko/crud/wiki/ServiceTypeorm)) 97 | 98 | ## Documentation 99 | 100 | - [General Information](https://github.com/rewiko/crud/wiki#why) 101 | - [CRUD Controllers](https://github.com/rewiko/crud/wiki/Controllers#description) 102 | - [CRUD ORM Services](https://github.com/rewiko/crud/wiki/Services#description) 103 | - [Handling Requests](https://github.com/rewiko/crud/wiki/Requests#description) 104 | 105 | ## Support 106 | 107 | Any support is welcome. At least you can give us a star. 108 | 109 | ## Contributors 110 | 111 | ### Code Contributors 112 | 113 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 114 | 115 | 116 | ### Financial Contributors 117 | 118 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)] 119 | 120 | #### Individuals 121 | 122 | 123 | 124 | #### Organizations 125 | 126 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)] 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | ## License 140 | 141 | [MIT](LICENSE) 142 | -------------------------------------------------------------------------------- /packages/crud/README.md: -------------------------------------------------------------------------------- 1 |
2 |

CRUD (@rewiko/crud)

3 |
4 |
5 | for RESTful APIs built with NestJs 6 |
7 | 8 |
9 | 10 | 51 | 52 |
53 | Built by 54 | @MichaelYali and 55 | 56 | Contributors 57 | 58 |
59 | 60 |
61 | 62 | We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@rewiko/crud` microframework very useful. 63 | 64 | ## Features 65 | 66 | CRUD usage 67 | 68 | - Super easy to install and start using the full-featured controllers and services :point_right: 69 | 70 | - DB and service agnostic extendable CRUD controllers 71 | 72 | - Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc. 73 | 74 | - Framework agnostic package with query builder for a frontend usage 75 | 76 | - Query, path params and DTOs validation included 77 | 78 | - Overriding controller methods with ease 79 | 80 | - Tiny config (including globally) 81 | 82 | - Additional helper decorators 83 | 84 | - Swagger documentation 85 | 86 | ## Install 87 | 88 | ```shell 89 | npm i @rewiko/crud class-transformer class-validator 90 | ``` 91 | 92 | ## Packages 93 | 94 | - [**@rewiko/crud**](https://www.npmjs.com/package/@rewiko/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/rewiko/crud/wiki/Controllers#description)) 95 | - [**@rewiko/crud-request**](https://www.npmjs.com/package/@rewiko/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/rewiko/crud/wiki/Requests#frontend-usage)) 96 | - [**@rewiko/crud-typeorm**](https://www.npmjs.com/package/@rewiko/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/rewiko/crud/wiki/ServiceTypeorm)) 97 | 98 | ## Documentation 99 | 100 | - [General Information](https://github.com/rewiko/crud/wiki#why) 101 | - [CRUD Controllers](https://github.com/rewiko/crud/wiki/Controllers#description) 102 | - [CRUD ORM Services](https://github.com/rewiko/crud/wiki/Services#description) 103 | - [Handling Requests](https://github.com/rewiko/crud/wiki/Requests#description) 104 | 105 | ## Support 106 | 107 | Any support is welcome. At least you can give us a star. 108 | 109 | ## Contributors 110 | 111 | ### Code Contributors 112 | 113 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 114 | 115 | 116 | ### Financial Contributors 117 | 118 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)] 119 | 120 | #### Individuals 121 | 122 | 123 | 124 | #### Organizations 125 | 126 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)] 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | ## License 140 | 141 | [MIT](LICENSE) 142 | -------------------------------------------------------------------------------- /packages/crud-typeorm/README.md: -------------------------------------------------------------------------------- 1 |
2 |

CRUD (@rewiko/crud-typeorm)

3 |
4 |
5 | for RESTful APIs built with NestJs 6 |
7 | 8 |
9 | 10 | 51 | 52 |
53 | Built by 54 | @MichaelYali and 55 | 56 | Contributors 57 | 58 |
59 | 60 |
61 | 62 | We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@rewiko/crud` microframework very useful. 63 | 64 | ## Features 65 | 66 | CRUD usage 67 | 68 | - Super easy to install and start using the full-featured controllers and services :point_right: 69 | 70 | - DB and service agnostic extendable CRUD controllers 71 | 72 | - Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc. 73 | 74 | - Framework agnostic package with query builder for a frontend usage 75 | 76 | - Query, path params and DTOs validation included 77 | 78 | - Overriding controller methods with ease 79 | 80 | - Tiny config (including globally) 81 | 82 | - Additional helper decorators 83 | 84 | - Swagger documentation 85 | 86 | ## Install 87 | 88 | ```shell 89 | npm i @rewiko/crud-typeorm @nestjs/typeorm typeorm 90 | ``` 91 | 92 | ## Packages 93 | 94 | - [**@rewiko/crud**](https://www.npmjs.com/package/@rewiko/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/rewiko/crud/wiki/Controllers#description)) 95 | - [**@rewiko/crud-request**](https://www.npmjs.com/package/@rewiko/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/rewiko/crud/wiki/Requests#frontend-usage)) 96 | - [**@rewiko/crud-typeorm**](https://www.npmjs.com/package/@rewiko/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/rewiko/crud/wiki/ServiceTypeorm)) 97 | 98 | ## Documentation 99 | 100 | - [General Information](https://github.com/rewiko/crud/wiki#why) 101 | - [CRUD Controllers](https://github.com/rewiko/crud/wiki/Controllers#description) 102 | - [CRUD ORM Services](https://github.com/rewiko/crud/wiki/Services#description) 103 | - [Handling Requests](https://github.com/rewiko/crud/wiki/Requests#description) 104 | 105 | ## Support 106 | 107 | Any support is welcome. At least you can give us a star. 108 | 109 | ## Contributors 110 | 111 | ### Code Contributors 112 | 113 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 114 | 115 | 116 | ### Financial Contributors 117 | 118 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)] 119 | 120 | #### Individuals 121 | 122 | 123 | 124 | #### Organizations 125 | 126 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)] 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | ## License 140 | 141 | [MIT](LICENSE) 142 | --------------------------------------------------------------------------------