mashafrancis/butternut-user

View on GitHub
src/base/crud.service.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { forwardRef, HttpException, HttpStatus, Inject } from '@nestjs/common';
import { validate, ValidatorOptions } from 'class-validator';
import { DateTime } from 'luxon';
import {
    DeepPartial,
    FindManyOptions,
    FindOneOptions,
    MongoRepository,
    ObjectLiteral,
} from 'typeorm';
import { ExtendedEntity, typeormFilterMapper } from '../app/_helpers';
import { SecurityService } from '../app/security/security.service';
import { RestVoterActionEnum } from '../app/security/voter';
import { config } from '../config';

export class CrudService<T extends ExtendedEntity> {
    protected repository: MongoRepository<T>;
    @Inject(forwardRef(() => SecurityService))
    protected readonly securityService: SecurityService;

    constructor(repository?: MongoRepository<T>) {
        if (repository) {
            this.repository = repository;
        }
    }

    public async findAll(options?: FindManyOptions<T>): Promise<T[]> {
        if (options.where) {
            options.where = typeormFilterMapper(options);
        }
        const entities = await this.repository.find(options);
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.READ_ALL,
            entities,
        );
        return entities;
    }

    public async findOneById(id: string): Promise<T> {
        try {
            const entity = await this.repository.findOneOrFail(id);
            await this.securityService.denyAccessUnlessGranted(
                RestVoterActionEnum.READ,
                entity,
            );
            return entity;
        } catch (e) {
            throw new HttpException(
                {
                    error: 'Database',
                    message: 'Item not found',
                },
                HttpStatus.NOT_FOUND,
            );
        }
    }

    public async findOne(options?: FindOneOptions<T>): Promise<T> {
        if (options.where) {
            options.where = typeormFilterMapper(options);
        }
        const entity = await this.repository.findOne(options);
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.READ,
            entity,
        );
        return entity;
    }

    public async create(data: DeepPartial<T>): Promise<T> {
        const entity: T = this.repository.create(data);
        entity.createdAt = DateTime.utc();
        entity.updatedAt = DateTime.utc();
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.CREATE,
            entity,
        );
        await this.validate(entity, {
            groups: ['create'],
        });
        return entity.save();
    }

    public async saveAll(data: DeepPartial<T>[]): Promise<T[]> {
        return this.repository.save(data);
    }

    public async update(data: DeepPartial<T> | T): Promise<T> {
        const id = String(data.id || '');
        return this.patch(id, data);
    }

    public async updateAll(query, data: any): Promise<boolean> {
        if (query) {
            // tslint:disable-next-line:no-parameter-reassignment
            query = typeormFilterMapper({ where: query });
        }
        const response = await this.repository.updateMany(query, data);
        return !!response.matchedCount;
    }

    public async patch(id: string, data: DeepPartial<T> | T): Promise<T> {
        let entity: T = null;
        if (data instanceof ExtendedEntity) {
            entity = data;
        } else {
            entity = await this.findOneById(id);
            if (data.id) {
                delete data.id;
            }
            this.repository.merge(entity, data);
        }
        let createdAt = entity.createdAt;
        if (!createdAt) {
            createdAt = DateTime.utc();
        }
        entity.createdAt = createdAt;
        entity.updatedAt = DateTime.utc();
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.UPDATE,
            entity,
        );
        await this.validate(entity, {
            groups: ['update'],
        });
        return entity.save();
    }

    public async delete(id: string): Promise<T> {
        const entity: T = await this.findOneById(id);
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.UPDATE,
            entity,
        );
        await this.repository.delete(id);
        return entity;
    }

    public deleteAll(conditions?: ObjectLiteral): Promise<any> {
        if (conditions) {
            // tslint:disable-next-line:no-parameter-reassignment
            conditions = typeormFilterMapper({ where: conditions });
        }
        return this.repository.deleteMany(conditions);
    }

    public async softDelete({ id }: DeepPartial<T>): Promise<T> {
        const entity = await this.findOneById(id as any);
        await this.securityService.denyAccessUnlessGranted(
            RestVoterActionEnum.SOFT_DELETE,
            entity,
        );
        entity.isDeleted = true;
        entity.updatedAt = DateTime.utc();
        return entity.save();
    }

    protected async validate(entity: T, options?: ValidatorOptions) {
        const errors = await validate(entity, {
            ...config.validator,
            options,
        } as ValidatorOptions);
        if (errors.length) {
            throw new HttpException(
                {
                    message: errors,
                    error: 'Validation',
                },
                HttpStatus.UNPROCESSABLE_ENTITY,
            );
        }
    }
}