diff --git a/src/api/mustache/generator/ViewModelFactory.ts b/src/api/mustache/generator/ViewModelFactory.ts index b039509c5..e190c6e58 100644 --- a/src/api/mustache/generator/ViewModelFactory.ts +++ b/src/api/mustache/generator/ViewModelFactory.ts @@ -122,17 +122,15 @@ export class ViewModelFactory { } private _createChildrenFor(typeRef: Identifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel[] { + const schema = this.tsIndex.resolve(typeRef); + if (!schema || !("typeFamily" in schema)) return []; if (isComplexTypeIdentifier(typeRef)) { - return this.tsIndex - .resourceChildren(typeRef) - .filter(isComplexTypeIdentifier) + return (schema.typeFamily?.complexTypes ?? []) .filter(this.filterPred) .map((childRef: Identifier) => this._createFor(childRef, cache, nestedIn)); } if (isResourceIdentifier(typeRef)) { - return this.tsIndex - .resourceChildren(typeRef) - .filter(isResourceIdentifier) + return (schema.typeFamily?.resources ?? []) .filter(this.filterPred) .map((childRef: Identifier) => this._createFor(childRef, cache, nestedIn)); } diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index dcd033197..7dbde15fe 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -365,8 +365,7 @@ export class Python extends Writer { } private shouldImportResourceFamily(resource: RegularTypeSchema): boolean { - assert(this.tsIndex !== undefined); - return resource.identifier.kind === "resource" && this.tsIndex.resourceChildren(resource.identifier).length > 0; + return resource.identifier.kind === "resource" && (resource.typeFamily?.resources?.length ?? 0) > 0; } private generateExportsDeclaration( @@ -448,8 +447,7 @@ export class Python extends Writer { } private generateResourceTypeField(schema: RegularTypeSchema): void { - assert(this.tsIndex !== undefined); - const hasChildren = this.tsIndex.resourceChildren(schema.identifier).length > 0; + const hasChildren = (schema.typeFamily?.resources?.length ?? 0) > 0; if (hasChildren) { this.line(`${this.nameFormatFunction("resourceType")}: str = Field(`); @@ -660,7 +658,7 @@ export class Python extends Writer { ); const families: Record = {}; for (const resource of this.tsIndex.collectResources()) { - const children: string[] = this.tsIndex.resourceChildren(resource.identifier).map((c) => c.name); + const children = (resource.typeFamily?.resources ?? []).map((c) => c.name); if (children.length > 0) { const familyName = `${resource.identifier.name}Family`; families[familyName] = children; diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 2e01b6bf9..2ab7acf82 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -10,7 +10,6 @@ import { isNestedIdentifier, isPrimitiveIdentifier, isProfileTypeSchema, - isResourceIdentifier, isResourceTypeSchema, isSpecializationTypeSchema, type Name, @@ -225,7 +224,11 @@ export class TypeScript extends Writer { const typeFamilyFields: { fieldName: string; familyTypeName: string }[] = []; for (const [fieldName, field] of Object.entries(schema.fields ?? {})) { if (isChoiceDeclarationField(field) || !field.type) continue; - if (isResourceIdentifier(field.type) && tsIndex.resourceChildren(field.type).length > 0) { + const fieldTypeSchema = tsIndex.resolve(field.type); + if ( + isSpecializationTypeSchema(fieldTypeSchema) && + (fieldTypeSchema.typeFamily?.resources?.length ?? 0) > 0 + ) { typeFamilyFields.push({ fieldName: tsFieldName(fieldName), familyTypeName: field.type.name }); } } @@ -257,8 +260,7 @@ export class TypeScript extends Writer { } this.curlyBlock(["export", "interface", name, extendsClause], () => { if (isResourceTypeSchema(schema)) { - const possibleResourceTypes = [schema.identifier]; - possibleResourceTypes.push(...tsIndex.resourceChildren(schema.identifier)); + const possibleResourceTypes = [schema.identifier, ...(schema.typeFamily?.resources ?? [])]; const openSetSuffix = this.opts.openResourceTypeSet && possibleResourceTypes.length > 1 ? " | string" : ""; this.lineSM( diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 2670dfa0d..d0c30d8ac 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -115,11 +115,7 @@ export function extractDependencies( return concatIdentifiers(filtered); } -function transformFhirSchemaResource( - register: Register, - fhirSchema: RichFHIRSchema, - logger?: CodegenLog, -): TypeSchema[] { +export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSchema, logger?: CodegenLog): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); let base: Identifier | undefined; @@ -151,16 +147,9 @@ function transformFhirSchemaResource( description: fhirSchema.description, dependencies, extensions, + typeFamily: undefined, // NOTE: should be populateTypeFamily later. }; const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger); return [typeSchema, ...bindingSchemas]; } - -export async function transformFhirSchema( - register: Register, - fhirSchema: RichFHIRSchema, - logger?: CodegenLog, -): Promise { - return transformFhirSchemaResource(register, fhirSchema, logger); -} diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 6762cdfb0..5a26bbea8 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -118,7 +118,7 @@ export const generateTypeSchemas = async ( continue; } - for (const schema of await transformFhirSchema(register, fhirSchema, logger)) { + for (const schema of transformFhirSchema(register, fhirSchema, logger)) { schemasWithSources.push({ schema, sourcePackage: pkgId, diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 013affaae..6e851e017 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -94,8 +94,8 @@ type IdentifierBase = { }; type PrimitiveIdentifier = { kind: "primitive-type" } & IdentifierBase; -type ComplexTypeIdentifier = { kind: "complex-type" } & IdentifierBase; -type ResourceIdentifier = { kind: "resource" } & IdentifierBase; +export type ComplexTypeIdentifier = { kind: "complex-type" } & IdentifierBase; +export type ResourceIdentifier = { kind: "resource" } & IdentifierBase; export type ValueSetIdentifier = { kind: "value-set" } & IdentifierBase; export type NestedIdentifier = { kind: "nested" } & IdentifierBase; export type BindingIdentifier = { kind: "binding" } & IdentifierBase; @@ -271,6 +271,11 @@ export interface RegularTypeSchema { fields?: { [k: string]: Field }; nested?: NestedType[]; dependencies?: Identifier[]; + /** Transitive children grouped by kind (e.g. Resource → { resources: [DomainResource, Patient, …] }) */ + typeFamily?: { + resources?: ResourceIdentifier[]; + complexTypes?: ComplexTypeIdentifier[]; + }; } export interface RegularField { diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index ddba1c282..bcec80d73 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -12,9 +12,11 @@ import { type Identifier, isChoiceDeclarationField, isChoiceInstanceField, + isComplexTypeIdentifier, isComplexTypeTypeSchema, isLogicalTypeSchema, isProfileTypeSchema, + isResourceIdentifier, isResourceTypeSchema, isSpecializationTypeSchema, type PkgName, @@ -103,66 +105,41 @@ export const sortAsDeclarationSequence = (schemas: RegularTypeSchema[]): Regular }; /////////////////////////////////////////////////////////// -// Type Schema Relations - -interface TypeRelation { - parent: Identifier; - child: Identifier; -} - -const resourceRelatives = (schemas: TypeSchema[]): TypeRelation[] => { - const regularSchemas = schemas.filter( - (e) => isResourceTypeSchema(e) || isLogicalTypeSchema(e) || isComplexTypeTypeSchema(e), - ); - - const directPairs: TypeRelation[] = []; - const childrenByParent = new Map(); - - for (const schema of regularSchemas) { - if (schema.base) { - directPairs.push({ parent: schema.base, child: schema.identifier }); - const parentName = schema.base.name; - let children = childrenByParent.get(parentName); - if (!children) { - children = []; - childrenByParent.set(parentName, children); - } - children.push(schema.identifier); - } - } +// Type Family - const transitiveCache = new Map(); - const getTransitiveChildren = (parentName: string): Identifier[] => { - const cached = transitiveCache.get(parentName); - if (cached) return cached; +/** Populate `typeFamily` on specialization schemas with transitive children grouped by kind. */ +const populateTypeFamily = (schemas: TypeSchema[]): void => { + const directChildrenByParent: Record = {}; + for (const schema of schemas) { + if (!isSpecializationTypeSchema(schema) || !schema.base) continue; + const parentUrl = schema.base.url; + if (!directChildrenByParent[parentUrl]) directChildrenByParent[parentUrl] = []; + directChildrenByParent[parentUrl].push(schema.identifier); + } - const directChildren = childrenByParent.get(parentName) ?? []; - const result: Identifier[] = [...directChildren]; - for (const child of directChildren) { - result.push(...getTransitiveChildren(child.name)); + const transitiveCache: Record = {}; + const getTransitiveChildren = (parentUrl: string): Identifier[] => { + if (transitiveCache[parentUrl]) return transitiveCache[parentUrl]; + const direct = directChildrenByParent[parentUrl] ?? []; + const result: Identifier[] = [...direct]; + for (const child of direct) { + result.push(...getTransitiveChildren(child.url)); } - transitiveCache.set(parentName, result); + transitiveCache[parentUrl] = result; return result; }; - const seen = new Set(); - const allPairs: TypeRelation[] = []; - - for (const pair of directPairs) { - const key = `${pair.parent.name}|${pair.child.name}`; - seen.add(key); - allPairs.push(pair); - - for (const transitiveChild of getTransitiveChildren(pair.child.name)) { - const transitiveKey = `${pair.parent.name}|${transitiveChild.name}`; - if (!seen.has(transitiveKey)) { - seen.add(transitiveKey); - allPairs.push({ parent: pair.parent, child: transitiveChild }); - } - } + for (const schema of schemas) { + if (!isSpecializationTypeSchema(schema)) continue; + const allChildren = getTransitiveChildren(schema.identifier.url); + if (allChildren.length === 0) continue; + const resources = allChildren.filter(isResourceIdentifier); + const complexTypes = allChildren.filter(isComplexTypeIdentifier); + const family: NonNullable = {}; + if (resources.length > 0) family.resources = resources; + if (complexTypes.length > 0) family.complexTypes = complexTypes; + if (Object.keys(family).length > 0) schema.typeFamily = family; } - - return allPairs; }; /////////////////////////////////////////////////////////// @@ -170,7 +147,6 @@ const resourceRelatives = (schemas: TypeSchema[]): TypeRelation[] => { export type TypeSchemaIndex = { _schemaIndex: Record>; - _relations: TypeRelation[]; schemas: TypeSchema[]; schemasByPackage: Record; register?: Register; @@ -180,7 +156,6 @@ export type TypeSchemaIndex = { collectProfiles: () => ProfileTypeSchema[]; resolve: (id: Identifier) => TypeSchema | undefined; resolveByUrl: (pkgName: PkgName, url: CanonicalUrl) => TypeSchema | undefined; - resourceChildren: (id: Identifier) => Identifier[]; tryHierarchy: (schema: TypeSchema) => TypeSchema[] | undefined; hierarchy: (schema: TypeSchema) => TypeSchema[]; findLastSpecialization: (schema: TypeSchema) => TypeSchema; @@ -241,7 +216,7 @@ export const mkTypeSchemaIndex = ( for (const schema of schemas) { append(schema); } - const relations = resourceRelatives(schemas); + populateTypeFamily(schemas); const resolve = (id: Identifier) => { if (id.kind === "nested") return nestedIndex[id.url]?.[id.package]; @@ -269,10 +244,6 @@ export const mkTypeSchemaIndex = ( return undefined; }; - const resourceChildren = (id: Identifier): Identifier[] => { - return relations.filter((relative) => relative.parent.name === id.name).map((relative) => relative.child); - }; - const tryHierarchy = (schema: TypeSchema): TypeSchema[] | undefined => { const res: TypeSchema[] = []; let cur: TypeSchema | undefined = schema; @@ -476,7 +447,6 @@ export const mkTypeSchemaIndex = ( return { _schemaIndex: index, - _relations: relations, schemas, schemasByPackage: groupByPackages(schemas), register, @@ -486,7 +456,6 @@ export const mkTypeSchemaIndex = ( collectProfiles: () => schemas.filter(isProfileTypeSchema), resolve, resolveByUrl, - resourceChildren, tryHierarchy, hierarchy, findLastSpecialization, diff --git a/test/api/write-generator/__snapshots__/introspection.test.ts.snap b/test/api/write-generator/__snapshots__/introspection.test.ts.snap index 0f8b3613c..396621700 100644 --- a/test/api/write-generator/__snapshots__/introspection.test.ts.snap +++ b/test/api/write-generator/__snapshots__/introspection.test.ts.snap @@ -1712,9 +1712,9 @@ exports[`IntrospectionWriter - TypeSchema output Check OperationOutcome introspe exports[`IntrospectionWriter - TypeSchema output Check all introspection data in a single ndjson file 1`] = ` "{"identifier":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"OperationOutcome","url":"http://hl7.org/fhir/StructureDefinition/OperationOutcome"},"base":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"DomainResource","url":"http://hl7.org/fhir/StructureDefinition/DomainResource"},"fields":{"issue":{"type":{"kind":"nested","package":"hl7.fhir.r4.core","version":"4.0.1","name":"issue","url":"http://hl7.org/fhir/StructureDefinition/OperationOutcome#issue"},"array":true,"required":true,"excluded":false}},"nested":[{"identifier":{"kind":"nested","package":"hl7.fhir.r4.core","version":"4.0.1","name":"issue","url":"http://hl7.org/fhir/StructureDefinition/OperationOutcome#issue"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"BackboneElement","url":"http://hl7.org/fhir/StructureDefinition/BackboneElement"},"fields":{"severity":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":true,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueSeverity","url":"urn:fhir:binding:IssueSeverity"},"enum":{"isOpen":false,"values":["fatal","error","warning","information"]}},"code":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":true,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueType","url":"urn:fhir:binding:IssueType"},"enum":{"isOpen":false,"values":["invalid","structure","required","value","invariant","security","login","unknown","expired","forbidden","suppressed","processing","not-supported","duplicate","multiple-matches","not-found","deleted","too-long","code-invalid","extension","too-costly","business-rule","conflict","transient","lock-error","no-store","exception","timeout","incomplete","throttled","informational"]}},"details":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"CodeableConcept","url":"http://hl7.org/fhir/StructureDefinition/CodeableConcept"},"required":false,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueDetails","url":"urn:fhir:binding:IssueDetails"}},"diagnostics":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false},"location":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":true},"expression":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":true}}}],"description":"A collection of error, warning, or information messages that result from a system action.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"BackboneElement","url":"http://hl7.org/fhir/StructureDefinition/BackboneElement"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"CodeableConcept","url":"http://hl7.org/fhir/StructureDefinition/CodeableConcept"},{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"DomainResource","url":"http://hl7.org/fhir/StructureDefinition/DomainResource"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueDetails","url":"urn:fhir:binding:IssueDetails"},{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueSeverity","url":"urn:fhir:binding:IssueSeverity"},{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueType","url":"urn:fhir:binding:IssueType"}]} -{"identifier":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"DomainResource","url":"http://hl7.org/fhir/StructureDefinition/DomainResource"},"base":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"fields":{"text":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},"required":false,"excluded":false,"array":false},"contained":{"type":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"required":false,"excluded":false,"array":true}},"description":"A resource that includes narrative, extensions, and contained resources.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"}]} -{"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"BackboneElement","url":"http://hl7.org/fhir/StructureDefinition/BackboneElement"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{},"description":"Base StructureDefinition for BackboneElement Type: Base definition for all elements that are defined inside a resource - but not those in a data type.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"}]} -{"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{"id":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false}},"description":"Base StructureDefinition for Element Type: Base definition for all elements in a resource.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"}]} +{"identifier":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"DomainResource","url":"http://hl7.org/fhir/StructureDefinition/DomainResource"},"base":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"fields":{"text":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},"required":false,"excluded":false,"array":false},"contained":{"type":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"required":false,"excluded":false,"array":true}},"description":"A resource that includes narrative, extensions, and contained resources.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"}],"typeFamily":{"resources":[{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"OperationOutcome","url":"http://hl7.org/fhir/StructureDefinition/OperationOutcome"}]}} +{"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"BackboneElement","url":"http://hl7.org/fhir/StructureDefinition/BackboneElement"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{},"description":"Base StructureDefinition for BackboneElement Type: Base definition for all elements that are defined inside a resource - but not those in a data type.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"}],"typeFamily":{"complexTypes":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Dosage","url":"http://hl7.org/fhir/StructureDefinition/Dosage"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"ElementDefinition","url":"http://hl7.org/fhir/StructureDefinition/ElementDefinition"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"MarketingStatus","url":"http://hl7.org/fhir/StructureDefinition/MarketingStatus"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Population","url":"http://hl7.org/fhir/StructureDefinition/Population"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"ProdCharacteristic","url":"http://hl7.org/fhir/StructureDefinition/ProdCharacteristic"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"ProductShelfLife","url":"http://hl7.org/fhir/StructureDefinition/ProductShelfLife"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"SubstanceAmount","url":"http://hl7.org/fhir/StructureDefinition/SubstanceAmount"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Timing","url":"http://hl7.org/fhir/StructureDefinition/Timing"}]}} +{"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{"id":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false}},"description":"Base StructureDefinition for Element Type: Base definition for all elements in a resource.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"}],"typeFamily":{"complexTypes":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"BackboneElement","url":"http://hl7.org/fhir/StructureDefinition/BackboneElement"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"CodeableConcept","url":"http://hl7.org/fhir/StructureDefinition/CodeableConcept"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Coding","url":"http://hl7.org/fhir/StructureDefinition/Coding"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Meta","url":"http://hl7.org/fhir/StructureDefinition/Meta"}]}} {"identifier":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"base":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"description":"Base StructureDefinition for code type: A string which has at least one character and no leading or trailing whitespace and where there is no whitespace other than single spaces in the contents","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"}]} {"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"CodeableConcept","url":"http://hl7.org/fhir/StructureDefinition/CodeableConcept"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{"coding":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Coding","url":"http://hl7.org/fhir/StructureDefinition/Coding"},"required":false,"excluded":false,"array":true},"text":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false}},"description":"Base StructureDefinition for CodeableConcept Type: A concept that may be defined by a formal reference to a terminology or ontology or may be provided by text.","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Coding","url":"http://hl7.org/fhir/StructureDefinition/Coding"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"}]} {"identifier":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"description":"Base StructureDefinition for string Type: A sequence of Unicode characters","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"}]} @@ -1722,7 +1722,7 @@ exports[`IntrospectionWriter - TypeSchema output Check all introspection data in {"identifier":{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueSeverity","url":"urn:fhir:binding:IssueSeverity"},"valueset":{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"IssueSeverity","url":"http://hl7.org/fhir/ValueSet/issue-severity"},"strength":"required","enum":{"isOpen":false,"values":["fatal","error","warning","information"]},"dependencies":[{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"IssueSeverity","url":"http://hl7.org/fhir/ValueSet/issue-severity"}]} {"identifier":{"kind":"binding","package":"shared","version":"1.0.0","name":"IssueType","url":"urn:fhir:binding:IssueType"},"valueset":{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"IssueType","url":"http://hl7.org/fhir/ValueSet/issue-type"},"strength":"required","enum":{"isOpen":false,"values":["invalid","structure","required","value","invariant","security","login","unknown","expired","forbidden","suppressed","processing","not-supported","duplicate","multiple-matches","not-found","deleted","too-long","code-invalid","extension","too-costly","business-rule","conflict","transient","lock-error","no-store","exception","timeout","incomplete","throttled","informational"]},"dependencies":[{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"IssueType","url":"http://hl7.org/fhir/ValueSet/issue-type"}]} {"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Narrative","url":"http://hl7.org/fhir/StructureDefinition/Narrative"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{"status":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":true,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"NarrativeStatus","url":"urn:fhir:binding:NarrativeStatus"},"enum":{"isOpen":false,"values":["generated","extensions","additional","empty"]}},"div":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"xhtml","url":"http://hl7.org/fhir/StructureDefinition/xhtml"},"required":true,"excluded":false,"array":false}},"description":"Base StructureDefinition for Narrative Type: A human-readable summary of the resource conveying the essential clinical and business information for the resource.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"xhtml","url":"http://hl7.org/fhir/StructureDefinition/xhtml"},{"kind":"binding","package":"shared","version":"1.0.0","name":"NarrativeStatus","url":"urn:fhir:binding:NarrativeStatus"}]} -{"identifier":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"fields":{"id":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false},"meta":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Meta","url":"http://hl7.org/fhir/StructureDefinition/Meta"},"required":false,"excluded":false,"array":false},"implicitRules":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"},"required":false,"excluded":false,"array":false},"language":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":false,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"Language","url":"urn:fhir:binding:Language"},"enum":{"isOpen":true,"values":["ar","bn","cs","da","de","de-AT","de-CH","de-DE","el","en","en-AU","en-CA","en-GB","en-IN","en-NZ","en-SG","en-US","es","es-AR","es-ES","es-UY","fi","fr","fr-BE","fr-CH","fr-FR","fy","fy-NL","hi","hr","it","it-CH","it-IT","ja","ko","nl","nl-BE","nl-NL","no","no-NO","pa","pl","pt","pt-BR","ru","ru-RU","sr","sr-RS","sv","sv-SE","te","zh","zh-CN","zh-HK","zh-SG","zh-TW"]}}},"description":"This is the base resource type for everything.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Meta","url":"http://hl7.org/fhir/StructureDefinition/Meta"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"},{"kind":"binding","package":"shared","version":"1.0.0","name":"Language","url":"urn:fhir:binding:Language"}]} +{"identifier":{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Resource","url":"http://hl7.org/fhir/StructureDefinition/Resource"},"fields":{"id":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false},"meta":{"type":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Meta","url":"http://hl7.org/fhir/StructureDefinition/Meta"},"required":false,"excluded":false,"array":false},"implicitRules":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"},"required":false,"excluded":false,"array":false},"language":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":false,"excluded":false,"array":false,"binding":{"kind":"binding","package":"shared","version":"1.0.0","name":"Language","url":"urn:fhir:binding:Language"},"enum":{"isOpen":true,"values":["ar","bn","cs","da","de","de-AT","de-CH","de-DE","el","en","en-AU","en-CA","en-GB","en-IN","en-NZ","en-SG","en-US","es","es-AR","es-ES","es-UY","fi","fr","fr-BE","fr-CH","fr-FR","fy","fy-NL","hi","hr","it","it-CH","it-IT","ja","ko","nl","nl-BE","nl-NL","no","no-NO","pa","pl","pt","pt-BR","ru","ru-RU","sr","sr-RS","sv","sv-SE","te","zh","zh-CN","zh-HK","zh-SG","zh-TW"]}}},"description":"This is the base resource type for everything.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Meta","url":"http://hl7.org/fhir/StructureDefinition/Meta"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"},{"kind":"binding","package":"shared","version":"1.0.0","name":"Language","url":"urn:fhir:binding:Language"}],"typeFamily":{"resources":[{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"DomainResource","url":"http://hl7.org/fhir/StructureDefinition/DomainResource"},{"kind":"resource","package":"hl7.fhir.r4.core","version":"4.0.1","name":"OperationOutcome","url":"http://hl7.org/fhir/StructureDefinition/OperationOutcome"}]}} {"identifier":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Coding","url":"http://hl7.org/fhir/StructureDefinition/Coding"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"fields":{"system":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"},"required":false,"excluded":false,"array":false},"version":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false},"code":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},"required":false,"excluded":false,"array":false},"display":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},"required":false,"excluded":false,"array":false},"userSelected":{"type":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"boolean","url":"http://hl7.org/fhir/StructureDefinition/boolean"},"required":false,"excluded":false,"array":false}},"description":"Base StructureDefinition for Coding Type: A reference to a code defined by a terminology system.","dependencies":[{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"boolean","url":"http://hl7.org/fhir/StructureDefinition/boolean"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"code","url":"http://hl7.org/fhir/StructureDefinition/code"},{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"string","url":"http://hl7.org/fhir/StructureDefinition/string"},{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"uri","url":"http://hl7.org/fhir/StructureDefinition/uri"}]} {"identifier":{"kind":"primitive-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"xhtml","url":"http://hl7.org/fhir/StructureDefinition/xhtml"},"base":{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"},"description":"Base StructureDefinition for xhtml Type","dependencies":[{"kind":"complex-type","package":"hl7.fhir.r4.core","version":"4.0.1","name":"Element","url":"http://hl7.org/fhir/StructureDefinition/Element"}]} {"identifier":{"kind":"binding","package":"shared","version":"1.0.0","name":"NarrativeStatus","url":"urn:fhir:binding:NarrativeStatus"},"valueset":{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"NarrativeStatus","url":"http://hl7.org/fhir/ValueSet/narrative-status"},"strength":"required","enum":{"isOpen":false,"values":["generated","extensions","additional","empty"]},"dependencies":[{"kind":"value-set","package":"hl7.fhir.r4.core","version":"4.0.1","name":"NarrativeStatus","url":"http://hl7.org/fhir/ValueSet/narrative-status"}]} diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 51871de30..e619f27de 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -24,6 +24,21 @@ describe("TypeScript Writer Generator", async () => { expect(ccTs).toContain("export interface CodeableConcept"); expect(ccTs).toContain("coding?: Coding[]"); }); + it("generates BundleEntry with generic type-family parameter", async () => { + const bundleTs = result.filesGenerated["generated/types/hl7-fhir-r4-core/Bundle.ts"]; + expect(bundleTs).toContain("export interface BundleEntry"); + expect(bundleTs).toContain("resource?: T"); + }); + it("generates BundleEntryResponse with generic type-family parameter", async () => { + const bundleTs = result.filesGenerated["generated/types/hl7-fhir-r4-core/Bundle.ts"]; + expect(bundleTs).toContain("export interface BundleEntryResponse"); + expect(bundleTs).toContain("outcome?: T"); + }); + it("generates DomainResource with generic type-family parameter", async () => { + const domainResourceTs = result.filesGenerated["generated/types/hl7-fhir-r4-core/DomainResource.ts"]; + expect(domainResourceTs).toContain("export interface DomainResource"); + expect(domainResourceTs).toContain("contained?: T[]"); + }); }); describe("TypeScript CDA with Logical Model Promotion to Resource", async () => { diff --git a/test/unit/typeschema/utils.test.ts b/test/unit/typeschema/utils.test.ts index cf9aa34c9..f58651457 100644 --- a/test/unit/typeschema/utils.test.ts +++ b/test/unit/typeschema/utils.test.ts @@ -190,6 +190,130 @@ describe("TypeSchema Index", () => { }); }); + describe("typeFamily", () => { + it("should populate typeFamily.resources on resource schemas with children", () => { + const resourceSchema: RegularTypeSchema = { + identifier: { + name: "Resource" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Resource" as CanonicalUrl, + }, + }; + const domainSchema: RegularTypeSchema = { + identifier: { + name: "DomainResource" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/DomainResource" as CanonicalUrl, + }, + base: resourceSchema.identifier, + }; + const patientSchema: RegularTypeSchema = { + identifier: { + name: "Patient" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Patient" as CanonicalUrl, + }, + base: domainSchema.identifier, + }; + + mkTypeSchemaIndex([resourceSchema, domainSchema, patientSchema], {}); + + // Resource has DomainResource and Patient as transitive resource children + expect(resourceSchema.typeFamily?.resources?.map((id) => id.name as string).sort()).toEqual([ + "DomainResource", + "Patient", + ]); + // DomainResource has Patient as child + expect(domainSchema.typeFamily?.resources?.map((id) => id.name as string)).toEqual(["Patient"]); + // Patient is a leaf — no typeFamily + expect(patientSchema.typeFamily).toBeUndefined(); + }); + + it("should populate typeFamily.complexTypes on complex-type hierarchies", () => { + const elementSchema: RegularTypeSchema = { + identifier: { + name: "Element" as Name, + package: "test", + kind: "complex-type", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Element" as CanonicalUrl, + }, + }; + const backboneSchema: RegularTypeSchema = { + identifier: { + name: "BackboneElement" as Name, + package: "test", + kind: "complex-type", + version: "1.0.0", + url: "http://example.org/StructureDefinition/BackboneElement" as CanonicalUrl, + }, + base: elementSchema.identifier, + }; + + mkTypeSchemaIndex([elementSchema, backboneSchema], {}); + + // Element has BackboneElement as a complex-type child + expect(elementSchema.typeFamily?.complexTypes?.map((id) => id.name as string)).toEqual(["BackboneElement"]); + expect(elementSchema.typeFamily?.resources).toBeUndefined(); + // BackboneElement is a leaf — no typeFamily + expect(backboneSchema.typeFamily).toBeUndefined(); + }); + + it("should populate both resources and complexTypes for mixed hierarchies", () => { + const resourceSchema: RegularTypeSchema = { + identifier: { + name: "Resource" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Resource" as CanonicalUrl, + }, + }; + const patientSchema: RegularTypeSchema = { + identifier: { + name: "Patient" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Patient" as CanonicalUrl, + }, + base: resourceSchema.identifier, + }; + const elementSchema: RegularTypeSchema = { + identifier: { + name: "Element" as Name, + package: "test", + kind: "complex-type", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Element" as CanonicalUrl, + }, + }; + const codingSchema: RegularTypeSchema = { + identifier: { + name: "Coding" as Name, + package: "test", + kind: "complex-type", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Coding" as CanonicalUrl, + }, + base: elementSchema.identifier, + }; + + mkTypeSchemaIndex([resourceSchema, patientSchema, elementSchema, codingSchema], {}); + + expect(resourceSchema.typeFamily?.resources?.map((id) => id.name as string)).toEqual(["Patient"]); + expect(resourceSchema.typeFamily?.complexTypes).toBeUndefined(); + expect(elementSchema.typeFamily?.complexTypes?.map((id) => id.name as string)).toEqual(["Coding"]); + expect(elementSchema.typeFamily?.resources).toBeUndefined(); + }); + }); + describe("flatProfile", () => { it("should flatten a profile with a single constraint", () => { const baseSchema: RegularTypeSchema = { diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index b2d51468a..219a1ae58 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -60,12 +60,12 @@ export const resolveTs = async ( ) => { const rfs = register.resolveFs(pkgMeta, url as CanonicalUrl); if (!rfs) throw new Error("Failed to resolve registered FHIR schema"); - return await transformFhirSchema(register, rfs, logger); + return transformFhirSchema(register, rfs, logger); }; export const registerFsAndMkTs = async (register: Register, fs: PFS, logger: CodegenLog) => { const rfs = registerFs(register, fs); - return await transformFhirSchema(register, rfs, logger); + return transformFhirSchema(register, rfs, logger); }; export const transformVS = async (register: Register, pkg: PackageMeta, vs: PVS) => {