diff --git a/packages/apps/human-app/server/.env.example b/packages/apps/human-app/server/.env.example index 15dda61543..d5d1988d68 100644 --- a/packages/apps/human-app/server/.env.example +++ b/packages/apps/human-app/server/.env.example @@ -4,6 +4,8 @@ REPUTATION_ORACLE_URL= REDIS_PORT= REDIS_HOST= CACHE_TTL_ORACLE_DISCOVERY= +CACHE_TTL_ORACLE_STATS= +CACHE_TTL_USER_STATS= E2E_TESTING_EMAIL_ADDRESS= E2E_TESTING_PASSWORD= E2E_TESTING_EXCHANGE_ORACLE_URL= diff --git a/packages/apps/human-app/server/src/app.module.ts b/packages/apps/human-app/server/src/app.module.ts index 1fbd4f02f3..4ec9d16d5b 100644 --- a/packages/apps/human-app/server/src/app.module.ts +++ b/packages/apps/human-app/server/src/app.module.ts @@ -19,7 +19,9 @@ import { JobsDiscoveryModule } from './modules/jobs-discovery/jobs-discovery.mod import { JobsDiscoveryController } from './modules/jobs-discovery/jobs-discovery.controller'; import { JobAssignmentController } from './modules/job-assignment/job-assignment.controller'; import { JobAssignmentModule } from './modules/job-assignment/job-assignment.module'; -import { RequestContext } from './common/utils/request-context.util'; +import { StatisticsModule } from './modules/statistics/statistics.module'; +import { StatisticsController } from './modules/statistics/statistics.controller'; +import { ExternalApiModule } from './integrations/external-api/external-api.module'; @Module({ imports: [ @@ -38,8 +40,10 @@ import { RequestContext } from './common/utils/request-context.util'; JobsDiscoveryModule, JobAssignmentModule, ReputationOracleModule, + ExternalApiModule, CommonConfigModule, OracleDiscoveryModule, + StatisticsModule, ], controllers: [ AppController, @@ -48,8 +52,8 @@ import { RequestContext } from './common/utils/request-context.util'; JobsDiscoveryController, OracleDiscoveryController, JobAssignmentController, + StatisticsController, ], - providers: [RequestContext], exports: [HttpModule], }) export class AppModule {} diff --git a/packages/apps/human-app/server/src/common/config/environment-config.service.ts b/packages/apps/human-app/server/src/common/config/environment-config.service.ts index 293b8e48cb..4d840354a4 100644 --- a/packages/apps/human-app/server/src/common/config/environment-config.service.ts +++ b/packages/apps/human-app/server/src/common/config/environment-config.service.ts @@ -2,35 +2,66 @@ import Joi from 'joi'; import { ConfigService } from '@nestjs/config'; import { Injectable } from '@nestjs/common'; +const DEFAULT_PORT = 5010; +const DEFAULT_HOST = 'localhost'; +const DEFAULT_REDIS_PORT = 6379; +const DEFAULT_REDIS_HOST = DEFAULT_HOST; +const DEFAULT_REPUTATION_ORACLE_URL = ''; +const DEFAULT_CACHE_TTL_ORACLE_STATS = 12 * 60 * 60; +const DEFAULT_CACHE_TTL_USER_STATS = 15 * 60; +const DEFAULT_CACHE_TTL_ORACLE_DISCOVERY = 24 * 60 * 60; @Injectable() export class EnvironmentConfigService { + constructor(private configService: ConfigService) {} get host(): string { - return this.configService.get('HOST', 'localhost'); + return this.configService.get('HOST', DEFAULT_HOST); } - get port(): string { - return this.configService.get('PORT', '5010'); + get port(): number { + return this.configService.get('PORT', DEFAULT_PORT); } get reputationOracleUrl(): string { - return this.configService.get('REPUTATION_ORACLE_URL', ''); + return this.configService.get( + 'REPUTATION_ORACLE_URL', + DEFAULT_REPUTATION_ORACLE_URL, + ); } get cachePort(): number { - return this.configService.get('REDIS_PORT', 6379); + return this.configService.get( + 'REDIS_PORT', + DEFAULT_REDIS_PORT, + ); } get cacheHost(): string { - return this.configService.get('REDIS_HOST', 'localhost'); + return this.configService.get( + 'REDIS_HOST', + DEFAULT_REDIS_HOST, + ); + } + get cacheTtlOracleStats(): number { + return this.configService.get( + 'CACHE_TTL_ORACLE_STATS', + DEFAULT_CACHE_TTL_ORACLE_STATS, + ); + } + + get cacheTtlUserStats(): number { + return this.configService.get( + 'CACHE_TTL_USER_STATS', + DEFAULT_CACHE_TTL_USER_STATS, + ); } get cacheTtlOracleDiscovery(): number { return this.configService.get( 'CACHE_TTL_ORACLE_DISCOVERY', - 24 * 60 * 60, + DEFAULT_CACHE_TTL_ORACLE_DISCOVERY, ); } } export const envValidator = Joi.object({ - HOST: Joi.string().default('localhost'), - PORT: Joi.string().default(5010), + HOST: Joi.string().default(DEFAULT_HOST), + PORT: Joi.number().default(DEFAULT_PORT), REPUTATION_ORACLE_URL: Joi.string().required(), }); diff --git a/packages/apps/human-app/server/src/common/interceptors/auth-token.middleware.ts b/packages/apps/human-app/server/src/common/interceptors/auth-token.middleware.ts deleted file mode 100644 index f917c2c543..0000000000 --- a/packages/apps/human-app/server/src/common/interceptors/auth-token.middleware.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { RequestContext } from '../utils/request-context.util'; -import { Injectable, NestMiddleware } from '@nestjs/common'; - -@Injectable() -export class TokenMiddleware implements NestMiddleware { - constructor(private readonly requestContext: RequestContext) {} - - use(req: any, res: any, next: () => void) { - const authorization = req.headers['authorization']; - this.requestContext.token = authorization; - next(); - } -} diff --git a/packages/apps/human-app/server/src/common/utils/request-context.util.ts b/packages/apps/human-app/server/src/common/utils/request-context.util.ts deleted file mode 100644 index 2e3fda64e9..0000000000 --- a/packages/apps/human-app/server/src/common/utils/request-context.util.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable, Scope } from '@nestjs/common'; - -@Injectable({ scope: Scope.REQUEST }) -export class RequestContext { - token: string; -} diff --git a/packages/apps/human-app/server/src/integrations/external-api/external-api.gateway.ts b/packages/apps/human-app/server/src/integrations/external-api/external-api.gateway.ts new file mode 100644 index 0000000000..f6a2f23805 --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/external-api/external-api.gateway.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; +import { AxiosRequestConfig } from 'axios'; +import { lastValueFrom } from 'rxjs'; +import { + UserStatisticsCommand, + UserStatisticsResponse, +} from '../../modules/statistics/interfaces/user-statistics.interface'; +import { HttpService } from '@nestjs/axios'; +import { + OracleStatisticsCommand, + OracleStatisticsResponse, +} from '../../modules/statistics/interfaces/oracle-statistics.interface'; +import { + JobAssignmentCommand, + JobAssignmentData, + JobAssignmentParams, + JobAssignmentResponse, + JobsFetchParams, + JobsFetchParamsCommand, + JobsFetchParamsData, + JobsFetchResponse, +} from '../../modules/job-assignment/interfaces/job-assignment.interface'; +import { + JobsDiscoveryParams, + JobsDiscoveryParamsCommand, + JobsDiscoveryParamsData, + JobsDiscoveryResponse, +} from '../../modules/jobs-discovery/interfaces/jobs-discovery.interface'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; + +@Injectable() +export class ExternalApiGateway { + constructor( + private httpService: HttpService, + @InjectMapper() private mapper: Mapper, + ) {} + private async callExternalHttpUtilRequest( + options: AxiosRequestConfig, + ): Promise { + const response = await lastValueFrom(this.httpService.request(options)); + return response.data; + } + async fetchUserStatistics( + command: UserStatisticsCommand, + ): Promise { + const options: AxiosRequestConfig = { + method: 'GET', + url: `${command.oracleUrl}/stats/assignment`, + headers: { + Authorization: `Bearer ${command.token}`, + }, + }; + return this.callExternalHttpUtilRequest(options); + } + async fetchOracleStatistics( + command: OracleStatisticsCommand, + ): Promise { + const options: AxiosRequestConfig = { + method: 'GET', + url: `${command.oracleUrl}/stats`, + }; + return this.callExternalHttpUtilRequest(options); + } + async fetchAssignedJobs( + command: JobsFetchParamsCommand, + ): Promise { + const options: AxiosRequestConfig = { + method: 'GET', + url: `${command.exchangeOracleUrl}/assignment`, + params: this.mapper.map( + command.data, + JobsFetchParams, + JobsFetchParamsData, + ), + }; + return this.callExternalHttpUtilRequest(options); + } + async postNewJobAssignment( + command: JobAssignmentCommand, + ): Promise { + const options: AxiosRequestConfig = { + method: 'POST', + url: `${command.exchangeOracleUrl}/assignment`, + data: this.mapper.map( + command.data, + JobAssignmentParams, + JobAssignmentData, + ), + headers: { + Authorization: `Bearer ${command.token}`, + }, + }; + return this.callExternalHttpUtilRequest(options); + } + async fetchDiscoveredJobs(command: JobsDiscoveryParamsCommand) { + const options: AxiosRequestConfig = { + method: 'GET', + url: `${command.exchangeOracleUrl}/jobs`, + params: this.mapper.map( + command.data, + JobsDiscoveryParams, + JobsDiscoveryParamsData, + ), + headers: { + Authorization: `Bearer ${command.token}`, + }, + }; + return this.callExternalHttpUtilRequest(options); + } +} diff --git a/packages/apps/human-app/server/src/integrations/external-api/external-api.mapper.ts b/packages/apps/human-app/server/src/integrations/external-api/external-api.mapper.ts new file mode 100644 index 0000000000..1bebc5009f --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/external-api/external-api.mapper.ts @@ -0,0 +1,65 @@ +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { + CamelCaseNamingConvention, + createMap, + forMember, + mapFrom, + Mapper, + namingConventions, + SnakeCaseNamingConvention, +} from '@automapper/core'; +import { + JobAssignmentData, + JobAssignmentParams, + JobsFetchParams, + JobsFetchParamsData, +} from '../../modules/job-assignment/interfaces/job-assignment.interface'; +import { + JobsDiscoveryParams, + JobsDiscoveryParamsData, +} from '../../modules/jobs-discovery/interfaces/jobs-discovery.interface'; + +@Injectable() +export class ExternalApiProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper: Mapper) => { + createMap( + mapper, + JobAssignmentParams, + JobAssignmentData, + namingConventions({ + source: new CamelCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }), + ); + createMap( + mapper, + JobsFetchParams, + JobsFetchParamsData, + namingConventions({ + source: new CamelCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }), + ); + createMap( + mapper, + JobsDiscoveryParams, + JobsDiscoveryParamsData, + // Automapper has problem with mapping arrays, thus explicit conversion + forMember( + (destination) => destination.fields, + mapFrom((source) => source.fields), + ), + namingConventions({ + source: new CamelCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }), + ); + }; + } +} diff --git a/packages/apps/human-app/server/src/integrations/external-api/external-api.module.ts b/packages/apps/human-app/server/src/integrations/external-api/external-api.module.ts new file mode 100644 index 0000000000..579148f244 --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/external-api/external-api.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ExternalApiGateway } from './external-api.gateway'; +import { ExternalApiProfile } from './external-api.mapper'; + +@Module({ + imports: [HttpModule], + providers: [ExternalApiGateway, ExternalApiProfile], + exports: [ExternalApiGateway], +}) +export class ExternalApiModule {} diff --git a/packages/apps/human-app/server/src/integrations/external-api/spec/external-api.gateway.spec.ts b/packages/apps/human-app/server/src/integrations/external-api/spec/external-api.gateway.spec.ts new file mode 100644 index 0000000000..6eaa089011 --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/external-api/spec/external-api.gateway.spec.ts @@ -0,0 +1,114 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { ExternalApiGateway } from '../external-api.gateway'; +import { + oracleStatsCommandFixture, + statisticsOracleUrl, + userStatsCommandFixture, +} from '../../../modules/statistics/spec/statistics.fixtures'; +import { AutomapperModule } from '@automapper/nestjs'; +import { classes } from '@automapper/classes'; +import nock, { RequestBodyMatcher } from 'nock'; +import { of } from 'rxjs'; +import { + jobAssignmentCommandFixture, + jobAssignmentDataFixture, + jobAssignmentOracleUrl, + jobsFetchParamsCommandFixture, + jobsFetchParamsDataFixtureAsString, +} from '../../../modules/job-assignment/spec/job-assignment.fixtures'; +import { ExternalApiProfile } from '../external-api.mapper'; +import { + jobsDiscoveryParamsCommandFixture, + paramsDataFixtureAsString, +} from '../../../modules/jobs-discovery/spec/jobs-discovery.fixtures'; + +describe('ExternalApiGateway', () => { + let gateway: ExternalApiGateway; + let httpService: HttpService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + ExternalApiProfile, + ExternalApiGateway, + { + provide: HttpService, + useValue: { + request: jest.fn().mockReturnValue(of({ data: 'mocked response' })), + }, + }, + ], + }).compile(); + + gateway = module.get(ExternalApiGateway); + httpService = module.get(HttpService); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); + + describe('fetchUserStatistics', () => { + it('should successfully call the requested url for user statistics', async () => { + const command = userStatsCommandFixture; + nock(statisticsOracleUrl) + .get('/stats/assignment') + .matchHeader('Authorization', `Bearer ${command.token}`) + .reply(200); + await gateway.fetchUserStatistics(command); + expect(httpService.request).toHaveBeenCalled(); + }); + }); + describe('fetchOracleStatistics', () => { + it('should successfully call the requested url for oracle statistics', async () => { + const command = oracleStatsCommandFixture; + nock(statisticsOracleUrl).get('/stats').reply(200); + await gateway.fetchOracleStatistics(command); + expect(httpService.request).toHaveBeenCalled(); + }); + }); + describe('fetchAssignedJobs', () => { + it('should successfully call get assigned jobs', async () => { + const command = jobsFetchParamsCommandFixture; + nock(jobAssignmentOracleUrl) + .get(`/assignment${jobsFetchParamsDataFixtureAsString}`) + .reply(200); + await gateway.fetchAssignedJobs(command); + expect(httpService.request).toHaveBeenCalled(); + }); + }); + describe('postNewJobAssignment', () => { + it('should successfully post new job assignment', async () => { + const command = jobAssignmentCommandFixture; + const data = jobAssignmentDataFixture; + const matcher: RequestBodyMatcher = { + escrowAddress: data.escrow_address, + chainId: data.chain_id, + }; + nock(jobAssignmentOracleUrl).post('/assignment', matcher).reply(200); + await gateway.postNewJobAssignment(command); + expect(httpService.request).toHaveBeenCalled(); + }); + }); + + describe('fetchDiscoveredJobs', () => { + it('should successfully call get discovered jobs', async () => { + const command = jobsDiscoveryParamsCommandFixture; + nock(jobAssignmentOracleUrl) + .get(`/assignment${paramsDataFixtureAsString}`) + .reply(200); + await gateway.fetchDiscoveredJobs(command); + expect(httpService.request).toHaveBeenCalled(); + }); + }); + + afterEach(() => { + nock.cleanAll(); + }); +}); diff --git a/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.gateway.ts b/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.gateway.ts index c0bdc968c2..b470e5f529 100644 --- a/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.gateway.ts +++ b/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.gateway.ts @@ -52,12 +52,8 @@ export class ReputationOracleGateway { private async handleRequestToReputationOracle( options: AxiosRequestConfig, ): Promise { - try { - const response = await lastValueFrom(this.httpService.request(options)); - return response.data; - } catch (error) { - throw error; - } + const response = await lastValueFrom(this.httpService.request(options)); + return response.data; } async sendWorkerSignup(command: SignupWorkerCommand): Promise { const signupWorkerData = this.mapper.map( diff --git a/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.mapper.ts b/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.mapper.ts index 2eefabab9b..f082683cd1 100644 --- a/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.mapper.ts +++ b/packages/apps/human-app/server/src/integrations/reputation-oracle/reputation-oracle.mapper.ts @@ -9,7 +9,10 @@ import { SignupWorkerCommand, SignupWorkerData, } from '../../modules/user-worker/interfaces/worker-registration.interface'; -import { SigninWorkerCommand, SigninWorkerData } from '../../modules/user-worker/interfaces/worker-signin.interface'; +import { + SigninWorkerCommand, + SigninWorkerData, +} from '../../modules/user-worker/interfaces/worker-signin.interface'; @Injectable() export class ReputationOracleProfile extends AutomapperProfile { diff --git a/packages/apps/human-app/server/src/modules/job-assignment/interfaces/job-assignment.interface.ts b/packages/apps/human-app/server/src/modules/job-assignment/interfaces/job-assignment.interface.ts index 101c479bd8..6e4ff04fad 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/interfaces/job-assignment.interface.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/interfaces/job-assignment.interface.ts @@ -10,28 +10,29 @@ export class JobAssignmentDto { @AutoMap() @ApiProperty({ example: 'string' }) exchange_oracle_url: string; - @AutoMap() @ApiProperty({ example: 'string' }) escrow_address: string; - @AutoMap() @ApiProperty({ example: 0 }) chain_id: number; } -export class JobAssignmentCommand { +export class JobAssignmentParams { @AutoMap() - exchange_oracle_url: string; + chainId: number; @AutoMap() - escrow_address: string; + escrowAddress: string; +} +export class JobAssignmentCommand { + data: JobAssignmentParams; @AutoMap() - chain_id: number; + token: string; + @AutoMap() + exchangeOracleUrl: string; } export class JobAssignmentData { - @AutoMap() - exchange_oracle_url: string; @AutoMap() escrow_address: string; @AutoMap() @@ -56,70 +57,67 @@ export class JobsFetchParamsDto { @AutoMap() @ApiProperty({ example: 'string' }) exchange_oracle_url: string; - @AutoMap() @ApiProperty({ example: 'string', required: false }) assignment_id: string; - @AutoMap() @ApiProperty({ example: 'string', required: false }) escrow_address: string; - @AutoMap() @ApiProperty({ example: 0, required: false }) chain_id: number; - @AutoMap() @ApiProperty({ example: 'job type', required: false }) job_type: string; - @AutoMap() @ApiProperty({ example: 'ACTIVE', required: false }) status: StatusEnum; - @AutoMap() @ApiProperty({ example: 5, default: 5, maximum: 10, required: false }) page_size: number; - @AutoMap() @ApiProperty({ example: 0, default: 0, required: false }) page: number; - @AutoMap() @ApiProperty({ example: 'ASC', default: 'ASC', required: false }) sort: SortOrder; - @AutoMap() - @ApiProperty({ example: 'created_at', default: 'created_at', required: false }) + @ApiProperty({ + example: 'created_at', + default: 'created_at', + required: false, + }) sort_field: SortField; } -export class JobsFetchParamsCommand { +export class JobsFetchParams { @AutoMap() - exchange_oracle_url: string; + assignmentId: string; @AutoMap() - assignment_id: string; + escrowAddress: string; @AutoMap() - escrow_address: string; - @AutoMap() - chain_id: number; + chainId: number; @AutoMap() - job_type: string; + jobType: string; @AutoMap() status: StatusEnum; @AutoMap() - page_size: number; + pageSize: number; @AutoMap() page: number; @AutoMap() sort: SortOrder; @AutoMap() - sort_field: SortField; + sortField: SortField; +} +export class JobsFetchParamsCommand { + @AutoMap() + exchangeOracleUrl: string; + @AutoMap() + data: JobsFetchParams; } export class JobsFetchParamsData { - @AutoMap() - exchange_oracle_url: string; @AutoMap() assignment_id: string; @AutoMap() diff --git a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts index ea625c5744..e894470804 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, - Get, + Get, Headers, Post, Query, UsePipes, @@ -35,12 +35,14 @@ export class JobAssignmentController { @UsePipes(new ValidationPipe()) public async assignJob( @Body() jobAssignmentDto: JobAssignmentDto, + @Headers('authorization') token: string, ): Promise { const jobAssignmentCommand = this.mapper.map( jobAssignmentDto, JobAssignmentDto, JobAssignmentCommand, ); + jobAssignmentCommand.token = token; return this.jobAssignmentService.processJobAssignment(jobAssignmentCommand); } diff --git a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.mapper.ts b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.mapper.ts index 022209328b..a441017299 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.mapper.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.mapper.ts @@ -1,12 +1,21 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { createMap, Mapper } from '@automapper/core'; +import { + CamelCaseNamingConvention, + createMap, + forMember, + mapFrom, + Mapper, + mapWith, + namingConventions, + SnakeCaseNamingConvention, +} from '@automapper/core'; import { JobAssignmentCommand, - JobAssignmentData, JobAssignmentDto, + JobAssignmentParams, + JobsFetchParams, JobsFetchParamsCommand, - JobsFetchParamsData, JobsFetchParamsDto, } from './interfaces/job-assignment.interface'; @@ -18,10 +27,61 @@ export class JobAssignmentProfile extends AutomapperProfile { override get profile() { return (mapper: Mapper) => { - createMap(mapper, JobAssignmentDto, JobAssignmentCommand); - createMap(mapper, JobAssignmentCommand, JobAssignmentData); - createMap(mapper, JobsFetchParamsDto, JobsFetchParamsCommand); - createMap(mapper, JobsFetchParamsCommand, JobsFetchParamsData); + createMap( + mapper, + JobAssignmentDto, + JobAssignmentParams, + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), + ); + createMap( + mapper, + JobAssignmentDto, + JobAssignmentCommand, + forMember( + (destination) => destination.data, + mapWith(JobAssignmentParams, JobAssignmentDto, (source) => source), + ), + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), + ); + createMap( + mapper, + JobsFetchParamsDto, + JobsFetchParams, + // forMember usage cause: https://github.com/nartc/mapper/issues/583 + forMember( + (destination) => destination.pageSize, + mapFrom((source: JobsFetchParamsDto) => source.page_size), + ), + forMember( + (destination) => destination.sortField, + mapFrom((source: JobsFetchParamsDto) => source.sort_field), + ), + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), + ); + createMap( + mapper, + JobsFetchParamsDto, + JobsFetchParamsCommand, + forMember( + (destination) => destination.data, + mapFrom((source: JobsFetchParamsDto) => + mapper.map(source, JobsFetchParamsDto, JobsFetchParams), + ), + ), + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), + ); }; } } diff --git a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.module.ts b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.module.ts index 3a54e51309..4710912784 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.module.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.module.ts @@ -1,18 +1,11 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { JobAssignmentService } from './job-assignment.service'; import { JobAssignmentProfile } from './job-assignment.mapper'; -import { HttpModule } from '@nestjs/axios'; -import { TokenMiddleware } from '../../common/interceptors/auth-token.middleware'; -import { JobAssignmentController } from './job-assignment.controller'; -import { RequestContext } from '../../common/utils/request-context.util'; +import { Module } from '@nestjs/common'; +import { ExternalApiModule } from '../../integrations/external-api/external-api.module'; @Module({ - imports: [HttpModule], - providers: [JobAssignmentService, JobAssignmentProfile, RequestContext], + imports: [ExternalApiModule], + providers: [JobAssignmentService, JobAssignmentProfile], exports: [JobAssignmentService], }) -export class JobAssignmentModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(TokenMiddleware).forRoutes(JobAssignmentController); - } -} +export class JobAssignmentModule {} diff --git a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.service.ts b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.service.ts index 92478bcea4..1acdedf008 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.service.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.service.ts @@ -1,69 +1,24 @@ import { Injectable } from '@nestjs/common'; -import { lastValueFrom } from 'rxjs'; import { JobsFetchParamsCommand, - JobsFetchParamsData, JobAssignmentResponse, JobAssignmentCommand, - JobAssignmentData, JobsFetchResponse, } from './interfaces/job-assignment.interface'; -import { HttpService } from '@nestjs/axios'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; -import { RequestContext } from '../../common/utils/request-context.util'; - +import { ExternalApiGateway } from '../../integrations/external-api/external-api.gateway'; @Injectable() export class JobAssignmentService { - constructor( - public httpService: HttpService, - @InjectMapper() private readonly mapper: Mapper, - private readonly requestContext: RequestContext, - ) {} + constructor(private readonly externalApiGateway: ExternalApiGateway) {} async processJobAssignment( - jobAssignmentCommand: JobAssignmentCommand, + command: JobAssignmentCommand, ): Promise { - const jobAssignmentData = this.mapper.map( - jobAssignmentCommand, - JobAssignmentCommand, - JobAssignmentData, - ); - try { - const url = jobAssignmentData.exchange_oracle_url; - const token = this.requestContext.token; - const options = { - method: 'POST', - url: `${url}/assignment`, - data: jobAssignmentData, - headers: { Authorization: `Bearer ${token}` }, - }; - const response = await lastValueFrom(this.httpService.request(options)); - return response.data; - } catch (error) { - throw error; - } + return this.externalApiGateway.postNewJobAssignment(command); } async processGetAssignedJobs( - jobsAssignmentParamsCommand: JobsFetchParamsCommand, + command: JobsFetchParamsCommand, ): Promise { - const jobsAssignmentParamsData = this.mapper.map( - jobsAssignmentParamsCommand, - JobsFetchParamsCommand, - JobsFetchParamsData, - ); - try { - const url = jobsAssignmentParamsData.exchange_oracle_url; - const options = { - method: 'GET', - url: `${url}/assignment`, - params: jobsAssignmentParamsData, - }; - const response = await lastValueFrom(this.httpService.request(options)); - return response.data; - } catch (error) { - throw error; - } + return this.externalApiGateway.fetchAssignedJobs(command); } } diff --git a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts index f8a000c5a2..9794b7848b 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts @@ -14,7 +14,7 @@ import { jobAssignmentResponseFixture, jobsFetchParamsDtoFixture, jobsFetchParamsCommandFixture, - jobsFetchResponseFixture, + jobsFetchResponseFixture, jobAssignmentToken, } from './job-assignment.fixtures'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; @@ -69,7 +69,7 @@ describe('JobAssignmentController', () => { it('should call service processJobAssignment method with proper fields set', async () => { const dto: JobAssignmentDto = jobAssignmentDtoFixture; const command: JobAssignmentCommand = jobAssignmentCommandFixture; - await controller.assignJob(dto); + await controller.assignJob(dto, jobAssignmentToken); expect(jobAssignmentService.processJobAssignment).toHaveBeenCalledWith( command, ); @@ -78,7 +78,7 @@ describe('JobAssignmentController', () => { it('should return the result of service processJobAssignment method', async () => { const dto: JobAssignmentDto = jobAssignmentDtoFixture; const command: JobAssignmentCommand = jobAssignmentCommandFixture; - const result = await controller.assignJob(dto); + const result = await controller.assignJob(dto, jobAssignmentToken); expect(result).toEqual( jobAssignmentServiceMock.processJobAssignment(command), ); @@ -93,15 +93,5 @@ describe('JobAssignmentController', () => { command, ); }); - - it('should return the result of service processGetAssignedJobs method', async () => { - const dto: JobsFetchParamsDto = jobsFetchParamsDtoFixture; - const command: JobsFetchParamsCommand = - jobsFetchParamsCommandFixture; - const result = await controller.getAssignedJobs(dto); - expect(result).toEqual( - jobAssignmentServiceMock.processGetAssignedJobs(command), - ); - }); }); }); diff --git a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.fixtures.ts b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.fixtures.ts index eb74e865ab..77787f51b1 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.fixtures.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.fixtures.ts @@ -2,7 +2,9 @@ import { JobAssignmentCommand, JobAssignmentData, JobAssignmentDto, + JobAssignmentParams, JobAssignmentResponse, + JobsFetchParams, JobsFetchParamsCommand, JobsFetchParamsData, JobsFetchParamsDto, @@ -14,64 +16,114 @@ import { SortOrder, StatusEnum, } from '../../../common/enums/job-assignment'; - +const EXCHANGE_ORACLE_URL = 'https://www.example.com/api'; +const ESCROW_ADDRESS = 'test_address'; +const CHAIN_ID = 1; +const JOB_TYPE = 'test_type'; +const STATUS = StatusEnum.ACTIVE; +const PAGE_SIZE = 5; +const PAGE = 0; +const SORT = SortOrder.ASC; +const SORT_FIELD = SortField.CREATED_AT; +const ASSIGNMENT_ID = 'test_id'; +const REWARD_AMOUNT = 'test_amount'; +const REWARD_TOKEN = 'test'; +const CREATED_AT = 'test_date_1'; +const UPDATED_AT = 'test_date_2'; +const EXPIRES_AT = 'test_date_3'; +const URL = 'test_url'; +const TOKEN = 'test_user_token'; +export const jobAssignmentToken = TOKEN; +export const jobAssignmentOracleUrl = EXCHANGE_ORACLE_URL; export const jobAssignmentDtoFixture: JobAssignmentDto = { - exchange_oracle_url: 'https://www.example.com/api', - escrow_address: 'test_address', - chain_id: 1, + exchange_oracle_url: EXCHANGE_ORACLE_URL, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, }; -export const jobAssignmentCommandFixture: JobAssignmentCommand = - jobAssignmentDtoFixture; +const jobAssignmentParams: JobAssignmentParams = { + chainId: CHAIN_ID, + escrowAddress: ESCROW_ADDRESS, +}; +export const jobAssignmentCommandFixture: JobAssignmentCommand = { + data: jobAssignmentParams, + token: TOKEN, + exchangeOracleUrl: EXCHANGE_ORACLE_URL, +}; -export const jobAssignmentDataFixture: JobAssignmentData = - jobAssignmentCommandFixture; +export const jobAssignmentDataFixture: JobAssignmentData = { + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, +}; export const jobAssignmentResponseFixture: JobAssignmentResponse = { - assignment_id: 'test_id', - escrow_address: 'test_address', - chain_id: 1, - job_type: 'test_type', - url: 'test_url', - status: 'test_status', - reward_amount: 'test_amount', - reward_token: 'test_token', - created_at: 'test_date', - updated_at: 'test_date', - expires_at: 'test_date', + assignment_id: ASSIGNMENT_ID, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + url: URL, + status: STATUS, + reward_amount: REWARD_AMOUNT, + reward_token: REWARD_TOKEN, + created_at: CREATED_AT, + updated_at: UPDATED_AT, + expires_at: EXPIRES_AT, }; export const jobsFetchParamsDtoFixture: JobsFetchParamsDto = { - exchange_oracle_url: 'https://www.example.com/api', - assignment_id: 'test_id', - escrow_address: 'test_address', - chain_id: 1, - job_type: 'test_type', - status: StatusEnum.ACTIVE, - page_size: 5, - page: 0, - sort: SortOrder.ASC, - sort_field: SortField.CREATED_AT, + exchange_oracle_url: EXCHANGE_ORACLE_URL, + assignment_id: ASSIGNMENT_ID, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + status: STATUS, + page_size: PAGE_SIZE, + page: PAGE, + sort: SORT, + sort_field: SORT_FIELD, }; -export const jobsFetchParamsCommandFixture: JobsFetchParamsCommand = - jobsFetchParamsDtoFixture; +const jobsFetchParams: JobsFetchParams = { + assignmentId: ASSIGNMENT_ID, + escrowAddress: ESCROW_ADDRESS, + chainId: CHAIN_ID, + jobType: JOB_TYPE, + status: STATUS, + pageSize: PAGE_SIZE, + page: PAGE, + sort: SORT, + sortField: SORT_FIELD, +}; +export const jobsFetchParamsCommandFixture: JobsFetchParamsCommand = { + data: jobsFetchParams, + exchangeOracleUrl: EXCHANGE_ORACLE_URL, +}; -export const jobsFetchParamsDataFixture: JobsFetchParamsData = - jobsFetchParamsCommandFixture; +export const jobsFetchParamsDataFixture: JobsFetchParamsData = { + assignment_id: ASSIGNMENT_ID, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + status: STATUS, + page_size: PAGE_SIZE, + page: PAGE, + sort: SORT, + sort_field: SORT_FIELD, +}; +export const jobsFetchParamsDataFixtureAsString = `assignment_id=${ASSIGNMENT_ID}&escrow_address=${ESCROW_ADDRESS}&chain_id=${CHAIN_ID}&job_type=${JOB_TYPE}&status=${STATUS}&page_size=${PAGE_SIZE}&page=${PAGE}&sort=${SORT}&sort_field=${SORT_FIELD}`; export const jobsFetchResponseItemFixture: JobsFetchResponseItem = { - assignment_id: 'test_id', - escrow_address: 'test_address', - chain_id: 1, - job_type: 'test_type', - url: 'test_url', - status: 'test_status', - reward_amount: 'test_amount', - reward_token: 'test_token', - created_at: 'test_date', - updated_at: 'test_date', - expires_at: 'test_date', + assignment_id: ASSIGNMENT_ID, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + url: URL, + status: STATUS, + reward_amount: REWARD_AMOUNT, + reward_token: REWARD_TOKEN, + created_at: CREATED_AT, + updated_at: UPDATED_AT, + expires_at: EXPIRES_AT, }; export const jobsFetchResponseFixture: JobsFetchResponse = { diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/interfaces/jobs-discovery.interface.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/interfaces/jobs-discovery.interface.ts index 540236d850..2a16c493b8 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/interfaces/jobs-discovery.interface.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/interfaces/jobs-discovery.interface.ts @@ -10,64 +10,51 @@ export class JobsDiscoveryParamsDto { @AutoMap() @ApiProperty({ example: 'string' }) exchange_oracle_url: string; - @AutoMap() @ApiProperty({ example: 'string' }) escrow_address: string; - @AutoMap() @ApiProperty({ example: 0 }) chain_id: number; - @AutoMap() @ApiProperty({ example: 5, default: 5, maximum: 10 }) page_size: number; - @AutoMap() @ApiProperty({ example: 0, default: 0 }) page: number; - @AutoMap() @ApiProperty({ example: 'ASC', default: 'ASC' }) sort: SortOrder; - @AutoMap() @ApiProperty({ example: 'created_at', default: 'created_at' }) sort_field: SortField; - @AutoMap() @ApiProperty({ example: 'job type' }) job_type: string; - @AutoMap() @ApiProperty({ example: ['job_title'] }) fields: JobFields[]; } -export class JobsDiscoveryParamsCommand { +export class JobsDiscoveryParams { @AutoMap() - exchange_oracle_url: string; + escrowAddress: string; @AutoMap() - escrow_address: string; + chainId: number; @AutoMap() - chain_id: number; - @AutoMap() - page_size: number; + pageSize: number; @AutoMap() page: number; @AutoMap() sort: SortOrder; @AutoMap() - sort_field: SortField; + sortField: SortField; @AutoMap() - job_type: string; + jobType: string; @AutoMap() fields: JobFields[]; } - export class JobsDiscoveryParamsData { - @AutoMap() - exchange_oracle_url: string; @AutoMap() escrow_address: string; @AutoMap() @@ -85,6 +72,13 @@ export class JobsDiscoveryParamsData { @AutoMap() fields: JobFields[]; } +export class JobsDiscoveryParamsCommand { + @AutoMap() + exchangeOracleUrl: string; + token: string; + @AutoMap() + data: JobsDiscoveryParams; +} export class JobsDiscoveryResponseItem { escrow_address: string; diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts index 2ea6ad9256..05a8e7875e 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Headers, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; @@ -24,12 +24,15 @@ export class JobsDiscoveryController { }) public async discoverJobs( @Query() jobsDiscoveryParamsDto: JobsDiscoveryParamsDto, + @Headers('authorization') token: string, ): Promise { - const jobsDiscoveryParamsCommand = this.mapper.map( - jobsDiscoveryParamsDto, - JobsDiscoveryParamsDto, - JobsDiscoveryParamsCommand, - ); + const jobsDiscoveryParamsCommand: JobsDiscoveryParamsCommand = + this.mapper.map( + jobsDiscoveryParamsDto, + JobsDiscoveryParamsDto, + JobsDiscoveryParamsCommand, + ); + jobsDiscoveryParamsCommand.token = token; return await this.jobsDiscoveryService.processJobsDiscovery( jobsDiscoveryParamsCommand, ); diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.ts index 2cb804876f..cb23b4f86c 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.ts @@ -1,9 +1,17 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { + CamelCaseNamingConvention, + createMap, + forMember, + mapFrom, + Mapper, + namingConventions, + SnakeCaseNamingConvention, +} from '@automapper/core'; +import { + JobsDiscoveryParams, JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, JobsDiscoveryParamsDto, } from './interfaces/jobs-discovery.interface'; @@ -18,20 +26,40 @@ export class JobsDiscoveryProfile extends AutomapperProfile { createMap( mapper, JobsDiscoveryParamsDto, - JobsDiscoveryParamsCommand, + JobsDiscoveryParams, + // forMember usage cause: https://github.com/nartc/mapper/issues/583 + forMember( + (destination) => destination.pageSize, + mapFrom((source) => source.page_size), + ), forMember( - (d) => d.fields, - mapFrom((s) => s.fields), + (destination) => destination.sortField, + mapFrom((source) => source.sort_field), ), + // Automapper has problem with mapping arrays, thus explicit conversion + forMember( + (destination) => destination.fields, + mapFrom((source) => source.fields), + ), + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), ); createMap( mapper, + JobsDiscoveryParamsDto, JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, forMember( - (d) => d.fields, - mapFrom((s) => s.fields), + (destination) => destination.data, + mapFrom((source: JobsDiscoveryParamsDto) => + mapper.map(source, JobsDiscoveryParamsDto, JobsDiscoveryParams), + ), ), + namingConventions({ + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }), ); }; } diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts index b6c7d871ca..3d1ab5f4fc 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts @@ -1,18 +1,11 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { JobsDiscoveryService } from './jobs-discovery.service'; import { JobsDiscoveryProfile } from './jobs-discovery.mapper'; -import { HttpModule } from '@nestjs/axios'; -import { TokenMiddleware } from '../../common/interceptors/auth-token.middleware'; -import { JobsDiscoveryController } from './jobs-discovery.controller'; -import { RequestContext } from '../../common/utils/request-context.util'; +import { Module } from '@nestjs/common'; +import { ExternalApiModule } from '../../integrations/external-api/external-api.module'; @Module({ - imports: [HttpModule], - providers: [JobsDiscoveryService, JobsDiscoveryProfile, RequestContext], + imports: [ExternalApiModule], + providers: [JobsDiscoveryService, JobsDiscoveryProfile], exports: [JobsDiscoveryService], }) -export class JobsDiscoveryModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(TokenMiddleware).forRoutes(JobsDiscoveryController); - } -} +export class JobsDiscoveryModule {} diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts index 509a8ed8f8..52d158bc13 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts @@ -1,44 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { lastValueFrom } from 'rxjs'; import { JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, JobsDiscoveryResponse, } from './interfaces/jobs-discovery.interface'; -import { HttpService } from '@nestjs/axios'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; -import { RequestContext } from '../../common/utils/request-context.util'; - +import { ExternalApiGateway } from '../../integrations/external-api/external-api.gateway'; @Injectable() export class JobsDiscoveryService { - constructor( - public httpService: HttpService, - @InjectMapper() private readonly mapper: Mapper, - private readonly requestContext: RequestContext, - ) {} + constructor(private readonly externalApiGateway: ExternalApiGateway) {} async processJobsDiscovery( - jobsDiscoveryParamsCommand: JobsDiscoveryParamsCommand, + command: JobsDiscoveryParamsCommand, ): Promise { - const jobsDiscoveryParamsData = this.mapper.map( - jobsDiscoveryParamsCommand, - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, - ); - try { - const url = jobsDiscoveryParamsCommand.exchange_oracle_url; - const token = this.requestContext.token; - const options = { - method: 'GET', - url: `${url}/jobs`, - params: jobsDiscoveryParamsData, - headers: { Authorization: `Bearer ${token}` }, - }; - const response = await lastValueFrom(this.httpService.request(options)); - return response.data; - } catch (error) { - throw error; - } + return this.externalApiGateway.fetchDiscoveredJobs(command); } } diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts index a1215cc3a9..ec638c300f 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts @@ -3,20 +3,15 @@ import { JobsDiscoveryController } from '../jobs-discovery.controller'; import { Test, TestingModule } from '@nestjs/testing'; import { jobsDiscoveryServiceMock } from './jobs-discovery.service.mock'; import { - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsDto, -} from '../interfaces/jobs-discovery.interface'; -import { - commandFixture, + jobsDiscoveryParamsCommandFixture, dtoFixture, + jobDiscoveryToken, responseFixture, } from './jobs-discovery.fixtures'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { JobsDiscoveryProfile } from '../jobs-discovery.mapper'; import { HttpService } from '@nestjs/axios'; -import { TokenMiddleware } from '../../../common/interceptors/auth-token.middleware'; -import { RequestContext } from '../../../common/utils/request-context.util'; describe('JobsDiscoveryController', () => { let controller: JobsDiscoveryController; @@ -43,7 +38,6 @@ describe('JobsDiscoveryController', () => { ), }, }, - RequestContext, ], }) .overrideProvider(JobsDiscoveryService) @@ -61,44 +55,12 @@ describe('JobsDiscoveryController', () => { describe('processJobsDiscovery', () => { it('should call service processJobsDiscovery method with proper fields set', async () => { - const dto: JobsDiscoveryParamsDto = dtoFixture; - const command: JobsDiscoveryParamsCommand = commandFixture; - await controller.discoverJobs(dto); + const dto = dtoFixture; + const command = jobsDiscoveryParamsCommandFixture; + await controller.discoverJobs(dto, jobDiscoveryToken); expect(jobsDiscoveryService.processJobsDiscovery).toHaveBeenCalledWith( command, ); }); - - it('should return the result of service processJobsDiscovery method', async () => { - const dto: JobsDiscoveryParamsDto = dtoFixture; - const command: JobsDiscoveryParamsCommand = commandFixture; - const result = await controller.discoverJobs(dto); - expect(result).toEqual( - jobsDiscoveryServiceMock.processJobsDiscovery(command), - ); - }); - }); - describe('TokenMiddleware', () => { - let middleware: TokenMiddleware; - let requestContext: RequestContext; - - beforeEach(() => { - requestContext = new RequestContext(); - middleware = new TokenMiddleware(requestContext); - }); - - it('should set token in requestContext', () => { - const mockReq = { - headers: { - authorization: 'Bearer token', - }, - }; - - const next = jest.fn(); - middleware.use(mockReq, {}, next); - - expect(requestContext.token).toEqual('Bearer token'); - expect(next).toHaveBeenCalled(); - }); }); }); diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts index 8b0ebbb922..3616a342ee 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts @@ -1,5 +1,7 @@ import { + JobsDiscoveryParams, JobsDiscoveryParamsCommand, + JobsDiscoveryParamsData, JobsDiscoveryParamsDto, JobsDiscoveryResponseItem, } from '../interfaces/jobs-discovery.interface'; @@ -8,30 +10,70 @@ import { SortField, SortOrder, } from '../../../common/enums/jobs-discovery'; - +const EXCHANGE_ORACLE_URL = 'test_url'; +const ESCROW_ADDRESS = 'test_address'; +const CHAIN_ID = 1; +const PAGE_SIZE = 10; +const PAGE = 1; +const SORT = SortOrder.ASC; +const SORT_FIELD = SortField.CREATED_AT; +const JOB_TYPE = 'type'; +const FIELDS = [JobFields.JOB_TITLE, JobFields.REWARD_AMOUNT]; +const TOKEN = 'test-token'; +const JOB_DESCRIPTION = 'Description of the test job'; +const REWARD_AMOUNT = '100'; +const REWARD_TOKEN = 'ETH'; +const CREATED_AT = '2024-03-01T12:00:00Z'; +const JOB_TITLE = 'test job'; +export const jobDiscoveryToken = TOKEN; export const dtoFixture: JobsDiscoveryParamsDto = { - exchange_oracle_url: 'test_url', - escrow_address: 'test_address', - chain_id: 1, - page_size: 10, - page: 1, - sort: SortOrder.ASC, - sort_field: SortField.CREATED_AT, - job_type: 'type', - fields: [JobFields.JOB_TITLE, JobFields.REWARD_AMOUNT], + exchange_oracle_url: EXCHANGE_ORACLE_URL, + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + page_size: PAGE_SIZE, + page: PAGE, + sort: SORT, + sort_field: SORT_FIELD, + job_type: JOB_TYPE, + fields: FIELDS, }; -export const commandFixture: JobsDiscoveryParamsCommand = dtoFixture; +const dataFixture: JobsDiscoveryParams = { + escrowAddress: ESCROW_ADDRESS, + chainId: CHAIN_ID, + pageSize: PAGE_SIZE, + page: PAGE, + sort: SORT, + sortField: SORT_FIELD, + jobType: JOB_TYPE, + fields: FIELDS, +}; +const paramsDataFixture: JobsDiscoveryParamsData = { + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + page_size: PAGE_SIZE, + page: PAGE, + sort: SORT, + sort_field: SORT_FIELD, + job_type: JOB_TYPE, + fields: FIELDS, +}; +export const paramsDataFixtureAsString = `?escrow_address=${paramsDataFixture.escrow_address}&chain_id=${paramsDataFixture.chain_id}&page_size=${paramsDataFixture.page_size}&page=${paramsDataFixture.page}&sort=${paramsDataFixture.sort}&sort_field=${paramsDataFixture.sort_field}&job_type=${paramsDataFixture.job_type}&fields=${paramsDataFixture.fields.join(',')}`; +export const jobsDiscoveryParamsCommandFixture: JobsDiscoveryParamsCommand = { + data: dataFixture, + exchangeOracleUrl: EXCHANGE_ORACLE_URL, + token: TOKEN, +}; export const responseItemFixture: JobsDiscoveryResponseItem = { - escrow_address: 'test_address', - chain_id: 1, - job_type: 'type', - job_title: 'Test Job', - job_description: 'Description of the test job', - reward_amount: '100', - reward_token: 'ETH', - created_at: '2024-03-01T12:00:00Z', + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + job_title: JOB_TITLE, + job_description: JOB_DESCRIPTION, + reward_amount: REWARD_AMOUNT, + reward_token: REWARD_TOKEN, + created_at: CREATED_AT, }; export const responseFixture: JobsDiscoveryResponseItem[] = [ diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts index 758fb807e0..f8c3985a17 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts @@ -1,10 +1,8 @@ import { Module } from '@nestjs/common'; -import { OracleDiscoveryController } from './oracle-discovery.controller'; import { OracleDiscoveryService } from './oracle-discovery.serivce'; import { OracleDiscoveryProfile } from './oracle-discovery.mapper'; @Module({ - controllers: [OracleDiscoveryController], providers: [OracleDiscoveryService, OracleDiscoveryProfile], exports: [OracleDiscoveryService], }) diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.service.spec.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.service.spec.ts index 56a965ae04..8f8b198406 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.service.spec.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.service.spec.ts @@ -7,7 +7,10 @@ import { OracleDiscoveryCommand, OracleDiscoveryResponse, } from '../interface/oracle-discovery.interface'; -import { EnvironmentConfigService, envValidator } from '../../../common/config/environment-config.service'; +import { + EnvironmentConfigService, + envValidator, +} from '../../../common/config/environment-config.service'; import { CommonConfigModule } from '../../../common/config/common-config.module'; import { ConfigModule } from '@nestjs/config'; diff --git a/packages/apps/human-app/server/src/modules/statistics/interfaces/oracle-statistics.interface.ts b/packages/apps/human-app/server/src/modules/statistics/interfaces/oracle-statistics.interface.ts new file mode 100644 index 0000000000..7c43877ade --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/interfaces/oracle-statistics.interface.ts @@ -0,0 +1,12 @@ +export class OracleStatisticsResponse { + escrows_processed: number; + escrows_active: number; + escrows_cancelled: number; + workers_amount: number; + assignments_completed: number; + assignments_rejected: number; + assignments_expired: number; +} +export class OracleStatisticsCommand { + oracleUrl: string; +} diff --git a/packages/apps/human-app/server/src/modules/statistics/interfaces/user-statistics.interface.ts b/packages/apps/human-app/server/src/modules/statistics/interfaces/user-statistics.interface.ts new file mode 100644 index 0000000000..2c658d01ff --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/interfaces/user-statistics.interface.ts @@ -0,0 +1,11 @@ +export class UserStatisticsResponse { + assignments_amount: number; + submissions_sent: number; + assignments_completed: number; + assignments_rejected: number; + assignments_expired: number; +} +export class UserStatisticsCommand { + oracleUrl: string; + token: string; +} diff --git a/packages/apps/human-app/server/src/modules/statistics/spec/statistics.fixtures.ts b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.fixtures.ts new file mode 100644 index 0000000000..e72636cab2 --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.fixtures.ts @@ -0,0 +1,67 @@ +import { + UserStatisticsCommand, + UserStatisticsResponse, +} from '../interfaces/user-statistics.interface'; +import { + OracleStatisticsCommand, + OracleStatisticsResponse, +} from '../interfaces/oracle-statistics.interface'; +import { AxiosRequestConfig } from 'axios'; + +const ASSIGNMENTS_AMOUNT = 2137; +const SUBMISSIONS_SENT = 1969; +const ASSIGNMENTS_COMPLETED_USER = 3; +const ASSIGNMENTS_REJECTED_USER = 666; +const ASSIGNMENTS_EXPIRED_USER = 42; +const ESCROWS_PROCESSED = 34290311; +const ESCROWS_ACTIVE = 451132343; +const ESCROWS_CANCELLED = 7833; +const WORKERS_AMOUNT = 3409; +const ASSIGNMENTS_COMPLETED_ORACLE = 154363; +const ASSIGNMENTS_REJECTED_ORACLE = 231; +const ASSIGNMENTS_EXPIRED_ORACLE = 434; +const ORACLE_URL = 'https://test.oracle.com'; +const TOKEN = 'test-token'; +export const statisticsToken = TOKEN; +export const statisticsOracleUrl = ORACLE_URL; +export const userStatsResponseFixture: UserStatisticsResponse = { + assignments_amount: ASSIGNMENTS_AMOUNT, + submissions_sent: SUBMISSIONS_SENT, + assignments_completed: ASSIGNMENTS_COMPLETED_USER, + assignments_rejected: ASSIGNMENTS_REJECTED_USER, + assignments_expired: ASSIGNMENTS_EXPIRED_USER, +}; + +export const oracleStatsResponseFixture: OracleStatisticsResponse = { + escrows_processed: ESCROWS_PROCESSED, + escrows_active: ESCROWS_ACTIVE, + escrows_cancelled: ESCROWS_CANCELLED, + workers_amount: WORKERS_AMOUNT, + assignments_completed: ASSIGNMENTS_COMPLETED_ORACLE, + assignments_rejected: ASSIGNMENTS_REJECTED_ORACLE, + assignments_expired: ASSIGNMENTS_EXPIRED_ORACLE, +}; + +export const userStatsCommandFixture: UserStatisticsCommand = { + oracleUrl: ORACLE_URL, + token: TOKEN, +}; + +export const oracleStatsCommandFixture: OracleStatisticsCommand = { + oracleUrl: ORACLE_URL, +}; +export const requestContextFixture = { + token: TOKEN, +}; +export const userStatsOptionsFixture: AxiosRequestConfig = { + method: 'GET', + url: `${ORACLE_URL}/stats/assignment`, + headers: { + Authorization: `Bearer ${TOKEN}`, + }, +}; + +export const oracleStatsOptionsFixture: AxiosRequestConfig = { + method: 'GET', + url: `${ORACLE_URL}/stats`, +}; diff --git a/packages/apps/human-app/server/src/modules/statistics/spec/statistics.service.mock.ts b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.service.mock.ts new file mode 100644 index 0000000000..9b1fd241f1 --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.service.mock.ts @@ -0,0 +1,9 @@ +import { + oracleStatsResponseFixture, + userStatsResponseFixture, +} from './statistics.fixtures'; + +export const statisticsServiceMock = { + getUserStats: jest.fn().mockResolvedValue(userStatsResponseFixture), + getOracleStats: jest.fn().mockResolvedValue(oracleStatsResponseFixture), +}; diff --git a/packages/apps/human-app/server/src/modules/statistics/spec/statistisc.controller.spec.ts b/packages/apps/human-app/server/src/modules/statistics/spec/statistisc.controller.spec.ts new file mode 100644 index 0000000000..cbd9342e78 --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/spec/statistisc.controller.spec.ts @@ -0,0 +1,53 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { StatisticsController } from '../statistics.controller'; +import { StatisticsService } from '../statistics.service'; +import { statisticsServiceMock } from './statistics.service.mock'; +import { + oracleStatsCommandFixture, + oracleStatsResponseFixture, statisticsOracleUrl, statisticsToken, userStatsCommandFixture, + userStatsResponseFixture, +} from './statistics.fixtures'; + +describe('StatisticsController', () => { + let controller: StatisticsController; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [StatisticsController], + providers: [StatisticsService], + }) + .overrideProvider(StatisticsService) + .useValue(statisticsServiceMock) + .compile(); + + controller = module.get(StatisticsController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getOracleStatistics', () => { + it('should call getOracleStats service method with correct parameters', async () => { + const oracleUrl = statisticsOracleUrl; + const result = await controller.getOracleStatistics(oracleUrl); + + expect(statisticsServiceMock.getOracleStats).toHaveBeenCalledWith(oracleStatsCommandFixture); + expect(result).toEqual(oracleStatsResponseFixture); + }); + }); + + describe('getUserStatistics', () => { + it('should call getUserStats service method with correct parameters', async () => { + const oracleUrl = statisticsOracleUrl; + const token = statisticsToken; + const result = await controller.getUserStatistics(oracleUrl, token); + + expect(statisticsServiceMock.getUserStats).toHaveBeenCalledWith(userStatsCommandFixture); + expect(result).toEqual(userStatsResponseFixture); + }); + }); +}); diff --git a/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts b/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts new file mode 100644 index 0000000000..b76ec63fac --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Get, + Headers, + Param, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { StatisticsService } from './statistics.service'; +import { + OracleStatisticsCommand, + OracleStatisticsResponse, +} from './interfaces/oracle-statistics.interface'; +import { + UserStatisticsCommand, + UserStatisticsResponse, +} from './interfaces/user-statistics.interface'; + +@Controller() +export class StatisticsController { + constructor(private readonly service: StatisticsService) {} + @ApiTags('Statistics') + @Get('/stats') + @ApiOperation({ summary: 'General Oracle Statistics' }) + @UsePipes(new ValidationPipe()) + public getOracleStatistics( + @Param('url') oracleUrl: string, + ): Promise { + const command = { oracleUrl: oracleUrl } as OracleStatisticsCommand; + return this.service.getOracleStats(command); + } + + @ApiTags('Statistics') + @Get('stats/assignment') + @ApiOperation({ summary: 'Statistics for requesting user' }) + @UsePipes(new ValidationPipe()) + public getUserStatistics( + @Param('url') oracleUrl: string, + @Headers('authorization') token: string, + ): Promise { + const command: UserStatisticsCommand = { + oracleUrl: oracleUrl, + token: token, + } as UserStatisticsCommand; + return this.service.getUserStats(command); + } +} diff --git a/packages/apps/human-app/server/src/modules/statistics/statistics.module.ts b/packages/apps/human-app/server/src/modules/statistics/statistics.module.ts new file mode 100644 index 0000000000..7792a4a3c1 --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/statistics.module.ts @@ -0,0 +1,10 @@ +import { StatisticsService } from './statistics.service'; +import { Module } from '@nestjs/common'; +import { ExternalApiModule } from '../../integrations/external-api/external-api.module'; + +@Module({ + imports: [ExternalApiModule], + providers: [StatisticsService], + exports: [StatisticsService], +}) +export class StatisticsModule {} diff --git a/packages/apps/human-app/server/src/modules/statistics/statistics.service.ts b/packages/apps/human-app/server/src/modules/statistics/statistics.service.ts new file mode 100644 index 0000000000..0720d9908f --- /dev/null +++ b/packages/apps/human-app/server/src/modules/statistics/statistics.service.ts @@ -0,0 +1,57 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + UserStatisticsCommand, + UserStatisticsResponse, +} from './interfaces/user-statistics.interface'; +import { + OracleStatisticsCommand, + OracleStatisticsResponse, +} from './interfaces/oracle-statistics.interface'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { ExternalApiGateway } from '../../integrations/external-api/external-api.gateway'; + +@Injectable() +export class StatisticsService { + constructor( + @Inject(CACHE_MANAGER) private cacheManager: Cache, + private externalApiGateway: ExternalApiGateway, + private configService: EnvironmentConfigService, + ) {} + async getOracleStats( + command: OracleStatisticsCommand, + ): Promise { + const url = command.oracleUrl; + const cachedStatistics: OracleStatisticsResponse | undefined = + await this.cacheManager.get(url); + if (cachedStatistics) { + return cachedStatistics; + } + const response: OracleStatisticsResponse = + await this.externalApiGateway.fetchOracleStatistics(command); + await this.cacheManager.set( + url, + response, + this.configService.cacheTtlOracleStats, + ); + return response; + } + async getUserStats( + command: UserStatisticsCommand, + ): Promise { + const userCacheKey = command.oracleUrl + command.token; + const cachedStatistics: UserStatisticsResponse | undefined = + await this.cacheManager.get(userCacheKey); + if (cachedStatistics) { + return cachedStatistics; + } + const response = this.externalApiGateway.fetchUserStatistics(command); + await this.cacheManager.set( + userCacheKey, + response, + this.configService.cacheTtlUserStats, + ); + return response; + } +} diff --git a/packages/apps/human-app/server/src/modules/user-operator/operator.module.ts b/packages/apps/human-app/server/src/modules/user-operator/operator.module.ts index ee4cb1a809..4dd6f55a6f 100644 --- a/packages/apps/human-app/server/src/modules/user-operator/operator.module.ts +++ b/packages/apps/human-app/server/src/modules/user-operator/operator.module.ts @@ -1,17 +1,11 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { OperatorService } from './operator.service'; import { ReputationOracleModule } from '../../integrations/reputation-oracle/reputation-oracle.module'; import { OperatorProfile } from './operator.mapper'; -import { TokenMiddleware } from '../../common/interceptors/auth-token.middleware'; -import { OperatorController } from './operator.controller'; +import { Module } from '@nestjs/common'; @Module({ imports: [ReputationOracleModule], providers: [OperatorService, OperatorProfile], exports: [OperatorService], }) -export class OperatorModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(TokenMiddleware).forRoutes(OperatorController); - } -} \ No newline at end of file +export class OperatorModule {} diff --git a/packages/apps/human-app/server/src/modules/user-worker/worker.module.ts b/packages/apps/human-app/server/src/modules/user-worker/worker.module.ts index 04bb1ceef4..8b2c04cbbd 100644 --- a/packages/apps/human-app/server/src/modules/user-worker/worker.module.ts +++ b/packages/apps/human-app/server/src/modules/user-worker/worker.module.ts @@ -1,17 +1,11 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { WorkerService } from './worker.service'; import { ReputationOracleModule } from '../../integrations/reputation-oracle/reputation-oracle.module'; import { WorkerProfile } from './worker.mapper'; -import { TokenMiddleware } from '../../common/interceptors/auth-token.middleware'; -import { WorkerController } from './worker.controller'; +import { Module } from '@nestjs/common'; @Module({ imports: [ReputationOracleModule], providers: [WorkerService, WorkerProfile], exports: [WorkerService], }) -export class WorkerModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(TokenMiddleware).forRoutes(WorkerController); - } -} \ No newline at end of file +export class WorkerModule {}