From 37d318d12732d36a08afd3442bbeaeaccf5ff3a5 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:17:03 +1200 Subject: [PATCH] Fix(datatrakWeb): Limit entity results to 100 --- .../api-client/src/connections/EntityApi.ts | 19 ++++++++++++--- packages/database/src/modelClasses/Entity.js | 23 ++++++++++++------- .../src/routes/EntityDescendantsRoute.ts | 2 ++ .../EntitySelector/EntitySelector.tsx | 1 + .../EntityDescendantRoutes.test.ts | 13 +++++++++++ .../hierarchy/EntityDescendantsRoute.ts | 16 +++++++++---- .../middleware/attachCommonEntityContext.ts | 2 +- .../src/routes/hierarchy/types.ts | 1 + .../server-boilerplate/src/models/Entity.ts | 6 ++++- .../EntityDescendantsRequest.ts | 1 + 10 files changed, 66 insertions(+), 18 deletions(-) diff --git a/packages/api-client/src/connections/EntityApi.ts b/packages/api-client/src/connections/EntityApi.ts index 64f26538b8..7abb9d1a3f 100644 --- a/packages/api-client/src/connections/EntityApi.ts +++ b/packages/api-client/src/connections/EntityApi.ts @@ -160,15 +160,28 @@ export class EntityApi extends BaseApi { field?: string; fields?: string[]; filter?: any; + pageSize?: number; }, includeRootEntity = false, isPublic = false, ) { - return this.connection.get(`hierarchy/${hierarchyName}/${entityCode}/descendants`, { - ...this.stringifyQueryParameters(queryOptions), + const { pageSize, ...otherQueryOptions } = queryOptions || {}; + const params: { + pageSize?: number; + includeRootEntity: string; + isPublic: string; + field?: string; + fields?: string; + filter?: string; + } = { + ...this.stringifyQueryParameters(otherQueryOptions), includeRootEntity: `${includeRootEntity}`, isPublic: `${isPublic}`, - }); + }; + if (pageSize) { + params.pageSize = pageSize; + } + return this.connection.get(`hierarchy/${hierarchyName}/${entityCode}/descendants`, params); } public async getDescendantsOfEntities( diff --git a/packages/database/src/modelClasses/Entity.js b/packages/database/src/modelClasses/Entity.js index 5363ecaf46..747cc7a125 100644 --- a/packages/database/src/modelClasses/Entity.js +++ b/packages/database/src/modelClasses/Entity.js @@ -183,8 +183,8 @@ export class EntityRecord extends DatabaseRecord { return this.model.getAncestorsOfEntities(hierarchyId, [this.id], criteria); } - async getDescendants(hierarchyId, criteria) { - return this.model.getDescendantsOfEntities(hierarchyId, [this.id], criteria); + async getDescendants(hierarchyId, criteria, options) { + return this.model.getDescendantsOfEntities(hierarchyId, [this.id], criteria, options); } async getRelatives(hierarchyId, criteria) { @@ -442,9 +442,10 @@ export class EntityModel extends MaterializedViewLogDatabaseModel { * @param {*} ancestorsOrDescendants * @param {*} entityIds * @param {*} criteria + * @param {*} options * @returns {Promise} */ - async getRelationsOfEntities(ancestorsOrDescendants, entityIds, criteria) { + async getRelationsOfEntities(ancestorsOrDescendants, entityIds, criteria, options) { const cacheKey = this.getCacheKey(this.getRelationsOfEntities.name, arguments); const [joinTablesOn, filterByEntityId] = ancestorsOrDescendants === ENTITY_RELATION_TYPE.ANCESTORS @@ -461,6 +462,7 @@ export class EntityModel extends MaterializedViewLogDatabaseModel { joinWith: RECORDS.ANCESTOR_DESCENDANT_RELATION, joinCondition: ['entity.id', joinTablesOn], sort: ['generational_distance ASC'], + ...options, }, ); const relationData = await Promise.all(relations.map(async r => r.getData())); @@ -477,11 +479,16 @@ export class EntityModel extends MaterializedViewLogDatabaseModel { }); } - async getDescendantsOfEntities(hierarchyId, entityIds, criteria) { - return this.getRelationsOfEntities(ENTITY_RELATION_TYPE.DESCENDANTS, entityIds, { - entity_hierarchy_id: hierarchyId, - ...criteria, - }); + async getDescendantsOfEntities(hierarchyId, entityIds, criteria, options) { + return this.getRelationsOfEntities( + ENTITY_RELATION_TYPE.DESCENDANTS, + entityIds, + { + entity_hierarchy_id: hierarchyId, + ...criteria, + }, + options, + ); } async getRelativesOfEntities(hierarchyId, entityIds, criteria) { diff --git a/packages/datatrak-web-server/src/routes/EntityDescendantsRoute.ts b/packages/datatrak-web-server/src/routes/EntityDescendantsRoute.ts index a2e0fdb17e..d6cec5925c 100644 --- a/packages/datatrak-web-server/src/routes/EntityDescendantsRoute.ts +++ b/packages/datatrak-web-server/src/routes/EntityDescendantsRoute.ts @@ -63,6 +63,7 @@ export class EntityDescendantsRoute extends Route { filter: { countryCode, projectCode, grandparentId, parentId, type, ...restOfFilter }, searchString, fields = DEFAULT_FIELDS, + pageSize, } = query; if (isLoggedIn) { @@ -108,6 +109,7 @@ export class EntityDescendantsRoute extends Route { { fields, filter, + pageSize, }, false, !isLoggedIn, diff --git a/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx b/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx index 4b57afa8be..2584ec2e4e 100644 --- a/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx +++ b/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx @@ -36,6 +36,7 @@ const useSearchResults = (searchValue, filter, projectCode, disableSearch = fals fields: ['id', 'parent_name', 'code', 'name', 'type'], filter, searchString: debouncedSearch, + pageSize: 100, }, !disableSearch, ); diff --git a/packages/entity-server/src/__tests__/__integration__/EntityDescendantRoutes.test.ts b/packages/entity-server/src/__tests__/__integration__/EntityDescendantRoutes.test.ts index 5395480f01..cc02c40049 100644 --- a/packages/entity-server/src/__tests__/__integration__/EntityDescendantRoutes.test.ts +++ b/packages/entity-server/src/__tests__/__integration__/EntityDescendantRoutes.test.ts @@ -162,6 +162,19 @@ describe('descendants', () => { expect(entities).toBeArray(); expect(entities).toIncludeSameMembers(getEntitiesWithFields([], ['code', 'name', 'type'])); }); + + it('can limit by page size', async () => { + const { body: entities } = await app.get('hierarchy/redblue/KANTO/descendants', { + query: { + fields: 'code,name', + filter: 'type==city', + pageSize: 5, + }, + }); + + expect(entities).toBeArray(); + expect(entities.length).toBe(5); + }); }); describe('/hierarchy/:hierarchyName/descendants', () => { diff --git a/packages/entity-server/src/routes/hierarchy/EntityDescendantsRoute.ts b/packages/entity-server/src/routes/hierarchy/EntityDescendantsRoute.ts index c584c9fc80..4de008e623 100644 --- a/packages/entity-server/src/routes/hierarchy/EntityDescendantsRoute.ts +++ b/packages/entity-server/src/routes/hierarchy/EntityDescendantsRoute.ts @@ -17,16 +17,22 @@ export type DescendantsRequest = SingleEntityRequest< SingleEntityRequestParams, EntityResponse[], RequestBody, - EntityRequestQuery & { includeRootEntity?: string } + EntityRequestQuery & { includeRootEntity?: string; pageSize?: number } >; export class EntityDescendantsRoute extends Route { public async buildResponse() { const { hierarchyId, entity, fields, field, filter } = this.req.ctx; - const { includeRootEntity: includeRootEntityString = 'false' } = this.req.query; + const { includeRootEntity: includeRootEntityString = 'false', pageSize } = this.req.query; const includeRootEntity = includeRootEntityString?.toLowerCase() === 'true'; - const descendants = await entity.getDescendants(hierarchyId, { - ...filter, - }); + const descendants = await entity.getDescendants( + hierarchyId, + { + ...filter, + }, + { + limit: pageSize, + }, + ); const responseEntities = includeRootEntity ? [entity].concat(descendants) : descendants; return formatEntitiesForResponse( diff --git a/packages/entity-server/src/routes/hierarchy/middleware/attachCommonEntityContext.ts b/packages/entity-server/src/routes/hierarchy/middleware/attachCommonEntityContext.ts index 3bda95dd9e..02ef046636 100644 --- a/packages/entity-server/src/routes/hierarchy/middleware/attachCommonEntityContext.ts +++ b/packages/entity-server/src/routes/hierarchy/middleware/attachCommonEntityContext.ts @@ -4,8 +4,8 @@ */ import { NextFunction, Request, Response } from 'express'; import { PermissionsError } from '@tupaia/utils'; -import { extractEntityFieldsFromQuery, extractEntityFieldFromQuery } from './fields'; import { CommonContext } from '../types'; +import { extractEntityFieldsFromQuery, extractEntityFieldFromQuery } from './fields'; const throwNoAccessError = (hierarchyName: string) => { throw new PermissionsError(`No access to requested hierarchy: ${hierarchyName}`); diff --git a/packages/entity-server/src/routes/hierarchy/types.ts b/packages/entity-server/src/routes/hierarchy/types.ts index 3e589c4731..c30341f5f7 100644 --- a/packages/entity-server/src/routes/hierarchy/types.ts +++ b/packages/entity-server/src/routes/hierarchy/types.ts @@ -71,6 +71,7 @@ export type CommonContext = { fields: ExtendedEntityFieldName[]; filter: EntityFilter; field?: FlattableEntityFieldName; + pageSize?: string; }; export interface SingleEntityContext extends CommonContext { diff --git a/packages/server-boilerplate/src/models/Entity.ts b/packages/server-boilerplate/src/models/Entity.ts index bf6b1d7103..d5aaa08e84 100644 --- a/packages/server-boilerplate/src/models/Entity.ts +++ b/packages/server-boilerplate/src/models/Entity.ts @@ -17,7 +17,11 @@ export type EntityFilter = DbFilter; export interface EntityRecord extends Entity, BaseEntityRecord { getChildren: (hierarchyId: string, criteria?: EntityFilter) => Promise; getParent: (hierarchyId: string) => Promise; - getDescendants: (hierarchyId: string, criteria?: EntityFilter) => Promise; + getDescendants: ( + hierarchyId: string, + criteria?: EntityFilter, + options?: Record, + ) => Promise; getAncestors: (hierarchyId: string, criteria?: EntityFilter) => Promise; getAncestorOfType: (hierarchyId: string, type: string) => Promise; getRelatives: (hierarchyId: string, criteria?: EntityFilter) => Promise; diff --git a/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts b/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts index 606c163ad3..2ac0f3fae1 100644 --- a/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts +++ b/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts @@ -28,4 +28,5 @@ export type ReqQuery = { type?: string; }; searchString?: string; + pageSize?: number; };