Add env flag to filter DocumentReference type locally in ITI-67

DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY, when enabled, skips the `type`
query param on GET /DocumentReference to the document registry and
filters the resulting searchset by type client-side instead, matching
the same system|code / |code / code token semantics used elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gomez Auad 2026-07-16 22:04:08 +00:00
parent c5b0d1c480
commit 387ee331ed
6 changed files with 126 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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;
}
/**

View File

@ -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', () => {

View File

@ -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}