babelfhir-ts 1.6.8 → 1.6.9

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.
@@ -112,14 +112,20 @@ export const EMITTED_RANGES = {
112
112
  /**
113
113
  * Prefab UI runtime for `--prefab` output.
114
114
  *
115
- * 0.3.0 is the floor: it introduced wire protocol 0.3, where the theme is
116
- * compiled into the top-level `css` array, `stylesheets` carries external
117
- * URLs, and `mode` forces a colour scheme. Emitted renderers pass `theme`
118
- * and spread `PrefabAppOptions` into `PrefabApp`, so they rely on a >= 0.3
115
+ * 0.3.0 introduced wire protocol 0.3, where the theme is compiled into the
116
+ * top-level `css` array, `stylesheets` carries external URLs, and `mode`
117
+ * forces a colour scheme. Emitted renderers pass `theme` and spread
118
+ * `PrefabAppOptions` into `PrefabApp`, so they rely on a >= 0.3
119
119
  * serializer/renderer pair to apply themes. A 0.2.x renderer would silently
120
120
  * drop them.
121
+ *
122
+ * 0.3.10 is the floor because `AutoFormField` gained `options` there, and the
123
+ * emitted form now carries the bound ValueSet's concepts on every coded
124
+ * field. Below it the property is not in the type and is ignored by
125
+ * `autoForm`, so a required-binding field renders as free text: the form
126
+ * still builds, and every constraint the binding states is gone from it.
121
127
  */
122
- prefab: '^0.3.0',
128
+ prefab: '^0.3.10',
123
129
  /**
124
130
  * Generated FHIR client base package (`@babelfhir-ts/client-<slug>`).
125
131
  *
@@ -28,9 +28,9 @@ function ensurePrefabDir(outputDir) {
28
28
  * Emit `prefab/<InterfaceName>Prefab.ts` for a single profile.
29
29
  * Registers the profile so {@link finalizePrefabOutput} can build the barrel.
30
30
  */
31
- export function writePrefabProfile(outputDir, interfaceName, fields, resourceType) {
31
+ export function writePrefabProfile(outputDir, interfaceName, fields, resourceType, valueSets) {
32
32
  const dir = ensurePrefabDir(outputDir);
33
- const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields, { resourceType });
33
+ const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields, { resourceType, valueSets });
34
34
  const filePath = path.join(dir, `${interfaceName}Prefab.ts`);
35
35
  fs.writeFileSync(filePath, code);
36
36
  let map = emittedProfiles.get(outputDir);
@@ -68,6 +68,17 @@ function emitToRowBody(descriptors) {
68
68
  const lines = descriptors.map(d => ` ${d.name}: ${rowAccessor(d)},`);
69
69
  return lines.join('\n');
70
70
  }
71
+ /**
72
+ * Emit an `AutoFormOption[]` literal for a bound field.
73
+ *
74
+ * The label is omitted when the concept carried no display, because prefab
75
+ * already falls back to the value and restating it would only make the
76
+ * generated file longer.
77
+ */
78
+ function emitOptionsLiteral(options) {
79
+ const entries = options.map(o => o.label ? `{ value: ${JSON.stringify(o.value)}, label: ${JSON.stringify(o.label)} }` : `{ value: ${JSON.stringify(o.value)} }`);
80
+ return `[${entries.join(', ')}]`;
81
+ }
71
82
  /** Emit `autoForm` field descriptors. */
72
83
  function emitFormFields(descriptors) {
73
84
  const lines = [];
@@ -80,6 +91,7 @@ function emitFormFields(descriptors) {
80
91
  continue;
81
92
  if (d.isArray)
82
93
  continue; // v1: scalars only
94
+ const options = d.enumOptions?.length ? d.enumOptions : undefined;
83
95
  let type = 'text';
84
96
  if (d.kind === 'number')
85
97
  type = 'number';
@@ -87,14 +99,13 @@ function emitFormFields(descriptors) {
87
99
  type = 'switch';
88
100
  else if (d.kind === 'date')
89
101
  type = 'date';
90
- else if (d.enumValues && d.enumValues.length > 0)
102
+ else if (options)
91
103
  type = 'select';
92
104
  const extra = [`name: '${d.name}'`, `label: ${JSON.stringify(d.label)}`, `type: '${type}'`];
93
105
  if (d.required)
94
106
  extra.push('required: true');
95
- // NOTE: @maxhealth.tech/prefab AutoFormField does not yet carry an `options`
96
- // property for select inputs; the enum list is elided until the upstream
97
- // type is widened. See prefab-ui#TODO.
107
+ if (options)
108
+ extra.push(`options: ${emitOptionsLiteral(options)}`);
98
109
  lines.push(` { ${extra.join(', ')} },`);
99
110
  seen.add(d.name);
100
111
  }
@@ -206,7 +217,7 @@ function emitBrowserTitleExpr(titleDescriptor, interfaceName) {
206
217
  : `selected.dot('${titleDescriptor.name}')`;
207
218
  }
208
219
  export function generatePrefabRenderer(interfaceName, fields, options) {
209
- const rawDescriptors = fields.flatMap(describeField);
220
+ const rawDescriptors = fields.flatMap(f => describeField(f, options?.valueSets));
210
221
  const descriptors = dedupeDescriptors(rawDescriptors);
211
222
  const resourceType = options?.resourceType;
212
223
  const tableDescriptors = pickTableDescriptors(descriptors);
@@ -8,6 +8,8 @@
8
8
  * This module is deliberately small and data-driven so it can later be
9
9
  * extracted into a shared codegen-spec package (see design notes).
10
10
  */
11
+ import { lookupBoundValueSet } from '../requiredBindings.js';
12
+ import { ctx } from '../../fhir/versionContext.js';
11
13
  /** Lower-cased FHIR primitive type codes. */
12
14
  const PRIMITIVE_TEXT = new Set([
13
15
  'string', 'code', 'uri', 'url', 'canonical', 'id', 'oid', 'uuid', 'markdown', 'xhtml',
@@ -23,6 +25,29 @@ function labelFor(name) {
23
25
  const spaced = name.replace(/([A-Z])/g, ' $1').replace(/[._-]/g, ' ').trim();
24
26
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
25
27
  }
28
+ /**
29
+ * Build the choice list for a bound field, or undefined when there is none to
30
+ * offer.
31
+ *
32
+ * The parsed ValueSet is preferred over the codes the SD parser sampled onto
33
+ * the binding: it carries the concept displays, and it is the whole set rather
34
+ * than a subset. Both paths are capped at the same `STORE_CODES` threshold the
35
+ * parser and the interface emitter already use to decide a ValueSet is small
36
+ * enough to enumerate — past it a Select stops being a usable control and the
37
+ * field stays a free-text input.
38
+ */
39
+ function selectOptionsFor(field, valueSets) {
40
+ const limit = ctx().valueSetThresholds.STORE_CODES;
41
+ const resolved = lookupBoundValueSet(field.binding?.uri, valueSets);
42
+ const concepts = resolved?.valueSet.concepts;
43
+ if (concepts?.length && concepts.length <= limit) {
44
+ return concepts.map(c => ({ value: c.code, ...(c.display ? { label: c.display } : {}) }));
45
+ }
46
+ const sampled = field.binding?.codes?.filter(c => c.code).map(c => ({ value: c.code }));
47
+ if (sampled?.length && sampled.length <= limit)
48
+ return sampled;
49
+ return undefined;
50
+ }
26
51
  function kindForType(fhirType) {
27
52
  if (PRIMITIVE_TEXT.has(fhirType))
28
53
  return 'text';
@@ -86,7 +111,7 @@ function kindForType(fhirType) {
86
111
  * on the typed interface. Detail views for nested backbone elements are a
87
112
  * follow-up once slicing-aware tree rendering lands.
88
113
  */
89
- export function describeField(field) {
114
+ export function describeField(field, valueSets) {
90
115
  if (field.isForbidden)
91
116
  return [];
92
117
  if (!field.type)
@@ -132,7 +157,7 @@ export function describeField(field) {
132
157
  label: labelFor(variantPropName),
133
158
  valueSetUri: field.binding?.uri,
134
159
  bindingStrength: field.binding?.strength,
135
- enumValues: field.binding?.codes?.map(c => c.code).filter(Boolean),
160
+ enumOptions: selectOptionsFor(field, valueSets),
136
161
  });
137
162
  }
138
163
  return out;
@@ -158,7 +183,7 @@ export function describeField(field) {
158
183
  label: labelFor(bareName),
159
184
  valueSetUri: field.binding?.uri,
160
185
  bindingStrength: field.binding?.strength,
161
- enumValues: field.binding?.codes?.map(c => c.code).filter(Boolean),
186
+ enumOptions: selectOptionsFor(field, valueSets),
162
187
  }];
163
188
  }
164
189
  /**
@@ -21,17 +21,31 @@ import { stripVersionFromCanonicalUrl } from '../core/utils.js';
21
21
  import { sanitizeValueSetName } from './valueset/valueSetGenerator.js';
22
22
  /** Types FHIR permits a binding on. */
23
23
  const BINDABLE_TYPES = ['code', 'Coding', 'CodeableConcept', 'Quantity', 'string', 'uri'];
24
- export function resolveRequiredBindingValueSet(bindingUri, valueSets) {
24
+ /**
25
+ * Resolve a binding URI to its parsed ValueSet, versioned key first.
26
+ *
27
+ * A binding may cite `…/ValueSet/foo|1.0.0` while the package indexes the
28
+ * ValueSet under its unversioned canonical, so both keys have to be tried.
29
+ * Every emitter that reads concepts off a binding needs the same two lookups,
30
+ * which is why it lives here rather than in each of them.
31
+ */
32
+ export function lookupBoundValueSet(bindingUri, valueSets) {
25
33
  if (!bindingUri || !valueSets)
26
34
  return undefined;
27
- let vs = valueSets.get(bindingUri);
28
- let resolvedUri = bindingUri;
29
- if (!vs && bindingUri.includes('|')) {
30
- resolvedUri = stripVersionFromCanonicalUrl(bindingUri);
31
- vs = valueSets.get(resolvedUri);
32
- }
33
- if (!vs)
35
+ const direct = valueSets.get(bindingUri);
36
+ if (direct)
37
+ return { valueSet: direct, resolvedUri: bindingUri };
38
+ if (!bindingUri.includes('|'))
39
+ return undefined;
40
+ const strippedUri = stripVersionFromCanonicalUrl(bindingUri);
41
+ const stripped = valueSets.get(strippedUri);
42
+ return stripped ? { valueSet: stripped, resolvedUri: strippedUri } : undefined;
43
+ }
44
+ export function resolveRequiredBindingValueSet(bindingUri, valueSets) {
45
+ const resolved = lookupBoundValueSet(bindingUri, valueSets);
46
+ if (!resolved || !bindingUri)
34
47
  return undefined;
48
+ const { valueSet: vs, resolvedUri } = resolved;
35
49
  const displayName = resolvedUri.split('/').pop() || vs.name;
36
50
  if (vs.concepts.length === 0) {
37
51
  if (vs.systems && vs.systems.length > 0) {
@@ -750,7 +750,7 @@ export async function processStructureDefinition(sd, ctx) {
750
750
  }
751
751
  if (flags?.prefab) {
752
752
  const { writePrefabProfile } = await import('./emitters/prefab/prefabEmitter.js');
753
- writePrefabProfile(outputDir, interfaceName, aligned.alignedNewFields, sd.type);
753
+ writePrefabProfile(outputDir, interfaceName, aligned.alignedNewFields, sd.type, valueSets);
754
754
  }
755
755
  logger.log(`Generated artifacts for ${interfaceName}`);
756
756
  return interfaceName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.6.8",
3
+ "version": "1.6.9",
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",
@@ -87,7 +87,7 @@
87
87
  "devDependencies": {
88
88
  "@eslint/js": "^10.0.1",
89
89
  "@max-health-inc/config": "^3.1.0",
90
- "@maxhealth.tech/prefab": "^0.3.7",
90
+ "@maxhealth.tech/prefab": "^0.3.11",
91
91
  "@types/node": "^25.9.5",
92
92
  "@types/semver": "^7.8.0",
93
93
  "@types/unzipper": "^0.10.11",