diff --git a/src/endpoints/identities/entities/identity.sort.criteria.ts b/src/endpoints/identities/entities/identity.sort.criteria.ts index 6b5231941..531f1ad3d 100644 --- a/src/endpoints/identities/entities/identity.sort.criteria.ts +++ b/src/endpoints/identities/entities/identity.sort.criteria.ts @@ -1,4 +1,5 @@ export enum IdentitySortCriteria { validators = 'validators', - stake = 'stake' + stake = 'stake', + locked = 'locked' } diff --git a/src/endpoints/identities/identities.controller.ts b/src/endpoints/identities/identities.controller.ts index cc9c7bcc6..9a3c15034 100644 --- a/src/endpoints/identities/identities.controller.ts +++ b/src/endpoints/identities/identities.controller.ts @@ -1,4 +1,4 @@ -import { ParseArrayPipe, ParseEnumPipe } from "@multiversx/sdk-nestjs-common"; +import { ParseArrayPipe, ParseEnumArrayPipe } from "@multiversx/sdk-nestjs-common"; import { Controller, DefaultValuePipe, Get, HttpException, HttpStatus, Param, ParseIntPipe, Query, Res } from "@nestjs/common"; import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; import { Identity } from "./entities/identity"; @@ -18,12 +18,12 @@ export class IdentitiesController { @ApiQuery({ name: 'from', description: 'Number of items to skip for the result set', required: false }) @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) @ApiQuery({ name: 'identities', description: 'Filter by comma-separated list of identities', required: false }) - @ApiQuery({ name: 'sort', description: 'Sort criteria (validators)', required: false, enum: IdentitySortCriteria }) + @ApiQuery({ name: 'sort', description: 'Sort criteria (comma-separated list: validators,stake,locked)', required: false }) async getIdentities( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(10000), ParseIntPipe) size: number, @Query('identities', ParseArrayPipe) identities: string[] = [], - @Query('sort', new ParseEnumPipe(IdentitySortCriteria)) sort?: IdentitySortCriteria, + @Query('sort', new ParseEnumArrayPipe(IdentitySortCriteria)) sort?: IdentitySortCriteria[], ): Promise { return await this.identitiesService.getIdentities(new QueryPagination({ from, size }), identities, sort); } diff --git a/src/endpoints/identities/identities.service.ts b/src/endpoints/identities/identities.service.ts index 74bb032b1..37215be60 100644 --- a/src/endpoints/identities/identities.service.ts +++ b/src/endpoints/identities/identities.service.ts @@ -40,22 +40,47 @@ export class IdentitiesService { return identity ? identity.avatar : undefined; } - async getIdentities(queryPagination: QueryPagination, ids: string[], sort?: IdentitySortCriteria): Promise { + async getIdentities(queryPagination: QueryPagination, ids: string[], sort?: IdentitySortCriteria[]): Promise { const { from, size } = queryPagination; let identities = await this.getAllIdentities(); if (ids.length > 0) { identities = identities.filter(x => x.identity && ids.includes(x.identity)); } - switch (sort) { + if (sort && sort.length > 0) { + identities = identities.sort((a, b) => this.compareWithCriteria(a, b, sort, 0)); + } + + return identities.slice(from, from + size); + } + + private compareWithCriteria(a: Identity, b: Identity, criteria: IdentitySortCriteria[], currentIndex: number): number { + if (currentIndex >= criteria.length) { + return 0; + } + + const currentCriterion = criteria[currentIndex]; + let comparison: number; + + switch (currentCriterion) { case IdentitySortCriteria.validators: - identities = identities.sortedDescending(x => x.validators ?? 0); + comparison = (b.validators ?? 0) - (a.validators ?? 0); break; case IdentitySortCriteria.stake: - identities = identities.sortedDescending(x => Number(x.stake) ?? 0); + comparison = Number(b.stake ?? '0') - Number(a.stake ?? '0'); + break; + case IdentitySortCriteria.locked: + comparison = Number(b.locked ?? '0') - Number(a.locked ?? '0'); + break; + default: + comparison = 0; } - return identities.slice(from, from + size); + if (comparison === 0 && currentIndex < criteria.length - 1) { + return this.compareWithCriteria(a, b, criteria, currentIndex + 1); + } + + return comparison; } async getAllIdentities(): Promise { diff --git a/src/test/unit/controllers/identities.controller.spec.ts b/src/test/unit/controllers/identities.controller.spec.ts index b159977b1..e92305430 100644 --- a/src/test/unit/controllers/identities.controller.spec.ts +++ b/src/test/unit/controllers/identities.controller.spec.ts @@ -5,6 +5,7 @@ import { mockIdentityService } from "./services.mock/identity.services.mock"; import { IdentitiesController } from "src/endpoints/identities/identities.controller"; import { PublicAppModule } from "src/public.app.module"; import { IdentitiesService } from "src/endpoints/identities/identities.service"; +import { IdentitySortCriteria } from "src/endpoints/identities/entities/identity.sort.criteria"; describe('IdentityController', () => { let app: INestApplication; @@ -38,6 +39,54 @@ describe('IdentityController', () => { expect(response.body.length).toBe(5); }); }); + + it('should properly handle single sort criteria', async () => { + const mockIdentitiesList = createMockIdentitiesList(5); + identitiesServiceMocks.getIdentities.mockResolvedValue(mockIdentitiesList); + + await request(app.getHttpServer()) + .get(`${path}?sort=validators`) + .expect(200) + .expect(() => { + expect(identitiesServiceMocks.getIdentities).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Array), + [IdentitySortCriteria.validators] + ); + }); + }); + + it('should properly handle multiple sort criteria', async () => { + const mockIdentitiesList = createMockIdentitiesList(5); + identitiesServiceMocks.getIdentities.mockResolvedValue(mockIdentitiesList); + + await request(app.getHttpServer()) + .get(`${path}?sort=validators,locked`) + .expect(200) + .expect(() => { + expect(identitiesServiceMocks.getIdentities).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Array), + [IdentitySortCriteria.validators, IdentitySortCriteria.locked] + ); + }); + }); + + it('should properly handle the stake sort criterion', async () => { + const mockIdentitiesList = createMockIdentitiesList(5); + identitiesServiceMocks.getIdentities.mockResolvedValue(mockIdentitiesList); + + await request(app.getHttpServer()) + .get(`${path}?sort=stake`) + .expect(200) + .expect(() => { + expect(identitiesServiceMocks.getIdentities).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Array), + [IdentitySortCriteria.stake] + ); + }); + }); }); describe("GET /identities/:identifier", () => {