diff --git a/package-lock.json b/package-lock.json index 6e07338..36b0b91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "dotenv-cli": "^6.0.0", "ioredis": "^5.3.0", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "uuid": "^9.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/package.json b/package.json index 8d70fc6..f0ecf22 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "dotenv-cli": "^6.0.0", "ioredis": "^5.3.0", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "uuid": "^9.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/prisma/migrations/20230206113946_init/migration.sql b/prisma/migrations/20230206113946_init/migration.sql index c864a52..3fdbaac 100644 --- a/prisma/migrations/20230206113946_init/migration.sql +++ b/prisma/migrations/20230206113946_init/migration.sql @@ -7,7 +7,7 @@ CREATE TABLE "territory" ( "name" TEXT NOT NULL, "shape" geometry NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "territory_pkey" PRIMARY KEY ("uuid") ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f136850..4539fc5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,7 @@ model Territory { name String @unique shape Unsupported("geometry") createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([shape], name: "shape_idx", type: Gist) @@map("territory") diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts index 188224b..c43225e 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -1,11 +1,9 @@ import { Injectable } from '@nestjs/common'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; -import { Point } from '../../domain/point.type'; import { DatabaseException } from '../../exceptions/database.exception'; import { ICollection } from '../../interfaces/collection.interface'; import { IRepository } from '../../interfaces/repository.interface'; import { PrismaService } from './prisma-service'; -import { Territory } from 'src/modules/territories/domain/entities/territory'; /** * Child classes MUST redefined _model property with appropriate model name @@ -185,23 +183,36 @@ export abstract class PrismaRepository implements IRepository { } } - async findForPoint(point: Point): Promise> { - const strPoint = `SELECT uuid, name FROM ${this._model} WHERE ST_Intersects(ST_GeomFromText('POINT(${point.lon} ${point.lat})',4326),shape) = true`; - const territories: Array = await this._prisma.$queryRawUnsafe(strPoint); + async findAllByQuery( + include: Array, + where: Array, + ): Promise> { + const query = `SELECT ${include.join(',')} FROM ${ + this._model + } WHERE ${where.join(' AND ')}`; + const data: Array = await this._prisma.$queryRawUnsafe(query); return Promise.resolve({ - data: territories, - total: territories.length, + data, + total: data.length, }); } - async createTerritory(territory: Territory): Promise { - const command = `INSERT INTO ${this._model} VALUES ('bb281075-1b98-4456-89d6-c643d3044a91','${territory.name}', ST_GeomFromGeoJSON('{"type":"MultiPolygon","coordinates":${territory.shape}}'),'2023-02-07 15:58:00','2023-02-07 15:58:00')`; - const affectedRowNumber = await this._prisma.$executeRawUnsafe(command); - if (affectedRowNumber == 1) { - return this.findOne({ - name: territory.name, - }); + async createWithFields(fields: object): Promise { + try { + const command = `INSERT INTO ${this._model} (${Object.keys(fields).join( + ',', + )}) VALUES (${Object.values(fields).join(',')})`; + return await this._prisma.$executeRawUnsafe(command); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } } - throw new DatabaseException(); } } diff --git a/src/modules/database/src/domain/territory-repository.ts b/src/modules/database/src/domain/territory-repository.ts index 355a831..1e4c1ca 100644 --- a/src/modules/database/src/domain/territory-repository.ts +++ b/src/modules/database/src/domain/territory-repository.ts @@ -1,3 +1,35 @@ import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; +import { ICollection } from '../interfaces/collection.interface'; +import { Point } from './point.type'; +import { v4 as uuidv4 } from 'uuid'; +import { Territory } from 'src/modules/territories/domain/entities/territory'; +import { DatabaseException } from '../exceptions/database.exception'; -export class TerritoryRepository extends PrismaRepository {} +export class TerritoryRepository extends PrismaRepository { + async findForPoint(point: Point): Promise> { + return await this.findAllByQuery( + ['uuid', 'name'], + [ + `ST_Intersects(ST_GeomFromText('POINT(${point.lon} ${point.lat})',4326),shape) = true`, + ], + ); + } + + async createTerritory(territory: Territory): Promise { + try { + const affectedRowNumber = await this.createWithFields({ + uuid: `'${uuidv4()}'`, + name: `'${territory.name}'`, + shape: `ST_GeomFromGeoJSON('{"type":"MultiPolygon","coordinates":${territory.shape}}')`, + }); + if (affectedRowNumber == 1) { + return this.findOne({ + name: territory.name, + }); + } + throw new DatabaseException(); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 3595eeb..cb585be 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -57,6 +57,24 @@ const mockPrismaService = { return Promise.resolve([fakeEntities, fakeEntities.length]); }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + $queryRawUnsafe: jest.fn().mockImplementation((query?: string) => { + return Promise.resolve(fakeEntities); + }), + $executeRawUnsafe: jest + .fn() + .mockResolvedValueOnce(fakeEntityCreated) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new Error('an unknown error'); + }), fake: { create: jest .fn() @@ -422,4 +440,47 @@ describe('PrismaRepository', () => { ).rejects.toBeInstanceOf(DatabaseException); }); }); + + describe('findAllByquery', () => { + it('should return an array of entities', async () => { + const entities = await fakeRepository.findAllByQuery( + ['uuid', 'name'], + ['name is not null'], + ); + expect(entities).toStrictEqual({ + data: fakeEntities, + total: fakeEntities.length, + }); + }); + }); + + describe('createwithFields', () => { + it('should create an entity', async () => { + jest.spyOn(prisma, '$queryRawUnsafe'); + + const newEntity = await fakeRepository.createWithFields({ + uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', + name: 'my-name', + }); + expect(newEntity).toBe(fakeEntityCreated); + expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.createWithFields({ + uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', + name: 'my-name', + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.createWithFields({ + name: 'my-name', + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); }); diff --git a/src/modules/territories/adapters/primaries/territories.controller.ts b/src/modules/territories/adapters/primaries/territories.controller.ts index 550455f..d662662 100644 --- a/src/modules/territories/adapters/primaries/territories.controller.ts +++ b/src/modules/territories/adapters/primaries/territories.controller.ts @@ -104,7 +104,7 @@ export class TerritoriesController { return this._mapper.map(territory, Territory, TerritoryPresenter); } catch (e) { if (e instanceof DatabaseException) { - if (e.message.includes('Already exists')) { + if (e.message.includes('already exists')) { throw new RpcException({ code: 6, message: 'Territory already exists', diff --git a/src/modules/territories/domain/usecases/create-territory.usecase.ts b/src/modules/territories/domain/usecases/create-territory.usecase.ts index 8258df0..27dde37 100644 --- a/src/modules/territories/domain/usecases/create-territory.usecase.ts +++ b/src/modules/territories/domain/usecases/create-territory.usecase.ts @@ -34,7 +34,7 @@ export class CreateTerritoryUseCase { return territory; } catch (error) { let key = 'territory.create.crit'; - if (error.message.includes('Already exists')) { + if (error.message.includes('already exists')) { key = 'territory.create.warning'; } this._loggingMessager.publish(