From 17f8863b3852984caad4f7f21c74b9f6b2b62eca Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 31 Mar 2025 14:17:12 +0300 Subject: [PATCH 1/2] add support for identities secondary sort criteria --- .../entities/identity.sort.criteria.ts | 3 +- .../identities/identities.controller.ts | 6 +-- .../identities/identities.service.ts | 27 +++++++--- .../controllers/identities.controller.spec.ts | 49 +++++++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) 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..07f7c3fd1 100644 --- a/src/endpoints/identities/identities.service.ts +++ b/src/endpoints/identities/identities.service.ts @@ -40,24 +40,37 @@ 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) { - case IdentitySortCriteria.validators: - identities = identities.sortedDescending(x => x.validators ?? 0); - break; - case IdentitySortCriteria.stake: - identities = identities.sortedDescending(x => Number(x.stake) ?? 0); + if (sort && sort.length > 0) { + for (const criterion of sort) { + if (Object.values(IdentitySortCriteria).includes(criterion as IdentitySortCriteria)) { + identities = this.applySorting(identities, criterion as IdentitySortCriteria); + } + } } return identities.slice(from, from + size); } + private applySorting(identities: Identity[], sortCriterion: IdentitySortCriteria): Identity[] { + switch (sortCriterion) { + case IdentitySortCriteria.validators: + return identities.sortedDescending(x => x.validators ?? 0); + case IdentitySortCriteria.stake: + return identities.sortedDescending(x => Number(x.stake) ?? 0); + case IdentitySortCriteria.locked: + return identities.sortedDescending(x => Number(x.locked) ?? 0); + default: + return identities; + } + } + async getAllIdentities(): Promise { return await this.cacheService.getOrSet( CacheInfo.Identities.key, 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", () => { From 977dd0553bc06ed10ea902e337a5f0f4b05727a6 Mon Sep 17 00:00:00 2001 From: Rebegea Dragos-Alexandru <42241923+dragos-rebegea@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:48:16 +0300 Subject: [PATCH 2/2] improve sorting with multiple criterias (#1479) --- .../identities/identities.service.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/endpoints/identities/identities.service.ts b/src/endpoints/identities/identities.service.ts index 07f7c3fd1..37215be60 100644 --- a/src/endpoints/identities/identities.service.ts +++ b/src/endpoints/identities/identities.service.ts @@ -48,27 +48,39 @@ export class IdentitiesService { } if (sort && sort.length > 0) { - for (const criterion of sort) { - if (Object.values(IdentitySortCriteria).includes(criterion as IdentitySortCriteria)) { - identities = this.applySorting(identities, criterion as IdentitySortCriteria); - } - } + identities = identities.sort((a, b) => this.compareWithCriteria(a, b, sort, 0)); } return identities.slice(from, from + size); } - private applySorting(identities: Identity[], sortCriterion: IdentitySortCriteria): Identity[] { - switch (sortCriterion) { + 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: - return identities.sortedDescending(x => x.validators ?? 0); + comparison = (b.validators ?? 0) - (a.validators ?? 0); + break; case IdentitySortCriteria.stake: - return identities.sortedDescending(x => Number(x.stake) ?? 0); + comparison = Number(b.stake ?? '0') - Number(a.stake ?? '0'); + break; case IdentitySortCriteria.locked: - return identities.sortedDescending(x => Number(x.locked) ?? 0); + comparison = Number(b.locked ?? '0') - Number(a.locked ?? '0'); + break; default: - return identities; + comparison = 0; + } + + if (comparison === 0 && currentIndex < criteria.length - 1) { + return this.compareWithCriteria(a, b, criteria, currentIndex + 1); } + + return comparison; } async getAllIdentities(): Promise {