From 68bf3cab219600db01dd13652696b42a8e511007 Mon Sep 17 00:00:00 2001 From: naftis Date: Tue, 4 Feb 2025 11:35:34 +0200 Subject: [PATCH 01/14] fix: slow render of location options --- .../components/form/FormFieldGenerator.tsx | 2 ++ packages/client/src/forms/utils.ts | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/form/FormFieldGenerator.tsx b/packages/client/src/components/form/FormFieldGenerator.tsx index 7cf310cf774..c3dd3849512 100644 --- a/packages/client/src/components/form/FormFieldGenerator.tsx +++ b/packages/client/src/components/form/FormFieldGenerator.tsx @@ -1040,6 +1040,7 @@ class FormSectionComponent extends React.Component { ...field, type: SELECT_WITH_OPTIONS, options: getFieldOptions( + sectionName, field, values, offlineCountryConfig, @@ -1050,6 +1051,7 @@ class FormSectionComponent extends React.Component { ? ({ ...field, options: getFieldOptions( + sectionName, field, values, offlineCountryConfig, diff --git a/packages/client/src/forms/utils.ts b/packages/client/src/forms/utils.ts index c732a35549c..639fc89089d 100644 --- a/packages/client/src/forms/utils.ts +++ b/packages/client/src/forms/utils.ts @@ -83,6 +83,7 @@ import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' import { Conditional } from './conditionals' import { UserDetails } from '@client/utils/userUtils' import * as SupportedIcons from '@opencrvs/components/lib/Icon/all-icons' +import { memoize } from 'lodash' export const VIEW_TYPE = { FORM: 'form', @@ -365,7 +366,8 @@ export function getNextSectionIds( export const getVisibleGroupFields = (group: IFormSectionGroup) => { return group.fields.filter((field) => !field.hidden) } -export const getFieldOptions = ( +export const getFieldOptionsSlow = ( + _sectionName: string, field: | ISelectFormFieldWithOptions | ISelectFormFieldWithDynamicOptions @@ -443,6 +445,22 @@ export const getFieldOptions = ( } } +/** Due to the large location trees with dependencies, generating options for them can be slow. We fix this by memoizing the options */ +export const getFieldOptions = memoize( + getFieldOptionsSlow, + (sectionName, field, values) => { + if ( + field.type === SELECT_WITH_OPTIONS || + field.type === DOCUMENT_UPLOADER_WITH_OPTION + ) { + return `field:${sectionName}.${field.name}` + } + + const dependencyVal = values[field.dynamicOptions.dependency!] as string + return `field:${sectionName}.${field.name},dependency:${field.dynamicOptions.dependency},dependencyValue:${dependencyVal}` + } +) + interface INested { [key: string]: any } From c4e265acc3e0fbee7eada60ab7ebbfca2582cc20 Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 5 Feb 2025 09:12:29 +0200 Subject: [PATCH 02/14] Start release v1.6.3 --- CHANGELOG.md | 2 ++ package.json | 2 +- packages/auth/package.json | 2 +- packages/client/package.json | 2 +- packages/commons/package.json | 2 +- packages/components/package.json | 2 +- packages/config/package.json | 2 +- packages/dashboards/package.json | 2 +- packages/data-seeder/package.json | 2 +- packages/documents/package.json | 2 +- packages/gateway/package.json | 2 +- packages/login/package.json | 2 +- packages/metrics/package.json | 2 +- packages/migration/package.json | 2 +- packages/notification/package.json | 2 +- packages/search/package.json | 2 +- packages/user-mgnt/package.json | 2 +- packages/webhooks/package.json | 2 +- packages/workflow/package.json | 2 +- 19 files changed, 20 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7426340f6d1..5b3aa85d745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.6.3](https://github.com/opencrvs/opencrvs-core/compare/v1.6.1...v1.6.3) + ## [1.6.2](https://github.com/opencrvs/opencrvs-core/compare/v1.6.1...v1.6.2) ### Bug fixes diff --git a/package.json b/package.json index a5a56a94ddb..e33d36f86e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "description": "OpenCRVS core workspace", "license": "MPL-2.0", - "version": "1.6.2", + "version": "1.6.3", "private": true, "os": [ "darwin", diff --git a/packages/auth/package.json b/packages/auth/package.json index 9735102ccbf..60cfd4adc24 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/auth", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS authentication service", "license": "MPL-2.0", "private": true, diff --git a/packages/client/package.json b/packages/client/package.json index f9a2ea75cfa..6220ae171d7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/client", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS client application", "license": "MPL-2.0", "private": true, diff --git a/packages/commons/package.json b/packages/commons/package.json index f17eea3f9c2..d8623d48b6e 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/commons", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS common modules and utils", "license": "MPL-2.0", "main": "./build/dist/index.js", diff --git a/packages/components/package.json b/packages/components/package.json index 234a0c9a15d..70b09b8a803 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@opencrvs/components", "main": "lib/index", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS UI Component library", "license": "MPL-2.0", "private": true, diff --git a/packages/config/package.json b/packages/config/package.json index 8c47e1fe10e..fde14976013 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/config", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS public configuration microservice", "license": "MPL-2.0", "scripts": { diff --git a/packages/dashboards/package.json b/packages/dashboards/package.json index a88774c8395..2877a9a84fe 100644 --- a/packages/dashboards/package.json +++ b/packages/dashboards/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/dashboards", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS performance dashboards", "type": "module", "main": "index.js", diff --git a/packages/data-seeder/package.json b/packages/data-seeder/package.json index d1dab40f45a..a0261e47be3 100644 --- a/packages/data-seeder/package.json +++ b/packages/data-seeder/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/data-seeder", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS data-seeder microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "license": "MPL-2.0", diff --git a/packages/documents/package.json b/packages/documents/package.json index 4a21b04cdd8..f292209ef47 100644 --- a/packages/documents/package.json +++ b/packages/documents/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/documents", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS Documents service", "license": "MPL-2.0", "private": true, diff --git a/packages/gateway/package.json b/packages/gateway/package.json index dbb310fe36a..8a26a73d0bb 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/gateway", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS API Gateway with GraphQL", "license": "MPL-2.0", "scripts": { diff --git a/packages/login/package.json b/packages/login/package.json index 6a8691c4a54..26e82375268 100644 --- a/packages/login/package.json +++ b/packages/login/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/login", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS login client application", "license": "MPL-2.0", "private": true, diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 365ccc4df70..660974aa926 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/metrics", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS metrics service", "license": "MPL-2.0", "private": true, diff --git a/packages/migration/package.json b/packages/migration/package.json index 0da12bae10c..41671ef55e7 100644 --- a/packages/migration/package.json +++ b/packages/migration/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/migration", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS migration microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "type": "module", diff --git a/packages/notification/package.json b/packages/notification/package.json index b9342625fa5..0cc4ef82fac 100644 --- a/packages/notification/package.json +++ b/packages/notification/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/notification", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS notification service", "license": "MPL-2.0", "private": true, diff --git a/packages/search/package.json b/packages/search/package.json index bf391d43abe..a28aca4f5d1 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/search", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS search service", "license": "MPL-2.0", "private": true, diff --git a/packages/user-mgnt/package.json b/packages/user-mgnt/package.json index 70dd9471027..6f68762af36 100644 --- a/packages/user-mgnt/package.json +++ b/packages/user-mgnt/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/user-mgnt", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS user management service", "license": "MPL-2.0", "private": true, diff --git a/packages/webhooks/package.json b/packages/webhooks/package.json index 4a390ce109c..41ef69da200 100644 --- a/packages/webhooks/package.json +++ b/packages/webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/webhooks", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS webhooks service", "license": "MPL-2.0", "private": true, diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 0872f6a131a..1bb8c9d9003 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/workflow", - "version": "1.6.2", + "version": "1.6.3", "description": "OpenCRVS workflow service", "license": "MPL-2.0", "private": true, From c0b2de827d03024d12d5552ada17437ba9c26bda Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Fri, 7 Feb 2025 09:36:06 +0200 Subject: [PATCH 03/14] chore: Refactor to only memoize for dynamic selects Dynamic selects are the only ones causing performance issues so only memoizing them reduces the regression risk --- .../register/mappings/query/field-mappings.ts | 7 ++- packages/client/src/forms/utils.ts | 47 ++++++++++++++----- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/client/src/forms/register/mappings/query/field-mappings.ts b/packages/client/src/forms/register/mappings/query/field-mappings.ts index 1201d601191..02ceaa99da9 100644 --- a/packages/client/src/forms/register/mappings/query/field-mappings.ts +++ b/packages/client/src/forms/register/mappings/query/field-mappings.ts @@ -1197,7 +1197,12 @@ export function questionnaireToTemplateFieldTransformer( if (!offlineCountryConfig) { return } - const options = getFieldOptions(field, queryData, offlineCountryConfig) + const options = getFieldOptions( + sectionId, + field, + queryData, + offlineCountryConfig + ) transformedData[sectionId][field.name] = options .find((option) => option.value === selectedQuestion.value) diff --git a/packages/client/src/forms/utils.ts b/packages/client/src/forms/utils.ts index 639fc89089d..a68bcda383c 100644 --- a/packages/client/src/forms/utils.ts +++ b/packages/client/src/forms/utils.ts @@ -55,7 +55,8 @@ import { BUTTON, Ii18nButtonFormField, IRedirectFormField, - REDIRECT + REDIRECT, + SELECT_WITH_DYNAMIC_OPTIONS } from '@client/forms' import { IntlShape, MessageDescriptor } from 'react-intl' import { @@ -445,19 +446,43 @@ export const getFieldOptionsSlow = ( } } +export const getFieldOptions = ( + _sectionName: string, + field: + | ISelectFormFieldWithOptions + | ISelectFormFieldWithDynamicOptions + | IDocumentUploaderWithOptionsFormField, + values: IFormSectionData, + offlineCountryConfig: IOfflineData, + declaration?: IFormData +) => { + if (field.type === SELECT_WITH_DYNAMIC_OPTIONS) { + return getMemoisedFieldOptions( + _sectionName, + field, + values, + offlineCountryConfig + ) + } + + return getFieldOptionsSlow( + _sectionName, + field, + values, + offlineCountryConfig, + declaration + ) +} + /** Due to the large location trees with dependencies, generating options for them can be slow. We fix this by memoizing the options */ -export const getFieldOptions = memoize( +const getMemoisedFieldOptions = memoize( getFieldOptionsSlow, (sectionName, field, values) => { - if ( - field.type === SELECT_WITH_OPTIONS || - field.type === DOCUMENT_UPLOADER_WITH_OPTION - ) { - return `field:${sectionName}.${field.name}` - } - - const dependencyVal = values[field.dynamicOptions.dependency!] as string - return `field:${sectionName}.${field.name},dependency:${field.dynamicOptions.dependency},dependencyValue:${dependencyVal}` + const dynamicField = field as ISelectFormFieldWithDynamicOptions + const dependencyVal = values[ + dynamicField.dynamicOptions.dependency! + ] as string + return `field:${sectionName}.${field.name},dependency:${dynamicField.dynamicOptions.dependency},dependencyValue:${dependencyVal}` } ) From cf61f00b5a16e4db7c8f16b5024c12d19adf8346 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Fri, 14 Feb 2025 14:23:47 +0200 Subject: [PATCH 04/14] Change loading entire location structure to only loading parents Fixes massive performance hit when loading 100k+ locations unnecessarily --- .../src/handlers/locations/hierarchy.test.ts | 63 +++++++++++++++---- .../src/handlers/locations/hierarchy.ts | 11 +--- .../handlers/locations/locationTreeSolver.ts | 31 +++++---- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/packages/config/src/handlers/locations/hierarchy.test.ts b/packages/config/src/handlers/locations/hierarchy.test.ts index 6969bbf9a55..081cf501bcf 100644 --- a/packages/config/src/handlers/locations/hierarchy.test.ts +++ b/packages/config/src/handlers/locations/hierarchy.test.ts @@ -11,48 +11,85 @@ import { resolveLocationParents } from './locationTreeSolver' import * as fixtures from '@opencrvs/commons/fixtures' import { UUID } from '@opencrvs/commons' +import { fetchFromHearth } from '@config/services/hearth' +import { SavedLocation } from '@opencrvs/commons/types' + +jest.mock('@config/services/hearth', () => ({ + fetchFromHearth: jest.fn() +})) + +const fetchFromHearthMock = fetchFromHearth as jest.Mock + +const mockHearthLocations = (locations: SavedLocation[]) => { + fetchFromHearthMock.mockImplementation((id: string) => { + const location = locations.find((l) => `Location/${l.id}` === id) + return Promise.resolve(location) + }) +} describe('resolveLocationParents', () => { - it('should return an empty array if the location has no parent', () => { + it('should return child location only if the location has no parent', async () => { const location = fixtures.savedLocation({ id: 'uuid1' as UUID, partOf: undefined }) - const result = resolveLocationParents(location, [location]) - expect(result).toEqual([]) + mockHearthLocations([location]) + + const result = await resolveLocationParents(location.id) + expect(result).toEqual([location]) }) - it('should resolve a single level parent correctly', () => { + it('should return child location only if the location parent is Location/0', async () => { + const topLevel = '0' as UUID + const location = fixtures.savedLocation({ + id: 'uuid1' as UUID, + partOf: { + reference: `Location/${topLevel}` + } + }) + + mockHearthLocations([location]) + + const result = await resolveLocationParents(location.id) + expect(result).toEqual([location]) + }) + + it('should resolve a single level parent correctly', async () => { const parent = fixtures.savedLocation({ id: 'uuid1' as UUID, partOf: undefined }) + const child = fixtures.savedLocation({ id: 'uuid2' as UUID, - partOf: { reference: 'Location/uuid1' } + partOf: { reference: `Location/${parent.id}` } }) - const result = resolveLocationParents(child, [child, parent]) - expect(result).toEqual([parent]) + mockHearthLocations([child, parent]) + + const result = await resolveLocationParents(child.id) + expect(result).toEqual([parent, child]) }) - it('should resolve multiple levels of parents correctly', () => { + it('should resolve multiple levels of parents correctly', async () => { const grandparent = fixtures.savedLocation({ id: 'uuid1' as UUID, partOf: undefined }) const parent = fixtures.savedLocation({ id: 'uuid2' as UUID, - partOf: { reference: 'Location/uuid1' } + partOf: { reference: `Location/${grandparent.id}` } }) const child = fixtures.savedLocation({ id: 'uuid3' as UUID, - partOf: { reference: 'Location/uuid2' } + partOf: { reference: `Location/${parent.id}` } }) - const locations = [child, parent, grandparent] + const locations = [grandparent, parent, child] + + mockHearthLocations(locations) - const result = resolveLocationParents(child, locations) - expect(result).toEqual([grandparent, parent]) + const result = await resolveLocationParents(child.id) + expect(result).toEqual(locations) }) }) diff --git a/packages/config/src/handlers/locations/hierarchy.ts b/packages/config/src/handlers/locations/hierarchy.ts index 4127eec8f27..03c52150409 100644 --- a/packages/config/src/handlers/locations/hierarchy.ts +++ b/packages/config/src/handlers/locations/hierarchy.ts @@ -9,20 +9,11 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { ServerRoute } from '@hapi/hapi' -import { fetchLocations } from '@config/services/hearth' import { resolveLocationParents } from './locationTreeSolver' import { UUID } from '@opencrvs/commons' -import { find } from 'lodash' -import { notFound } from '@hapi/boom' export const locationHierarchyHandler: ServerRoute['handler'] = async (req) => { const { locationId } = req.params as { locationId: UUID } - const locations = await fetchLocations() - const location = find(locations, { id: locationId }) - if (!location) { - return notFound() - } - - return [...resolveLocationParents(location, locations), location] + return await resolveLocationParents(locationId) } diff --git a/packages/config/src/handlers/locations/locationTreeSolver.ts b/packages/config/src/handlers/locations/locationTreeSolver.ts index dc9cb38ede6..91536cf4ba2 100644 --- a/packages/config/src/handlers/locations/locationTreeSolver.ts +++ b/packages/config/src/handlers/locations/locationTreeSolver.ts @@ -14,7 +14,7 @@ import { SavedLocation } from '@opencrvs/commons/types' import { UUID } from '@opencrvs/commons' -import { find } from 'lodash' +import { fetchFromHearth } from '@config/services/hearth' /** * Creates a new Map @@ -59,18 +59,25 @@ export const resolveLocationChildren = ( } /** Resolves any given location's parents multi-level up to the root node */ -export const resolveLocationParents = ( - location: SavedLocation, - locations: SavedLocation[] -): SavedLocation[] => { - const parent = - location.partOf && - find(locations, { - id: resourceIdentifierToUUID(location.partOf.reference) - }) +export const resolveLocationParents = async ( + locationId: UUID +): Promise => { + const current = await fetchFromHearth(`Location/${locationId}`) - if (!parent) { + if (!current) { return [] } - return [...resolveLocationParents(parent, locations), parent] + + const id = current.partOf?.reference + ? resourceIdentifierToUUID(current.partOf.reference) + : null + + // Handle case where top level location is Location/0 + if (!id || id === '0') { + return [current] + } + + const parents = await resolveLocationParents(id) + + return [...parents, current] } From 22c56c596d48952847665485c152b709156d3567 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Fri, 14 Feb 2025 14:35:12 +0200 Subject: [PATCH 05/14] Don't load entire location hierarchy if type is an office A performance optimisation because offices will never have children --- .../src/handlers/locations/children.test.ts | 40 +++++++++++++++++-- .../config/src/handlers/locations/children.ts | 20 ++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/config/src/handlers/locations/children.test.ts b/packages/config/src/handlers/locations/children.test.ts index 80beae4574d..b2d1a8c0ec2 100644 --- a/packages/config/src/handlers/locations/children.test.ts +++ b/packages/config/src/handlers/locations/children.test.ts @@ -11,6 +11,40 @@ import { UUID } from '@opencrvs/commons' import { resolveLocationChildren } from './locationTreeSolver' import * as fixtures from '@opencrvs/commons/fixtures' +import { resolveChildren } from './children' +import { fetchFromHearth, fetchLocations } from '@config/services/hearth' + +jest.mock('@config/services/hearth', () => ({ + fetchFromHearth: jest.fn(), + fetchLocations: jest.fn() +})) + +const fetchFromHearthMock = fetchFromHearth as jest.Mock + +describe('resolveChildren', () => { + describe('given a location of type office', () => { + test('does not fetch location hierarchy', async () => { + const office = fixtures.savedLocation({ + id: 'uuid1' as UUID, + type: { coding: [{ code: 'CRVS_OFFICE' }] } + }) + + fetchFromHearthMock.mockResolvedValue(office) + + const req = { params: { locationId: office.id } } as any + + // Cast handler to make it callable + const handler = resolveChildren as unknown as ( + req: Request + ) => Promise + + const res = await handler(req) + + expect(fetchLocations).not.toHaveBeenCalled() + expect(res).toEqual([office]) + }) + }) +}) describe('resolveLocationChildren', () => { test('empty locations', () => { @@ -38,7 +72,7 @@ describe('resolveLocationChildren', () => { }) const uuid2 = fixtures.savedLocation({ id: 'uuid2' as UUID, - partOf: { reference: 'Location/uuid1' } + partOf: { reference: `Location/${uuid1.id}` } }) const result = resolveLocationChildren(uuid1, [uuid1, uuid2]) @@ -53,11 +87,11 @@ describe('resolveLocationChildren', () => { }) const uuid2 = fixtures.savedLocation({ id: 'uuid2' as UUID, - partOf: { reference: 'Location/uuid1' } + partOf: { reference: `Location/${uuid1.id}` } }) const uuid3 = fixtures.savedLocation({ id: 'uuid3' as UUID, - partOf: { reference: 'Location/uuid2' } + partOf: { reference: `Location/${uuid2.id}` } }) const result = resolveLocationChildren(uuid1, [uuid1, uuid2, uuid3]) diff --git a/packages/config/src/handlers/locations/children.ts b/packages/config/src/handlers/locations/children.ts index af8f6f93282..9fdd83ccc5a 100644 --- a/packages/config/src/handlers/locations/children.ts +++ b/packages/config/src/handlers/locations/children.ts @@ -11,18 +11,24 @@ import { UUID } from '@opencrvs/commons' import { ServerRoute } from '@hapi/hapi' import { resolveLocationChildren } from './locationTreeSolver' -import { fetchLocations } from '@config/services/hearth' -import { find } from 'lodash' -import { notFound } from '@hapi/boom' +import { fetchFromHearth, fetchLocations } from '@config/services/hearth' +import { SavedLocation } from '@opencrvs/commons/types' export const resolveChildren: ServerRoute['handler'] = async (req) => { const { locationId } = req.params as { locationId: UUID } - const locations = await fetchLocations() - const location = find(locations, { id: locationId }) - if (!location) { - return notFound() + const location = await fetchFromHearth( + `Location/${locationId}` + ) + if (isTypeOf(location, 'CRVS_OFFICE')) { + return [location] } + const locations = await fetchLocations() + return [location, ...resolveLocationChildren(location, locations)] } + +function isTypeOf(location: SavedLocation, type: string) { + return location.type?.coding?.some((x) => x.code === type) +} From 78ac047f68cade87b4fcfc75d621b9e703007664 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 18 Feb 2025 16:38:58 +0200 Subject: [PATCH 06/14] fix: Replace hearth children query with direct mongo query Performance improvement --- packages/config/package.json | 2 + .../src/handlers/locations/children.test.ts | 54 --------- .../config/src/handlers/locations/children.ts | 10 +- .../locations/locationTreeSolver.test.ts | 100 ++++++++++++++++ .../handlers/locations/locationTreeSolver.ts | 64 +++++----- yarn.lock | 111 ++++++++++++++++++ 6 files changed, 247 insertions(+), 94 deletions(-) create mode 100644 packages/config/src/handlers/locations/locationTreeSolver.test.ts diff --git a/packages/config/package.json b/packages/config/package.json index fde14976013..7d8098cc513 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -30,6 +30,8 @@ "jsonwebtoken": "^9.0.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.21", + "mongodb": "^6.13.0", + "mongodb-memory-server": "^10.1.3", "mongoose": "^6.11.3", "pino": "^7.0.0", "tsconfig-paths": "^3.13.0", diff --git a/packages/config/src/handlers/locations/children.test.ts b/packages/config/src/handlers/locations/children.test.ts index b2d1a8c0ec2..d139344743a 100644 --- a/packages/config/src/handlers/locations/children.test.ts +++ b/packages/config/src/handlers/locations/children.test.ts @@ -9,7 +9,6 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { UUID } from '@opencrvs/commons' -import { resolveLocationChildren } from './locationTreeSolver' import * as fixtures from '@opencrvs/commons/fixtures' import { resolveChildren } from './children' import { fetchFromHearth, fetchLocations } from '@config/services/hearth' @@ -45,56 +44,3 @@ describe('resolveChildren', () => { }) }) }) - -describe('resolveLocationChildren', () => { - test('empty locations', () => { - const uuid1 = fixtures.savedLocation({ - id: 'uuid1' as UUID, - partOf: undefined - }) - const result = resolveLocationChildren(uuid1, []) - expect(result).toEqual([]) - }) - - test('parent with no children', () => { - const uuid1 = fixtures.savedLocation({ - id: 'uuid1' as UUID, - partOf: undefined - }) - const result = resolveLocationChildren(uuid1, [uuid1]) - expect(result).toEqual([]) - }) - - test('single level hierarchy', () => { - const uuid1 = fixtures.savedLocation({ - id: 'uuid1' as UUID, - partOf: undefined - }) - const uuid2 = fixtures.savedLocation({ - id: 'uuid2' as UUID, - partOf: { reference: `Location/${uuid1.id}` } - }) - const result = resolveLocationChildren(uuid1, [uuid1, uuid2]) - - expect(result).toContainEqual(uuid2) - expect(result).toHaveLength(1) - }) - - test('multi-level hierarchy', () => { - const uuid1 = fixtures.savedLocation({ - id: 'uuid1' as UUID, - partOf: undefined - }) - const uuid2 = fixtures.savedLocation({ - id: 'uuid2' as UUID, - partOf: { reference: `Location/${uuid1.id}` } - }) - const uuid3 = fixtures.savedLocation({ - id: 'uuid3' as UUID, - partOf: { reference: `Location/${uuid2.id}` } - }) - - const result = resolveLocationChildren(uuid1, [uuid1, uuid2, uuid3]) - expect(result).toEqual([uuid2, uuid3]) - }) -}) diff --git a/packages/config/src/handlers/locations/children.ts b/packages/config/src/handlers/locations/children.ts index 9fdd83ccc5a..e12e967ce33 100644 --- a/packages/config/src/handlers/locations/children.ts +++ b/packages/config/src/handlers/locations/children.ts @@ -10,9 +10,9 @@ */ import { UUID } from '@opencrvs/commons' import { ServerRoute } from '@hapi/hapi' -import { resolveLocationChildren } from './locationTreeSolver' -import { fetchFromHearth, fetchLocations } from '@config/services/hearth' +import { fetchFromHearth } from '@config/services/hearth' import { SavedLocation } from '@opencrvs/commons/types' +import { resolveLocationChildren } from './locationTreeSolver' export const resolveChildren: ServerRoute['handler'] = async (req) => { const { locationId } = req.params as { locationId: UUID } @@ -24,11 +24,11 @@ export const resolveChildren: ServerRoute['handler'] = async (req) => { return [location] } - const locations = await fetchLocations() + const children = await resolveLocationChildren(locationId) - return [location, ...resolveLocationChildren(location, locations)] + return [location, ...children] } function isTypeOf(location: SavedLocation, type: string) { - return location.type?.coding?.some((x) => x.code === type) + return location?.type?.coding?.some((x) => x.code === type) } diff --git a/packages/config/src/handlers/locations/locationTreeSolver.test.ts b/packages/config/src/handlers/locations/locationTreeSolver.test.ts new file mode 100644 index 00000000000..4628754013a --- /dev/null +++ b/packages/config/src/handlers/locations/locationTreeSolver.test.ts @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { MongoMemoryServer } from 'mongodb-memory-server' + +import { UUID } from '@opencrvs/commons' +import * as fixtures from '@opencrvs/commons/fixtures' +import { Location, SavedLocation } from '@opencrvs/commons/types' +import { Collection, MongoClient } from 'mongodb' + +let client: MongoClient +let mongoServer: MongoMemoryServer +let collection: Collection +let OLD_ENV: NodeJS.ProcessEnv + +const parent = fixtures.savedLocation({ + id: 'uuid1' as UUID, + partOf: undefined +}) +const child = fixtures.savedLocation({ + id: 'uuid2' as UUID, + partOf: { reference: parent.id as `Location/${UUID}` } +}) +const grandchild = fixtures.savedLocation({ + id: 'uuid3' as UUID, + partOf: { reference: child.id as `Location/${UUID}` } +}) + +const lateLoadModule = async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { resolveLocationChildren } = require('./locationTreeSolver') + return resolveLocationChildren +} + +describe('resolveChildren', () => { + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create() + const uri = mongoServer.getUri() + OLD_ENV = process.env + process.env.HEARTH_MONGO_URL = mongoServer.getUri() + + client = new MongoClient(uri) + const connectedClient = await client.connect() + + const db = connectedClient.db() + collection = db.collection('Location_view_with_plain_ids') + }) + + afterAll(async () => { + process.env = OLD_ENV + if (client) { + await client.close() + } + if (mongoServer) { + await mongoServer.stop() + } + }) + + beforeEach(async () => { + collection.deleteMany({}) + }) + + describe('given a location with no children', () => { + test.only('should return empty array', async () => { + // late import to allow env vars to be set before the module is loaded + const resolveLocationChildren = await lateLoadModule() + + await collection.insertMany([grandchild]) + + const children = (await resolveLocationChildren( + 'uuid1' as UUID + )) as SavedLocation[] + + expect(children).toHaveLength(0) + }) + }) + + describe('given a location with children', () => { + test.only('should return all descendants', async () => { + // late import to allow env vars to be set before the module is loaded + const resolveLocationChildren = await lateLoadModule() + + await collection.insertMany([parent, child, grandchild]) + + const children = (await resolveLocationChildren( + 'uuid1' as UUID + )) as SavedLocation[] + + expect(children).toHaveLength(2) + expect(children).toEqual(expect.arrayContaining([child, grandchild])) + }) + }) +}) diff --git a/packages/config/src/handlers/locations/locationTreeSolver.ts b/packages/config/src/handlers/locations/locationTreeSolver.ts index 91536cf4ba2..2f95268ad99 100644 --- a/packages/config/src/handlers/locations/locationTreeSolver.ts +++ b/packages/config/src/handlers/locations/locationTreeSolver.ts @@ -10,52 +10,46 @@ */ import { + Location, resourceIdentifierToUUID, SavedLocation } from '@opencrvs/commons/types' import { UUID } from '@opencrvs/commons' import { fetchFromHearth } from '@config/services/hearth' +import { MongoClient } from 'mongodb' +import { env } from '@config/environment' -/** - * Creates a new Map - * It sets the first-level children under their parents - */ -const resolveParentChildrenMap = (locations: SavedLocation[]) => { - const parentChildrenMap = new Map() +const client = new MongoClient(env.HEARTH_MONGO_URL) - for (const child of locations) { - if (!child.partOf) continue +export const resolveLocationChildren = async (id: UUID) => { + try { + const connectedClient = await client.connect() + const db = connectedClient.db() - const parentId = resourceIdentifierToUUID(child.partOf.reference) - const parentChildrenRelationship = parentChildrenMap.get(parentId) + const childQuery = [ + { + $match: { id: id } + }, + { + $graphLookup: { + from: 'Location_view_with_plain_ids', + startWith: '$id', + connectFromField: 'id', + connectToField: 'partOf.reference', + as: 'children' + } + } + ] - if (!parentChildrenRelationship) { - parentChildrenMap.set(parentId, [child]) - } else { - parentChildrenRelationship.push(child) - } - } + const result = await db + .collection('Location_view_with_plain_ids') + .aggregate(childQuery) + .toArray() - return parentChildrenMap -} -/** Resolves any given location's children multi-level down to the leaf node */ -export const resolveLocationChildren = ( - location: SavedLocation, - locations: SavedLocation[] -) => { - const parentChildrenMap = resolveParentChildrenMap(locations) - const children: SavedLocation[] = [] - const stack = parentChildrenMap.get(location.id) ?? [] - - while (stack.length) { - const child = stack.pop()! - children.push(child) - if (parentChildrenMap.get(child.id)) { - stack.push(...(parentChildrenMap.get(child.id) ?? [])) - } + return result.length ? result[0].children : [] + } finally { + await client.close() } - - return children } /** Resolves any given location's parents multi-level up to the root node */ diff --git a/yarn.lock b/yarn.lock index 8b1aea1ca9e..d722846b091 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5372,6 +5372,20 @@ dependencies: sparse-bitfield "^3.0.3" +"@mongodb-js/saslprep@^1.1.5": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz#e974bab8eca9faa88677d4ea4da8d09a52069004" + integrity sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw== + dependencies: + sparse-bitfield "^3.0.3" + +"@mongodb-js/saslprep@^1.1.9": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz#4373d7a87660ea44a0a7a461ff6d8bc832733a4b" + integrity sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg== + dependencies: + sparse-bitfield "^3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" @@ -10677,6 +10691,16 @@ bson@^4.7.2: dependencies: buffer "^5.6.0" +bson@^6.10.1: + version "6.10.2" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.10.2.tgz#092ba1d2abee7e126320003f88d6883c3b1897df" + integrity sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg== + +bson@^6.7.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.9.0.tgz#2be50049430dceaa9300402520fe03e4ed5fdfd6" + integrity sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig== + buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" @@ -18608,6 +18632,66 @@ mongodb-connection-string-url@^2.6.0: "@types/whatwg-url" "^8.2.1" whatwg-url "^11.0.0" +mongodb-connection-string-url@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz#c13e6ac284ae401752ebafdb8cd7f16c6723b141" + integrity sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg== + dependencies: + "@types/whatwg-url" "^11.0.2" + whatwg-url "^13.0.0" + +mongodb-memory-server-core@10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.2.tgz#4960f59e5c56a30dbde366ff69497b6ce5b2c4dc" + integrity sha512-5Wpz712CuDCKTn/40UZ+kMZlav4Y2imbpWuJU5wjuZk6s3+Jg8akTIBW9jQiFS8wgymu6iTg99Iw0XcypsLyQA== + dependencies: + async-mutex "^0.5.0" + camelcase "^6.3.0" + debug "^4.3.7" + find-cache-dir "^3.3.2" + follow-redirects "^1.15.9" + https-proxy-agent "^7.0.5" + mongodb "^6.9.0" + new-find-package-json "^2.0.0" + semver "^7.6.3" + tar-stream "^3.1.7" + tslib "^2.7.0" + yauzl "^3.1.3" + +mongodb-memory-server-core@10.1.3: + version "10.1.3" + resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.3.tgz#63ea51a54573e446567c0b3021fc727ba69833ba" + integrity sha512-ayBQHeV74wRHhgcAKpxHYI4th9Ufidy/m3XhJnLFRufKsOyDsyHYU3Zxv5Fm4hxsWE6wVd0GAVcQ7t7XNkivOg== + dependencies: + async-mutex "^0.5.0" + camelcase "^6.3.0" + debug "^4.3.7" + find-cache-dir "^3.3.2" + follow-redirects "^1.15.9" + https-proxy-agent "^7.0.5" + mongodb "^6.9.0" + new-find-package-json "^2.0.0" + semver "^7.6.3" + tar-stream "^3.1.7" + tslib "^2.7.0" + yauzl "^3.1.3" + +mongodb-memory-server@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-10.1.2.tgz#19412977dfba00af54c199bf75e34cb2a4aa9925" + integrity sha512-aDGEWuUVHTiBvaaq03LbpvvSk8IVtepbvp314p1cq7f2xdSpl7igMnYpPfYY5nkks1I5I6OL2ypHjaJj4kBp+g== + dependencies: + mongodb-memory-server-core "10.1.2" + tslib "^2.7.0" + +mongodb-memory-server@^10.1.3: + version "10.1.3" + resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-10.1.3.tgz#56cdeb4929eff96ccf1797922a1b39cd503a11ad" + integrity sha512-QCUjsIIXSYv/EgkpDAjfhlqRKo6N+qR6DD43q4lyrCVn24xQmvlArdWHW/Um5RS4LkC9YWC3XveSncJqht2Hbg== + dependencies: + mongodb-memory-server-core "10.1.3" + tslib "^2.7.0" + mongodb@4.17.2, mongodb@^4.17.1: version "4.17.2" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.17.2.tgz#237c0534e36a3449bd74c6bf6d32f87a1ca7200c" @@ -18620,6 +18704,33 @@ mongodb@4.17.2, mongodb@^4.17.1: "@aws-sdk/credential-providers" "^3.186.0" "@mongodb-js/saslprep" "^1.1.0" +mongodb@6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.9.0.tgz#743ebfff6b3c14b04ac6e00a55e30d4127d3016d" + integrity sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA== + dependencies: + "@mongodb-js/saslprep" "^1.1.5" + bson "^6.7.0" + mongodb-connection-string-url "^3.0.0" + +mongodb@^6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.13.0.tgz#366dc4987d4aeb4b6ef7ba18cd815ab2950fd045" + integrity sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q== + dependencies: + "@mongodb-js/saslprep" "^1.1.9" + bson "^6.10.1" + mongodb-connection-string-url "^3.0.0" + +mongodb@^6.9.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.10.0.tgz#20a9f1cf3c6829e75fc39e6d8c1c19f164209c2e" + integrity sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg== + dependencies: + "@mongodb-js/saslprep" "^1.1.5" + bson "^6.7.0" + mongodb-connection-string-url "^3.0.0" + mongoose@^6.11.3: version "6.13.0" resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-6.13.0.tgz#465522876237e4112a895fbd1cb4a87c580bc613" From 29126ec59ab1809209bc46f600576b361c962fe2 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 18 Feb 2025 17:30:37 +0200 Subject: [PATCH 07/14] fix: await collection delete and remove .only --- .../handlers/locations/locationTreeSolver.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/config/src/handlers/locations/locationTreeSolver.test.ts b/packages/config/src/handlers/locations/locationTreeSolver.test.ts index 4628754013a..91a5402749f 100644 --- a/packages/config/src/handlers/locations/locationTreeSolver.test.ts +++ b/packages/config/src/handlers/locations/locationTreeSolver.test.ts @@ -41,7 +41,9 @@ const lateLoadModule = async () => { describe('resolveChildren', () => { beforeAll(async () => { - mongoServer = await MongoMemoryServer.create() + mongoServer = await MongoMemoryServer.create({ + binary: { checkMD5: false } + }) const uri = mongoServer.getUri() OLD_ENV = process.env process.env.HEARTH_MONGO_URL = mongoServer.getUri() @@ -51,7 +53,7 @@ describe('resolveChildren', () => { const db = connectedClient.db() collection = db.collection('Location_view_with_plain_ids') - }) + }, 60000 /* Timeout to allow mongo binary download*/) afterAll(async () => { process.env = OLD_ENV @@ -64,11 +66,11 @@ describe('resolveChildren', () => { }) beforeEach(async () => { - collection.deleteMany({}) + await collection.deleteMany({}) }) describe('given a location with no children', () => { - test.only('should return empty array', async () => { + test('should return empty array', async () => { // late import to allow env vars to be set before the module is loaded const resolveLocationChildren = await lateLoadModule() @@ -83,7 +85,7 @@ describe('resolveChildren', () => { }) describe('given a location with children', () => { - test.only('should return all descendants', async () => { + test('should return all descendants', async () => { // late import to allow env vars to be set before the module is loaded const resolveLocationChildren = await lateLoadModule() From 2a2635d3043443770587a8335776956056dd2778 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Thu, 27 Feb 2025 16:37:13 +0200 Subject: [PATCH 08/14] fix: Remove unused fetchLocations and update test --- .../src/handlers/locations/children.test.ts | 43 +++++++++++++++---- packages/config/src/services/hearth.ts | 13 ------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/config/src/handlers/locations/children.test.ts b/packages/config/src/handlers/locations/children.test.ts index d139344743a..c37eeab9d8c 100644 --- a/packages/config/src/handlers/locations/children.test.ts +++ b/packages/config/src/handlers/locations/children.test.ts @@ -11,14 +11,22 @@ import { UUID } from '@opencrvs/commons' import * as fixtures from '@opencrvs/commons/fixtures' import { resolveChildren } from './children' -import { fetchFromHearth, fetchLocations } from '@config/services/hearth' +import { fetchFromHearth } from '@config/services/hearth' +import { resolveLocationChildren } from './locationTreeSolver' jest.mock('@config/services/hearth', () => ({ - fetchFromHearth: jest.fn(), - fetchLocations: jest.fn() + fetchFromHearth: jest.fn() +})) + +jest.mock('./locationTreeSolver', () => ({ + resolveLocationChildren: jest.fn() })) const fetchFromHearthMock = fetchFromHearth as jest.Mock +const resolveLocationChildrenMock = resolveLocationChildren as jest.Mock + +// Cast handler to make it callable +const handler = resolveChildren as unknown as (req: Request) => Promise describe('resolveChildren', () => { describe('given a location of type office', () => { @@ -32,15 +40,32 @@ describe('resolveChildren', () => { const req = { params: { locationId: office.id } } as any - // Cast handler to make it callable - const handler = resolveChildren as unknown as ( - req: Request - ) => Promise - const res = await handler(req) - expect(fetchLocations).not.toHaveBeenCalled() + expect(resolveLocationChildrenMock).not.toHaveBeenCalled() expect(res).toEqual([office]) }) }) + + describe('given a location with children', () => { + test('should return location and children', async () => { + const parent = fixtures.savedLocation({ + id: 'uuid1' as UUID, + type: { coding: [{ code: 'ADMIN_STRUCTURE' }] } + }) + + fetchFromHearthMock.mockResolvedValue(parent) + + const child1 = fixtures.savedLocation({ id: 'uuid2' as UUID }) + const child2 = fixtures.savedLocation({ id: 'uuid3' as UUID }) + + resolveLocationChildrenMock.mockResolvedValue([child1, child2]) + + const req = { params: { locationId: parent.id } } as any + + const res = await handler(req) + + expect(res).toEqual([parent, child1, child2]) + }) + }) }) diff --git a/packages/config/src/services/hearth.ts b/packages/config/src/services/hearth.ts index ab639d62a7f..3045c5eedeb 100644 --- a/packages/config/src/services/hearth.ts +++ b/packages/config/src/services/hearth.ts @@ -8,22 +8,9 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { Location, SavedBundle } from '@opencrvs/commons/types' import { FHIR_URL } from '@config/config/constants' import { joinURL } from '@opencrvs/commons' -export const fetchLocations = async () => { - const allLocationsUrl = joinURL(FHIR_URL, `Location?_count=0&status=active`) - const response = await fetch(allLocationsUrl) - - if (!response.ok) { - throw new Error(`Failed to fetch locations: ${await response.text()}`) - } - - const bundle = (await response.json()) as SavedBundle - return bundle.entry.map(({ resource }) => resource) -} - export const fetchFromHearth = async ( suffix: string, method = 'GET', From 40997bf9ee9327b8ed895e2323982b7881749b1e Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 19 Feb 2025 17:41:08 +0200 Subject: [PATCH 09/14] chore: move mongo-memory-server to dev dependencies --- packages/config/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/package.json b/packages/config/package.json index 7d8098cc513..b13cb818a53 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -31,7 +31,6 @@ "jwt-decode": "^2.2.0", "lodash": "^4.17.21", "mongodb": "^6.13.0", - "mongodb-memory-server": "^10.1.3", "mongoose": "^6.11.3", "pino": "^7.0.0", "tsconfig-paths": "^3.13.0", @@ -52,6 +51,7 @@ "eslint-plugin-import": "^2.17.3", "eslint-plugin-prettier": "^4.0.0", "mockingoose": "^2.15.2", + "mongodb-memory-server": "^10.1.3", "nodemon": "^3.0.0", "prettier": "^2.5.0", "ts-node": "^6.1.1", From f7f79909404e0f89dfb54b41d9e658d0160d6159 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 19 Feb 2025 17:41:47 +0200 Subject: [PATCH 10/14] chore: Use common isOffice method #reuasability --- packages/commons/src/fhir/location.ts | 4 ++++ packages/config/src/handlers/locations/children.ts | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/commons/src/fhir/location.ts b/packages/commons/src/fhir/location.ts index d2e1cd52bf3..4b5cf79b34e 100644 --- a/packages/commons/src/fhir/location.ts +++ b/packages/commons/src/fhir/location.ts @@ -121,6 +121,10 @@ export function isHealthFacility( return location.type?.coding?.[0].code === 'HEALTH_FACILITY' } +export function isOffice(location: Location): location is HealthFacility { + return location.type?.coding?.[0].code === 'CRVS_OFFICE' +} + export function getLocationType(location: Location): string | undefined { return location.type?.coding?.[0].code } diff --git a/packages/config/src/handlers/locations/children.ts b/packages/config/src/handlers/locations/children.ts index e12e967ce33..76087cb3104 100644 --- a/packages/config/src/handlers/locations/children.ts +++ b/packages/config/src/handlers/locations/children.ts @@ -9,9 +9,10 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { UUID } from '@opencrvs/commons' + import { ServerRoute } from '@hapi/hapi' import { fetchFromHearth } from '@config/services/hearth' -import { SavedLocation } from '@opencrvs/commons/types' +import { SavedLocation, isOffice } from '@opencrvs/commons/types' import { resolveLocationChildren } from './locationTreeSolver' export const resolveChildren: ServerRoute['handler'] = async (req) => { @@ -20,7 +21,7 @@ export const resolveChildren: ServerRoute['handler'] = async (req) => { const location = await fetchFromHearth( `Location/${locationId}` ) - if (isTypeOf(location, 'CRVS_OFFICE')) { + if (isOffice(location)) { return [location] } @@ -28,7 +29,3 @@ export const resolveChildren: ServerRoute['handler'] = async (req) => { return [location, ...children] } - -function isTypeOf(location: SavedLocation, type: string) { - return location?.type?.coding?.some((x) => x.code === type) -} From 25b4122ba67b122fc135d83e15f0ac1b41419a2e Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 19 Feb 2025 17:48:47 +0200 Subject: [PATCH 11/14] Add HEARTH_MONGO_URL to docker-compose --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index f70febc521e..b69d31b58c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -240,6 +240,7 @@ services: - GATEWAY_URL=http://gateway:7070/ - DOCUMENTS_URL=http://documents:9050 - CHECK_INVALID_TOKEN=true + - HEARTH_MONGO_URL=mongodb://mongo1/hearth-dev migration: image: opencrvs/ocrvs-migration:${VERSION} #platform: linux/amd64 From cd363c3d90c3a53d33b9865312242068a83619d3 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Thu, 20 Feb 2025 13:23:00 +0200 Subject: [PATCH 12/14] fix: Copy paste error --- packages/commons/src/fhir/location.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commons/src/fhir/location.ts b/packages/commons/src/fhir/location.ts index 4b5cf79b34e..30f4efa3928 100644 --- a/packages/commons/src/fhir/location.ts +++ b/packages/commons/src/fhir/location.ts @@ -121,7 +121,7 @@ export function isHealthFacility( return location.type?.coding?.[0].code === 'HEALTH_FACILITY' } -export function isOffice(location: Location): location is HealthFacility { +export function isOffice(location: Location): location is Office { return location.type?.coding?.[0].code === 'CRVS_OFFICE' } From 429baa43878e632109b74b5363d2211ebb5f98bc Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Thu, 27 Feb 2025 16:46:05 +0200 Subject: [PATCH 13/14] fix: cherry pick issues --- packages/config/src/config/constants.ts | 2 ++ packages/config/src/handlers/locations/locationTreeSolver.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/config/src/config/constants.ts b/packages/config/src/config/constants.ts index 2e578dcdccb..195335af8e6 100644 --- a/packages/config/src/config/constants.ts +++ b/packages/config/src/config/constants.ts @@ -35,6 +35,8 @@ export const CERT_PUBLIC_KEY_PATH = export const PRODUCTION = process.env.NODE_ENV === 'production' export const QA_ENV = process.env.QA_ENV || false export const FHIR_URL = process.env.FHIR_URL || 'http://localhost:3447/fhir' +export const HEARTH_MONGO_URL = + process.env.HEARTH_MONGO_URL || 'mongodb://localhost/hearth-dev' // Check if the token has been invalided in the auth service before it has expired // This needs to be a string to make it easy to pass as an ENV var. diff --git a/packages/config/src/handlers/locations/locationTreeSolver.ts b/packages/config/src/handlers/locations/locationTreeSolver.ts index 2f95268ad99..b360d2eeba5 100644 --- a/packages/config/src/handlers/locations/locationTreeSolver.ts +++ b/packages/config/src/handlers/locations/locationTreeSolver.ts @@ -17,9 +17,9 @@ import { import { UUID } from '@opencrvs/commons' import { fetchFromHearth } from '@config/services/hearth' import { MongoClient } from 'mongodb' -import { env } from '@config/environment' +import { HEARTH_MONGO_URL } from '@config/config/constants' -const client = new MongoClient(env.HEARTH_MONGO_URL) +const client = new MongoClient(HEARTH_MONGO_URL) export const resolveLocationChildren = async (id: UUID) => { try { From ccfcb8ab11118a66ccda1b05cae295b2af37800d Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Thu, 27 Feb 2025 18:29:33 +0200 Subject: [PATCH 14/14] chore: Remove random unused export to appease knip --- packages/workflow/src/test/utils.ts | 37 ----------------------------- 1 file changed, 37 deletions(-) diff --git a/packages/workflow/src/test/utils.ts b/packages/workflow/src/test/utils.ts index c00408910fa..79030dcba44 100644 --- a/packages/workflow/src/test/utils.ts +++ b/packages/workflow/src/test/utils.ts @@ -313,43 +313,6 @@ export const testFhirTaskBundle: Saved> = { ] } -export const fieldAgentPractitionerMock = JSON.stringify({ - resourceType: 'Bundle', - id: 'eacae600-a501-42d6-9d59-b8b94f3e50c1', - meta: { lastUpdated: '2018-11-27T17:13:20.662+00:00' }, - type: 'searchset', - total: 1, - link: [ - { - relation: 'self', - url: 'http://localhost:3447/fhir/Practitioner?telecom=phone%7C01711111111' - } - ], - entry: [ - { - fullUrl: - 'http://localhost:3447/fhir/Practitioner/b1f46aba-075d-431e-8aeb-ebc57a4a0ad0', - resource: { - resourceType: 'Practitioner', - identifier: [ - { use: 'official', system: 'mobile', value: '01711111111' } - ], - telecom: [{ system: 'phone', value: '01711111111' }], - name: [ - { use: 'en', family: 'Al Hasan', given: ['Shakib'] }, - { use: 'bn', family: '', given: [''] } - ], - gender: 'male', - meta: { - lastUpdated: '2018-11-25T17:31:08.062+00:00', - versionId: '7b21f3ac-2d92-46fc-9b87-c692aa81c858' - }, - id: 'e0daf66b-509e-4f45-86f3-f922b74f3dbf' - } - } - ] -}) - type PatientIdentifier = NonNullable[number] const drnIdentifier = {