findAdByUuid, functional basic AdModule

This commit is contained in:
sbriat 2023-05-04 12:07:53 +02:00
parent 47e4abf594
commit 5fbc399924
23 changed files with 510 additions and 64 deletions

54
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,54 @@
image: docker:20.10.22
stages:
- test
- build
##############
# TEST STAGE #
##############
test:
stage: test
image: docker/compose:latest
variables:
DOCKER_TLS_CERTDIR: ''
services:
- docker:dind
script:
- docker-compose -f docker-compose.ci.tools.yml -p ad-tools --env-file ci/.env.ci up -d
- sh ci/wait-up.sh
- docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d
# - docker exec -t v3-ad-api sh -c "npm run test:integration:ci"
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: always
###############
# BUILD STAGE #
###############
build:
stage: build
image: docker:20.10.22
variables:
DOCKER_TLS_CERTDIR: ''
services:
- docker:dind
before_script:
- echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
- docker pull $CI_REGISTRY_IMAGE:latest || true
- >
docker build
--pull
--cache-from $CI_REGISTRY_IMAGE:latest
--tag $CI_REGISTRY_IMAGE:$VERSION
--tag $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$VERSION
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main

15
ci/.env.ci Normal file
View File

@ -0,0 +1,15 @@
# SERVICE
SERVICE_URL=0.0.0.0
SERVICE_PORT=5006
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
# RABBIT MQ
RMQ_URI=amqp://v3-broker:5672
# MESSAGE BROKER
BROKER_IMAGE=rabbitmq:3-alpine
# POSTGRES
POSTGRES_IMAGE=postgis/postgis:15-3.3

33
ci/Dockerfile Normal file
View File

@ -0,0 +1,33 @@
###################
# BUILD FOR CI TESTING
###################
FROM node:18-alpine3.16
# Create app directory
WORKDIR /usr/src/app
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
# Install app dependencies
RUN npm ci
# Bundle app source
COPY . .
# Generate prisma client
RUN npx prisma generate
# Create a "dist" folder
RUN npm run build
# Run unit tests
RUN npm run test:unit:ci
# ESLint / Prettier
RUN npm run lint:check
RUN npm run pretty:check
# Start the server
CMD [ "node", "dist/main.js" ]

12
ci/wait-up.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
testlog() {
docker logs v3-db | grep -q "database system is ready to accept connections"
}
testlog 2> /dev/null
while [ $? -ne 0 ];
do
sleep 5
echo "Waiting for Test DB to be up..."
testlog 2> /dev/null
done

View File

@ -0,0 +1,19 @@
version: '3.8'
services:
v3-ad-api:
container_name: v3-ad-api
build:
dockerfile: ci/Dockerfile
context: .
env_file:
- ci/.env.ci
ports:
- 5006:5006
networks:
- v3-network
networks:
v3-network:
name: v3-network
external: true

View File

@ -0,0 +1,26 @@
version: '3.8'
services:
db:
container_name: v3-db
image: ${POSTGRES_IMAGE}
environment:
POSTGRES_DB: mobicoop
POSTGRES_USER: mobicoop
POSTGRES_PASSWORD: mobicoop
ports:
- 5432:5432
networks:
- v3-network
broker:
container_name: v3-broker
image: ${BROKER_IMAGE}
ports:
- 5672:5672
networks:
- v3-network
networks:
v3-network:
name: v3-network

6
package-lock.json generated
View File

@ -24,6 +24,7 @@
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2", "@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.13.0", "@prisma/client": "^4.13.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -3564,6 +3565,11 @@
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
"dev": true "dev": true
}, },
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
},
"node_modules/class-validator": { "node_modules/class-validator": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz",

View File

@ -46,6 +46,7 @@
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2", "@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.13.0", "@prisma/client": "^4.13.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",

View File

@ -1,28 +1,34 @@
-- CreateEnum
CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT');
-- CreateEnum
CREATE TYPE "AddressType" AS ENUM ('HOUSE_NUMBER', 'STREET_ADDRESS', 'LOCALITY', 'VENUE', 'OTHER');
-- CreateTable -- CreateTable
CREATE TABLE "ad" ( CREATE TABLE "ad" (
"uuid" UUID NOT NULL, "uuid" UUID NOT NULL,
"userUuid" UUID NOT NULL, "userUuid" UUID NOT NULL,
"driver" BOOLEAN NOT NULL, "driver" BOOLEAN,
"passenger" BOOLEAN NOT NULL, "passenger" BOOLEAN,
"frequency" INTEGER NOT NULL, "frequency" "Frequency" NOT NULL DEFAULT 'RECURRENT',
"fromDate" DATE NOT NULL, "fromDate" DATE NOT NULL,
"toDate" DATE NOT NULL, "toDate" DATE,
"monTime" TIMESTAMPTZ NOT NULL, "monTime" TIMESTAMPTZ,
"tueTime" TIMESTAMPTZ NOT NULL, "tueTime" TIMESTAMPTZ,
"wedTime" TIMESTAMPTZ NOT NULL, "wedTime" TIMESTAMPTZ,
"thuTime" TIMESTAMPTZ NOT NULL, "thuTime" TIMESTAMPTZ,
"friTime" TIMESTAMPTZ NOT NULL, "friTime" TIMESTAMPTZ,
"satTime" TIMESTAMPTZ NOT NULL, "satTime" TIMESTAMPTZ,
"sunTime" TIMESTAMPTZ NOT NULL, "sunTime" TIMESTAMPTZ,
"monMargin" INTEGER NOT NULL, "monMargin" INTEGER,
"tueMargin" INTEGER NOT NULL, "tueMargin" INTEGER,
"wedMargin" INTEGER NOT NULL, "wedMargin" INTEGER,
"thuMargin" INTEGER NOT NULL, "thuMargin" INTEGER,
"friMargin" INTEGER NOT NULL, "friMargin" INTEGER,
"satMargin" INTEGER NOT NULL, "satMargin" INTEGER,
"sunMargin" INTEGER NOT NULL, "sunMargin" INTEGER,
"seatsDriver" SMALLINT NOT NULL, "seatsDriver" SMALLINT,
"seatsPassenger" SMALLINT NOT NULL, "seatsPassenger" SMALLINT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -33,14 +39,15 @@ CREATE TABLE "ad" (
CREATE TABLE "address" ( CREATE TABLE "address" (
"uuid" UUID NOT NULL, "uuid" UUID NOT NULL,
"adUuid" UUID NOT NULL, "adUuid" UUID NOT NULL,
"position" SMALLINT NOT NULL,
"lon" DOUBLE PRECISION NOT NULL, "lon" DOUBLE PRECISION NOT NULL,
"lat" DOUBLE PRECISION NOT NULL, "lat" DOUBLE PRECISION NOT NULL,
"houseNumber" TEXT NOT NULL, "houseNumber" TEXT,
"street" TEXT NOT NULL, "street" TEXT,
"locality" TEXT NOT NULL, "locality" TEXT,
"postalCode" TEXT NOT NULL, "postalCode" TEXT,
"country" TEXT NOT NULL, "country" TEXT,
"type" SMALLINT NOT NULL, "type" "AddressType" DEFAULT 'OTHER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -60,4 +67,4 @@ CREATE INDEX "ad_fromDate_idx" ON "ad"("fromDate");
CREATE INDEX "ad_toDate_idx" ON "ad"("toDate"); CREATE INDEX "ad_toDate_idx" ON "ad"("toDate");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -13,27 +13,27 @@ datasource db {
model Ad { model Ad {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid userUuid String @db.Uuid
driver Boolean driver Boolean?
passenger Boolean passenger Boolean?
frequency Int frequency Frequency @default(RECURRENT)
fromDate DateTime @db.Date fromDate DateTime @db.Date
toDate DateTime @db.Date toDate DateTime? @db.Date
monTime DateTime @db.Timestamptz() monTime DateTime? @db.Timestamptz()
tueTime DateTime @db.Timestamptz() tueTime DateTime? @db.Timestamptz()
wedTime DateTime @db.Timestamptz() wedTime DateTime? @db.Timestamptz()
thuTime DateTime @db.Timestamptz() thuTime DateTime? @db.Timestamptz()
friTime DateTime @db.Timestamptz() friTime DateTime? @db.Timestamptz()
satTime DateTime @db.Timestamptz() satTime DateTime? @db.Timestamptz()
sunTime DateTime @db.Timestamptz() sunTime DateTime? @db.Timestamptz()
monMargin Int monMargin Int?
tueMargin Int tueMargin Int?
wedMargin Int wedMargin Int?
thuMargin Int thuMargin Int?
friMargin Int friMargin Int?
satMargin Int satMargin Int?
sunMargin Int sunMargin Int?
seatsDriver Int @db.SmallInt seatsDriver Int? @db.SmallInt
seatsPassenger Int @db.SmallInt seatsPassenger Int? @db.SmallInt
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
addresses Address[] addresses Address[]
@ -48,17 +48,31 @@ model Ad {
model Address { model Address {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid adUuid String @db.Uuid
position Int @db.SmallInt
lon Float lon Float
lat Float lat Float
houseNumber String houseNumber String?
street String street String?
locality String locality String?
postalCode String postalCode String?
country String country String?
type Int @db.SmallInt type AddressType? @default(OTHER)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
Ad Ad @relation(fields: [adUuid], references: [uuid]) Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
@@map("address") @@map("address")
} }
enum Frequency {
PUNCTUAL
RECURRENT
}
enum AddressType {
HOUSE_NUMBER
STREET_ADDRESS
LOCALITY
VENUE
OTHER
}

View File

@ -3,10 +3,13 @@ import { ConfigModule } from '@nestjs/config';
import { ConfigurationModule } from './modules/configuration/configuration.module'; import { ConfigurationModule } from './modules/configuration/configuration.module';
import { HealthModule } from './modules/health/health.module'; import { HealthModule } from './modules/health/health.module';
import { AdModule } from './modules/ad/ad.module'; import { AdModule } from './modules/ad/ad.module';
import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
ConfigurationModule, ConfigurationModule,
HealthModule, HealthModule,
AdModule, AdModule,

View File

@ -11,10 +11,9 @@ async function bootstrap() {
app.connectMicroservice<MicroserviceOptions>({ app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC, transport: Transport.GRPC,
options: { options: {
// package: ['ad', 'health'], package: ['ad', 'health'],
package: ['health'],
protoPath: [ protoPath: [
// join(__dirname, 'modules/ad/adapters/primaries/ad.proto'), join(__dirname, 'modules/ad/adapters/primaries/ad.proto'),
join(__dirname, 'modules/health/adapters/primaries/health.proto'), join(__dirname, 'modules/health/adapters/primaries/health.proto'),
], ],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,

View File

@ -1,8 +1,34 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdController } from './adapters/primaries/ad.controller';
import { DatabaseModule } from '../database/database.module';
import { CqrsModule } from '@nestjs/cqrs';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AdProfile } from './mappers/ad.profile';
import { AdsRepository } from './adapters/secondaries/ads.repository';
import { Messager } from './adapters/secondaries/messager';
import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase';
@Module({ @Module({
imports: [], imports: [
controllers: [], DatabaseModule,
providers: [], CqrsModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
],
controllers: [AdController],
providers: [AdProfile, AdsRepository, Messager, FindAdByUuidUseCase],
}) })
export class AdModule {} export class AdModule {}

View File

@ -0,0 +1,38 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request';
import { AdPresenter } from './ad.presenter';
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
import { Ad } from '../../domain/entities/ad';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class AdController {
constructor(
private readonly queryBus: QueryBus,
@InjectMapper() private readonly _mapper: Mapper,
) {}
@GrpcMethod('AdsService', 'FindOneByUuid')
async findOnebyUuid(data: FindAdByUuidRequest): Promise<AdPresenter> {
try {
console.log('ici');
const ad = await this.queryBus.execute(new FindAdByUuidQuery(data));
return this._mapper.map(ad, Ad, AdPresenter);
} catch (e) {
throw new RpcException({
code: e.code,
message: e.message,
});
}
}
}

View File

@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
export class AdPresenter {
@AutoMap()
uuid: string;
@AutoMap()
driver: boolean;
}

View File

@ -0,0 +1,86 @@
syntax = "proto3";
package ad;
service AdsService {
rpc FindOneByUuid(AdByUuid) returns (Ad);
rpc FindAll(AdFilter) returns (Ads);
rpc Create(Ad) returns (Ad);
rpc Update(Ad) returns (Ad);
rpc Delete(AdByUuid) returns (Empty);
}
message AdByUuid {
string uuid = 1;
}
message Ad {
string uuid = 1;
string userUuid = 2;
bool driver = 3;
bool passenger = 4;
int32 frequency = 5;
string fromDate = 6;
string toDate = 7;
Schedule schedule = 8;
MarginDurations marginDurations = 9;
int32 seatsPassenger = 10;
int32 seatsDriver = 11;
bool strict = 12;
Addresses addresses = 13;
}
message Schedule {
string mon = 1;
string tue = 2;
string wed = 3;
string thu = 4;
string fri = 5;
string sat = 6;
string sun = 7;
}
message MarginDurations {
int32 mon = 1;
int32 tue = 2;
int32 wed = 3;
int32 thu = 4;
int32 fri = 5;
int32 sat = 6;
int32 sun = 7;
}
message Addresses {
repeated Address address = 1;
}
message Address {
float lon = 1;
float lat = 2;
string houseNumber = 3;
string street = 4;
string locality = 5;
string postalCode = 6;
string country = 7;
AddressType type = 8;
}
enum AddressType {
HOUSE_NUMBER = 1;
STREET_ADDRESS = 2;
LOCALITY = 3;
VENUE = 4;
OTHER = 5;
}
message AdFilter {
optional int32 page = 1;
optional int32 perPage = 2;
}
message Ads {
repeated Ad data = 1;
int32 total = 2;
}
message Empty {}

View File

@ -0,0 +1,18 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class Messager extends IMessageBroker {
constructor(
private readonly _amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class FindAdByUuidRequest {
@IsString()
@IsNotEmpty()
uuid: string;
}

View File

@ -3,4 +3,7 @@ import { AutoMap } from '@automapper/classes';
export class Ad { export class Ad {
@AutoMap() @AutoMap()
uuid: string; uuid: string;
@AutoMap()
driver: boolean;
} }

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IMessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

View File

@ -0,0 +1,31 @@
import { NotFoundException } from '@nestjs/common';
import { QueryHandler } from '@nestjs/cqrs';
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
import { Messager } from '../../adapters/secondaries/messager';
import { Ad } from '../entities/ad';
@QueryHandler(FindAdByUuidQuery)
export class FindAdByUuidUseCase {
constructor(
private readonly repository: AdsRepository,
private readonly messager: Messager,
) {}
async execute(findAdByUuid: FindAdByUuidQuery): Promise<Ad> {
try {
const ad = await this.repository.findOneByUuid(findAdByUuid.uuid);
if (!ad) throw new NotFoundException();
return ad;
} catch (error) {
this.messager.publish(
'logging.ad.read.warning',
JSON.stringify({
query: findAdByUuid,
error,
}),
);
throw error;
}
}
}

View File

@ -0,0 +1,18 @@
import { createMap, Mapper } from '@automapper/core';
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common';
import { Ad } from '../domain/entities/ad';
import { AdPresenter } from '../adapters/primaries/ad.presenter';
@Injectable()
export class AdProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}
override get profile() {
return (mapper) => {
createMap(mapper, Ad, AdPresenter);
};
}
}

View File

@ -0,0 +1,9 @@
import { FindAdByUuidRequest } from '../domain/dtos/find-ad-by-uuid.request';
export class FindAdByUuidQuery {
readonly uuid: string;
constructor(findAdByUuidRequest: FindAdByUuidRequest) {
this.uuid = findAdByUuidRequest.uuid;
}
}