babelfhir-ts 1.5.6 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -353,10 +353,11 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
353
353
  }
354
354
  }
355
355
  // Conservative mode: if we failed to fetch base metadata (no base fields) skip emitting certain core properties
356
- // to avoid degrading their types to primitives. Only skip if not profiled and not fixed.
356
+ // to avoid degrading their types to primitives. Only skip if not profiled, not fixed, and no narrowing binding.
357
357
  if (baseFieldByLastSegment.size === 0 && parentInterfaceName === interfaceName && fieldParts.length === 2 && fieldParts[0] === baseResource) {
358
358
  const conservativeSkip = new Set(['gender', 'status', 'intent', 'hospitalization', 'payor', 'entry', 'concept', 'identifier', 'use', 'system', 'priority']);
359
- if (!field.isProfiled && field.fixedValue === undefined && conservativeSkip.has(fieldName)) {
359
+ const hasNarrowingBinding = !!(field.binding?.codes && field.binding.codes.length > 0 && field.binding.strength === 'required');
360
+ if (!field.isProfiled && field.fixedValue === undefined && !hasNarrowingBinding && conservativeSkip.has(fieldName)) {
360
361
  debug('conservative skip (no base metadata)', field.name);
361
362
  return;
362
363
  }
@@ -13,10 +13,13 @@ export function handleFlatField(args) {
13
13
  const conservative = baseFieldByLastSegment.size === 0;
14
14
  const isRootInterface = parentInterfaceName === interfaceName;
15
15
  const isNested = isRootInterface ? fieldParts.length > 1 : fieldParts.length >= 1;
16
+ // Does this field carry a narrowing required ValueSet binding with resolved codes?
17
+ // If so, it should bypass skip heuristics so we can emit a union literal type.
18
+ const hasNarrowingBinding = !!(field.binding?.codes && field.binding.codes.length > 0 && field.binding.strength === 'required');
16
19
  // Skip deeply nested enumerated fields at root level - they'll be handled by nested interfaces
17
20
  // (e.g., Patient.telecom.system should be emitted in UKCorePatientTelecom, not UKCorePatient)
18
21
  const enumeratedCodeFields = new Set(['system', 'use', 'status', 'gender', 'intent', 'priority', 'class', 'category', 'clinicalStatus', 'verificationStatus']);
19
- if (fieldParts.length >= 3 && enumeratedCodeFields.has(fieldName) && !field.fixedValue) {
22
+ if (fieldParts.length >= 3 && enumeratedCodeFields.has(fieldName) && !field.fixedValue && !hasNarrowingBinding) {
20
23
  debug('skip deeply nested enumerated field at root level', field.name);
21
24
  return true;
22
25
  }
@@ -73,7 +76,7 @@ export function handleFlatField(args) {
73
76
  // Even if cardinality or mustSupport changed, we should NOT emit if it would widen the type
74
77
  // (e.g., base has category?: ('food'|'medication'|'environment'|'biologic')[] but we'd emit category: string[])
75
78
  // TypeScript doesn't allow assigning string[] to a more specific string literal union array
76
- if (isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !field.isProfiled && !field.fixedValue) {
79
+ if (isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !field.isProfiled && !field.fixedValue && !hasNarrowingBinding) {
77
80
  debug('skip primitive override of base child - would widen type', field.name, 'mappedType=', mapped);
78
81
  return true;
79
82
  }
@@ -118,7 +121,7 @@ export function handleFlatField(args) {
118
121
  const enumeratedSkip = new Set(['status', 'gender', 'class', 'category', 'concept', 'entry', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority']);
119
122
  const mustSupportChanged = baseField && (!baseField.mustSupport && field.mustSupport);
120
123
  const cardinalityChanged = baseField && (baseField.isOptional !== field.isOptional || baseField.isArray !== field.isArray);
121
- if (parentInterfaceName === interfaceName && !field.fixedValue && !mustSupportChanged && !cardinalityChanged) {
124
+ if (parentInterfaceName === interfaceName && !field.fixedValue && !hasNarrowingBinding && !mustSupportChanged && !cardinalityChanged) {
122
125
  if (enumeratedSkip.has(fieldName))
123
126
  return true;
124
127
  if (baseResource === 'UsageContext' && fieldName === 'code' && field.type === 'string')
@@ -160,7 +163,7 @@ export function handleFlatField(args) {
160
163
  }
161
164
  // Special cases: known enumerated code fields (status, gender, intent, class, category) -> avoid overriding unless fixed or mustSupport changed
162
165
  const mustSupportChanged = baseField.mustSupport !== field.mustSupport && field.mustSupport === true;
163
- if (!field.fixedValue && !mustSupportChanged && ['status', 'gender', 'intent', 'class', 'category', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority'].includes(fieldName)) {
166
+ if (!field.fixedValue && !hasNarrowingBinding && !mustSupportChanged && ['status', 'gender', 'intent', 'class', 'category', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority'].includes(fieldName)) {
164
167
  return true;
165
168
  }
166
169
  // CodeSystem.concept & Bundle.entry etc complex arrays: avoid overriding unless truly adding constraints
@@ -171,11 +174,12 @@ export function handleFlatField(args) {
171
174
  if (['concept', 'entry', 'content'].includes(fieldName) && !field.fixedValue)
172
175
  return true;
173
176
  // Even if mustSupport changes, we should inherit the base enum type, not override with string
174
- if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'content', 'use', 'system', 'priority'].includes(fieldName) && isPrimitiveType(mapped)) {
177
+ if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'content', 'use', 'system', 'priority'].includes(fieldName) && isPrimitiveType(mapped) && !hasNarrowingBinding) {
175
178
  return true;
176
179
  }
177
180
  // Skip if identical to base (same type, array flag, optionality, AND mustSupport)
178
- if (!field.fixedValue && !hasExtensionSlices) {
181
+ // Exception: fields with narrowing bindings should still be emitted (they constrain the type)
182
+ if (!field.fixedValue && !hasExtensionSlices && !hasNarrowingBinding) {
179
183
  const mappedType = mapTypeToTS(field.type || 'string');
180
184
  const mustSupportSame = baseField.mustSupport === field.mustSupport;
181
185
  if ((baseField.type === field.type || baseField.type === mappedType) && baseField.isArray === field.isArray && baseField.isOptional === field.isOptional && mustSupportSame) {
@@ -449,6 +453,25 @@ export function handleFlatField(args) {
449
453
  }
450
454
  // Apply ValueSet binding constraints for CodeableConcept types
451
455
  fieldType = applyBindingConstraint(fieldType, field, fields);
456
+ // Apply ValueSet binding constraints for primitive code fields (generates union literal type)
457
+ if (isPrimitiveType(fieldType) && hasNarrowingBinding && field.binding?.codes) {
458
+ const vsUri = field.binding.uri?.split('|')[0];
459
+ const vs = vsUri && valueSets ? (valueSets.get(vsUri) || valueSets.get(field.binding.uri)) : undefined;
460
+ if (vs && vs.isSmall && vs.concepts.length > 0) {
461
+ // Import the ValueSet Code type from the generated valuesets directory
462
+ const sanitizedVSName = sanitizeValueSetName(vs.name);
463
+ const vsTypeName = `${sanitizedVSName}Code`;
464
+ importManager.addPathBasedImport(vsTypeName, './valuesets/ValueSet-' + sanitizedVSName);
465
+ fieldType = vsTypeName;
466
+ debug(`Applied code binding constraint for ${field.name}: ${vsTypeName}`);
467
+ }
468
+ else {
469
+ // Fall back to inline union from binding.codes (wrap in parens for array compatibility)
470
+ const inlineUnion = field.binding.codes.map(c => JSON.stringify(c.code)).join(' | ');
471
+ fieldType = field.binding.codes.length > 1 ? `(${inlineUnion})` : inlineUnion;
472
+ debug(`Applied inline code binding constraint for ${field.name}: ${fieldType}`);
473
+ }
474
+ }
452
475
  // Preserve arrayness from base when known; if base metadata is missing and field name is a known repeating element (e.g., 'coverage' on Account),
453
476
  // default to array to avoid incorrect narrowing.
454
477
  // Skip adding [] if fieldType already contains array syntax (e.g., from identifier/extension union types)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",