Ir al contenido

API reference

Referencia completa de la superficie pública de emafhir. Cada export listado abajo viene con su signature TypeScript real, qué hace y al menos un ejemplo de uso.

Factory tenant-aware. Es el entry point recomendado en Wave 1+.

function createEmaClient(config: EmaFhirClientConfig): EmaFhirClient;
type EmaFhirClientConfig = {
tenant: Tenant;
supabase: SupabaseClient;
disableMetaInjection?: boolean;
logger?: EmaFhirLogger;
};
type Tenant = {
id: string;
slug: string;
country: CountryCode;
aidbox: TenantAidboxConfig;
igVersion?: string;
flags?: Record<string, unknown>;
};
type TenantAidboxConfig = {
clientId: string;
clientSecret: string;
organizationId: string;
baseUrl: string;
tokenUrl?: string;
};
type CountryCode = "CL" | "MX" | "PE" | "CO" | "BR" | "AR" | "CR";

Uso:

import { createEmaClient } from 'emafhir';
import { createClient as createSupabase } from '@supabase/supabase-js';
const fhir = createEmaClient({
tenant: {
id: 'tenant-uuid',
slug: 'acme-clinic',
country: 'MX',
aidbox: {
clientId: 'client-id',
clientSecret: 'secret-decrypted-upstream',
organizationId: 'org-uuid',
baseUrl: 'https://aidbox.example.com',
},
igVersion: 'emaig@2026.05.1',
},
supabase: createSupabase('https://...', 'key'),
});
const patient = await fhir.create({
resourceType: 'Patient',
name: [{ given: ['Juan'], family: 'Pérez' }],
});
// patient.meta.profile = ['https://emahealth.io/fhir/mx/StructureDefinition/Patient']
// patient.meta.tag = [{ system: 'urn:ema:tenant', code: 'tenant-uuid' }, ...]

Throws: Error si falta tenant, tenant.country, tenant.aidbox.* o supabase.

Cliente bajo nivel sin meta-injection. Soportado indefinidamente. Usar cuando no necesitás auto-inyección o multi-país.

function createClient(options: FhirClientOptions): FhirClient;
type FhirClientOptions = {
baseUrl: string;
auth?: AuthStrategy;
defaultHeaders?: Record<string, string>;
organizationId?: string;
};

Uso:

import { createClient, bearerAuth } from 'emafhir';
const fhir = createClient({
baseUrl: 'https://aidbox.example.com',
auth: bearerAuth(userJwt),
organizationId: 'org-123',
});
const patient = await fhir.read('Patient', 'p1');

Hereda todos los métodos de FhirClient y agrega meta-injection automática + $translate opcional. Tipo público que devuelve createEmaClient.

async create<T extends FhirResource>(
resource: T,
options?: CreateOptions
): Promise<T>;
interface CreateOptions {
translate?: string[]; // FHIRPath de campos a enriquecer con $translate
}

Inyecta meta.profile y meta.tag antes del POST. Si options.translate está, ejecuta $translate en los fields indicados (append-only).

const obs = await fhir.create(
{
resourceType: 'Observation',
status: 'final',
code: { coding: [{ system: 'urn:local:lab', code: 'GLU' }] },
valueQuantity: { value: 5.5, unit: 'mg/dL' },
},
{ translate: ['code'] }
);
// obs.code.coding ahora contiene urn:local:lab + LOINC equivalente

Throws: FhirError si Aidbox rechaza (validación, conflicto, etc.).

async update<T extends FhirResource>(
resource: T,
options?: UpdateOptions
): Promise<T>;
interface UpdateOptions {
translate?: string[];
}

Reemplaza un recurso (PUT). Requiere resource.id. Inyecta meta igual que create.

async read<K extends ResourceType>(
resourceType: K,
id: string
): Promise<ResourceTypeMap[K]>;

Type-safe: el retorno se infiere automáticamente.

const patient = await fhir.read('Patient', 'p1');
// ^ Patient (inferido sin genérico explícito)

Throws: FhirError con e.isNotFound === true si 404.

search<K>(resourceType, params?) y searchResources<K>(...)

Sección titulada «search<K>(resourceType, params?) y searchResources<K>(...)»
async search<K extends ResourceType>(
resourceType: K,
params?: SearchParams
): Promise<Bundle<ResourceTypeMap[K]>>;
async searchResources<K extends ResourceType>(
resourceType: K,
params?: SearchParams
): Promise<ResourceTypeMap[K][]>;

search retorna Bundle completo (con paginación y links). searchResources extrae el array de recursos directamente.

async transaction(bundle: Bundle): Promise<Bundle>;

Ejecuta un Bundle de tipo transaction o batch. Inyecta meta en cada entry con resource antes del POST.

async translate<T extends FhirResource>(
resource: T,
fields: string[]
): Promise<T>;

Aplica $translate post-hoc a un recurso ya existente. Útil cuando no se hizo en el create.

async patch<K extends ResourceType>(
resourceType: K,
id: string,
patches: JsonPatch[]
): Promise<ResourceTypeMap[K]>;
interface JsonPatch {
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
path: string;
value?: unknown;
from?: string;
}

JSON Patch RFC 6902.

const updated = await fhir.patch('Patient', 'p1', [
{ op: 'replace', path: '/name/0/given/0', value: 'Carlos' },
{ op: 'add', path: '/birthDate', value: '1980-05-15' },
]);
async delete(resourceType: ResourceType | string, id: string): Promise<void>;
async vread<K extends ResourceType>(
resourceType: K,
id: string,
versionId: string
): Promise<ResourceTypeMap[K]>;

Lee una versión histórica específica.

async operation<T>(
path: string,
body?: unknown,
method: 'GET' | 'POST' = 'POST'
): Promise<T>;

Invoca operaciones FHIR genéricas ($expand, $validate, etc.).

const expansion = await fhir.operation<ValueSet>(
'/ValueSet/$expand?url=...',
undefined,
'GET'
);
async nextPage<T extends FhirResource>(
bundle: Bundle<T>
): Promise<Bundle<T> | null>;

Sigue el link relation="next" del Bundle anterior. Retorna null si no hay más páginas.

let bundle = await fhir.search('Patient', { _count: 50 });
while (bundle) {
for (const entry of bundle.entry ?? []) {
process(entry.resource);
}
bundle = await fhir.nextPage(bundle);
}
readonly tenant: Tenant;
readonly profileResolver: ProfileResolver;

Error de cualquier operación HTTP del SDK.

class FhirError extends Error {
status: number;
statusText: string;
operationOutcome?: OperationOutcome;
get isNotFound(): boolean; // status === 404
get isConflict(): boolean; // status === 409
get isValidationError(): boolean; // 400 o 422
get isUnauthorized(): boolean; // 401 o 403
get issues(): Array<{
severity: string;
code: string;
diagnostics?: string;
}>;
}

Uso:

try {
await fhir.read('Patient', 'bad-id');
} catch (e) {
if (e instanceof FhirError && e.isNotFound) {
return null; // tratar 404 como ausencia
}
throw e;
}
function ref(
type: ResourceType | (string & {}),
id: string,
display?: string
): Reference;

Construye un Reference FHIR.

import { ref } from 'emafhir';
const encounter = {
resourceType: 'Encounter',
subject: ref('Patient', 'p1', 'Juan Pérez'),
participant: [{ individual: ref('Practitioner', 'pr1') }],
};
function transaction(): TransactionBuilder;

Builder fluent para Bundle de tipo transaction o batch.

import { transaction } from 'emafhir';
const bundle = transaction()
.create(patient)
.update(observation)
.delete('Task', 'task-old')
.createIfNotExists(allergy, 'patient=Patient/p1&code=peanut')
.build();
await fhir.transaction(bundle);
class TransactionBuilder {
batch(): this; // Cambia type a "batch"
create(resource: FhirResource): this; // POST
update(resource: FhirResource): this; // PUT (requiere id)
delete(resourceType: string, id: string): this; // DELETE
createIfNotExists(resource: FhirResource, search: string): this; // POST + If-None-Exist
build(): Bundle;
}

injectMeta(resource, tenant, profileResolver)

Sección titulada «injectMeta(resource, tenant, profileResolver)»
async function injectMeta<R extends FhirResource>(
resource: R,
tenant: Tenant,
profileResolver: ProfileResolver
): Promise<R>;

Inyecta meta.profile (canonical sin versión) + meta.tag (urn:ema:tenant, urn:ema:ig-version). Idempotente y no-mutativo (devuelve un objeto nuevo).

Normalmente se invoca por EmaFhirClient automáticamente; útil para debugging o casos custom.

applyAutoTranslate(resource, fields, context)

Sección titulada «applyAutoTranslate(resource, fields, context)»
async function applyAutoTranslate<R extends Record<string, unknown>>(
resource: R,
fields: string[],
context: AutoTranslateContext
): Promise<R>;
type AutoTranslateContext = {
invokeTranslate: (params: { system: string; code: string }) => Promise<TranslateResult>;
logger: EmaFhirLogger;
};

Append-only. Si una llamada a $translate falla, se loggea warning y la operación procede.

function clientCredentials(options: ClientCredentialsOptions): ClientCredentialsAuth;
type ClientCredentialsOptions = {
tokenUrl: string;
clientId: string;
clientSecret: string;
scope?: string;
};

OAuth2 client_credentials. Cachea el token hasta su expiración (con 60s de margen).

import { clientCredentials, createClient } from 'emafhir';
const auth = clientCredentials({
tokenUrl: 'https://aidbox.example.com/auth/token',
clientId: 'my-client',
clientSecret: 'my-secret',
});
const fhir = createClient({ baseUrl: '...', auth });
function bearerAuth(token: string): BearerAuth;
class BearerAuth implements AuthStrategy {
getAccessToken(): Promise<string>;
setToken(token: string): void;
}

Bearer token estático (con setter para refresh).

interface AuthStrategy {
getAccessToken(): Promise<string>;
}

Para implementar tu propia estrategia (mTLS, custom JWT, etc.).

function search(): SearchBuilder;

Builder fluent para SearchParams.

import { search } from 'emafhir';
const params = search()
.where('name', 'García')
.whereExact('gender', 'female')
.whereDate('birthdate', 'ge', '1990-01-01')
.whereToken('identifier', 'urn:national:cl:run', '12345678-5')
.include('Patient', 'general-practitioner')
.sort('-_lastUpdated')
.count(20)
.build();
const bundle = await fhir.search('Patient', params);
class SearchBuilder {
where(key: string, value: string | number | boolean): this;
whereExact(key: string, value: string): this;
whereContains(key: string, value: string): this;
whereMissing(key: string, missing?: boolean): this;
whereToken(key: string, system: string, code: string): this;
whereDate(
key: string,
prefix: 'eq' | 'ne' | 'lt' | 'gt' | 'le' | 'ge' | 'sa' | 'eb' | 'ap',
date: string
): this;
whereReference(key: string, resourceType: string, id: string): this;
include(resourceType: string, param: string): this;
revinclude(resourceType: string, param: string): this;
sort(field: string, descending?: boolean): this;
count(n: number): this;
offset(n: number): this;
elements(...fields: string[]): this;
summary(mode: 'true' | 'text' | 'data' | 'count'): this;
build(): SearchParams;
}

Re-exporta todos los tipos FHIR R4 + tipos EMA. No tiene runtime — solo types.

import type {
// FHIR base
FhirResource, Bundle, Reference, CodeableConcept, Coding,
Identifier, Meta, Period, Quantity, ContactPoint, Address, HumanName,
Extension, Annotation, Dosage, Money, Ratio, Narrative, BundleEntry, BundleLink,
// 35 resources
Patient, Encounter, Condition, Observation, Procedure, AllergyIntolerance,
MedicationRequest, Medication, DiagnosticReport, Specimen,
DocumentReference, Communication, CarePlan, Task, Coverage, Flag, RelatedPerson,
Invoice, ChargeItem, PaymentNotice, Appointment, Schedule, Slot,
Organization, Location, Device, Practitioner, PractitionerRole, HealthcareService,
Questionnaire, QuestionnaireResponse, ActivityDefinition, ServiceRequest,
PlanDefinition,
// Type maps
ResourceTypeMap, ResourceType,
// EMA
CountryCode, Tenant, TenantAidboxConfig,
EmaFhirClientConfig, EmaFhirLogger,
} from 'emafhir/types';
type ResourceTypeMap = {
Patient: Patient;
Observation: Observation;
Encounter: Encounter;
// ... 35 entries
};
type ResourceType = keyof ResourceTypeMap;

Lo que permite que fhir.read('Patient', id) retorne Patient sin genérico explícito.

function validateRun(run: string): { valid: boolean; reason?: string; normalized?: string };
function validateCurp(curp: string): { valid: boolean; reason?: string; normalized?: string };
function validateRfc(rfc: string): { valid: boolean; reason?: string; type?: 'fisica' | 'moral'; normalized?: string };
function validateDni(dni: string): { valid: boolean; reason?: string; normalized?: string };

Sólo validan estructura + checksum. Nunca consultan registros oficiales (SII, RENAPO, SAT, RENIEC).

import {
validateRun,
validateCurp,
validateRfc,
validateDni,
} from 'emafhir/validators';
validateRun('12.345.678-5'); // { valid: true, normalized: '12345678-5' }
validateRun('12.345.678-K'); // { valid: false, normalized: null }
validateCurp('GARC850315HDFLLR03'); // { valid: true, normalized: 'GARC850315HDFLLR03' }
validateRfc('XAXX010101000'); // { valid: true, type: 'fisica', ... }
validateDni('12345678'); // { valid: true, normalized: '12345678' }

Renderer headless de Questionnaires + helpers para QuestionnaireResponse. Requiere react >= 18 como peer dep.

function useSDCForm(options: UseSDCFormOptions): UseSDCFormReturn;
type UseSDCFormOptions = {
questionnaire: Questionnaire;
initialResponse?: QuestionnaireResponse;
subject?: { reference?: string };
encounter?: { reference?: string };
autoSaveInterval?: number; // ms; 0 deshabilita
onAutoSave?: (response: QuestionnaireResponse) => void;
};
type UseSDCFormReturn = {
values: Record<string, unknown>; // linkId → answer
errors: Record<string, string>;
resolvedItems: ResolvedItem[]; // items aplanados con enableWhen resuelto
setValue: (linkId: string, value: unknown) => void;
validate: () => boolean;
submit: () => QuestionnaireResponse | null;
reset: () => void;
isDirty: boolean;
};

assembleQuestionnaire(questionnaire, resolver, options?)

Sección titulada «assembleQuestionnaire(questionnaire, resolver, options?)»
async function assembleQuestionnaire(
questionnaire: Questionnaire,
resolver: (canonicalUrl: string) => Promise<Questionnaire | undefined>,
options?: { maxDepth?: number }
): Promise<Questionnaire>;

Implementa la operación SDC $assemble — resuelve sub-questionnaires referenciados por extension sdc-questionnaire-subQuestionnaire.

function resolveItems(items: QuestionnaireItem[] | undefined): ResolvedItem[];
function buildResponse(
questionnaireUrl: string | undefined,
items: ResolvedItem[],
values: Record<string, unknown>,
options?: { status?: string; subject?: Reference; encounter?: Reference }
): QuestionnaireResponse;
function validateForm(items: ResolvedItem[], values: Record<string, unknown>): ValidationError[];
function isItemEnabled(item: ResolvedItem): boolean;
import { FhirError } from 'emafhir';
try {
const updated = await fhir.update(resource);
} catch (e) {
if (!(e instanceof FhirError)) throw e;
if (e.isConflict) {
// Optimistic lock: re-read y retry
const fresh = await fhir.read(resource.resourceType, resource.id!);
return fhir.update({ ...fresh, ...updates });
}
if (e.isValidationError) {
// Mostrar issues al usuario
return { errors: e.issues };
}
throw e;
}
const results: Patient[] = [];
let bundle = await fhir.search('Patient', { _count: 50 });
while (bundle) {
for (const entry of bundle.entry ?? []) {
if (entry.resource) results.push(entry.resource);
}
bundle = await fhir.nextPage(bundle);
}
const params = search()
.where('subject', `Patient/${patientId}`)
.include('Observation', 'performer')
.build();
const bundle = await fhir.search('Observation', params);
const observations = bundle.entry
?.filter((e) => e.search?.mode !== 'include')
.map((e) => e.resource) ?? [];
const performers = bundle.entry
?.filter((e) => e.search?.mode === 'include')
.map((e) => e.resource) ?? [];