diff --git a/.env.development b/.env.development index 67f6ce5..24432ff 100644 --- a/.env.development +++ b/.env.development @@ -6,6 +6,6 @@ DB_PASS=postgres DB_DATABASE=gerocuidado-saude-db DB_PORT=5003 -## TCP -AUTH_HOST=gerocuidado-usuario-api -AUTH_PORT=4001 +#TCP +USUARIO_HOST=gerocuidado-usuario-api +USUARIO_PORT=4001 diff --git a/.env.test b/.env.test index 6b73efd..3dc44a7 100644 --- a/.env.test +++ b/.env.test @@ -6,6 +6,6 @@ DB_PASS=postgres DB_DATABASE=gerocuidado-saude-db-test DB_PORT=5003 -## TCP -AUTH_HOST=gerocuidado-usuario-api -AUTH_PORT=4001 +#TCP +USUARIO_HOST=0.0.0.0 +USUARIO_PORT=8001 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ab7b42..33bc8bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master env: - GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} test-e2e: @@ -43,15 +43,3 @@ jobs: docker-compose -f docker-compose.test.yml up -V --force-recreate --build --abort-on-container-exit --exit-code-from gerocuidado-saude-api-test env: TEST: e2e - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 745572f..0000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Check -on: - pull_request: - workflow_dispatch: - -jobs: - test-unit: - name: Test Unit - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Test Unit - id: test-unit - run: | - TEST=unit docker-compose -f docker-compose.test.yml up -V --force-recreate --build --abort-on-container-exit --exit-code-from gerocuidado-saude-api-test - env: - TEST: unit - - test-e2e: - name: Test E2E - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Test E2E - id: test-e2e - run: | - TEST=e2e docker-compose -f docker-compose.test.yml up -V --force-recreate --build --abort-on-container-exit --exit-code-from gerocuidado-saude-api-test - env: - TEST: e2e - - sonarqube: - name: sonarqube - needs: ['test-unit', 'test-e2e'] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Install dependencies - run: yarn - - name: Test and coverage - run: yarn jest --coverage diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 06117bd..49e7c09 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,8 +9,8 @@ services: - DB_PASS=postgres - DB_DATABASE=gerocuidado-saude-db - DB_PORT=5003 - - AUTH_HOST=gerocuidado-usuario-api-prod - - AUTH_PORT=4001 + - USUARIO_HOST=gerocuidado-usuario-api-prod + - USUARIO_PORT=4001 ports: - '3003:3003' depends_on: diff --git a/e2e/idoso.e2e-spec.ts b/e2e/idoso.e2e-spec.ts new file mode 100644 index 0000000..385ac23 --- /dev/null +++ b/e2e/idoso.e2e-spec.ts @@ -0,0 +1,244 @@ +import { Controller, INestApplication, ValidationPipe } from '@nestjs/common'; +import { + ClientProxy, + ClientsModule, + MessagePattern, + Transport, +} from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import { Repository } from 'typeorm'; +import { AppModule } from '../src/app.module'; +import { ETipoSanguineo } from '../src/idoso/classes/tipo-sanguineo.enum'; +import { Idoso } from '../src/idoso/entities/idoso.entity'; +import { AllExceptionsFilter } from '../src/shared/filters/all-exceptions.filter'; +import { ModelNotFoundExceptionFilter } from '../src/shared/filters/model-not-found.exception-filter'; +import { DataTransformInterceptor } from '../src/shared/interceptors/data-transform.interceptor'; + +@Controller() +class AutenticacaoController { + @MessagePattern({ role: 'auth', cmd: 'check' }) + async validateToken(data: { jwt: string }) { + return true; + } +} + +describe('E2E - Idoso', () => { + let app: INestApplication; + let client: ClientProxy; + let repository: Repository; + let token: string = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5ceyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + + const idoso: Partial = { + id: undefined, + nome: 'Henrique', + foto: '1' as any, + idUsuario: 1, + dataNascimento: new Date().toISOString() as any, + tipoSanguineo: ETipoSanguineo.AB_Negativo, + telefoneResponsavel: '123456789', + descricao: 'desc', + }; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + AppModule, + ClientsModule.register([ + { + name: 'USUARIO_CLIENT', + transport: Transport.TCP, + options: { + host: '0.0.0.0', + port: 8001, + }, + }, + ]), + ], + controllers: [AutenticacaoController], + }).compile(); + + app = moduleFixture.createNestApplication(); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + }), + ); + app.useGlobalInterceptors(new DataTransformInterceptor()); + app.useGlobalFilters( + new AllExceptionsFilter(), + new ModelNotFoundExceptionFilter(), + ); + + app.connectMicroservice({ + transport: Transport.TCP, + options: { + host: '0.0.0.0', + port: 8001, + }, + }); + + await app.startAllMicroservices(); + await app.init(); + + client = app.get('USUARIO_CLIENT'); + await client.connect(); + + repository = app.get>(getRepositoryToken(Idoso)); + }); + + describe('POST - /api/saude/idoso', () => { + it('should successfully add a new "idoso"', async () => { + const res = await request(app.getHttpServer()) + .post('/idoso') + .set('Content-Type', 'application/json') + .send(idoso); + + expect(res.statusCode).toEqual(201); + expect(res.body.message).toEqual('Salvo com sucesso!'); + expect(res.body.data).toMatchObject({ + ...idoso, + id: res.body.data.id, + }); + + Object.assign(idoso, res.body.data); + delete idoso.foto; + }); + + it('should not add a new "idoso" when validations are incorrect', async () => { + const res = await request(app.getHttpServer()) + .post('/idoso') + .set('Content-Type', 'application/json') + .send({}); + + expect(res.statusCode).toEqual(400); + expect(res.body.message).toBeInstanceOf(Array); + expect(res.body.message).toEqual([ + 'idUsuario should not be empty', + 'idUsuario must be a number conforming to the specified constraints', + 'nome must be longer than or equal to 5 characters', + 'nome must be shorter than or equal to 60 characters', + 'nome should not be empty', + 'nome must be a string', + 'dataNascimento should not be empty', + 'dataNascimento must be a valid ISO 8601 date string', + 'telefoneResponsavel must be shorter than or equal to 11 characters', + 'telefoneResponsavel must be longer than or equal to 9 characters', + 'telefoneResponsavel should not be empty', + 'telefoneResponsavel must be a string', + 'descricao should not be empty', + 'descricao must be shorter than or equal to 500 characters', + 'descricao must be a string', + ]); + expect(res.body.data).toBeNull(); + }); + }); + + describe('GET - /api/saude/idoso/:id', () => { + it('should successfully get "idoso" by id', async () => { + const res = await request(app.getHttpServer()) + .get(`/idoso/${idoso.id}`) + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBeNull(); + const data = res.body.data; + delete data.foto; + expect(data).toMatchObject(idoso); + }); + + it('should return status 400 when id is invalid', async () => { + const wrongId = 'NaN'; + const res = await request(app.getHttpServer()) + .get(`/idoso/${wrongId}`) + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(); + + expect(res.statusCode).toEqual(400); + expect(res.body.message).toBeInstanceOf(Array); + expect(res.body.message).toEqual(['ID inválido']); + expect(res.body.data).toBeNull(); + }); + + it('should return status 404 when no "idoso" is found', async () => { + const res = await request(app.getHttpServer()) + .get('/idoso/9999') + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toEqual('Registro(s) não encontrado(s)!'); + expect(res.body.data).toBeNull(); + }); + }); + + describe('GET - /api/saude/idoso/', () => { + it('should successfully findAll "idoso"', async () => { + const filter = JSON.stringify({ + nome: idoso.nome, + id: idoso.id, + }); + + const res = await request(app.getHttpServer()) + .get('/idoso?filter=' + JSON.stringify(filter)) + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBeNull(); + expect(res.body.data.length).toEqual(1); + }); + }); + + describe('PATCH - /api/saude/idoso/:id', () => { + it('should successfully update "idoso" by id', async () => { + const update = { nome: 'Jose da Silva' }; + + const res = await request(app.getHttpServer()) + .patch(`/idoso/${idoso.id}`) + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(update); + + idoso.nome = update.nome; + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBe('Atualizado com sucesso!'); + const data = res.body.data; + delete data.foto; + expect(data).toMatchObject(idoso); + }); + }); + + describe('DELETE - /api/saude/idoso/:id', () => { + it('should successfully delete "idoso" by id', async () => { + const res = await request(app.getHttpServer()) + .delete(`/idoso/${idoso.id}`) + .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) + .send(); + + delete idoso.id; + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBe('Excluído com sucesso!'); + expect(res.body.data).toMatchObject(idoso); + }); + }); + + afterAll(async () => { + await repository.query('TRUNCATE idoso CASCADE'); + await repository.delete({}); + await app.close(); + await client.close(); + }); +}); diff --git a/e2e/jest-e2e.json b/e2e/jest-e2e.json index a673b7d..661078b 100644 --- a/e2e/jest-e2e.json +++ b/e2e/jest-e2e.json @@ -1,5 +1,10 @@ { - "moduleFileExtensions": ["ts", "tsx", "js", "json"], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ], "transform": { "^.+\\.tsx?$": "ts-jest" }, @@ -11,10 +16,22 @@ "!**/node_modules/**", "!**/vendor/**", "!src/**/main.{js,jsx,tsx,ts}", - "!src/**/ormconfig.{js,jsx,tsx,ts}" + "!src/**/migrations.{js,jsx,tsx,ts}", + "!src/**/ormconfig.{js,jsx,tsx,ts}", + "!src/migration/**", + "!src/shared/**", + "!src/config/**" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/src/" + ], + "coverageReporters": [ + "json", + "lcov" ], - "testPathIgnorePatterns": ["/node_modules/", "/src/"], - "coverageReporters": ["json", "lcov"], "testEnvironment": "node", - "coveragePathIgnorePatterns": [".spec.ts$"] -} + "coveragePathIgnorePatterns": [ + ".spec.ts$" + ] +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index b216675..8aa1b32 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AutenticacaoGuard } from './autenticacao.guard'; import { DbModule } from './config/db/db.module'; import { DbService } from './config/db/db.service'; +import { IdosoModule } from './idoso/idoso.module'; const ENV = process.env.NODE_ENV; @@ -21,19 +22,20 @@ const ENV = process.env.NODE_ENV; }), ClientsModule.registerAsync([ { - name: 'AUTH_CLIENT', + name: 'USUARIO_CLIENT', imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ transport: Transport.TCP, options: { - host: configService.get('AUTH_HOST'), - port: configService.get('AUTH_PORT'), + host: configService.get('USUARIO_HOST'), + port: configService.get('USUARIO_PORT'), }, }), inject: [ConfigService], }, ]), DbModule, + IdosoModule, ], controllers: [], providers: [ diff --git a/src/autenticacao.guard.spec.ts b/src/autenticacao.guard.spec.ts index 54570e1..0f264c3 100644 --- a/src/autenticacao.guard.spec.ts +++ b/src/autenticacao.guard.spec.ts @@ -29,7 +29,7 @@ describe('AutenticacaoGuard', () => { providers: [ AutenticacaoGuard, { - provide: 'AUTH_CLIENT', + provide: 'USUARIO_CLIENT', useValue: mockClientProxy, }, Reflector, diff --git a/src/autenticacao.guard.ts b/src/autenticacao.guard.ts index 201f21a..6485f7a 100644 --- a/src/autenticacao.guard.ts +++ b/src/autenticacao.guard.ts @@ -13,7 +13,7 @@ import { IS_PUBLIC_KEY } from './shared/decorators/public-route.decorator'; @Injectable() export class AutenticacaoGuard implements CanActivate { constructor( - @Inject('AUTH_CLIENT') + @Inject('USUARIO_CLIENT') private readonly _client: ClientProxy, private readonly _reflector: Reflector, ) {} diff --git a/src/idoso/classes/tipo-sanguineo.enum.ts b/src/idoso/classes/tipo-sanguineo.enum.ts new file mode 100644 index 0000000..8beafba --- /dev/null +++ b/src/idoso/classes/tipo-sanguineo.enum.ts @@ -0,0 +1,10 @@ +export enum ETipoSanguineo { + A_Positivo = 'A+', + A_Negativo = 'A-', + B_Positivo = 'B+', + B_Negativo = 'B-', + AB_Positivo = 'AB+', + AB_Negativo = 'AB-', + O_Positivo = 'O+', + O_Negativo = 'O-', +} diff --git a/src/idoso/dto/create-idoso-dto.ts b/src/idoso/dto/create-idoso-dto.ts new file mode 100644 index 0000000..a054eb3 --- /dev/null +++ b/src/idoso/dto/create-idoso-dto.ts @@ -0,0 +1,46 @@ +import { + IsDateString, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { ETipoSanguineo } from '../classes/tipo-sanguineo.enum'; + +export class CreateIdosoDto { + @IsNumber() + @IsNotEmpty() + idUsuario!: number; + + @IsOptional() + @IsString() + foto?: string; + + @IsString() + @IsNotEmpty() + @MaxLength(60) + @MinLength(5) + nome!: string; + + @IsDateString() + @IsNotEmpty() + dataNascimento!: Date; + + @IsOptional() + @IsEnum(ETipoSanguineo) + tipoSanguineo?: ETipoSanguineo; + + @IsString() + @IsNotEmpty() + @MinLength(9) + @MaxLength(11) + telefoneResponsavel!: string; + + @IsString() + @MaxLength(500) + @IsNotEmpty() + descricao?: string; +} diff --git a/src/idoso/dto/update-idoso.dto.ts b/src/idoso/dto/update-idoso.dto.ts new file mode 100644 index 0000000..1132357 --- /dev/null +++ b/src/idoso/dto/update-idoso.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateIdosoDto } from './create-idoso-dto'; + +export class UpdateIdosoDto extends PartialType(CreateIdosoDto) {} diff --git a/src/idoso/entities/idoso.entity.ts b/src/idoso/entities/idoso.entity.ts new file mode 100644 index 0000000..63575e0 --- /dev/null +++ b/src/idoso/entities/idoso.entity.ts @@ -0,0 +1,35 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { ETipoSanguineo } from '../classes/tipo-sanguineo.enum'; +import { CreateIdosoDto } from '../dto/create-idoso-dto'; +import { UpdateIdosoDto } from '../dto/update-idoso.dto'; + +@Entity({ name: 'idoso' }) +export class Idoso { + @PrimaryGeneratedColumn() + id!: number; + + @Column('integer') + idUsuario!: number; + + @Column('bytea', { nullable: true }) + foto!: Buffer; + + @Column('varchar', { length: 60 }) + nome!: string; + + @Column('timestamp') + dataNascimento!: Date; + + @Column('enum', { enum: ETipoSanguineo }) + tipoSanguineo?: ETipoSanguineo; + + @Column('varchar', { length: 11 }) + telefoneResponsavel!: string; + + @Column('varchar', { length: 500 }) + descricao?: string; + + constructor(createIdosoDto: CreateIdosoDto | UpdateIdosoDto) { + Object.assign(this, createIdosoDto); + } +} diff --git a/src/idoso/idoso.controller.spec.ts b/src/idoso/idoso.controller.spec.ts new file mode 100644 index 0000000..8cdeb20 --- /dev/null +++ b/src/idoso/idoso.controller.spec.ts @@ -0,0 +1,131 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Filtering } from '../shared/decorators/filtrate.decorator'; +import { OrderParams, Ordering } from '../shared/decorators/ordenate.decorator'; +import { + Pagination, + PaginationParams, +} from '../shared/decorators/paginate.decorator'; +import { ETipoSanguineo } from './classes/tipo-sanguineo.enum'; +import { Idoso } from './entities/idoso.entity'; +import { IdosoController } from './idoso.controller'; +import { IdosoService } from './idoso.service'; +import { IIdosoFilter } from './interfaces/idoso-filter.interface'; + +describe('IdosoController', () => { + let controller: IdosoController; + let service: IdosoService; + + const idosoDto = { + nome: 'Henrique', + foto: '1', + idUsuario: 1, + dataNascimento: new Date(), + tipoSanguineo: ETipoSanguineo.AB_Negativo, + telefoneResponsavel: '123456789', + descricao: 'desc', + }; + + const idoso = { + ...idosoDto, + id: 1, + foto: Buffer.from('1'), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + controllers: [IdosoController], + providers: [ + { + provide: IdosoService, + useValue: { + create: jest.fn(), + findOne: jest.fn(), + remove: jest.fn(), + update: jest.fn(), + findAll: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Idoso), + useValue: {}, + }, + ], + }).compile(); + + controller = module.get(IdosoController); + service = module.get(IdosoService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should create Idoso', async () => { + jest.spyOn(service, 'create').mockReturnValue(Promise.resolve(idoso)); + + const response = await controller.create(idosoDto); + expect(response.data).toEqual(idoso); + expect(response.message).toEqual('Salvo com sucesso!'); + }); + + it('should find Idoso', async () => { + jest.spyOn(service, 'findOne').mockReturnValue(Promise.resolve(idoso)); + + const response = await controller.findOne({ id: 1 }); + expect(response).toEqual(idoso); + }); + + it('should remove Idoso', async () => { + jest.spyOn(service, 'remove').mockReturnValue(Promise.resolve(idoso)); + + const response = await controller.remove({ id: 1 }); + expect(response.data).toEqual(idoso); + expect(response.message).toEqual('Excluído com sucesso!'); + }); + + it('should update Idoso', async () => { + jest.spyOn(service, 'update').mockReturnValue(Promise.resolve(idoso)); + + const response = await controller.update({ id: 1 }, { nome: 'Henrique' }); + expect(response.data).toEqual(idoso); + expect(response.message).toEqual('Atualizado com sucesso!'); + }); + + describe('findAll', () => { + const filter: IIdosoFilter = { + nome: 'Henrique', + id: 1, + }; + const filtering = new Filtering(JSON.stringify(filter)); + + const order: OrderParams = { + column: 'id', + dir: 'ASC', + }; + const ordering: Ordering = new Ordering(JSON.stringify(order)); + + const paginate: PaginationParams = { + limit: 10, + offset: 0, + }; + const pagination: Pagination = new Pagination(paginate); + + it('should findAll Idoso', async () => { + const expected = { data: [idoso], count: 1, pageSize: 1 }; + + jest.spyOn(service, 'findAll').mockReturnValue(Promise.resolve(expected)); + + const { data, count, pageSize } = await controller.findAll( + filtering, + pagination, + ordering, + ); + + expect(count).toEqual(1); + expect(pageSize).toEqual(1); + expect(data).toEqual([idoso]); + }); + }); +}); diff --git a/src/idoso/idoso.controller.ts b/src/idoso/idoso.controller.ts new file mode 100644 index 0000000..f636b71 --- /dev/null +++ b/src/idoso/idoso.controller.ts @@ -0,0 +1,61 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, +} from '@nestjs/common'; +import { HttpResponse } from '../shared/classes/http-response'; +import { Filtering, Filtrate } from '../shared/decorators/filtrate.decorator'; +import { Ordenate, Ordering } from '../shared/decorators/ordenate.decorator'; +import { Paginate, Pagination } from '../shared/decorators/paginate.decorator'; +import { Response } from '../shared/interceptors/data-transform.interceptor'; +import { ResponsePaginate } from '../shared/interfaces/response-paginate.interface'; +import { IdValidator } from '../shared/validators/id.validator'; +import { CreateIdosoDto } from './dto/create-idoso-dto'; +import { UpdateIdosoDto } from './dto/update-idoso.dto'; +import { Idoso } from './entities/idoso.entity'; +import { IdosoService } from './idoso.service'; +import { IIdosoFilter } from './interfaces/idoso-filter.interface'; + +@Controller('idoso') +export class IdosoController { + constructor(private readonly _service: IdosoService) {} + + @Get() + async findAll( + @Filtrate() queryParam: Filtering, + @Paginate() pagination: Pagination, + @Ordenate() ordering: Ordering, + ): Promise> { + return this._service.findAll(queryParam.filter, ordering, pagination); + } + + @Get(':id') + async findOne(@Param() param: IdValidator): Promise { + return this._service.findOne(param.id); + } + + @Patch(':id') + async update( + @Param() param: IdValidator, + @Body() body: UpdateIdosoDto, + ): Promise> { + const updated = await this._service.update(param.id, body); + return new HttpResponse(updated).onUpdated(); + } + + @Post() + async create(@Body() body: CreateIdosoDto) { + const created = await this._service.create(body); + return new HttpResponse(created).onCreated(); + } + + @Delete(':id') + async remove(@Param() param: IdValidator): Promise> { + const deleted = await this._service.remove(param.id); + return new HttpResponse(deleted).onDeleted(); + } +} diff --git a/src/idoso/idoso.module.ts b/src/idoso/idoso.module.ts new file mode 100644 index 0000000..d6289c9 --- /dev/null +++ b/src/idoso/idoso.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Idoso } from './entities/idoso.entity'; +import { IdosoController } from './idoso.controller'; +import { IdosoService } from './idoso.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Idoso])], + controllers: [IdosoController], + providers: [IdosoService, Repository], + exports: [IdosoService], +}) +export class IdosoModule {} diff --git a/src/idoso/idoso.service.spec.ts b/src/idoso/idoso.service.spec.ts new file mode 100644 index 0000000..aab7d43 --- /dev/null +++ b/src/idoso/idoso.service.spec.ts @@ -0,0 +1,141 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OrderParams, Ordering } from '../shared/decorators/ordenate.decorator'; +import { + Pagination, + PaginationParams, +} from '../shared/decorators/paginate.decorator'; +import { Idoso } from './entities/idoso.entity'; +import { IdosoService } from './idoso.service'; + +describe('IdosoService', () => { + let service: IdosoService; + let repository: Repository; + + const mockRepository = { + save: jest.fn(), + findOneOrFail: jest.fn(), + remove: jest.fn(), + findOne: jest.fn(), + createQueryBuilder: jest.fn(() => ({ + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + getManyAndCount: jest.fn(), + })), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRepositoryToken(Idoso), + useValue: mockRepository, + }, + IdosoService, + ], + }).compile(); + + service = module.get(IdosoService); + repository = module.get>(getRepositoryToken(Idoso)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create Idoso', async () => { + const idoso = { nome: 'Henrique' } as any; + jest.spyOn(repository, 'save').mockReturnValue({ id: 1 } as any); + const created = await service.create(idoso); + expect(created.id).toEqual(1); + }); + + it('should find Idoso', async () => { + jest.spyOn(repository, 'findOneOrFail').mockReturnValue({ id: 1 } as any); + + const found = await service.findOne(1); + expect(found.id).toEqual(1); + }); + + it('should find Idoso with foto', async () => { + jest.spyOn(repository, 'findOneOrFail').mockReturnValue({ + id: 1, + foto: Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD', 'utf-8'), + } as any); + + const found = await service.findOne(1, true); + expect(found.id).toEqual(1); + }); + + it('should remove Idoso', async () => { + jest.spyOn(repository, 'findOneOrFail').mockReturnValue({ id: 1 } as any); + jest.spyOn(repository, 'remove').mockReturnValue({ id: 1 } as any); + + const removed = await service.remove(1); + expect(removed.id).toEqual(1); + }); + + it('should update Idoso', async () => { + jest.spyOn(repository, 'findOneOrFail').mockReturnValue({ id: 1 } as any); + jest + .spyOn(repository, 'save') + .mockReturnValue({ id: 1, nome: 'Henrique' } as any); + + const found = await service.update(1, { nome: 'Henrique' }); + expect(found).toEqual({ id: 1, nome: 'Henrique' }); + }); + + it('should update Idoso with photo', async () => { + jest.spyOn(repository, 'findOneOrFail').mockReturnValue({ id: 1 } as any); + jest + .spyOn(repository, 'save') + .mockReturnValue({ id: 1, nome: 'Henrique', foto: '1' } as any); + + const found = await service.update(1, { nome: 'Henrique' }); + expect(found).toEqual({ + id: 1, + nome: 'Henrique', + foto: 'data:image/png;base64,1', + }); + }); + + describe('findAll', () => { + const idoso = { + id: 1, + nome: 'Henrique', + }; + + const order: OrderParams = { + column: 'id', + dir: 'ASC', + }; + const ordering: Ordering = new Ordering(JSON.stringify(order)); + + const paginate: PaginationParams = { + limit: 10, + offset: 0, + }; + const pagination: Pagination = new Pagination(paginate); + + it('should findAll Idoso', async () => { + jest.spyOn(repository, 'createQueryBuilder').mockReturnValue({ + where: () => ({ + limit: () => ({ + offset: () => ({ + orderBy: () => ({ + getManyAndCount: jest.fn().mockResolvedValueOnce([[idoso], 1]), + }), + }), + }), + }), + } as any); + + const { data, count } = await service.findAll({}, ordering, pagination); + expect(count).toEqual(1); + expect((data as Idoso[])[0]).toEqual(idoso); + }); + }); +}); diff --git a/src/idoso/idoso.service.ts b/src/idoso/idoso.service.ts new file mode 100644 index 0000000..a2a3db9 --- /dev/null +++ b/src/idoso/idoso.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Ordering } from '../shared/decorators/ordenate.decorator'; +import { Pagination } from '../shared/decorators/paginate.decorator'; +import { getImageUri } from '../shared/helpers/buffer-to-image'; +import { + getWhereClauseNumber, + getWhereClauseString, +} from '../shared/helpers/sql-query-helper'; +import { ResponsePaginate } from '../shared/interfaces/response-paginate.interface'; + +import { CreateIdosoDto } from './dto/create-idoso-dto'; +import { UpdateIdosoDto } from './dto/update-idoso.dto'; +import { Idoso } from './entities/idoso.entity'; +import { IIdosoFilter } from './interfaces/idoso-filter.interface'; + +@Injectable() +export class IdosoService { + constructor( + @InjectRepository(Idoso) + private readonly _repository: Repository, + ) {} + + async create(body: CreateIdosoDto): Promise { + const idoso = new Idoso(body); + return this._repository.save(idoso); + } + + async findOne(id: number, transformImage = false) { + const idoso = await this._repository.findOneOrFail({ where: { id } }); + if (transformImage && idoso.foto) { + idoso.foto = getImageUri(idoso.foto) as unknown as Buffer; + } + return idoso; + } + + async update(id: number, body: UpdateIdosoDto): Promise { + const found = await this.findOne(id); + const merged = Object.assign(found, body); + + const updated = await this._repository.save(merged); + + if (updated.foto) { + updated.foto = getImageUri(updated.foto) as unknown as Buffer & string; + } + + return updated; + } + + async findAll( + filter: IIdosoFilter, + ordering: Ordering, + paging: Pagination, + ): Promise> { + const limit = paging.limit; + const offset = paging.offset; + const sort = ordering.column; + const order = ordering.dir.toUpperCase() as 'ASC' | 'DESC'; + const where = this.buildWhereClause(filter); + + const [result, total] = await this._repository + .createQueryBuilder('idoso') + .where(`${where}`) + .limit(limit) + .offset(offset) + .orderBy(`"${sort}"`, order) + .getManyAndCount(); + + return { + data: result, + count: +total, + pageSize: +total, + }; + } + + private buildWhereClause(filter: IIdosoFilter): string { + let whereClause = '1 = 1 '; + + whereClause += getWhereClauseString(filter.nome, 'nome'); + whereClause += getWhereClauseNumber(filter.id, 'id'); + + return whereClause; + } + + async remove(id: number) { + const found = await this._repository.findOneOrFail({ where: { id } }); + return this._repository.remove(found); + } +} diff --git a/src/idoso/interfaces/idoso-filter.interface.ts b/src/idoso/interfaces/idoso-filter.interface.ts new file mode 100644 index 0000000..1921bcc --- /dev/null +++ b/src/idoso/interfaces/idoso-filter.interface.ts @@ -0,0 +1,4 @@ +export interface IIdosoFilter { + id?: number; + nome?: string; +} diff --git a/src/migration/1699209938134-CreateTableIdoso.ts b/src/migration/1699209938134-CreateTableIdoso.ts new file mode 100644 index 0000000..b32116d --- /dev/null +++ b/src/migration/1699209938134-CreateTableIdoso.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTableIdoso1699209938134 implements MigrationInterface { + name = 'CreateTableIdoso1699209938134'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."idoso_tiposanguineo_enum" AS ENUM('A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-')`, + ); + await queryRunner.query( + `CREATE TABLE "idoso" ("id" SERIAL NOT NULL, "idUsuario" integer NOT NULL, "nome" character varying(60) NOT NULL, "dataNascimento" TIMESTAMP NOT NULL, "tipoSanguineo" "public"."idoso_tiposanguineo_enum" NOT NULL, "telefoneResponsavel" character varying(11) NOT NULL, "descricao" character varying(500) NOT NULL, CONSTRAINT "PK_a9b234f8e2ba08e4a72313b78f5" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "idoso"`); + await queryRunner.query(`DROP TYPE "public"."idoso_tiposanguineo_enum"`); + } +} diff --git a/src/migration/1699291077900-UpdateTableIdoso.ts b/src/migration/1699291077900-UpdateTableIdoso.ts new file mode 100644 index 0000000..4b25258 --- /dev/null +++ b/src/migration/1699291077900-UpdateTableIdoso.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateTableIdoso1699291077900 implements MigrationInterface { + name = 'UpdateTableIdoso1699291077900'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "idoso" ADD "foto" bytea`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "idoso" DROP COLUMN "foto"`); + } +} diff --git a/src/shared/helpers/buffer-to-image.spec.ts b/src/shared/helpers/buffer-to-image.spec.ts index 1609ced..28e4946 100644 --- a/src/shared/helpers/buffer-to-image.spec.ts +++ b/src/shared/helpers/buffer-to-image.spec.ts @@ -1,4 +1,9 @@ -import { bufferToBase64, isBase64, isBase64Image } from './buffer-to-image'; +import { + bufferToBase64, + getImageUri, + isBase64, + isBase64Image, +} from './buffer-to-image'; describe('Buffer to image', () => { describe('bufferToBase64', () => { @@ -10,6 +15,13 @@ describe('Buffer to image', () => { expect(image).toEqual(str); }); + it('should getImageUri', async () => { + const str = '/9j/4AAQSkZJRgABAQAAAQABAAD'; + const buff = Buffer.from(str, 'utf-8'); + const image = getImageUri(buff); + expect(image).toEqual('data:image/png;base64,' + str); + }); + it('should be bufferToBase64 a empty if null', async () => { const str = 'null'; const buff = Buffer.from(str, 'utf-8'); diff --git a/src/shared/helpers/buffer-to-image.ts b/src/shared/helpers/buffer-to-image.ts index 0174480..3a64718 100644 --- a/src/shared/helpers/buffer-to-image.ts +++ b/src/shared/helpers/buffer-to-image.ts @@ -4,6 +4,10 @@ export const bufferToBase64 = (buffer: Buffer): string => { return isBase64(base64) ? base64 : ''; }; +export const getImageUri = (value: Buffer | string): string => { + return `data:image/png;base64,${Buffer.from(value).toString()}`; +}; + export function isBase64(value: unknown): boolean { return typeof value === 'string' && isBase64Image(value); }