diff --git a/.env.example b/.env.example index df56270..a3fdcd6 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,10 @@ MPI_URL=https://bus-test.msal.gob.ar/masterfile-federacion-service/fhir/Patient # URL del Document Registry si difiere del Bus principal DOCUMENT_REGISTRY_URL=https://bus-test.msal.gob.ar/fhir/DocumentReference +# ITI-67: si es true, no se envía el parámetro `type` en el GET /DocumentReference +# al índice de documentos; se pide todo por subject y se filtra por type localmente. +DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY=false + # Habilita logs detallados de requests/responses salientes al Bus (true | false) BUS_DEBUG=false diff --git a/bus-gateway/.env.example b/bus-gateway/.env.example index e8402af..50909d5 100644 --- a/bus-gateway/.env.example +++ b/bus-gateway/.env.example @@ -13,6 +13,10 @@ DOCUMENT_REGISTRY_URL=http://document-registry-host:8080 MPI_SCOPE=Patient/*.read,Patient/*.write DOCUMENT_REGISTRY_SCOPE=DocumentReference/*.read,DocumentReference/*.write +# ITI-67: si es true, no se envía el parámetro `type` en el GET /DocumentReference +# al índice de documentos; se pide todo por subject y se filtra por type localmente. +DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY=false + # HAPI FHIR Server FHIR_URL=http://hapi-fhir-host:8080/fhir diff --git a/bus-gateway/config/index.js b/bus-gateway/config/index.js index fc48736..43b8440 100644 --- a/bus-gateway/config/index.js +++ b/bus-gateway/config/index.js @@ -9,6 +9,9 @@ const config = { issuer: process.env.BUS_ISSUER, mpiScope: process.env.MPI_SCOPE, documentRegistryScope: process.env.DOCUMENT_REGISTRY_SCOPE, + // Si es true, ITI-67 no envía el parámetro `type` al índice de documentos: + // pide todos los DocumentReference del subject y filtra por type localmente. + filterDocumentTypeLocally: process.env.DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY === 'true', }, fhir: { url: process.env.FHIR_URL, diff --git a/bus-gateway/services/documentReference.js b/bus-gateway/services/documentReference.js index d9b2988..f59eefc 100644 --- a/bus-gateway/services/documentReference.js +++ b/bus-gateway/services/documentReference.js @@ -68,10 +68,67 @@ async function findDocumentReferenceBySubject(token, subjectSystem, subjectValue return response.data; } +/** + * Parses a FHIR token search value (`system|code`, `|code` o `code`) into its parts. + * `system === null` indica la forma `|code` (exige coding sin system). + * `system === undefined` indica la forma `code` (sin restricción de system). + * @param {string} token + * @returns {{ system: string|null|undefined, code: string }} + */ +function parseTypeToken(token) { + const pipeIndex = token.indexOf('|'); + if (pipeIndex === -1) { + return { system: undefined, code: token }; + } + const system = token.slice(0, pipeIndex); + return { system: system === '' ? null : system, code: token.slice(pipeIndex + 1) }; +} + +/** + * Indica si el CodeableConcept `type` de un DocumentReference matchea el token FHIR dado, + * replicando la semántica estándar de búsqueda por token (`system|code`, `|code` o `code`) + * que el índice de documentos aplicaría del lado del servidor. + * @param {object} resource - DocumentReference resource. + * @param {string} token - Token FHIR de DocumentReference.type. + * @returns {boolean} + */ +function matchesTypeToken(resource, token) { + const { system, code } = parseTypeToken(token); + const codings = resource?.type?.coding ?? []; + return codings.some(coding => { + if (system === null) { + return !coding.system && coding.code === code; + } + if (system !== undefined) { + return coding.system === system && coding.code === code; + } + return coding.code === code; + }); +} + +/** + * Filtra las entries de un Bundle searchset de DocumentReference por type, + * ajustando `total` a la cantidad de entries resultantes. + * @param {object} bundle - FHIR Bundle (searchset). + * @param {string} type - Token FHIR de DocumentReference.type. + * @returns {object} Bundle filtrado. + */ +function filterBundleByType(bundle, type) { + if (!bundle || !Array.isArray(bundle.entry)) { + return bundle; + } + const entry = bundle.entry.filter(e => matchesTypeToken(e.resource, type)); + return { ...bundle, entry, total: entry.length }; +} + /** * Searches DocumentReferences in the document registry by patient internal ID, * opcionalmente filtrando por tipo de documento * (GET /fhir/DocumentReference?patient=value[&type=system|code]). + * + * Si `config.bus.filterDocumentTypeLocally` está activo y se pidió `type`, el parámetro + * `type` no se envía al índice de documentos: se piden todos los DocumentReference del + * subject y el filtrado por tipo se hace localmente sobre el Bundle resultante. * @param {string} documentRegistryUrl - Base URL of the document registry service. * @param {string} token - Bearer access token. * @param {string} patientId - Internal patient ID in the document registry. @@ -80,15 +137,16 @@ async function findDocumentReferenceBySubject(token, subjectSystem, subjectValue */ async function findDocumentReferenceByPatient(token, patientId, type) { const request = createBusRequest(token); + const filterLocally = Boolean(type) && config.bus.filterDocumentTypeLocally; const params = { subject: patientId }; - if (type) { + if (type && !filterLocally) { params.type = type; } const response = await request.get( URL.parse(INDICE_ATENCION_URL), { params } ); - return response.data; + return filterLocally ? filterBundleByType(response.data, type) : response.data; } /** diff --git a/bus-gateway/tests/services/documentReference.test.js b/bus-gateway/tests/services/documentReference.test.js index 8e97ff2..cac4782 100644 --- a/bus-gateway/tests/services/documentReference.test.js +++ b/bus-gateway/tests/services/documentReference.test.js @@ -1,5 +1,6 @@ require('dotenv').config(); const axios = require('axios'); +const config = require('../../config'); const { getBusToken } = require('../../utils/busAuth'); const { createDocumentReference, @@ -76,6 +77,10 @@ describe('findDocumentReferenceBySubject', () => { }); describe('findDocumentReferenceByPatient', () => { + afterEach(() => { + config.bus.filterDocumentTypeLocally = false; + }); + it('GETs /fhir/DocumentReference with patient ID as subject param', async () => { const token = await acquireToken(); const bundle = { resourceType: 'Bundle', entry: [] }; @@ -89,6 +94,55 @@ describe('findDocumentReferenceByPatient', () => { ); expect(result).toEqual(bundle); }); + + it('sends the type param when filterDocumentTypeLocally is disabled', async () => { + config.bus.filterDocumentTypeLocally = false; + const token = await acquireToken(); + const bundle = { resourceType: 'Bundle', entry: [] }; + mockRequest.get.mockResolvedValue({ data: bundle }); + + await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', 'http://loinc.org|60591-5'); + + expect(mockRequest.get).toHaveBeenCalledWith( + expect.anything(), + { params: { subject: 'http://mpi/fhir/Patient/123', type: 'http://loinc.org|60591-5' } } + ); + }); + + it('omits the type param and filters the bundle locally when filterDocumentTypeLocally is enabled', async () => { + config.bus.filterDocumentTypeLocally = true; + const token = await acquireToken(); + const ipsEntry = { + resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://loinc.org', code: '60591-5' }] } }, + }; + const meowEntry = { + resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://loinc.org', code: '56445-0' }] } }, + }; + const bundle = { resourceType: 'Bundle', total: 2, entry: [ipsEntry, meowEntry] }; + mockRequest.get.mockResolvedValue({ data: bundle }); + + const result = await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', 'http://loinc.org|60591-5'); + + expect(mockRequest.get).toHaveBeenCalledWith( + expect.anything(), + { params: { subject: 'http://mpi/fhir/Patient/123' } } + ); + expect(result).toEqual({ resourceType: 'Bundle', total: 1, entry: [ipsEntry] }); + }); + + it('matches the bare `code` token form regardless of coding system', async () => { + config.bus.filterDocumentTypeLocally = true; + const token = await acquireToken(); + const matching = { + resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://other-system.org', code: '60591-5' }] } }, + }; + const bundle = { resourceType: 'Bundle', total: 1, entry: [matching] }; + mockRequest.get.mockResolvedValue({ data: bundle }); + + const result = await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', '60591-5'); + + expect(result.entry).toEqual([matching]); + }); }); describe('searchDocumentReference', () => { diff --git a/docker-compose.yml b/docker-compose.yml index 69e3916..7ee89c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,7 @@ services: DOCUMENT_REGISTRY_URL: ${DOCUMENT_REGISTRY_URL} MPI_SCOPE: ${MPI_SCOPE} DOCUMENT_REGISTRY_SCOPE: ${DOCUMENT_REGISTRY_SCOPE} + DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY: ${DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY:-false} FHIR_URL: http://hapi-fhir:8080/fhir BUNDLE_SIGNER_URL: ${BUNDLE_SIGNER_URL} BUS_DEBUG: ${BUS_DEBUG}