babelfhir-ts 1.1.0 → 1.1.3

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.
Files changed (69) hide show
  1. package/README.md +2 -0
  2. package/out/src/generator/{constants.js → core/constants.js} +2 -1
  3. package/out/src/generator/{utils.js → core/utils.js} +3 -3
  4. package/out/src/generator/{classGenerator.js → emitters/class/classGenerator.js} +30 -9
  5. package/out/src/generator/{classHeuristics.js → emitters/class/classHeuristics.js} +16 -2
  6. package/out/src/generator/{enrichResourceHandlers.js → emitters/class/enrichResourceHandlers.js} +42 -11
  7. package/out/src/generator/{enrichResourceTemplate.js → emitters/class/enrichResourceTemplate.js} +4 -2
  8. package/out/src/generator/{fieldPatternExtractor.js → emitters/class/fieldPatternExtractor.js} +1 -1
  9. package/out/src/generator/{randomUtilitiesTemplate.js → emitters/class/randomUtilitiesTemplate.js} +5 -3
  10. package/out/src/generator/{sliceElementGenerator.js → emitters/class/sliceElementGenerator.js} +21 -4
  11. package/out/src/generator/{clientGenerator.js → emitters/client/clientGenerator.js} +13 -10
  12. package/out/src/generator/{clientReadmeGenerator.js → emitters/client/clientReadmeGenerator.js} +6 -3
  13. package/out/src/generator/{smartAuthGenerator.js → emitters/client/smartAuthGenerator.js} +1 -1
  14. package/out/src/generator/{importManager.js → emitters/interface/importManager.js} +17 -17
  15. package/out/src/generator/{interfaceFieldProcessor.js → emitters/interface/interfaceFieldProcessor.js} +14 -14
  16. package/out/src/generator/{interfaceGenerator.js → emitters/interface/interfaceGenerator.js} +9 -11
  17. package/out/src/generator/{postProcessExtensions.js → emitters/interface/postProcessExtensions.js} +4 -4
  18. package/out/src/generator/{processFlatField.js → emitters/interface/processFlatField.js} +11 -7
  19. package/out/src/generator/{processNestedField.js → emitters/interface/processNestedField.js} +10 -7
  20. package/out/src/generator/{sliceValidatorGenerator.js → emitters/validator/sliceValidatorGenerator.js} +0 -2
  21. package/out/src/generator/{validatorGenerator.js → emitters/validator/validatorGenerator.js} +5 -4
  22. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +552 -0
  23. package/out/src/generator/emitters/zod/zodStubInstaller.js +77 -0
  24. package/out/src/generator/emitters/zod/zodTypes.js +27 -0
  25. package/out/src/generator/fhir/r4/context.js +21 -0
  26. package/out/src/generator/fhir/r4/context.ts +24 -0
  27. package/out/src/generator/fhir/r4/dataTypes.json +858 -0
  28. package/out/src/generator/fhir/r4/interfaces.json +664 -0
  29. package/out/src/generator/fhir/r4/resources.json +150 -0
  30. package/out/src/generator/{fhirR4Rules.js → fhir/r4/rules.js} +3 -3
  31. package/out/src/generator/fhir/r4/rules.ts +226 -0
  32. package/out/src/generator/fhir/r4b/context.js +21 -0
  33. package/out/src/generator/fhir/r4b/context.ts +24 -0
  34. package/out/src/generator/fhir/r4b/dataTypes.json +860 -0
  35. package/out/src/generator/fhir/r4b/interfaces.json +684 -0
  36. package/out/src/generator/fhir/r4b/resources.json +145 -0
  37. package/out/src/generator/fhir/r4b/rules.js +171 -0
  38. package/out/src/generator/fhir/r4b/rules.ts +190 -0
  39. package/out/src/generator/fhir/types.js +27 -0
  40. package/out/src/generator/fhir/types.ts +80 -0
  41. package/out/src/generator/fhir/versionContext.js +52 -0
  42. package/out/src/generator/fhir/versionContext.ts +66 -0
  43. package/out/src/generator/index.js +72 -33
  44. package/out/src/generator/{packageManager.js → parser/packageManager.js} +2 -2
  45. package/out/src/generator/{packageParser.js → parser/packageParser.js} +10 -6
  46. package/out/src/generator/{sdFetcher.js → parser/sdFetcher.js} +25 -19
  47. package/out/src/generator/{sdParser.js → parser/sdParser.js} +28 -28
  48. package/out/src/generator/{sliceAggregator.js → parser/sliceAggregator.js} +7 -4
  49. package/out/src/generator/{txClient.js → parser/txClient.js} +9 -4
  50. package/out/src/generator/{vsParser.js → parser/vsParser.js} +4 -4
  51. package/out/src/generator/sdProcessor.js +36 -15
  52. package/out/src/main.js +33 -8
  53. package/package.json +5 -3
  54. package/out/src/generator/fhirDataTypes.json +0 -858
  55. package/out/src/generator/fhirInterfaces.json +0 -665
  56. package/out/src/generator/fhirResources.json +0 -157
  57. /package/out/src/generator/{cacheConfig.js → core/cacheConfig.js} +0 -0
  58. /package/out/src/generator/{fetchUtils.js → core/fetchUtils.js} +0 -0
  59. /package/out/src/generator/{sdTypes.js → core/sdTypes.js} +0 -0
  60. /package/out/src/generator/{randomSupportGenerator.js → emitters/class/randomSupportGenerator.js} +0 -0
  61. /package/out/src/generator/{valueSetGenerator.js → emitters/valueset/valueSetGenerator.js} +0 -0
  62. /package/out/src/generator/{base-r4 → fhir/r4/base}/Bundle.json +0 -0
  63. /package/out/src/generator/{base-r4 → fhir/r4/base}/Composition.json +0 -0
  64. /package/out/src/generator/{base-r4 → fhir/r4/base}/Endpoint.json +0 -0
  65. /package/out/src/generator/{base-r4 → fhir/r4/base}/README.md +0 -0
  66. /package/out/src/generator/{base-r4 → fhir/r4/base}/Subscription.json +0 -0
  67. /package/out/src/generator/{base-r4 → fhir/r4/base}/ValueSet.json +0 -0
  68. /package/out/src/generator/{base-r4 → fhir/r4/base}/backport-subscription.json +0 -0
  69. /package/out/src/generator/{base-r4 → fhir/r4/base}/well-known-system-codes.json +0 -0
package/README.md CHANGED
@@ -135,6 +135,8 @@ Options:
135
135
  --no-cache Delete .cache folder after generation
136
136
  --no-classes Only generate interfaces and types (skip class generation)
137
137
  --no-client Skip FHIR client generation (client generated by default)
138
+ --schema <format> Generate schema files alongside outputs (supported: zod)
139
+ --fhir-version <ver> FHIR version to target: r4 (default) or r4b
138
140
  --package <pkg@version> Download FHIR package from registry and process it
139
141
  --registry <url> FHIR package registry URL (default: https://packages.simplifier.net)
140
142
  --tx-server <url> Terminology server URL for ValueSet expansion (e.g., https://tx.fhir.org/r4)
@@ -10,10 +10,11 @@ import { fileURLToPath } from 'url';
10
10
  import path from 'path';
11
11
  const require = createRequire(import.meta.url);
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
- // Resolve package.json — works from both src/generator/ (dev) and out/src/generator/ (compiled)
13
+ // Resolve package.json — works from src/generator/core (dev) and out/src/generator/core (compiled)
14
14
  const _pkgPath = [
15
15
  path.resolve(__dirname, '../../package.json'),
16
16
  path.resolve(__dirname, '../../../package.json'),
17
+ path.resolve(__dirname, '../../../../package.json'),
17
18
  ].find(p => {
18
19
  try {
19
20
  return JSON.parse(require('fs').readFileSync(p, 'utf8')).name === 'babelfhir-ts';
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { logger } from "../logger.js";
4
- import fhirResourceNames from "./fhirResources.json" with { type: "json" };
3
+ import { logger } from "../../logger.js";
4
+ import { ctx } from '../fhir/versionContext.js';
5
5
  /**
6
6
  * Strips the version suffix from a FHIR canonical URL.
7
7
  * FHIR canonical URLs use '|' to separate the URL from the version.
@@ -82,7 +82,7 @@ export function getResourceTypes(options) {
82
82
  if (extendsMatch) {
83
83
  const baseType = extendsMatch[1];
84
84
  // Check if it's a known FHIR resource type
85
- if (fhirResourceNames.includes(baseType)) {
85
+ if (ctx().resourceNames.includes(baseType)) {
86
86
  resourceTypes.set(profileName, baseType);
87
87
  }
88
88
  }
@@ -1,5 +1,5 @@
1
- import { FHIR_PRIMITIVE_TYPES, CORE_RESOURCE_BASES } from './fhirR4Rules.js';
2
- import { TS, } from './constants.js';
1
+ import { ctx } from '../../fhir/versionContext.js';
2
+ import { TS, } from '../../core/constants.js';
3
3
  import { primitivePlaceholder, generateCodeDefault, makeCodeableConcept, chooseChoiceVariant, applyRequiredFieldHeuristic, deriveIdentifierConfig, } from './classHeuristics.js';
4
4
  import { generateSliceElement, buildNestedRequirementsObject } from './sliceElementGenerator.js';
5
5
  /**
@@ -83,6 +83,7 @@ function generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBinding
83
83
  }
84
84
  export function generateClass(className, interfaceName, baseResource, requiredFields = [], profileUrl, fieldPatterns = [], forbiddenFields = [], valueSetBindings = {}, fieldOrder = []) {
85
85
  // Decide whether to emit resourceType. If baseResource points at a known resource or ends with 'Resource'.
86
+ const CORE_RESOURCE_BASES = ctx().coreResourceBases;
86
87
  const addResourceType = !!baseResource && CORE_RESOURCE_BASES.has(baseResource);
87
88
  const resourceTypeLine = addResourceType && baseResource && baseResource !== 'Resource' ? `resourceType: '${baseResource}',` : (addResourceType ? `resourceType: 'Resource',` : '');
88
89
  // Only true FHIR Resources (not datatypes like Identifier, Attachment, Extension) can carry meta.profile.
@@ -117,7 +118,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
117
118
  };
118
119
  // Helper to check if a field's type (or baseType) matches a given type name
119
120
  // Handles profiled types like 'CodeableConcept-uv-ips' mapping back to 'CodeableConcept'
120
- const isType = (rf, typeName) => rf.type === typeName || rf.baseType === typeName;
121
+ const _isType = (rf, typeName) => rf.type === typeName || rf.baseType === typeName;
121
122
  for (const rf0 of requiredFields) {
122
123
  if (rf0.name === 'id' || rf0.name === 'resourceType')
123
124
  continue;
@@ -367,10 +368,25 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
367
368
  requiredInits.push(`${key(rf.name)}: ${initExpr}`);
368
369
  continue;
369
370
  }
370
- if (['string', 'number', 'boolean', 'code'].includes(rf.type)) {
371
+ // Handle all FHIR primitive types: string-like (uri, url, canonical, etc.), number-like (decimal, integer, etc.)
372
+ if (['string', 'uri', 'url', 'canonical', 'oid', 'uuid', 'id', 'markdown', 'base64Binary', 'xhtml'].includes(rf.type)) {
373
+ const initExpr = buildValueExpression(rf, primitivePlaceholder('string'));
374
+ requiredInits.push(`${key(rf.name)}: ${initExpr}`);
375
+ continue;
376
+ }
377
+ if (['number', 'decimal', 'integer', 'positiveInt', 'unsignedInt'].includes(rf.type)) {
378
+ const initExpr = buildValueExpression(rf, primitivePlaceholder('number'));
379
+ requiredInits.push(`${key(rf.name)}: ${initExpr}`);
380
+ continue;
381
+ }
382
+ if (rf.type === 'boolean') {
383
+ const initExpr = buildValueExpression(rf, primitivePlaceholder('boolean'));
384
+ requiredInits.push(`${key(rf.name)}: ${initExpr}`);
385
+ continue;
386
+ }
387
+ if (rf.type === 'code') {
371
388
  // For 'code' type without binding, generate based on field name to pick a sensible default
372
- const expr = rf.type === 'code' ? generateCodeDefault(rf.name) : primitivePlaceholder(rf.type);
373
- const initExpr = buildValueExpression(rf, expr);
389
+ const initExpr = buildValueExpression(rf, generateCodeDefault(rf.name));
374
390
  requiredInits.push(`${key(rf.name)}: ${initExpr}`);
375
391
  continue;
376
392
  }
@@ -546,7 +562,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
546
562
  // Skip FHIR primitive types - they cannot have nested properties (only _fieldName extensions)
547
563
  if (rf.nestedRequirements && rf.nestedRequirements.length > 0 &&
548
564
  rf.type !== 'Reference' && rf.type !== 'Extension' &&
549
- !FHIR_PRIMITIVE_TYPES.has(rf.type)) {
565
+ !ctx().primitiveTypes.has(rf.type)) {
550
566
  const objectLiteral = buildNestedRequirementsObject(rf.nestedRequirements);
551
567
  if (objectLiteral) {
552
568
  const initExpr = buildValueExpression(rf, objectLiteral, { isObjectLiteral: true });
@@ -562,8 +578,13 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
562
578
  requiredInits.push(`${key(rf.name)}: ${initExpr}`);
563
579
  continue;
564
580
  }
565
- // For other complex types not in skeleton map, don't initialize - let enrichment handle it
566
- // This includes Extension, BackboneElement, and other complex types
581
+ // For other complex types not in skeleton map, generate a minimal empty object
582
+ // This handles logical model types (e.g., DocumentSection) and other custom complex types
583
+ if (rf.type !== 'Extension' && !ctx().primitiveTypes.has(rf.type)) {
584
+ const initExpr = buildValueExpression(rf, '{}', { isObjectLiteral: true });
585
+ requiredInits.push(`${key(rf.name)}: ${initExpr}`);
586
+ continue;
587
+ }
567
588
  }
568
589
  // Special constraint handling: Organization org-1 requires name OR identifier
569
590
  // If neither is already in requiredInits, add name as the simplest solution
@@ -1,4 +1,4 @@
1
- import { TS, FALLBACK_CODES, CODE_FIELD_DEFAULTS, DEFAULT_CONTENT_TYPE, DEFAULT_BASE64, resolveValueSetDefault, pickPreferredCode, } from './constants.js';
1
+ import { TS, FALLBACK_CODES, CODE_FIELD_DEFAULTS, DEFAULT_CONTENT_TYPE, DEFAULT_BASE64, resolveValueSetDefault, pickPreferredCode, } from '../../core/constants.js';
2
2
  /**
3
3
  * Generate a simple placeholder value expression for a primitive type.
4
4
  * We keep this VERY small to avoid pulling in heavy util logic.
@@ -40,7 +40,21 @@ export function chooseChoiceVariant(rf) {
40
40
  // Extract the type from the slice name (e.g., "valueQuantity" -> "Quantity")
41
41
  const typeSuffix = slice.name.slice(base.length);
42
42
  if (typeSuffix) {
43
- return { propertyName: slice.name, chosenType: typeSuffix };
43
+ // Normalize primitive type suffixes to lowercase to match typeOptions convention
44
+ // (e.g., "Boolean" -> "boolean", "Integer" -> "integer", "DateTime" -> "dateTime")
45
+ const normalizedType = typeSuffix === 'Boolean' ? 'boolean'
46
+ : typeSuffix === 'Integer' ? 'integer'
47
+ : typeSuffix === 'String' ? 'string'
48
+ : typeSuffix === 'DateTime' ? 'dateTime'
49
+ : typeSuffix === 'Instant' ? 'instant'
50
+ : typeSuffix === 'Decimal' ? 'decimal'
51
+ : typeSuffix === 'Date' ? 'date'
52
+ : typeSuffix === 'Time' ? 'time'
53
+ : typeSuffix === 'Code' ? 'code'
54
+ : typeSuffix === 'PositiveInt' ? 'positiveInt'
55
+ : typeSuffix === 'UnsignedInt' ? 'unsignedInt'
56
+ : typeSuffix;
57
+ return { propertyName: slice.name, chosenType: normalizedType };
44
58
  }
45
59
  }
46
60
  }
@@ -1,4 +1,5 @@
1
- import { DEFAULT_COUNTRY, TS } from './constants.js';
1
+ import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
2
+ import { versionString } from '../../fhir/versionContext.js';
2
3
  export function generateEnrichResourceEnd() {
3
4
  return ` // Device (exact match — not DeviceRequest)
4
5
  if (/^Device$/i.test(baseResource)) {
@@ -29,7 +30,9 @@ export function generateEnrichResourceEnd() {
29
30
  // ValueSet - url must be a valid absolute URI (HTTP URL or proper URN)
30
31
  if (/ValueSet/i.test(baseResource)) {
31
32
  if (!('status' in base)) base.status = 'active';
32
- if (!('url' in base)) base.url = 'https://acme.org/fhir/ValueSet/' + randomId();
33
+ if (!('url' in base) || (typeof base.url === 'string' && !base.url.startsWith('http') && !base.url.startsWith('urn:'))) {
34
+ base.url = 'https://acme.org/fhir/ValueSet/' + randomId();
35
+ }
33
36
  // name must be machine-readable identifier (no spaces, starts with letter)
34
37
  if (!('name' in base)) base.name = 'ExampleValueSet' + randomId().replace(/-/g, '');
35
38
  else if (typeof base.name === 'string' && /^[0-9a-f-]+$/i.test(base.name)) {
@@ -40,9 +43,9 @@ export function generateEnrichResourceEnd() {
40
43
  if (!('compose' in base)) base.compose = { include: [{ system: '${TS.LOINC}', concept: [{ code: '4548-4', display: 'Hemoglobin A1c/Hemoglobin.total in Blood' }] }] };
41
44
  // Handle useContext when pattern for 'code' is present (ISiK ValueSet)
42
45
  // The pattern key 'code' refers to useContext.code
43
- if (fieldPatterns && 'code' in fieldPatterns && !('useContext' in base)) {
46
+ if (fieldPatterns && 'code' in fieldPatterns) {
44
47
  const codePattern = fieldPatterns['code'] as Record<string, unknown>;
45
- base.useContext = [{
48
+ const validUseContext = {
46
49
  code: {
47
50
  system: codePattern.system || '${TS.USAGE_CONTEXT_TYPE}',
48
51
  code: codePattern.code || 'focus'
@@ -50,7 +53,18 @@ export function generateEnrichResourceEnd() {
50
53
  valueCodeableConcept: {
51
54
  coding: [{ system: 'http://hl7.org/fhir/resource-types', code: 'ValueSet' }]
52
55
  }
53
- }];
56
+ };
57
+ if (!('useContext' in base)) {
58
+ base.useContext = [validUseContext];
59
+ } else if (Array.isArray(base.useContext)) {
60
+ // Repair empty useContext entries generated by skeleton fallback
61
+ for (let i = 0; i < (base.useContext as Array<Record<string, unknown>>).length; i++) {
62
+ const entry = (base.useContext as Array<Record<string, unknown>>)[i];
63
+ if (!entry || Object.keys(entry).length === 0) {
64
+ (base.useContext as Array<Record<string, unknown>>)[i] = validUseContext;
65
+ }
66
+ }
67
+ }
54
68
  }
55
69
  }
56
70
 
@@ -58,7 +72,9 @@ export function generateEnrichResourceEnd() {
58
72
  if (/CodeSystem/i.test(baseResource)) {
59
73
  if (!('status' in base)) base.status = 'active';
60
74
  if (!('content' in base)) base.content = 'complete';
61
- if (!('url' in base)) base.url = 'https://acme.org/fhir/CodeSystem/' + randomId();
75
+ if (!('url' in base) || (typeof base.url === 'string' && !base.url.startsWith('http') && !base.url.startsWith('urn:'))) {
76
+ base.url = 'https://acme.org/fhir/CodeSystem/' + randomId();
77
+ }
62
78
  if (!('name' in base)) base.name = 'ExampleCodeSystem';
63
79
  if (!('version' in base)) base.version = '1.0.0';
64
80
  if (!('concept' in base)) base.concept = [{ code: 'example', display: 'Example Code' }];
@@ -121,9 +137,14 @@ export function generateEnrichResourceEnd() {
121
137
  }
122
138
 
123
139
  // Binary
124
- if (/Binary/i.test(baseResource)) {
140
+ if (/^Binary$/i.test(baseResource)) {
125
141
  if (!('contentType' in base)) base.contentType = 'application/pdf';
126
- if (!('data' in base)) base.data = 'SGVsbG8gV29ybGQ=';
142
+ if (!('data' in base)) {
143
+ base.data = 'SGVsbG8gV29ybGQ=';
144
+ } else if (typeof base.data === 'string' && !/^[A-Za-z0-9+/=\\s]+$/.test(base.data)) {
145
+ // data must be valid base64 — replace if skeleton generated a non-base64 value
146
+ base.data = 'SGVsbG8gV29ybGQ=';
147
+ }
127
148
  }
128
149
 
129
150
  // Coverage
@@ -202,7 +223,7 @@ export function generateEnrichResourceEnd() {
202
223
  .filter(ext => ext && typeof ext === 'object' && 'url' in ext && typeof ext.url === 'string');
203
224
  // If no valid extensions remain, add a placeholder FHIR version extension
204
225
  if (validExtensions.length === 0) {
205
- validExtensions.push({ url: 'http://hl7.org/fhir/StructureDefinition/endpoint-fhir-version', valueCode: '4.0.1' });
226
+ validExtensions.push({ url: 'http://hl7.org/fhir/StructureDefinition/endpoint-fhir-version', valueCode: '${versionString()}' });
206
227
  }
207
228
  base.extension = validExtensions;
208
229
  }
@@ -393,17 +414,27 @@ export function generateEnrichResourceEnd() {
393
414
 
394
415
  // Generic narrative for DomainResource derivatives
395
416
  // Bundle, Binary, and Parameters are pure Resource (not DomainResource) and do NOT support text/narrative
396
- if (!/^(Bundle|Binary|Parameters)$/.test(baseResource)) {
417
+ // Data types (CodeableConcept, Identifier, etc.) do NOT have resourceType and must be excluded
418
+ if ('resourceType' in base && !/^(Bundle|Binary|Parameters)$/.test(baseResource)) {
397
419
  if (!('text' in base)) {
398
420
  // Check if there's a pattern constraint for text.status
399
421
  const textStatusPattern = getPrimitivePattern('text.status');
400
422
  const status = textStatusPattern !== undefined ? textStatusPattern : 'generated';
401
423
  base.text = { status: status, div: '<div xmlns="http://www.w3.org/1999/xhtml">Generated</div>' };
402
424
  } else if (typeof base.text === 'object' && base.text !== null) {
425
+ const textObj = base.text as Record<string, unknown>;
403
426
  // Apply text.status pattern to existing text object
404
427
  const textStatusPattern = getPrimitivePattern('text.status');
405
428
  if (textStatusPattern !== undefined) {
406
- (base.text as Record<string, unknown>).status = textStatusPattern;
429
+ textObj.status = textStatusPattern;
430
+ }
431
+ // Backfill div if missing — Narrative.div is required (1..1)
432
+ if (!('div' in textObj)) {
433
+ textObj.div = '<div xmlns="http://www.w3.org/1999/xhtml">Generated</div>';
434
+ }
435
+ // Backfill status if missing — Narrative.status is required (1..1)
436
+ if (!('status' in textObj)) {
437
+ textObj.status = 'generated';
407
438
  }
408
439
  }
409
440
  }
@@ -1,4 +1,4 @@
1
- import { DEFAULT_COUNTRY, TS } from './constants.js';
1
+ import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
2
2
  export function generateEnrichResourceStart() {
3
3
  return `
4
4
  /** Resolved code from a ValueSet binding */
@@ -500,7 +500,9 @@ export function enrichResource(
500
500
  // Force correct status - 'active' is NOT valid for QuestionnaireResponse.status
501
501
  base.status = 'completed';
502
502
  // questionnaire must be an absolute canonical URL, not a relative reference
503
- if (!('questionnaire' in base)) base.questionnaire = 'urn:uuid:' + randomUUID();
503
+ if (!('questionnaire' in base) || (typeof base.questionnaire === 'string' && !base.questionnaire.startsWith('http') && !base.questionnaire.startsWith('urn:'))) {
504
+ base.questionnaire = 'urn:uuid:' + randomUUID();
505
+ }
504
506
  if (!('subject' in base)) base.subject = { reference: 'Patient/' + randomId() };
505
507
  if (!('authored' in base)) base.authored = new Date().toISOString();
506
508
  if (!('author' in base)) base.author = { reference: 'Practitioner/' + randomId() };
@@ -1,4 +1,4 @@
1
- import { fetchStructureDefinition } from './sdParser.js';
1
+ import { fetchStructureDefinition } from '../../parser/sdParser.js';
2
2
  export async function extractFieldPatternsAndMetadata(patternSources, baseResource, sd, requiredFields, candidateRequiredFields, nestedRequirementsMap, resolvedProfilePatterns) {
3
3
  const patternMap = new Map();
4
4
  for (const src of patternSources) {
@@ -1,8 +1,10 @@
1
- import { DEFAULT_COUNTRY, TS } from './constants.js';
1
+ import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
2
+ import { fhirModuleSpecifier } from '../../fhir/versionContext.js';
2
3
  export function generateRandomUtilities() {
4
+ const fhirModule = fhirModuleSpecifier();
3
5
  return `// Auto-generated helper for random resource generation with optional deterministic seeding
4
- // Uses official FHIR R4 TypeScript definitions
5
- import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity } from 'fhir/r4';
6
+ // Uses official FHIR TypeScript definitions
7
+ import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity } from '${fhirModule}';
6
8
  let __seed = Date.now() & 0xffffffff;
7
9
  export function setRandomSeed(seed: number) { __seed = seed >>> 0; }
8
10
  function rng(): number { // simple LCG (Numerical Recipes variant)
@@ -1,7 +1,20 @@
1
- import { logger } from '../logger.js';
1
+ import { logger } from '../../../logger.js';
2
2
  import { capitalise, deriveIdentifierConfig } from './classHeuristics.js';
3
- import { TS, DEFAULT_BASE64, DEFAULT_BASE64_HELLO, DEFAULT_CONTENT_TYPE, EXAMPLE_ID_SYSTEM, DEFAULT_COUNTRY, RESOURCE_SKELETONS, compositionSkeleton, bundleEntry, matchSkeletonBySliceName, } from './constants.js';
3
+ import { TS, DEFAULT_BASE64, DEFAULT_BASE64_HELLO, DEFAULT_CONTENT_TYPE, EXAMPLE_ID_SYSTEM, DEFAULT_COUNTRY, RESOURCE_SKELETONS, compositionSkeleton, bundleEntry, matchSkeletonBySliceName, } from '../../core/constants.js';
4
4
  const log = logger.withTag('classGenerator');
5
+ /**
6
+ * Fields that are arrays in FHIR base complex types (0..*) and must always
7
+ * be serialized as JSON arrays, even when a profile constrains max to 1.
8
+ * FHIR JSON encoding is governed by the base StructureDefinition cardinality.
9
+ */
10
+ const FHIR_BASE_ARRAY_FIELDS = new Set([
11
+ 'coding', // CodeableConcept.coding is 0..*
12
+ 'extension', // Element.extension is 0..*
13
+ 'line', // Address.line is 0..*
14
+ 'given', // HumanName.given is 0..*
15
+ 'prefix', // HumanName.prefix is 0..*
16
+ 'suffix', // HumanName.suffix is 0..*
17
+ ]);
5
18
  /**
6
19
  * Build an object literal expression from nested requirements.
7
20
  * This handles BackboneElement and other complex types with deeply nested required fields.
@@ -116,8 +129,12 @@ export function buildNestedRequirementsObject(nestedRequirements) {
116
129
  // Type-based defaults
117
130
  value = getDefaultValueForType(req.type, lastPart);
118
131
  }
119
- // If the final segment is an array segment, wrap the value in an array
120
- if (allArraySegments.has(lastPart) && !Array.isArray(value)) {
132
+ // If the final segment is an array segment, wrap the value in an array.
133
+ // Also wrap if the field is inherently an array in FHIR base types
134
+ // (profiles may constrain max=1 but JSON encoding stays as array).
135
+ const shouldWrap = (allArraySegments.has(lastPart) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
136
+ && !Array.isArray(value);
137
+ if (shouldWrap) {
121
138
  current[lastPart] = [value];
122
139
  }
123
140
  else {
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { getResourceTypes, getBaseResourceType } from "./utils.js";
4
- import { logger } from "../logger.js";
3
+ import { getResourceTypes, getBaseResourceType } from "../../core/utils.js";
4
+ import { logger } from "../../../logger.js";
5
5
  import { generateSmartAuth, generateSmartClient } from './smartAuthGenerator.js';
6
6
  import { generateReadme, installBaseClientTypes } from './clientReadmeGenerator.js';
7
+ import { versionSlug } from '../../fhir/versionContext.js';
7
8
  const log = logger.withTag('client');
8
9
  /**
9
10
  * Generate FHIR client code for all resource types
@@ -34,7 +35,7 @@ export function generateClient(options) {
34
35
  generateSmartClient(clientDir, resourceTypes);
35
36
  generateIndex(clientDir, resourceTypes);
36
37
  generateReadme(clientDir);
37
- // Install base client type declarations so tsc can resolve @babelfhir-ts/client-r4
38
+ // Install base client type declarations so tsc can resolve @babelfhir-ts/client-<version>
38
39
  installBaseClientTypes(options.outputDir);
39
40
  log.success(`Generated FHIR client with ${resourceTypes.length} resource types + SMART auth`);
40
41
  }
@@ -545,17 +546,19 @@ function generateFhirClient(clientDir, resourceTypes) {
545
546
  }`;
546
547
  })
547
548
  .join("\n\n");
549
+ const slug = versionSlug();
550
+ const clientPkg = `@babelfhir-ts/client-${slug}`;
548
551
  const content = `import {
549
552
  FhirReadClient as BaseFhirReadClient,
550
553
  FhirWriteClient as BaseFhirWriteClient,
551
554
  type FetchFn,
552
- } from "@babelfhir-ts/client-r4";
555
+ } from "${clientPkg}";
553
556
  import type * as GeneratedTypes from "../index.js";
554
557
 
555
558
  /**
556
559
  * Profile-specific FHIR Read Client.
557
- * Extends the base R4 read client with typed profile accessors.
558
- * Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
560
+ * Extends the base ${slug.toUpperCase()} read client with typed profile accessors.
561
+ * Inherits all base ${slug.toUpperCase()} resource methods from ${clientPkg}.
559
562
  */
560
563
  export class FhirReadClient extends BaseFhirReadClient {
561
564
  ${readerMethods}
@@ -563,8 +566,8 @@ ${readerMethods}
563
566
 
564
567
  /**
565
568
  * Profile-specific FHIR Write Client.
566
- * Extends the base R4 write client with typed profile accessors.
567
- * Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
569
+ * Extends the base ${slug.toUpperCase()} write client with typed profile accessors.
570
+ * Inherits all base ${slug.toUpperCase()} resource methods from ${clientPkg}.
568
571
  */
569
572
  export class FhirWriteClient extends BaseFhirWriteClient {
570
573
  ${writerMethods}
@@ -572,7 +575,7 @@ ${writerMethods}
572
575
 
573
576
  /**
574
577
  * Main FHIR Client — works with plain fetch or an authenticated fetch wrapper.
575
- * Provides both base R4 accessors (inherited) and profile-specific accessors.
578
+ * Provides both base ${slug.toUpperCase()} accessors (inherited) and profile-specific accessors.
576
579
  *
577
580
  * @example
578
581
  * // Unauthenticated
@@ -581,7 +584,7 @@ ${writerMethods}
581
584
  * // With custom fetch (e.g. from SmartFhirClient)
582
585
  * const client = new FhirClient("https://fhir.example.com", authenticatedFetch);
583
586
  *
584
- * // Base R4 methods (inherited from @babelfhir-ts/client-r4)
587
+ * // Base ${slug.toUpperCase()} methods (inherited from ${clientPkg})
585
588
  * const pt = await client.read().patient().read("123");
586
589
  *
587
590
  * // Profile-specific methods (generated)
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { versionSlug } from '../../fhir/versionContext.js';
3
4
  /**
4
5
  * Generate README.md
5
6
  */
@@ -189,16 +190,18 @@ const client = new FhirClient('https://fhir.example.com/fhir', loggingFetch);
189
190
  fs.writeFileSync(path.join(clientDir, "README.md"), content);
190
191
  }
191
192
  /**
192
- * Install @babelfhir-ts/client-r4 type declarations into the generated output's
193
+ * Install @babelfhir-ts/client-<version> type declarations into the generated output's
193
194
  * node_modules so tsc can resolve the import during compilation.
194
195
  * At runtime, the consumer installs the actual package via npm.
195
196
  */
196
197
  export function installBaseClientTypes(outputDir) {
197
- const pkgDir = path.join(outputDir, "node_modules", "@babelfhir-ts", "client-r4");
198
+ const slug = versionSlug();
199
+ const clientPkg = `client-${slug}`;
200
+ const pkgDir = path.join(outputDir, "node_modules", "@babelfhir-ts", clientPkg);
198
201
  fs.mkdirSync(pkgDir, { recursive: true });
199
202
  // Minimal package.json
200
203
  fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({
201
- name: "@babelfhir-ts/client-r4",
204
+ name: `@babelfhir-ts/${clientPkg}`,
202
205
  version: "0.0.0-types",
203
206
  types: "index.d.ts"
204
207
  }, null, 2));
@@ -504,7 +504,7 @@ export class SmartAuth {
504
504
  /**
505
505
  * Generate smart-client.ts — SmartFhirClient that wraps FhirClient with SMART auth
506
506
  */
507
- export function generateSmartClient(clientDir, resourceTypes) {
507
+ export function generateSmartClient(clientDir, _resourceTypes) {
508
508
  const content = `import { FhirClient, FhirReadClient, FhirWriteClient } from "./fhir-client.js";
509
509
  import { SmartAuth } from "./smart-auth.js";
510
510
  import type { SmartConfig, SmartToken, LaunchMode } from "./smart-auth.js";
@@ -1,10 +1,10 @@
1
- import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
2
- import { sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from './utils.js';
3
- import { logger } from "../logger.js";
1
+ import { ctx, fhirModuleSpecifier } from '../../fhir/versionContext.js';
2
+ import { sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
3
+ import { logger } from "../../../logger.js";
4
4
  /**
5
5
  * Manages TypeScript import statements for generated FHIR interfaces.
6
6
  * Handles three types of imports:
7
- * 1. FHIR base types from '@types/fhir' (via 'fhir/r4')
7
+ * 1. FHIR base types from '@types/fhir' (via 'fhir/<version>')
8
8
  * 2. Custom local types (other generated profiles in the same package)
9
9
  * 3. External profiled types (to be fetched/generated separately)
10
10
  * 4. Path-based imports (custom relative paths, e.g., ValueSets in subdirectories)
@@ -162,10 +162,10 @@ export class ImportManager {
162
162
  */
163
163
  generateImportStatements() {
164
164
  const statements = [];
165
- // FHIR imports from 'fhir/r4'
165
+ // FHIR imports from version-specific module
166
166
  if (this.fhirImports.size > 0) {
167
167
  const sortedFhirImports = Array.from(this.fhirImports).sort();
168
- statements.push(`import { ${sortedFhirImports.join(', ')} } from "fhir/r4";`);
168
+ statements.push(`import { ${sortedFhirImports.join(', ')} } from "${fhirModuleSpecifier()}";`);
169
169
  }
170
170
  // Path-based imports (e.g., ValueSets from subdirectories)
171
171
  const sortedPathBasedImports = Array.from(this.pathBasedImports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
@@ -196,8 +196,8 @@ export class ImportManager {
196
196
  * Removes import statements for types that aren't actually used.
197
197
  */
198
198
  pruneUnusedImports(output) {
199
- // Prune unused FHIR imports
200
- output = output.replace(/import \{([^}]+)\} from "fhir\/r4";?/g, (full, inside) => {
199
+ // Prune unused FHIR imports (match any fhir/<version> module specifier)
200
+ output = output.replace(/import \{([^}]+)\} from "fhir\/[^"]+";?/g, (full, inside) => {
201
201
  const used = [];
202
202
  const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
203
203
  for (const name of specifiers) {
@@ -206,7 +206,7 @@ export class ImportManager {
206
206
  used.push(name);
207
207
  }
208
208
  }
209
- return used.length ? `import { ${used.join(', ')} } from "fhir/r4";` : '';
209
+ return used.length ? `import { ${used.join(', ')} } from "${fhirModuleSpecifier()}";` : '';
210
210
  });
211
211
  // Prune unused custom imports (both path-based and standard custom imports)
212
212
  output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
@@ -226,14 +226,14 @@ export class ImportManager {
226
226
  }
227
227
  /**
228
228
  * Reorganizes imports to prefer local types over FHIR types where appropriate.
229
- * Moves locally-generated types from fhir/r4 imports to local imports.
229
+ * Moves locally-generated types from fhir/<version> imports to local imports.
230
230
  */
231
231
  reorganizeImports(output) {
232
232
  if (!this.existingProfiles || this.existingProfiles.size === 0) {
233
233
  return output;
234
234
  }
235
- // Extract ALL types from fhir/r4 imports (there may be multiple import lines)
236
- const fhirImportRegex = /import \{([^}]+)\} from "fhir\/r4";/g;
235
+ // Extract ALL types from fhir/<version> imports (there may be multiple import lines)
236
+ const fhirImportRegex = /import \{([^}]+)\} from "fhir\/[^"]+";/g;
237
237
  const allFhirTypes = [];
238
238
  let match;
239
239
  while ((match = fhirImportRegex.exec(output)) !== null) {
@@ -247,7 +247,7 @@ export class ImportManager {
247
247
  const remainingFhirTypes = [];
248
248
  for (const typeName of allFhirTypes) {
249
249
  if (this.existingProfiles.has(typeName)) {
250
- // Never move FhirResource to local imports; it must stay from fhir/r4
250
+ // Never move FhirResource to local imports; it must stay from fhir/<version>
251
251
  if (typeName !== 'FhirResource') {
252
252
  localTypes.push(typeName);
253
253
  }
@@ -259,12 +259,12 @@ export class ImportManager {
259
259
  remainingFhirTypes.push(typeName);
260
260
  }
261
261
  }
262
- // Remove ALL old fhir/r4 import lines
263
- output = output.replace(/import \{[^}]+\} from "fhir\/r4";\n?/g, '');
262
+ // Remove ALL old fhir/<version> import lines
263
+ output = output.replace(/import \{[^}]+\} from "fhir\/[^"]+";\n?/g, '');
264
264
  // Add back the imports at the top
265
265
  let newImports = '';
266
266
  if (remainingFhirTypes.length > 0) {
267
- newImports += `import { ${remainingFhirTypes.join(', ')} } from "fhir/r4";\n`;
267
+ newImports += `import { ${remainingFhirTypes.join(', ')} } from "${fhirModuleSpecifier()}";\n`;
268
268
  }
269
269
  for (const localType of localTypes) {
270
270
  newImports += `import { ${localType} } from "./${localType}";\n`;
@@ -287,7 +287,7 @@ export class ImportManager {
287
287
  * Checks if a type is a known FHIR type from @types/fhir.
288
288
  */
289
289
  isFhirType(type) {
290
- return type === 'FhirResource' || fhirInterfaceNames.includes(type);
290
+ return type === 'FhirResource' || ctx().interfaceNames.includes(type);
291
291
  }
292
292
  /**
293
293
  * Checks if a type name looks like a primitive that shouldn't be imported.