Skip to content

Commit

Permalink
feat: makes the endpoint idempotent
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardoconti committed Jan 11, 2024
1 parent a5d54b5 commit 99b18cc
Show file tree
Hide file tree
Showing 19 changed files with 151 additions and 6 deletions.
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { CreateChargeController } from './presentation/controllers/create-charge.controller';
import { CreditCardModule } from '@libs/credit-card/credit-card.module';
import { IdempotencyModule } from '../idempotency/idempotency.module';

@Module({
imports: [CreditCardModule],
imports: [CreditCardModule, IdempotencyModule],
controllers: [CreateChargeController],
})
export class CreditCardApiModule {}
@@ -1,8 +1,17 @@
import { CreateChargeUseCase } from '@libs/credit-card/app/use-cases/create-charge.use-case';
import { CreateChargeInputProps } from '@libs/credit-card/domain/contracts/psp-service.interface';
import { ICreateChargeUseCase } from '@libs/credit-card/domain/use-cases/create-charge.use-case';
import { Body, Controller, Inject, Post } from '@nestjs/common';
import { IdempotencyKeyInterceptor } from '@api/idempotency/infra/interceptors';
import { CreateChargeUseCase } from '@libs/credit-card/app/use-cases';
import { CreateChargeInputProps } from '@libs/credit-card/domain/contracts';
import { ICreateChargeUseCase } from '@libs/credit-card/domain/use-cases';

import {
Body,
Controller,
Inject,
Post,
UseInterceptors,
} from '@nestjs/common';

@UseInterceptors(IdempotencyKeyInterceptor)
@Controller('charge')
export class CreateChargeController {
constructor(
Expand Down
@@ -0,0 +1,4 @@
export type IdempotencyModel = {
key: string;
response: any;
};
@@ -0,0 +1,7 @@
import { IdempotencyModel } from '../models/idempotency.model';

export interface IIdempotencyRepository {
find(key: string): Promise<IdempotencyModel | undefined>;
preSave(key: string): Promise<void>;
update(key: string, response: any): Promise<void>;
}
@@ -0,0 +1 @@
export * from './idempotency.repository';
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { Redis } from '../infra/redis/redis';
import { IdempotencyRepository } from './infra/repositories/idempotency.repository';

@Module({
providers: [Redis, IdempotencyRepository],
exports: [IdempotencyRepository],
})
export class IdempotencyModule {}
@@ -0,0 +1,64 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
BadRequestException,
Inject,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { IdempotencyRepository } from '../repositories';
import { IIdempotencyRepository } from '@api/idempotency/domain/repositories';

@Injectable()
export class IdempotencyKeyInterceptor implements NestInterceptor {
constructor(
@Inject(IdempotencyRepository)
private readonly idempotencyRepository: IIdempotencyRepository,
) {}

async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const idempotencyKey = request.headers['x-idempotency-key'];

if (!idempotencyKey) {
throw new BadRequestException(
"Header 'x-idempotency-key' is required for this request.",
);
}

if (!this.isValidUUID(idempotencyKey)) {
throw new BadRequestException(
"Header 'x-idempotency-key' must be a UUID.",
);
}

const idempotencyModel = await this.idempotencyRepository.find(
idempotencyKey,
);

if (idempotencyModel) {
return of(idempotencyModel.response);
}

await this.idempotencyRepository.preSave(idempotencyKey);

return next.handle().pipe(
tap(async (data) => {
await this.idempotencyRepository.update(idempotencyKey, data);
return data;
}),
);
}

private isValidUUID(uuid: string) {
const uuidRegex =
/(?:^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$)|(?:^0{8}-0{4}-0{4}-0{4}-0{12}$)/u;
return uuidRegex.test(uuid);
}
}
@@ -0,0 +1 @@
export * from './idempotency-key.interceptor';
@@ -0,0 +1,24 @@
import { Redis } from 'apps/nestjs-rabbitmq-example/src/infra/redis/redis';
import { IIdempotencyRepository } from '../../domain/repositories/idempotency.repository';
import { Injectable } from '@nestjs/common';
import { IdempotencyModel } from '../../domain/models/idempotency.model';

@Injectable()
export class IdempotencyRepository implements IIdempotencyRepository {
constructor(private readonly redis: Redis) {}

async find(key: string): Promise<IdempotencyModel | undefined> {
const data = await this.redis.get(key);
if (data) {
return { key, response: data };
}
}

async preSave(key: string): Promise<void> {
await this.redis.set(key);
}

async update(key: string, response: any): Promise<void> {
await this.redis.set(key, response);
}
}
@@ -0,0 +1 @@
export * from './idempotency.repository';
14 changes: 14 additions & 0 deletions apps/nestjs-rabbitmq-example/src/infra/redis/redis.ts
@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class Redis {
private static data = new Map<string, any>();

async get(key: string) {
return Promise.resolve(Redis.data.get(key));
}

async set(key: string, value?: any) {
Promise.resolve(Redis.data.set(key, value));
}
}
1 change: 1 addition & 0 deletions libs/credit-card/src/app/use-cases/index.ts
@@ -0,0 +1 @@
export * from './create-charge.use-case';
2 changes: 2 additions & 0 deletions libs/credit-card/src/domain/contracts/index.ts
@@ -0,0 +1,2 @@
export * from './psp-service.interface';
export * from './publisher.interface';
1 change: 1 addition & 0 deletions libs/credit-card/src/domain/entities/index.ts
@@ -0,0 +1 @@
export * from './credit-card-charge.entity';
1 change: 1 addition & 0 deletions libs/credit-card/src/domain/use-cases/index.ts
@@ -0,0 +1 @@
export * from './create-charge.use-case';
1 change: 1 addition & 0 deletions libs/credit-card/src/infra/psp/pagarme/index.ts
@@ -0,0 +1 @@
export * from './pagarme.service';
1 change: 1 addition & 0 deletions libs/credit-card/src/infra/rmq/publisher/index.ts
@@ -0,0 +1 @@
export * from './create-charge.publisher';
2 changes: 1 addition & 1 deletion nest-cli.json
Expand Up @@ -4,7 +4,7 @@
"sourceRoot": "apps/nestjs-rabbitmq-example/src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true,
"webpack": false,
"tsConfigPath": "apps/nestjs-rabbitmq-example/tsconfig.app.json"
},
"monorepo": true,
Expand Down
3 changes: 3 additions & 0 deletions tsconfig.json
Expand Up @@ -20,6 +20,9 @@
"paths": {
"@libs/credit-card/*": [
"libs/credit-card/src/*"
],
"@api/*": [
"apps/nestjs-rabbitmq-example/src/*"
]
}
}
Expand Down

0 comments on commit 99b18cc

Please sign in to comment.