babelfhir-ts 1.6.10 → 1.6.11

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Canonical-URL string handling, with no dependencies of its own.
3
+ *
4
+ * A leaf on purpose. This lived in `utils.ts`, which imports the FHIR version
5
+ * context, so `constants.ts` calling it pulled the whole generator graph in
6
+ * behind a config module: constants → utils → versionContext → types →
7
+ * createRules / fetchUtils → constants. Node evaluates that cycle in an order
8
+ * that leaves `TS` and `USER_AGENT` uninitialized whenever `constants.ts` is the
9
+ * entry point, which is exactly what a script importing it directly does — so
10
+ * `scripts/check-ig-versions.ts` failed on import with `Cannot access 'TS'
11
+ * before initialization` while the CLI, entered through `main.ts`, was fine.
12
+ *
13
+ * Keep this module free of imports so both layers can depend on it.
14
+ */
15
+ /**
16
+ * A canonical URL with any version suffix removed:
17
+ * `http://…/ValueSet/foo|4.0.1` -> `http://…/ValueSet/foo`.
18
+ */
19
+ export function stripVersionFromCanonicalUrl(url) {
20
+ if (!url)
21
+ return url;
22
+ const pipeIndex = url.indexOf('|');
23
+ return pipeIndex >= 0 ? url.substring(0, pipeIndex) : url;
24
+ }
@@ -6,7 +6,7 @@
6
6
  * maintenance trivial.
7
7
  */
8
8
  import { createRequire } from 'module';
9
- import { stripVersionFromCanonicalUrl } from './utils.js';
9
+ import { stripVersionFromCanonicalUrl } from './canonicalUrl.js';
10
10
  import { fileURLToPath } from 'url';
11
11
  import path from 'path';
12
12
  const require = createRequire(import.meta.url);
@@ -2,18 +2,11 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { logger } from "../../logger.js";
4
4
  import { ctx } from '../fhir/versionContext.js';
5
- /**
6
- * Strips the version suffix from a FHIR canonical URL.
7
- * FHIR canonical URLs use '|' to separate the URL from the version.
8
- * Example: "http://hl7.org/fhir/StructureDefinition/translation|5.3.0-ballot-tc1"
9
- * -> "http://hl7.org/fhir/StructureDefinition/translation"
10
- */
11
- export function stripVersionFromCanonicalUrl(url) {
12
- if (!url)
13
- return url;
14
- const pipeIndex = url.indexOf('|');
15
- return pipeIndex >= 0 ? url.substring(0, pipeIndex) : url;
16
- }
5
+ // Defined in a leaf module so `constants.ts` can call it without importing this
6
+ // one — see canonicalUrl.ts. Re-exported here because 20+ call sites import it
7
+ // from utils, and used below in this file too.
8
+ import { stripVersionFromCanonicalUrl } from "./canonicalUrl.js";
9
+ export { stripVersionFromCanonicalUrl };
17
10
  /**
18
11
  * An element path with any slice suffix removed: `identifier:NPI` -> `identifier`.
19
12
  *
@@ -20,7 +20,7 @@ export interface CodeableConceptLike { text?: string; coding?: CodingLike[] }
20
20
  export interface ReferenceLike { reference?: string; display?: string; type?: string; identifier?: IdentifierLike }
21
21
  export interface QuantityLike { value?: number; unit?: string; code?: string; comparator?: string }
22
22
  export interface PeriodLike { start?: string; end?: string }
23
- export interface IdentifierLike { use?: string; system?: string; value?: string }
23
+ export interface IdentifierLike { use?: string; system?: string; value?: string; type?: CodeableConceptLike }
24
24
  export interface AddressLike { line?: string[]; city?: string; state?: string; postalCode?: string; country?: string; text?: string; use?: string }
25
25
  export interface ContactPointLike { system?: string; value?: string; use?: string }
26
26
  export interface AttachmentLike { contentType?: string; url?: string; data?: string; title?: string }
@@ -30,8 +30,14 @@ export interface RangeLike { low?: QuantityLike; high?: QuantityLike }
30
30
  export function formatHumanName(n?: NameLike): string | undefined {
31
31
  if (!n) return undefined;
32
32
  if (n.text) return n.text;
33
- const given = (n.given ?? []).join(' ');
34
- const full = [given, n.family].filter(Boolean).join(' ').trim();
33
+ // prefix and suffix are part of the name, not decoration: dropping them
34
+ // renders "Jane Doe" for a record that says "Dr. Jane Doe MD".
35
+ const full = [
36
+ (n.prefix ?? []).join(' '),
37
+ (n.given ?? []).join(' '),
38
+ n.family,
39
+ (n.suffix ?? []).join(' '),
40
+ ].filter(Boolean).join(' ').trim();
35
41
  return full || undefined;
36
42
  }
37
43
 
@@ -83,12 +89,17 @@ export function formatQuantity(q?: QuantityLike): string | undefined {
83
89
  export function formatPeriod(p?: PeriodLike): string | undefined {
84
90
  if (!p) return undefined;
85
91
  if (!p.start && !p.end) return undefined;
86
- return \`\${p.start ?? '?'} – \${p.end ?? '?'}\`;
92
+ // An absent end means the period has not closed, which is not the same as
93
+ // an end nobody recorded.
94
+ return \`\${p.start ?? '?'} – \${p.end ?? 'ongoing'}\`;
87
95
  }
88
96
 
89
97
  export function formatIdentifier(i?: IdentifierLike): string | undefined {
90
- if (!i) return undefined;
91
- return i.value || undefined;
98
+ if (!i?.value) return undefined;
99
+ // A bare "12345" says nothing next to a second identifier; the type is what
100
+ // tells the reader which number they are looking at.
101
+ const label = formatCodeableConcept(i.type);
102
+ return label ? \`\${label}: \${i.value}\` : i.value;
92
103
  }
93
104
 
94
105
  export function formatAddress(a?: AddressLike): string | undefined {
@@ -237,10 +248,11 @@ import { registerPipe, type PipeFn } from '@maxhealth.tech/prefab';
237
248
  const humanName: PipeFn = (value) => {
238
249
  const format = (n: unknown): string => {
239
250
  if (!n || typeof n !== 'object') return '';
240
- const name = n as { text?: string; given?: string[]; family?: string };
251
+ const name = n as { text?: string; prefix?: string[]; given?: string[]; family?: string; suffix?: string[] };
241
252
  if (name.text) return name.text;
242
- const given = Array.isArray(name.given) ? name.given.join(' ') : '';
243
- return [given, name.family].filter(Boolean).join(' ').trim();
253
+ const joined = (v: unknown): string => (Array.isArray(v) ? v.join(' ') : '');
254
+ return [joined(name.prefix), joined(name.given), name.family, joined(name.suffix)]
255
+ .filter(Boolean).join(' ').trim();
244
256
  };
245
257
  if (Array.isArray(value)) {
246
258
  return value.map(format).filter(Boolean).join(', ');
@@ -273,13 +285,25 @@ const address: PipeFn = (value) => {
273
285
  return format(value);
274
286
  };
275
287
 
276
- /** Format a single ContactPoint (telecom) into its raw \`value\`. */
288
+ /**
289
+ * Format a ContactPoint (telecom).
290
+ *
291
+ * One contact renders bare, because whatever labels it already says which
292
+ * system it is. A list mixes systems, so each entry names its own — without it
293
+ * a phone number and a fax number are two indistinguishable strings.
294
+ */
277
295
  const contactPoint: PipeFn = (value) => {
278
296
  const format = (cp: unknown): string => {
279
297
  if (!cp || typeof cp !== 'object') return '';
280
298
  return String((cp as { value?: unknown }).value ?? '');
281
299
  };
282
- if (Array.isArray(value)) return value.map(format).filter(Boolean).join(', ');
300
+ const labelled = (cp: unknown): string => {
301
+ const val = format(cp);
302
+ if (!val) return '';
303
+ const sys = (cp as { system?: unknown }).system;
304
+ return typeof sys === 'string' && sys ? sys + ': ' + val : val;
305
+ };
306
+ if (Array.isArray(value)) return value.map(labelled).filter(Boolean).join(', ');
283
307
  return format(value);
284
308
  };
285
309
 
@@ -294,7 +318,12 @@ const contactPointBySystem: PipeFn = (value, system) => {
294
318
  .join(', ');
295
319
  };
296
320
 
297
- /** Format ContactPoint[] entries whose \`system\` is neither 'phone' nor 'email'. */
321
+ /**
322
+ * Format ContactPoint[] entries whose \`system\` is neither 'phone' nor 'email'.
323
+ *
324
+ * This is the leftover bucket — fax, pager, url, sms — so each entry names its
325
+ * system, exactly as a mixed list does above.
326
+ */
298
327
  const contactPointOther: PipeFn = (value) => {
299
328
  if (!Array.isArray(value)) return '';
300
329
  return value
@@ -302,16 +331,28 @@ const contactPointOther: PipeFn = (value) => {
302
331
  const s = cp && (cp as { system?: unknown }).system;
303
332
  return typeof s === 'string' && s !== 'phone' && s !== 'email';
304
333
  })
305
- .map((cp) => String((cp as { value?: unknown }).value ?? ''))
334
+ .map((cp) => {
335
+ const val = String((cp as { value?: unknown }).value ?? '');
336
+ return val ? String((cp as { system?: unknown }).system) + ': ' + val : '';
337
+ })
306
338
  .filter(Boolean)
307
339
  .join(', ');
308
340
  };
309
341
 
310
- /** Format an Identifier — returns the \`value\` field. */
342
+ /** Format an Identifier as \`type: value\`, or the bare value when untyped. */
311
343
  const identifier: PipeFn = (value) => {
312
344
  const format = (i: unknown): string => {
313
345
  if (!i || typeof i !== 'object') return '';
314
- return String((i as { value?: unknown }).value ?? '');
346
+ const ident = i as {
347
+ value?: unknown;
348
+ type?: { text?: string; coding?: Array<{ display?: string; code?: string }> };
349
+ };
350
+ const val = String(ident.value ?? '');
351
+ if (!val) return '';
352
+ const type = ident.type;
353
+ const first = type && Array.isArray(type.coding) ? type.coding[0] : undefined;
354
+ const label = (type && type.text) || (first && (first.display ?? first.code)) || '';
355
+ return label ? label + ': ' + val : val;
315
356
  };
316
357
  if (Array.isArray(value)) return value.map(format).filter(Boolean).join(', ');
317
358
  return format(value);
@@ -417,7 +458,7 @@ const period: PipeFn = (value) => {
417
458
  if (!p || typeof p !== 'object') return '';
418
459
  const per = p as { start?: string; end?: string };
419
460
  if (!per.start && !per.end) return '';
420
- return \`\${per.start ?? '?'} – \${per.end ?? '?'}\`;
461
+ return \`\${per.start ?? '?'} – \${per.end ?? 'ongoing'}\`;
421
462
  };
422
463
  if (Array.isArray(value)) return value.map(format).filter(Boolean).join(', ');
423
464
  return format(value);
@@ -263,12 +263,17 @@ emitInvariants = false) {
263
263
  }
264
264
  }
265
265
  // Collect refinements (pattern constraints, slice cardinality, FHIRPath invariants, required bindings)
266
- // Only emit refinements for Resource profiles (not DataType profiles like Identifier, Coding)
267
- // because DataType random() methods don't produce pattern-aware data.
268
- const isDataTypeProfile = getZodPackageTypes().has(rootPrefix);
269
- const refinementResult = isDataTypeProfile
270
- ? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map(), invariantEvaluators: [] }
271
- : buildRefinements(allFields, rootPrefix, valueSets, emitInvariants);
266
+ //
267
+ // Emitted for DataType profiles too, Extension among them. They used to be skipped
268
+ // wholesale because "DataType random() methods don't produce pattern-aware data" —
269
+ // a property of the test fixtures, deciding what conformance rules a shipped
270
+ // schema enforces. The cost was that an Extension profile's zod schema checked its
271
+ // fixed url and nothing else: sdc's AssembleExpectation carries a required binding
272
+ // to the assemble-expectation ValueSet, validate() enforced it, and the schema
273
+ // accepted any code at all. Over a hundred profiles disagreed that way, and the
274
+ // disagreement was recorded as a parity failure rather than as the missing check
275
+ // it was.
276
+ const refinementResult = buildRefinements(allFields, rootPrefix, valueSets, emitInvariants);
272
277
  // Merge ValueSet imports from binding refinements
273
278
  for (const [name, importPath] of refinementResult.valueSetImports) {
274
279
  if (!valueSetImports.has(name)) {
@@ -10,7 +10,6 @@
10
10
  * are genuinely identical across R4 / R4B / R5 so they live here as shared
11
11
  * constants.
12
12
  */
13
- import { TS } from '../core/constants.js';
14
13
  // ── Shared constants (version-independent) ──────────────────────────────────
15
14
  /** FHIR primitive type → TypeScript type mapping (same for R4/R4B/R5). */
16
15
  const PRIMITIVE_MAP = {
@@ -46,8 +45,6 @@ export const VALUESET_THRESHOLDS = {
46
45
  UNION_TYPE: 20,
47
46
  STORE_CODES: 50,
48
47
  };
49
- /** Re-export centralized terminology systems for backward compatibility. */
50
- export const FHIR_TERMINOLOGY_SYSTEMS = TS;
51
48
  // ── Cardinality / child-type helpers ────────────────────────────────────────
52
49
  /**
53
50
  * Fields whose `use` / `type` properties are enum codes that should *not*
@@ -13,7 +13,6 @@
13
13
 
14
14
  import type { FhirRules } from './types.js';
15
15
  import type { DerivedVersionData } from './corePackageResolver.js';
16
- import { TS } from '../core/constants.js';
17
16
 
18
17
  // ── Shared constants (version-independent) ──────────────────────────────────
19
18
 
@@ -54,9 +53,6 @@ export const VALUESET_THRESHOLDS = {
54
53
  STORE_CODES: 50,
55
54
  } as const;
56
55
 
57
- /** Re-export centralized terminology systems for backward compatibility. */
58
- export const FHIR_TERMINOLOGY_SYSTEMS = TS;
59
-
60
56
  // ── Cardinality / child-type helpers ────────────────────────────────────────
61
57
 
62
58
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.6.10",
3
+ "version": "1.6.11",
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",
@@ -86,32 +86,32 @@
86
86
  },
87
87
  "devDependencies": {
88
88
  "@eslint/js": "^10.0.1",
89
- "@max-health-inc/config": "^3.1.0",
89
+ "@max-health-inc/config": "^3.3.0",
90
90
  "@maxhealth.tech/prefab": "^0.3.12",
91
- "@types/node": "^25.9.5",
91
+ "@types/node": "^26.4.0",
92
92
  "@types/semver": "^7.8.0",
93
93
  "@types/unzipper": "^0.10.11",
94
- "@vitest/coverage-v8": "^4.1.10",
95
- "@vitest/eslint-plugin": "^1.6.26",
96
- "@vitest/ui": "^4.1.10",
94
+ "@vitest/coverage-v8": "^4.1.11",
95
+ "@vitest/eslint-plugin": "^1.6.27",
96
+ "@vitest/ui": "^4.1.11",
97
97
  "cross-env": "^10.1.0",
98
- "eslint": "^10.8.0",
98
+ "eslint": "^10.9.1",
99
99
  "fhirpath": "^5.2.0",
100
- "globals": "^17.9.0",
100
+ "globals": "^17.11.0",
101
101
  "rimraf": "^6.1.3",
102
102
  "semver": "^7.8.5",
103
103
  "typescript": "^6.0.3",
104
- "typescript-eslint": "^8.66.0",
104
+ "typescript-eslint": "^8.68.0",
105
105
  "vitepress": "^1.6.4",
106
- "vitepress-plugin-llms": "^1.13.4",
107
- "vitest": "^4.1.10",
108
- "zod": "4.3.6"
106
+ "vitepress-plugin-llms": "^1.13.5",
107
+ "vitest": "^4.1.11",
108
+ "zod": "4.4.3"
109
109
  },
110
110
  "dependencies": {
111
111
  "@types/fhir": "^0.0.44",
112
112
  "consola": "^3.4.2",
113
113
  "tar": "^7.5.22",
114
- "tsx": "^4.23.7",
114
+ "tsx": "^4.23.12",
115
115
  "unzipper": "^0.12.5"
116
116
  },
117
117
  "overrides": {