fhirsmith 0.9.6 → 0.10.0
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.
- package/CHANGELOG.md +51 -0
- package/library/folder-content-loader.js +91 -0
- package/npmprojector/npmprojector.js +2 -6
- package/package.json +1 -1
- package/packages/package-crawler.js +123 -6
- package/packages/packages.js +104 -0
- package/publisher/publisher.js +290 -12
- package/registry/crawler.js +85 -6
- package/registry/registry.js +24 -7
- package/server.js +11 -2
- package/stats.js +26 -18
- package/translations/Messages.properties +2 -1
- package/tx/cs/cs-cs.js +8 -0
- package/tx/cs/cs-loinc.js +1 -0
- package/tx/cs/cs-provider-list.js +2 -1
- package/tx/cs/cs-snomed.js +142 -59
- package/tx/data/snomed-testing.cache +0 -0
- package/tx/html/home-metrics.liquid +10 -10
- package/tx/library/canonical-resource.js +4 -2
- package/tx/library/codesystem.js +31 -0
- package/tx/library/conceptmap.js +24 -0
- package/tx/library/designations.js +27 -20
- package/tx/library/renderer.js +303 -22
- package/tx/library/ucum-types.js +4 -1
- package/tx/library/valueset.js +46 -0
- package/tx/library.js +65 -21
- package/tx/operation-context.js +122 -27
- package/tx/params.js +36 -8
- package/tx/provider.js +6 -3
- package/tx/tx-html.js +34 -0
- package/tx/tx.js +92 -30
- package/tx/vs/vs-vsac.js +157 -9
- package/tx/workers/cache-control.js +186 -0
- package/tx/workers/{related.js → compare.js} +27 -27
- package/tx/workers/expand.js +100 -96
- package/tx/workers/lookup.js +6 -0
- package/tx/workers/metadata.js +11 -6
- package/tx/workers/read.js +1 -1
- package/tx/workers/translate.js +20 -29
- package/tx/workers/validate.js +18 -10
- package/tx/workers/worker.js +134 -47
- package/tx/xversion/xv-bundle.js +1 -2
- package/tx/xversion/xv-codesystem.js +5 -2
- package/tx/xversion/xv-parameters.js +4 -4
- package/tx/xversion/xv-resource.js +2 -2
- package/tx/xversion/xv-terminologyCapabilities.js +11 -6
- package/tx/xversion/xv-valueset.js +7 -7
- package/publisher/task-draft.js +0 -463
- package/tx/data/OperationDefinition-ValueSet-related.json +0 -133
|
@@ -46,7 +46,7 @@ class SearchFilterText {
|
|
|
46
46
|
validateParameter(value, 'value', String);
|
|
47
47
|
validateOptionalParameter(returnRating, 'returnRating', Boolean);
|
|
48
48
|
|
|
49
|
-
if (this.
|
|
49
|
+
if (this.isNull) {
|
|
50
50
|
return returnRating ? {passes: true, rating: 0} : true;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -610,35 +610,42 @@ class Designations {
|
|
|
610
610
|
|
|
611
611
|
const matchTypes = [LangMatchType.FULL, LangMatchType.LANG_REGION, LangMatchType.LANG];
|
|
612
612
|
|
|
613
|
-
for (const
|
|
614
|
-
for (const
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
supplements
|
|
613
|
+
for (const activeOnly of [true, false]) {
|
|
614
|
+
for (const matchType of matchTypes) {
|
|
615
|
+
for (const cd of this.designations) {
|
|
616
|
+
if (this._langMatches(lang, cd.language, matchType) && this.isDisplay(cd) && (!activeOnly || cd.isActive())) {
|
|
617
|
+
if (supplements && cd.source) {
|
|
618
|
+
supplements.add(cd.source);
|
|
619
|
+
}
|
|
620
|
+
return cd;
|
|
618
621
|
}
|
|
619
|
-
return cd;
|
|
620
622
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
623
|
+
for (const cd of this.designations) {
|
|
624
|
+
if (this._langMatches(lang, cd.language, matchType) && this._isPreferred(cd)&& (!activeOnly || cd.isActive())) {
|
|
625
|
+
if (supplements && cd.source) {
|
|
626
|
+
supplements.add(cd.source);
|
|
627
|
+
}
|
|
628
|
+
return cd;
|
|
626
629
|
}
|
|
627
|
-
return cd;
|
|
628
630
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
for (const cd of this.designations) {
|
|
632
|
+
if (this._langMatches(lang, cd.language, matchType) && (!activeOnly || cd.isActive())) {
|
|
633
|
+
if (supplements && cd.source) {
|
|
634
|
+
supplements.add(cd.source);
|
|
635
|
+
}
|
|
636
|
+
return cd;
|
|
634
637
|
}
|
|
635
|
-
return cd;
|
|
636
638
|
}
|
|
637
639
|
}
|
|
638
640
|
}
|
|
639
641
|
}
|
|
640
642
|
for (const cd of this.designations) {
|
|
641
|
-
if (!cd.language && this.isDisplay(cd)) {
|
|
643
|
+
if (!cd.language && this.isDisplay(cd) && cd.isActive()) {
|
|
644
|
+
if (supplements && cd.source) {
|
|
645
|
+
supplements.add(cd.source);
|
|
646
|
+
}
|
|
647
|
+
return cd;
|
|
648
|
+
} if (!cd.language && this.isDisplay(cd)) {
|
|
642
649
|
if (supplements && cd.source) {
|
|
643
650
|
supplements.add(cd.source);
|
|
644
651
|
}
|
package/tx/library/renderer.js
CHANGED
|
@@ -3,6 +3,27 @@ const {Extensions} = require("./extensions");
|
|
|
3
3
|
const {div} = require("../../library/html");
|
|
4
4
|
const {getValuePrimitive} = require("../../library/utilities");
|
|
5
5
|
const {getValueName} = require("../../library/utilities");
|
|
6
|
+
const {InvalidError} = require("./errors");
|
|
7
|
+
|
|
8
|
+
// Valid FHIR value sets used when validating values before rendering, so that
|
|
9
|
+
// illegal input produces an error that names the offending value rather than
|
|
10
|
+
// being silently rendered or causing a generic crash downstream.
|
|
11
|
+
const VALID_FILTER_OPS = new Set([
|
|
12
|
+
'=', 'is-a', 'descendent-of', 'is-not-a', 'regex', 'in', 'not-in',
|
|
13
|
+
'generalizes', 'child-of', 'descendent-leaf', 'exists'
|
|
14
|
+
]);
|
|
15
|
+
const VALID_CONCEPTMAP_RELATIONSHIPS = new Set([
|
|
16
|
+
'related-to', 'equivalent', 'source-is-narrower-than-target',
|
|
17
|
+
'source-is-broader-than-target', 'not-related-to'
|
|
18
|
+
]);
|
|
19
|
+
const VALID_CONCEPTMAP_EQUIVALENCES = new Set([
|
|
20
|
+
'relatedto', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower',
|
|
21
|
+
'specializes', 'inexact', 'unmatched', 'disjoint'
|
|
22
|
+
]);
|
|
23
|
+
const VALID_CODESYSTEM_CONTENT = new Set([
|
|
24
|
+
'not-present', 'example', 'fragment', 'complete', 'supplement'
|
|
25
|
+
]);
|
|
26
|
+
const VALID_PUBLICATION_STATUS = new Set(['draft', 'active', 'retired', 'unknown']);
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* @typedef {Object} TerminologyLinkResolver
|
|
@@ -239,10 +260,12 @@ class Renderer {
|
|
|
239
260
|
}
|
|
240
261
|
|
|
241
262
|
renderMetadataLastUpdated(res, tbl) {
|
|
242
|
-
if (res.meta?.
|
|
263
|
+
if (res.meta?.lastUpdated) {
|
|
264
|
+
this._requireValidDate(res.meta.lastUpdated, 'meta.lastUpdated');
|
|
243
265
|
let tr = tbl.tr();
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
// RES_REND_UPDATED is "Last updated: {0}" — supply the formatted date as
|
|
267
|
+
// the {0} parameter so the placeholder is substituted.
|
|
268
|
+
tr.td().b().tx(this.translate('RES_REND_UPDATED', [this.displayDate(res.meta.lastUpdated)]));
|
|
246
269
|
}
|
|
247
270
|
}
|
|
248
271
|
|
|
@@ -318,7 +341,7 @@ class Renderer {
|
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
renderLinkComma(x, uri) {
|
|
321
|
-
let {desc, url} = this.linkResolver ? this.linkResolver.resolveURL(this.opContext, uri) : null;
|
|
344
|
+
let {desc, url} = (this.linkResolver ? this.linkResolver.resolveURL(this.opContext, uri) : null) || {};
|
|
322
345
|
if (url) {
|
|
323
346
|
x.commaItem(desc, url);
|
|
324
347
|
} else {
|
|
@@ -328,10 +351,17 @@ class Renderer {
|
|
|
328
351
|
|
|
329
352
|
|
|
330
353
|
async renderCoding(x, coding) {
|
|
354
|
+
if (coding === null || coding === undefined || typeof coding !== 'object') {
|
|
355
|
+
this._invalid('Coding', coding, 'invalid Coding');
|
|
356
|
+
}
|
|
357
|
+
if (coding.code !== undefined && coding.code !== null &&
|
|
358
|
+
(typeof coding.code !== 'string' || coding.code.trim() === '')) {
|
|
359
|
+
this._invalid('Coding.code', coding.code, 'invalid code');
|
|
360
|
+
}
|
|
331
361
|
let {
|
|
332
362
|
desc,
|
|
333
363
|
url
|
|
334
|
-
} = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, coding.system, coding.version, coding.code) : null;
|
|
364
|
+
} = (this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, coding.system, coding.version, coding.code) : null) || {};
|
|
335
365
|
if (url) {
|
|
336
366
|
x.ah(url).tx(desc);
|
|
337
367
|
} else {
|
|
@@ -347,10 +377,254 @@ class Renderer {
|
|
|
347
377
|
return this.opContext.i18n.formatPhrasePlural(msgId, this.opContext.langs, num,[]);
|
|
348
378
|
}
|
|
349
379
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Determine the BCP-47 locale to use for formatting, derived from the user's
|
|
382
|
+
* requested languages (parsed from the Accept-Language header). The region
|
|
383
|
+
* subtag (e.g. US vs GB) is what selects day/month ordering and month names.
|
|
384
|
+
* Falls back to 'en-US' when no usable language is available.
|
|
385
|
+
* @returns {string} a BCP-47 locale tag accepted by Intl
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
_formatLocale() {
|
|
389
|
+
const langs = this.opContext && this.opContext.langs;
|
|
390
|
+
if (langs) {
|
|
391
|
+
for (const lang of langs) {
|
|
392
|
+
if (lang.language && lang.language !== '*') {
|
|
393
|
+
let tag = lang.language;
|
|
394
|
+
if (lang.script) tag += '-' + lang.script;
|
|
395
|
+
if (lang.region) tag += '-' + lang.region;
|
|
396
|
+
try {
|
|
397
|
+
if (Intl.DateTimeFormat.supportedLocalesOf(tag).length > 0) {
|
|
398
|
+
return tag;
|
|
399
|
+
}
|
|
400
|
+
// Locale is structurally valid but not supported by the runtime;
|
|
401
|
+
// Intl will still resolve it to a sensible fallback, so use it.
|
|
402
|
+
return tag;
|
|
403
|
+
} catch (_) {
|
|
404
|
+
// Structurally invalid tag — skip and try the next language.
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return 'en-US';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── Validation helpers ──────────────────────────────────────────────────────
|
|
413
|
+
// These exist so that illegal input is reported with an error that names the
|
|
414
|
+
// offending field and value, rather than being silently rendered or causing a
|
|
415
|
+
// generic "Cannot read properties of null" style crash further down.
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Render a value for inclusion in an error message, distinguishing null,
|
|
419
|
+
* undefined, objects, and primitives, and truncating long values.
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
_showValue(value) {
|
|
423
|
+
if (value === undefined) return 'undefined';
|
|
424
|
+
if (value === null) return 'null';
|
|
425
|
+
if (typeof value === 'object') {
|
|
426
|
+
let s;
|
|
427
|
+
try { s = JSON.stringify(value); } catch (_) { s = Object.prototype.toString.call(value); }
|
|
428
|
+
return s.length > 80 ? s.slice(0, 77) + '...' : s;
|
|
429
|
+
}
|
|
430
|
+
const s = String(value);
|
|
431
|
+
return `'${s.length > 80 ? s.slice(0, 77) + '...' : s}'`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Throw an InvalidError that identifies the offending value and its location.
|
|
436
|
+
* @param {string} path - dotted path to the offending element (e.g. "ConceptMap.group[0].target[0].relationship")
|
|
437
|
+
* @param {*} value - the offending value
|
|
438
|
+
* @param {string} what - short description of what is wrong (e.g. "invalid ConceptMap relationship")
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
_invalid(path, value, what) {
|
|
442
|
+
throw new InvalidError(`${what} at ${path}: ${this._showValue(value)}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Validate, unwrap, and type-check a resource before rendering. Accepts either
|
|
447
|
+
* a raw resource object or a wrapper exposing `.json`. Throws an InvalidError
|
|
448
|
+
* naming the problem when the input is missing, not an object, or of the wrong
|
|
449
|
+
* resourceType.
|
|
450
|
+
* @param {*} res - the resource (or wrapper) to render
|
|
451
|
+
* @param {string} expectedType - the FHIR resourceType this method renders
|
|
452
|
+
* @param {string} method - the calling method name, for the error message
|
|
453
|
+
* @returns {object} the unwrapped resource object
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
_resolveResource(res, expectedType, method) {
|
|
457
|
+
if (res === null || res === undefined) {
|
|
458
|
+
throw new InvalidError(`${method}: no resource supplied (got ${res === null ? 'null' : 'undefined'})`);
|
|
459
|
+
}
|
|
460
|
+
if (typeof res !== 'object' || Array.isArray(res)) {
|
|
461
|
+
throw new InvalidError(`${method}: expected a ${expectedType} resource object but got ${Array.isArray(res) ? 'an array' : typeof res}`);
|
|
462
|
+
}
|
|
463
|
+
const r = (res.json !== undefined && res.json !== null) ? res.json : res;
|
|
464
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
465
|
+
throw new InvalidError(`${method}: expected a ${expectedType} resource object but got ${this._showValue(r)}`);
|
|
353
466
|
}
|
|
467
|
+
if (r.resourceType !== undefined && r.resourceType !== expectedType) {
|
|
468
|
+
throw new InvalidError(`${method}: expected resourceType '${expectedType}' but found ${this._showValue(r.resourceType)}`);
|
|
469
|
+
}
|
|
470
|
+
return r;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* True when `value` is a syntactically valid FHIR date/dateTime/instant.
|
|
475
|
+
* Used both to validate (via _requireValidDate) and to drive display.
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
_isValidFhirDate(value) {
|
|
479
|
+
if (typeof value !== 'string') return false;
|
|
480
|
+
if (/^\d{4}$/.test(value)) return true;
|
|
481
|
+
|
|
482
|
+
let m = /^(\d{4})-(\d{2})$/.exec(value);
|
|
483
|
+
if (m) {
|
|
484
|
+
const y = Number(m[1]), mo = Number(m[2]);
|
|
485
|
+
const d = new Date(Date.UTC(y, mo - 1, 1));
|
|
486
|
+
return !isNaN(d.getTime()) && d.getUTCFullYear() === y && d.getUTCMonth() === mo - 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
490
|
+
if (m) {
|
|
491
|
+
const y = Number(m[1]), mo = Number(m[2]), da = Number(m[3]);
|
|
492
|
+
const d = new Date(Date.UTC(y, mo - 1, da));
|
|
493
|
+
return !isNaN(d.getTime()) && d.getUTCFullYear() === y &&
|
|
494
|
+
d.getUTCMonth() === mo - 1 && d.getUTCDate() === da;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (value.includes('T')) {
|
|
498
|
+
const dt = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
499
|
+
if (!dt) return false;
|
|
500
|
+
if (isNaN(new Date(value).getTime())) return false;
|
|
501
|
+
const mo = Number(dt[2]), da = Number(dt[3]), hh = Number(dt[4]), mi = Number(dt[5]), ss = Number(dt[6]);
|
|
502
|
+
return mo >= 1 && mo <= 12 && da >= 1 && da <= 31 && hh <= 23 && mi <= 59 && ss <= 60;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Require a value (when present) to be a valid FHIR date; otherwise throw an
|
|
509
|
+
* error naming the field and value. Empty/absent values are allowed and
|
|
510
|
+
* returned unchanged.
|
|
511
|
+
* @private
|
|
512
|
+
*/
|
|
513
|
+
_requireValidDate(value, path) {
|
|
514
|
+
if (value === null || value === undefined || value === '') return value;
|
|
515
|
+
if (!this._isValidFhirDate(value)) {
|
|
516
|
+
this._invalid(path, value, 'invalid date');
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Require a value to be a non-empty string code; otherwise throw an error
|
|
523
|
+
* naming the field and value.
|
|
524
|
+
* @private
|
|
525
|
+
*/
|
|
526
|
+
_requireValidCode(value, path) {
|
|
527
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
528
|
+
this._invalid(path, value, 'invalid code');
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Require a value (when present) to be a member of an allowed set; otherwise
|
|
535
|
+
* throw an error naming the field and value.
|
|
536
|
+
* @private
|
|
537
|
+
*/
|
|
538
|
+
_requireAllowed(value, allowed, path, what) {
|
|
539
|
+
if (value === null || value === undefined) return value;
|
|
540
|
+
if (!allowed.has(value)) {
|
|
541
|
+
this._invalid(path, value, what);
|
|
542
|
+
}
|
|
543
|
+
return value;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Format a FHIR date / dateTime / instant value into a human-readable string
|
|
548
|
+
* using the user's locale. The output precision follows the input precision:
|
|
549
|
+
* a year stays a year, a year-month becomes "Month Year", a full date becomes
|
|
550
|
+
* a localised date, and a dateTime/instant becomes a localised date and time.
|
|
551
|
+
*
|
|
552
|
+
* Unparseable values are returned unchanged so the renderer never throws on
|
|
553
|
+
* unexpected input.
|
|
554
|
+
*
|
|
555
|
+
* @param {string} value - a FHIR date, dateTime, or instant (e.g. "2024",
|
|
556
|
+
* "2024-03", "2024-03-15", "2024-03-15T10:30:00Z")
|
|
557
|
+
* @returns {string} the localised representation, or '' for empty input
|
|
558
|
+
*/
|
|
559
|
+
displayDate(value) {
|
|
560
|
+
if (value === null || value === undefined || value === '') {
|
|
561
|
+
return '';
|
|
562
|
+
}
|
|
563
|
+
if (typeof value !== 'string') {
|
|
564
|
+
value = String(value);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const locale = this._formatLocale();
|
|
568
|
+
|
|
569
|
+
// Year only — nothing to localise.
|
|
570
|
+
if (/^\d{4}$/.test(value)) {
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Year-month → "Month Year".
|
|
575
|
+
let m = /^(\d{4})-(\d{2})$/.exec(value);
|
|
576
|
+
if (m) {
|
|
577
|
+
const year = Number(m[1]), month = Number(m[2]);
|
|
578
|
+
const d = new Date(Date.UTC(year, month - 1, 1));
|
|
579
|
+
// Reject values that Date silently rolled over (e.g. month 13).
|
|
580
|
+
if (isNaN(d.getTime()) || d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1) {
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
return new Intl.DateTimeFormat(locale, {
|
|
584
|
+
year: 'numeric', month: 'long', timeZone: 'UTC'
|
|
585
|
+
}).format(d);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Full date → localised date.
|
|
589
|
+
m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
590
|
+
if (m) {
|
|
591
|
+
const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]);
|
|
592
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
593
|
+
// Reject values that Date silently rolled over (e.g. 2024-02-30).
|
|
594
|
+
if (isNaN(d.getTime()) || d.getUTCFullYear() !== year ||
|
|
595
|
+
d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) {
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
return new Intl.DateTimeFormat(locale, {
|
|
599
|
+
year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
|
|
600
|
+
}).format(d);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// dateTime / instant → localised date and time.
|
|
604
|
+
if (value.includes('T')) {
|
|
605
|
+
const d = new Date(value);
|
|
606
|
+
if (isNaN(d.getTime())) return value;
|
|
607
|
+
const hasTimezone = /(Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
608
|
+
const opts = {
|
|
609
|
+
year: 'numeric', month: 'long', day: 'numeric',
|
|
610
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
611
|
+
};
|
|
612
|
+
if (hasTimezone) {
|
|
613
|
+
// We don't know the user's timezone, so render the absolute instant in
|
|
614
|
+
// UTC and label it, rather than silently using the server's zone.
|
|
615
|
+
opts.timeZone = 'UTC';
|
|
616
|
+
opts.timeZoneName = 'short';
|
|
617
|
+
}
|
|
618
|
+
return new Intl.DateTimeFormat(locale, opts).format(d);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Unrecognised format — return unchanged.
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async renderValueSet(vs) {
|
|
626
|
+
vs = this._resolveResource(vs, 'ValueSet', 'renderValueSet');
|
|
627
|
+
this._requireAllowed(vs.status, VALID_PUBLICATION_STATUS, 'ValueSet.status', 'invalid status');
|
|
354
628
|
|
|
355
629
|
let div_ = div();
|
|
356
630
|
div_.h2().tx("Properties");
|
|
@@ -369,9 +643,9 @@ class Renderer {
|
|
|
369
643
|
}
|
|
370
644
|
|
|
371
645
|
async renderCodeSystem(cs, sourcePackage) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
646
|
+
cs = this._resolveResource(cs, 'CodeSystem', 'renderCodeSystem');
|
|
647
|
+
this._requireAllowed(cs.status, VALID_PUBLICATION_STATUS, 'CodeSystem.status', 'invalid status');
|
|
648
|
+
this._requireAllowed(cs.content, VALID_CODESYSTEM_CONTENT, 'CodeSystem.content', 'invalid CodeSystem content');
|
|
375
649
|
|
|
376
650
|
let div_ = div();
|
|
377
651
|
|
|
@@ -399,7 +673,7 @@ class Renderer {
|
|
|
399
673
|
p.tx(" ");
|
|
400
674
|
p.startCommaList("and");
|
|
401
675
|
for (let ext of supplements) {
|
|
402
|
-
this.renderLinkComma(p, ext);
|
|
676
|
+
this.renderLinkComma(p, getValuePrimitive(ext));
|
|
403
677
|
}
|
|
404
678
|
p.stopCommaList();
|
|
405
679
|
p.tx(".");
|
|
@@ -447,6 +721,7 @@ class Renderer {
|
|
|
447
721
|
li.tx(":");
|
|
448
722
|
const ul = li.ul();
|
|
449
723
|
for (let c of inc.concept) {
|
|
724
|
+
this._requireValidCode(c.code, 'ValueSet.compose.include.concept.code');
|
|
450
725
|
const li = ul.li();
|
|
451
726
|
const link = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, inc.system, inc.version, c.code) : null;
|
|
452
727
|
if (link) {
|
|
@@ -467,6 +742,7 @@ class Renderer {
|
|
|
467
742
|
li.startCommaList("and");
|
|
468
743
|
for (let f of inc.filter) {
|
|
469
744
|
let op = this.readFilterOp(f);
|
|
745
|
+
this._requireAllowed(op, VALID_FILTER_OPS, 'ValueSet.compose.include.filter.op', 'invalid filter operator');
|
|
470
746
|
if (op == 'exists') {
|
|
471
747
|
if (f.value == "true") {
|
|
472
748
|
li.commaItem(f.property+" "+ this.translate('VALUE_SET_EXISTS'));
|
|
@@ -485,13 +761,15 @@ class Renderer {
|
|
|
485
761
|
}
|
|
486
762
|
li.stopCommaList();
|
|
487
763
|
}
|
|
488
|
-
} else {
|
|
764
|
+
} else if (inc.valueSet && inc.valueSet.length > 0) {
|
|
489
765
|
li.tx(this.translatePlural(inc.valueSet.length, 'VALUE_SET_RULES_INC'));
|
|
490
766
|
li.startCommaList("and");
|
|
491
767
|
for (let vs of inc.valueSet) {
|
|
492
768
|
this.renderLinkComma(li, vs);
|
|
493
769
|
}
|
|
494
770
|
li.stopCommaList();
|
|
771
|
+
} else {
|
|
772
|
+
this._invalid('ValueSet.compose.include', inc, 'invalid ValueSet include (must specify a system or at least one valueSet)');
|
|
495
773
|
}
|
|
496
774
|
}
|
|
497
775
|
|
|
@@ -713,6 +991,7 @@ class Renderer {
|
|
|
713
991
|
}
|
|
714
992
|
|
|
715
993
|
async addConceptRow(tbl, concept, level, cs, columnInfo) {
|
|
994
|
+
this._requireValidCode(concept.code, 'CodeSystem.concept.code');
|
|
716
995
|
const tr = tbl.tr();
|
|
717
996
|
|
|
718
997
|
// Apply styling for deprecated concepts
|
|
@@ -1304,9 +1583,8 @@ class Renderer {
|
|
|
1304
1583
|
}
|
|
1305
1584
|
|
|
1306
1585
|
async renderCapabilityStatement(cs) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1586
|
+
cs = this._resolveResource(cs, 'CapabilityStatement', 'renderCapabilityStatement');
|
|
1587
|
+
this._requireAllowed(cs.status, VALID_PUBLICATION_STATUS, 'CapabilityStatement.status', 'invalid status');
|
|
1310
1588
|
|
|
1311
1589
|
let div_ = div();
|
|
1312
1590
|
|
|
@@ -1575,9 +1853,8 @@ class Renderer {
|
|
|
1575
1853
|
}
|
|
1576
1854
|
|
|
1577
1855
|
async renderTerminologyCapabilities(tc) {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1856
|
+
tc = this._resolveResource(tc, 'TerminologyCapabilities', 'renderTerminologyCapabilities');
|
|
1857
|
+
this._requireAllowed(tc.status, VALID_PUBLICATION_STATUS, 'TerminologyCapabilities.status', 'invalid status');
|
|
1581
1858
|
|
|
1582
1859
|
let div_ = div();
|
|
1583
1860
|
|
|
@@ -1675,9 +1952,8 @@ class Renderer {
|
|
|
1675
1952
|
* metadata table (reusing renderMetadataTable), then group-by-group rendering.
|
|
1676
1953
|
*/
|
|
1677
1954
|
async renderConceptMap(cm) {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
}
|
|
1955
|
+
cm = this._resolveResource(cm, 'ConceptMap', 'renderConceptMap');
|
|
1956
|
+
this._requireAllowed(cm.status, VALID_PUBLICATION_STATUS, 'ConceptMap.status', 'invalid status');
|
|
1681
1957
|
|
|
1682
1958
|
let div_ = div();
|
|
1683
1959
|
|
|
@@ -2080,8 +2356,12 @@ class Renderer {
|
|
|
2080
2356
|
*/
|
|
2081
2357
|
renderConceptMapRelationship(tr, tgt) {
|
|
2082
2358
|
if (tgt.relationship) {
|
|
2359
|
+
this._requireAllowed(tgt.relationship, VALID_CONCEPTMAP_RELATIONSHIPS,
|
|
2360
|
+
'ConceptMap.group.element.target.relationship', 'invalid ConceptMap relationship');
|
|
2083
2361
|
tr.td().tx(this.presentRelationshipCode(tgt.relationship));
|
|
2084
2362
|
} else if (tgt.equivalence) {
|
|
2363
|
+
this._requireAllowed(tgt.equivalence, VALID_CONCEPTMAP_EQUIVALENCES,
|
|
2364
|
+
'ConceptMap.group.element.target.equivalence', 'invalid ConceptMap equivalence');
|
|
2085
2365
|
tr.td().tx(this.presentEquivalenceCode(tgt.equivalence));
|
|
2086
2366
|
} else {
|
|
2087
2367
|
tr.td().tx("(" + "equivalent" + ")");
|
|
@@ -2252,6 +2532,7 @@ class Renderer {
|
|
|
2252
2532
|
return f.op;
|
|
2253
2533
|
}
|
|
2254
2534
|
}
|
|
2535
|
+
|
|
2255
2536
|
}
|
|
2256
2537
|
|
|
2257
2538
|
module.exports = { Renderer };
|
package/tx/library/ucum-types.js
CHANGED
|
@@ -939,7 +939,10 @@ class Registry {
|
|
|
939
939
|
}
|
|
940
940
|
|
|
941
941
|
register(handler) {
|
|
942
|
-
|
|
942
|
+
// Key on getCode() — the interface every handler implements. Some handlers
|
|
943
|
+
// (CelsiusHandler/FahrenheitHandler) override getCode() without setting a
|
|
944
|
+
// `.code` field, so keying on `.code` would register them under `undefined`.
|
|
945
|
+
this.handlers.set(handler.getCode(), handler);
|
|
943
946
|
}
|
|
944
947
|
}
|
|
945
948
|
|
package/tx/library/valueset.js
CHANGED
|
@@ -25,6 +25,52 @@ class ValueSet extends CanonicalResource {
|
|
|
25
25
|
this.jsonObj = valueSetToR5(jsonObj, fhirVersion);
|
|
26
26
|
this.validate();
|
|
27
27
|
this.buildMaps();
|
|
28
|
+
// Precalculated at construction so callers (e.g. the resource cache) have a
|
|
29
|
+
// cheap O(1) sense of how large this resource is.
|
|
30
|
+
this._conceptCount = ValueSet._countConcepts(this.jsonObj);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Number of concepts this ValueSet carries: the enumerated concepts in its
|
|
35
|
+
* compose (include + exclude `concept` lists) plus any concepts in an inline
|
|
36
|
+
* expansion (`expansion.contains`, counted recursively). Note this counts the
|
|
37
|
+
* concepts *present in the resource*, not the (possibly unbounded) size of the
|
|
38
|
+
* value set's full expansion. Precalculated at construction time.
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
conceptCount() {
|
|
42
|
+
return this._conceptCount;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static _countConcepts(jsonObj) {
|
|
46
|
+
let n = 0;
|
|
47
|
+
const compose = jsonObj && jsonObj.compose;
|
|
48
|
+
if (compose) {
|
|
49
|
+
for (const inc of (compose.include || [])) {
|
|
50
|
+
n += Array.isArray(inc.concept) ? inc.concept.length : 0;
|
|
51
|
+
}
|
|
52
|
+
for (const exc of (compose.exclude || [])) {
|
|
53
|
+
n += Array.isArray(exc.concept) ? exc.concept.length : 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (jsonObj && jsonObj.expansion) {
|
|
57
|
+
n += ValueSet._countContains(jsonObj.expansion.contains);
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static _countContains(items) {
|
|
63
|
+
if (!Array.isArray(items)) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
let n = 0;
|
|
67
|
+
for (const it of items) {
|
|
68
|
+
n++;
|
|
69
|
+
if (it && it.contains) {
|
|
70
|
+
n += ValueSet._countContains(it.contains);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return n;
|
|
28
74
|
}
|
|
29
75
|
|
|
30
76
|
/**
|