Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/fhir-eswatini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"type": "module",
"fhir": {
"specUrl": "http://172.209.216.154/definitions.json.zip",
"adaptorGeneratedDate": "2026-06-02T15:23:39.432Z",
"adaptorGeneratedDate": "2026-06-05T10:42:23.868Z",
"generatorVersion": "0.7.9",
"options": {
"base": "fhir-4"
Expand Down
1 change: 1 addition & 0 deletions packages/fhir-eswatini/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ export function organization(type: any, props?: any) {
* @public
* @function
* @param {object} props - Properties to apply to the resource (includes common and custom properties).
* @param {} [props._birthDate] - undefined
Comment thread
hunterachieng marked this conversation as resolved.
* @param {boolean} [props.active] - Whether this patient's record is in active use
* @param {Address} [props.address] - An address for the individual
* @param {date} [props.birthDate] - Date of birth: YYYY-MM-DD
Expand Down
1 change: 1 addition & 0 deletions packages/fhir-eswatini/src/datatypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const {
coding,
composite,
concept,
ensureConceptText,
ext,
extendSystemMap,
extendValues,
Expand Down
32 changes: 32 additions & 0 deletions packages/fhir-eswatini/src/profiles/SzPatient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { builders as FHIR } from "@openfn/language-fhir-4";
type MaybeArray<T> = T | T[];

export type Patient_SzPatient_Props = {
_birthDate?: any;
active?: boolean;
address?: FHIR.Address[];
birthDate?: string;
Expand Down Expand Up @@ -135,6 +136,37 @@ export default function(props: Partial<Patient_SzPatient_Props>) {
}
}

{
if (!_.isNil(props._birthDate)) {
if (_.isPlainObject(props._birthDate)) {
resource._birthDate = Object.assign({}, props._birthDate);
} else {
delete resource._birthDate;
resource._birthDate = {};

dt.addExtension(
resource._birthDate,
"http://hl7.org/fhir/StructureDefinition/patient-birthTime",
props._birthDate
);
}
}

if (!_.isNil(props._birthTime)) {
delete resource._birthTime;

if (!resource._birthDate) {
resource._birthDate = {};
}

dt.addExtension(
resource._birthDate,
"http://hl7.org/fhir/StructureDefinition/patient-birthTime",
props._birthTime
);
}
}

if (!_.isNil(props.deceased)) {
delete resource.deceased;
dt.composite(resource, "deceased", props.deceased);
Expand Down
35 changes: 35 additions & 0 deletions packages/fhir-eswatini/test/resources/Patient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,39 @@ describe('SzPatient', () => {
valueDateTime: '2025-06-01T10:00:00Z',
});
});

it('should map _birthDate primitive extension shorthand', () => {
const resource = b.patient('SzPatient', {
birthDate: '10/07/1990',
_birthDate: '2000-01-01T14:35:45-05:00',
Comment thread
hunterachieng marked this conversation as resolved.
});

assert.deepEqual(resource.birthDate, '10/07/1990');
assert.deepEqual(resource._birthDate, {
extension: [
{
url: 'http://hl7.org/fhir/StructureDefinition/patient-birthTime',
valueDateTime: '2000-01-01T14:35:45-05:00',
},
],
});
});

it('should map _birthTime shorthand into _birthDate extension', () => {
const resource = b.patient('SzPatient', {
birthDate: '10/07/1990',
_birthTime: '2000-01-01T14:35:45-05:00',
});

assert.deepEqual(resource.birthDate, '10/07/1990');
assert.deepEqual(resource._birthDate, {
extension: [
{
url: 'http://hl7.org/fhir/StructureDefinition/patient-birthTime',
valueDateTime: '2000-01-01T14:35:45-05:00',
},
],
});
assert.equal(resource._birthTime, undefined);
});
});
5 changes: 4 additions & 1 deletion packages/fhir-eswatini/types/builders.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ declare type Organization_SzOrganization_Props = {
};

declare type Patient_SzPatient_Props = {
_birthDate?: any;
active?: boolean;
address?: builders.Address[];
birthDate?: string;
Expand Down Expand Up @@ -676,6 +677,7 @@ declare const cc: (codings: (builders.Coding | [string, string, Omit<builders.Co
declare const coding: typeof builders.coding;
declare const composite: (object: any, key: any, value: any) => void;
declare const concept: (codings: (builders.Coding | [string, string, Omit<builders.Coding, "system" | "code">?]) | (builders.Coding | [string, string, Omit<builders.Coding, "system" | "code">?])[], extra?: Omit<builders.CodeableConcept, "coding">) => builders.CodeableConcept;
declare const ensureConceptText: (concept: any) => void;
declare const ext: (url: string, value: any, props?: Omit<builders.Extension, "url">) => {
extension: ({
url: string;
Expand Down Expand Up @@ -1058,6 +1060,7 @@ declare function organization(props: Organization_SzOrganization_Props): any;
* @public
* @function
* @param {object} props - Properties to apply to the resource (includes common and custom properties).
* @param {} [props._birthDate] - undefined
* @param {boolean} [props.active] - Whether this patient's record is in active use
* @param {Address} [props.address] - An address for the individual
* @param {date} [props.birthDate] - Date of birth: YYYY-MM-DD
Expand Down Expand Up @@ -1239,5 +1242,5 @@ declare function serviceRequest(type: "SzReferral", props: ServiceRequest_SzRefe
declare function specimen(type: "SzLabSpecimen", props: Specimen_SzLabSpecimen_Props): any;
declare function specimen(props: Specimen_SzLabSpecimen_Props): any;

export { addExtension, appointment, c, cc, coding, composite, concept, condition, encounter, episodeOfCare, ext, extendSystemMap, extendValues, extension, findExtension, id, identifier, location, lookupValue, mapSystems, mapValues, medication, medicationDispense, medicationRequest, observation, organization, patient, practitioner, procedure, ref, reference, serviceRequest, setSystemMap, setValues, specimen, value };
export { addExtension, appointment, c, cc, coding, composite, concept, condition, encounter, ensureConceptText, episodeOfCare, ext, extendSystemMap, extendValues, extension, findExtension, id, identifier, location, lookupValue, mapSystems, mapValues, medication, medicationDispense, medicationRequest, observation, organization, patient, practitioner, procedure, ref, reference, serviceRequest, setSystemMap, setValues, specimen, value };

13 changes: 13 additions & 0 deletions tools/generate-fhir/src/codegen/generate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ export const generateType = (
continue;
}

// Primitive extension props (_birthDate etc) are typed as `any` --
// their content depends on which extensions are present
if ((s as any).isPrimitiveExtension) {
props.push(
b.tsPropertySignature(
b.identifier(key),
b.tsTypeAnnotation(b.tsAnyKeyword()),
true,
),
);
continue;
}

// TODO need to handle this stuff!
// let type;
// if (s.typeDef) {
Expand Down
126 changes: 126 additions & 0 deletions tools/generate-fhir/src/generate-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,136 @@ const mapSimpleProp = (propName: string, mapping: Mapping, schema: Schema) => {
return ifPropInInput(propName, [assignProp], elseStatement);
};

const mapPrimitiveTypeDef = (propName: string, schema: Schema) => {
// propName is the underscore prop, eg _birthDate
const primitivePropName = propName;
// Only retrieve extension-backed child props
const primitiveExtensions = Object.entries(schema.typeDef || {}).filter(
([, spec]: [string, any]) => spec.extension,
);
const statements: StatementKind[] = [];

const inputPropRef = (name: string) => safelyRefProp(INPUT_NAME, name);
const resourcePropRef = (name: string) => safelyRefProp(RESOURCE_NAME, name);
const copyPrimitiveMetadata = (name: string) =>
b.expressionStatement(
b.assignmentExpression(
'=',
resourcePropRef(name),
b.callExpression(
b.memberExpression(b.identifier('Object'), b.identifier('assign')),
[b.objectExpression([]), inputPropRef(name)],
),
),
);
const createEmptyPrimitiveMetadata = (name: string) =>
b.expressionStatement(
b.assignmentExpression('=', resourcePropRef(name), b.objectExpression([])),
);
const addPrimitiveExtension = (
targetName: string,
sourceName: string,
url: string,
) =>
b.expressionStatement(
b.callExpression(
b.memberExpression(b.identifier('dt'), b.identifier('addExtension')),
[resourcePropRef(targetName), b.stringLiteral(url), inputPropRef(sourceName)],
),
);

if (primitiveExtensions.length === 1) {
const [[, spec]] = primitiveExtensions as [string, any][];
statements.push(
ifPropInInput(
primitivePropName,
[
b.ifStatement(
b.callExpression(
b.memberExpression(b.identifier('_'), b.identifier('isPlainObject')),
[inputPropRef(primitivePropName)],
),
b.blockStatement([
// If the caller already passed primitive metadata, preserve it
copyPrimitiveMetadata(primitivePropName),
]),
b.blockStatement([
// Otherwise treat the underscore value as shorthand for the single known extension
b.expressionStatement(
b.unaryExpression('delete', resourcePropRef(primitivePropName)),
),
createEmptyPrimitiveMetadata(primitivePropName),
addPrimitiveExtension(
primitivePropName,
primitivePropName,
spec.extension.url,
),
]),
),
],
),
);
} else {
statements.push(
ifPropInInput(
primitivePropName,
[
b.ifStatement(
b.callExpression(
b.memberExpression(b.identifier('_'), b.identifier('isPlainObject')),
[inputPropRef(primitivePropName)],
),
b.blockStatement([
// Multiple extension choices: only pass through explicit primitive metadata objects
copyPrimitiveMetadata(primitivePropName),
]),
),
],
),
);
}

for (const [key, spec] of primitiveExtensions as [string, any][]) {
// Support shorthand child props like _birthTime and rewrite them into _birthDate.extension[]
const inputPropName = `_${key}`;
statements.push(
ifPropInInput(
inputPropName,
[
b.expressionStatement(
b.unaryExpression('delete', resourcePropRef(inputPropName)),
),
b.ifStatement(
b.unaryExpression('!', resourcePropRef(primitivePropName)),
b.blockStatement([
// Create the primitive metadata container before appending extension shorthand
createEmptyPrimitiveMetadata(primitivePropName),
]),
),
addPrimitiveExtension(
primitivePropName,
inputPropName,
spec.extension.url,
),
],
),
);
}

return b.blockStatement(statements);
};

const isPrimitiveTypeDefParent = (schema: Schema) =>
!!(schema as any).isPrimitiveExtension && !!(schema as any).typeDef;

// map a type def (ie, a nested object) property by property
// TODO this is designed to handle singleton and array types
// The array stuff adds a lot of complication and I need tests on both formats
const mapTypeDef = (propName: string, mapping: Mapping, schema: Schema) => {
if (isPrimitiveTypeDefParent(schema)) {
return mapPrimitiveTypeDef(propName, schema);
}

const statements: any[] = [];

statements.push(
Expand Down
36 changes: 31 additions & 5 deletions tools/generate-fhir/src/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ async function parseProp(
data,
) {
let [parent, prop] = path.split('.');
const isExtensionPath = prop === 'extension';
// TODO skip if multiple dots

if (/\[x\]/.test(prop)) {
Expand All @@ -366,7 +367,16 @@ async function parseProp(
if (schema.props[parent]) {
const def: PropDef = {};

if (!data.type || schema.props[parent].type.includes('date')) {
// Keep primitive props
const isExtensionChild = isExtensionPath;
const hasSlice = !!data.sliceName;
const parentTypes = schema.props[parent].type || [];
const isPrimitiveParent =
!schema.props[parent].typeDef &&
parentTypes.length > 0 &&
parentTypes.every(type => type[0] === type[0]?.toLowerCase());

if (!data.type || (isPrimitiveParent && !(isExtensionChild && hasSlice))) {
return;
}

Expand All @@ -384,7 +394,8 @@ async function parseProp(
type.profile.length &&
type.profile[0].match(/\/StructureDefinition/)
) {
const typeId = type.profile[0].split('/').at(-1);
const extensionUrl = type.profile[0].split('|')[0];
const typeId = extensionUrl.split('/').at(-1);
const spec = fullSpec[typeId];

if (spec) {
Expand All @@ -396,7 +407,11 @@ async function parseProp(
// look for extension.value[x] in the spec
};
} else {
console.log('WARNING: spec not found for ', typeId);
// Some extension profiles are not in the downloaded spec
// The profile URL is still enough for codegen
def.extension = {
url: extensionUrl,
};
}
} else {
simpleType = typeDefs[type.code] || type.code;
Expand Down Expand Up @@ -431,8 +446,19 @@ async function parseProp(
// }

if (Object.keys(def).length) {
schema.props[parent].typeDef ??= {};
schema.props[parent].typeDef[prop] = def;
if (isPrimitiveParent && isExtensionChild) {
// primitive extension slices go on a top-level _parent prop, eg _birthDate
const underscoreProp = `_${parent}`;
schema.props[underscoreProp] ??= {
type: [],
isPrimitiveExtension: true,
typeDef: {},
};
schema.props[underscoreProp].typeDef[prop] = def;
} else {
schema.props[parent].typeDef ??= {};
schema.props[parent].typeDef[prop] = def;
}
}
}
}
Expand Down
Loading
Loading