fhirsmith 0.18.1 → 1.0.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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/ips/profile-editor.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* the rest: a row of the second - type, system, code or value set, the rest), and
|
|
15
15
|
* other rules (FHIRPath). A row's expression is made from it. Saving posts the title and the
|
|
16
16
|
* constraints (key, description, expression); the server checks them, and keeps the keys.
|
|
17
|
+
* With cfg.mode 'rows' it is the profile's own page instead: the rows are the page's tables,
|
|
18
|
+
* and one at a time is added or edited, in a dialog, and saved by itself.
|
|
17
19
|
*/
|
|
18
20
|
(function () {
|
|
19
21
|
'use strict';
|
|
@@ -22,6 +24,8 @@
|
|
|
22
24
|
var LOINC = cfg.sectionSystem;
|
|
23
25
|
var dirty = false;
|
|
24
26
|
var saving = false;
|
|
27
|
+
// what to draw again when a row's fields change shape: the whole editor, or the row being edited
|
|
28
|
+
var redraw = function () { draw(); };
|
|
25
29
|
|
|
26
30
|
function el(tag, attrs, children) {
|
|
27
31
|
var e = document.createElement(tag);
|
|
@@ -359,11 +363,13 @@
|
|
|
359
363
|
url.hidden = select.value !== '*';
|
|
360
364
|
url.addEventListener('input', function () { r.entSystem = url.value.trim(); changed(); onChange(); });
|
|
361
365
|
select.addEventListener('change', function () {
|
|
362
|
-
var
|
|
366
|
+
var was = r.entSystem;
|
|
367
|
+
var wasVs = was === VS;
|
|
363
368
|
r.entSystem = select.value === '*' ? url.value.trim() : select.value;
|
|
364
369
|
if (!select.value) { r.entCode = ''; r.entCard = ''; r.entVs = ''; }
|
|
365
370
|
changed();
|
|
366
|
-
|
|
371
|
+
// the code (or value set) input comes and goes with the choice
|
|
372
|
+
if (!was || wasVs !== (r.entSystem === VS) || !select.value || select.value === '*') { redraw(); } else { onChange(); }
|
|
367
373
|
});
|
|
368
374
|
cell.appendChild(el('div', { class: 'ips-pf-pair' }, [select, url]));
|
|
369
375
|
if (r.entSystem === VS) {
|
|
@@ -414,7 +420,7 @@
|
|
|
414
420
|
r.resSystem = select.value === '*' ? url.value.trim() : select.value;
|
|
415
421
|
changed();
|
|
416
422
|
// the code column becomes a value set's, or a code's
|
|
417
|
-
if (wasVs !== (r.resSystem === VS)) {
|
|
423
|
+
if (wasVs !== (r.resSystem === VS)) { redraw(); } else { onChange(); }
|
|
418
424
|
});
|
|
419
425
|
cell.appendChild(select);
|
|
420
426
|
cell.appendChild(url);
|
|
@@ -573,6 +579,279 @@
|
|
|
573
579
|
.catch(function (e) { saving = false; show(['The profile was not saved: ' + e.message]); });
|
|
574
580
|
}
|
|
575
581
|
|
|
582
|
+
// ---- the profile's page: a rule at a time, added or edited in a dialog ----
|
|
583
|
+
|
|
584
|
+
// what must be true of the resources a rule is about: the rest of the rule, by kind
|
|
585
|
+
var NAME_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)?$/;
|
|
586
|
+
var RULE_KINDS = [
|
|
587
|
+
['present', 'Must be present somewhere'],
|
|
588
|
+
['subject', 'Patient must match the Document'],
|
|
589
|
+
['element', 'Must have an element'],
|
|
590
|
+
['conforms', 'Conform to a profile'],
|
|
591
|
+
['category', 'Has a category'],
|
|
592
|
+
['other', 'Something else (FHIRPath)']
|
|
593
|
+
];
|
|
594
|
+
function allOf(body) { return new RegExp('^\\.\\s*(?:' + body + '|all\\s*\\(\\s*' + body + '\\s*\\))$'); }
|
|
595
|
+
var CONFORMS_RE = allOf('conformsTo\\s*\\(\\s*' + STR + '\\s*\\)');
|
|
596
|
+
var CATEGORY_RE = allOf('category\\s*\\.\\s*hasCoding\\s*\\(\\s*' + STR + '\\s*,\\s*' + STR + '\\s*(?:,\\s*' + STR + '\\s*)?\\)');
|
|
597
|
+
|
|
598
|
+
/** the rule's kind, and its fields, from the rest of a rule about resources */
|
|
599
|
+
function ruleFields(r) {
|
|
600
|
+
var rest = (r.resRest || '').trim();
|
|
601
|
+
var m;
|
|
602
|
+
r.ruleKind = 'other';
|
|
603
|
+
r.ruleOther = rest;
|
|
604
|
+
r.ruleUrl = ''; r.ruleSystem = cfg.categorySystem || ''; r.ruleCode = ''; r.ruleVersion = ''; r.rulePath = ''; r.ruleDisplay = '';
|
|
605
|
+
r.ruleAll = true;
|
|
606
|
+
if (/^\.\s*exists\s*\(\s*\)$/.test(rest)) {
|
|
607
|
+
r.ruleKind = 'present';
|
|
608
|
+
} else if (/^\.\s*all\s*\(\s*sameSubject\s*\(\s*\)\s*\)$/.test(rest)) {
|
|
609
|
+
r.ruleKind = 'subject';
|
|
610
|
+
} else if ((m = CONFORMS_RE.exec(rest))) {
|
|
611
|
+
r.ruleKind = 'conforms';
|
|
612
|
+
r.ruleAll = m[1] === undefined;
|
|
613
|
+
r.ruleUrl = unquote(m[1] !== undefined ? m[1] : m[2]);
|
|
614
|
+
} else if ((m = CATEGORY_RE.exec(rest))) {
|
|
615
|
+
r.ruleKind = 'category';
|
|
616
|
+
r.ruleAll = m[1] === undefined;
|
|
617
|
+
var g = m[1] !== undefined ? [m[1], m[2], m[3]] : [m[4], m[5], m[6]];
|
|
618
|
+
r.ruleSystem = unquote(g[0]); r.ruleCode = unquote(g[1]); r.ruleVersion = g[2] !== undefined ? unquote(g[2]) : '';
|
|
619
|
+
} else if ((m = /^\.\s*([A-Za-z][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z][A-Za-z0-9_]*)?)\s*\.\s*exists\s*\(\s*\)$/.exec(rest))) {
|
|
620
|
+
r.ruleKind = 'element';
|
|
621
|
+
r.rulePath = m[1].replace(/\s+/g, '');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** the rest of a rule about resources, from its kind and fields, or null with what is wrong */
|
|
625
|
+
function ruleRest(r) {
|
|
626
|
+
var each = function (body) { return r.ruleAll ? '.all(' + body + ')' : '.' + body; };
|
|
627
|
+
switch (r.ruleKind) {
|
|
628
|
+
case 'present': return { rest: '.exists()', problems: [] };
|
|
629
|
+
case 'subject': return { rest: '.all(sameSubject())', problems: [] };
|
|
630
|
+
case 'element': return NAME_RE.test(r.rulePath.trim()) ? { rest: '.' + r.rulePath.trim() + '.exists()', problems: [] } : { rest: null, problems: ['rulePath'] };
|
|
631
|
+
case 'conforms': return CANONICAL_RE.test(r.ruleUrl.trim()) ? { rest: each('conformsTo(' + quote(r.ruleUrl.trim()) + ')'), problems: [] } : { rest: null, problems: ['ruleUrl'] };
|
|
632
|
+
case 'category': {
|
|
633
|
+
var problems = [];
|
|
634
|
+
if (!r.ruleSystem.trim()) { problems.push('ruleSystem'); }
|
|
635
|
+
if (!r.ruleCode.trim()) { problems.push('ruleCode'); }
|
|
636
|
+
if (problems.length) { return { rest: null, problems: problems }; }
|
|
637
|
+
return { rest: each('category.hasCoding(' + quote(r.ruleSystem.trim()) + ', ' + quote(r.ruleCode.trim()) + (r.ruleVersion.trim() ? ', ' + quote(r.ruleVersion.trim()) : '') + ')'), problems: [] };
|
|
638
|
+
}
|
|
639
|
+
default: {
|
|
640
|
+
var rest = (r.ruleOther || '').trim();
|
|
641
|
+
return rest && rest !== '.' ? { rest: rest.charAt(0) === '.' ? rest : '.' + rest, problems: [] } : { rest: null, problems: ['ruleOther'] };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/** the expression of a row in the dialog, or null with what is wrong */
|
|
646
|
+
function dialogExpression(r) {
|
|
647
|
+
if (r.mode !== 'resource') { return rowExpression(r); }
|
|
648
|
+
var t = ruleRest(r);
|
|
649
|
+
r.resRest = t.rest || '';
|
|
650
|
+
var e = resourceExpression(r);
|
|
651
|
+
return { expression: t.rest ? e.expression : null, problems: t.problems.concat(e.problems.filter(function (p) { return p !== 'resRest'; })) };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function rowsMode() {
|
|
655
|
+
var MODE = { section: 'section', resource: 'resource', other: 'text' };
|
|
656
|
+
var TITLES = { section: 'a required section or entry', resource: 'a rule about resources', text: 'a rule' };
|
|
657
|
+
var byKey = {};
|
|
658
|
+
(cfg.profile.constraints || []).forEach(function (c) { byKey[c.key] = c; });
|
|
659
|
+
var dialog = el('dialog', { class: 'ips-pf-dialog', 'aria-labelledby': 'ips-pf-dialog-title' });
|
|
660
|
+
document.body.appendChild(dialog);
|
|
661
|
+
var editing = null;
|
|
662
|
+
|
|
663
|
+
function showIn(box, problems, kind) {
|
|
664
|
+
box.innerHTML = '';
|
|
665
|
+
if (!problems.length) { return; }
|
|
666
|
+
var alert = el('div', { class: 'alert alert-' + (kind || 'danger'), style: 'margin: 0' });
|
|
667
|
+
if (problems.length === 1) { alert.textContent = problems[0]; } else {
|
|
668
|
+
alert.appendChild(el('ul', { style: 'margin: 0' }, problems.map(function (p) { return el('li', { text: p }); })));
|
|
669
|
+
}
|
|
670
|
+
box.appendChild(alert);
|
|
671
|
+
}
|
|
672
|
+
// a labelled part of the dialog: a label, what goes in it, and a hint
|
|
673
|
+
function field(label, controls, hint) {
|
|
674
|
+
return el('div', { class: 'ips-pf-field' }, [el('label', { class: 'ips-pf-label', text: label })].concat(controls).concat(hint ? [el('div', { class: 'ips-pf-hint', text: hint })] : []));
|
|
675
|
+
}
|
|
676
|
+
function howMany(r, fieldName, update, fields) {
|
|
677
|
+
var input = cardInput(r, fieldName, update);
|
|
678
|
+
fields[fieldName] = input;
|
|
679
|
+
return el('div', { class: 'ips-pf-pair ips-pf-how' }, [el('span', { text: 'How many' }), input,
|
|
680
|
+
el('span', { class: 'ips-pf-hint', text: 'blank: at least one; or min..max - 1..1, 0..1, 1..*' })]);
|
|
681
|
+
}
|
|
682
|
+
// a code system for the category: one of the named ones, or another by URL
|
|
683
|
+
function categorySystem(r, update) {
|
|
684
|
+
var cell = el('div', { class: 'ips-pf-pair' });
|
|
685
|
+
var select = el('select', { class: 'form-control ips-ed-input' });
|
|
686
|
+
cfg.systems.forEach(function (s) { select.appendChild(el('option', { value: s.url, text: s.name })); });
|
|
687
|
+
(cfg.categorySystems || []).forEach(function (s) { select.appendChild(el('option', { value: s.url, text: s.name })); });
|
|
688
|
+
select.appendChild(el('option', { value: '*', text: 'Another system...' }));
|
|
689
|
+
var known = Array.prototype.some.call(select.options, function (o) { return o.value === r.ruleSystem; });
|
|
690
|
+
select.value = known ? r.ruleSystem : '*';
|
|
691
|
+
var url = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'code system URL' });
|
|
692
|
+
url.value = known ? '' : r.ruleSystem;
|
|
693
|
+
url.hidden = known;
|
|
694
|
+
url.addEventListener('input', function () { r.ruleSystem = url.value.trim(); changed(); update(); });
|
|
695
|
+
select.addEventListener('change', function () {
|
|
696
|
+
url.hidden = select.value !== '*';
|
|
697
|
+
r.ruleSystem = select.value === '*' ? url.value.trim() : select.value;
|
|
698
|
+
changed();
|
|
699
|
+
update();
|
|
700
|
+
});
|
|
701
|
+
cell.appendChild(select);
|
|
702
|
+
cell.appendChild(url);
|
|
703
|
+
cell.url = url;
|
|
704
|
+
return cell;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** the dialog's contents, (re)drawn for the row being edited */
|
|
708
|
+
function drawDialog() {
|
|
709
|
+
var e = editing;
|
|
710
|
+
var r = e.r;
|
|
711
|
+
dialog.innerHTML = '';
|
|
712
|
+
var fields = {};
|
|
713
|
+
var exprLine = el('div', { class: 'ips-pf-expr' });
|
|
714
|
+
var update = function () {
|
|
715
|
+
var t = dialogExpression(r);
|
|
716
|
+
exprLine.textContent = r.mode === 'text' ? '' : (t.expression || '(incomplete - the fields in red)');
|
|
717
|
+
Object.keys(fields).forEach(function (k) { fields[k].classList.toggle('ips-pf-bad', t.problems.indexOf(k) >= 0); });
|
|
718
|
+
};
|
|
719
|
+
var body = el('div', { class: 'ips-pf-dialog-body' });
|
|
720
|
+
var human = el('textarea', { class: 'form-control', rows: 2, placeholder: 'e.g. A problem list, with the patient\'s hypertension' });
|
|
721
|
+
human.value = r.human;
|
|
722
|
+
human.addEventListener('input', function () { r.human = human.value; changed(); });
|
|
723
|
+
body.appendChild(field('Expectation', [human], 'What is expected, as people will read it.'));
|
|
724
|
+
|
|
725
|
+
if (r.mode === 'section') {
|
|
726
|
+
body.appendChild(field('Section', [sectionCell(r, update), howMany(r, 'secCard', update, fields)]));
|
|
727
|
+
var ent = entryCell(r, update);
|
|
728
|
+
if (ent.code) { fields.entCode = ent.code.input; }
|
|
729
|
+
if (ent.vs) { fields.entVs = ent.vs.input; }
|
|
730
|
+
body.appendChild(field('Entries', [ent].concat(r.entSystem ? [howMany(r, 'entCard', update, fields)] : []),
|
|
731
|
+
'Optionally, entries the section must have: with a code in a code system, or a code in a value set.'));
|
|
732
|
+
} else if (r.mode === 'resource') {
|
|
733
|
+
var type = typeCell(r, update);
|
|
734
|
+
var system = systemCell(r, update);
|
|
735
|
+
var byVs = r.resSystem === VS;
|
|
736
|
+
var code = byVs ? vsInput(r, 'resVs', 'resVsTitle', update) : codeInput(r, 'resCode', 'resDisplay', function () { return r.resSystem; }, update);
|
|
737
|
+
fields.resType = type.other;
|
|
738
|
+
fields.resSystem = system.url;
|
|
739
|
+
fields[byVs ? 'resVs' : 'resCode'] = code.input;
|
|
740
|
+
body.appendChild(field('Resources', [type], 'The type of resource, or any.'));
|
|
741
|
+
body.appendChild(field('With a code', [system, code], 'Anywhere in the document, whatever section they are in.'));
|
|
742
|
+
var kind = el('select', { class: 'form-control ips-ed-input' });
|
|
743
|
+
RULE_KINDS.forEach(function (k) { kind.appendChild(el('option', { value: k[0], text: k[1] })); });
|
|
744
|
+
kind.value = r.ruleKind;
|
|
745
|
+
kind.addEventListener('change', function () { r.ruleKind = kind.value; changed(); drawDialog(); });
|
|
746
|
+
var more = [];
|
|
747
|
+
if (r.ruleKind === 'element') {
|
|
748
|
+
var path = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'e.g. onset, or code.text' });
|
|
749
|
+
path.value = r.rulePath;
|
|
750
|
+
path.addEventListener('input', function () { r.rulePath = path.value; changed(); update(); });
|
|
751
|
+
fields.rulePath = path;
|
|
752
|
+
more.push(field('Element', [path], 'An element of the resources, or an element of one of their elements (one dot, no deeper).'));
|
|
753
|
+
} else if (r.ruleKind === 'conforms') {
|
|
754
|
+
var url = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'the profile\'s canonical URL' });
|
|
755
|
+
url.value = r.ruleUrl;
|
|
756
|
+
url.addEventListener('input', function () { r.ruleUrl = url.value; changed(); update(); });
|
|
757
|
+
fields.ruleUrl = url;
|
|
758
|
+
more.push(field('Profile', [url], 'Each of the resources must conform to it. It can be one of the clinical profiles, or a profile the validator has loaded.'));
|
|
759
|
+
} else if (r.ruleKind === 'category') {
|
|
760
|
+
var csys = categorySystem(r, update);
|
|
761
|
+
var ccode = codeInput(r, 'ruleCode', 'ruleDisplay', function () { return r.ruleSystem; }, update);
|
|
762
|
+
var version = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'version (optional)' });
|
|
763
|
+
version.value = r.ruleVersion;
|
|
764
|
+
version.addEventListener('input', function () { r.ruleVersion = version.value; changed(); update(); });
|
|
765
|
+
fields.ruleSystem = csys.url;
|
|
766
|
+
fields.ruleCode = ccode.input;
|
|
767
|
+
more.push(field('Category', [csys, ccode, version], 'Each of the resources must have a category with this code.'));
|
|
768
|
+
} else if (r.ruleKind === 'other') {
|
|
769
|
+
var other = el('textarea', { class: 'form-control ips-ed-input', rows: 3, placeholder: '.count() = 1', style: 'font-family: monospace' });
|
|
770
|
+
other.value = r.ruleOther;
|
|
771
|
+
other.addEventListener('input', function () { r.ruleOther = other.value; changed(); update(); });
|
|
772
|
+
fields.ruleOther = other;
|
|
773
|
+
more.push(field('FHIRPath', [other], 'FHIRPath on the resources found, starting with a dot.'));
|
|
774
|
+
}
|
|
775
|
+
body.appendChild(field('What must be true', [kind].concat(more)));
|
|
776
|
+
} else {
|
|
777
|
+
var text = el('textarea', { class: 'form-control ips-ed-input', rows: 5, placeholder: 'A FHIRPath expression on the document (the Bundle), true when it meets the expectation', style: 'font-family: monospace' });
|
|
778
|
+
text.value = r.expression;
|
|
779
|
+
text.addEventListener('input', function () { r.expression = text.value; changed(); });
|
|
780
|
+
body.appendChild(field('FHIRPath', [text], 'Evaluated on the document (the Bundle): the rule is met when it is true.'));
|
|
781
|
+
}
|
|
782
|
+
if (r.mode !== 'text') { body.appendChild(field('As FHIRPath', [exprLine])); }
|
|
783
|
+
body.appendChild(e.msg);
|
|
784
|
+
|
|
785
|
+
var save = el('button', { type: 'button', class: 'btn btn-primary', text: 'Save', onclick: function () { saveRow(); } });
|
|
786
|
+
var cancel = el('button', { type: 'button', class: 'btn btn-default', text: 'Cancel', onclick: function () { stop(false); } });
|
|
787
|
+
e.save = save;
|
|
788
|
+
dialog.appendChild(el('div', { class: 'ips-pf-dialog-head' }, [
|
|
789
|
+
el('h4', { id: 'ips-pf-dialog-title', text: (r.key ? 'Edit ' : 'Add ') + TITLES[r.mode] }),
|
|
790
|
+
el('button', { type: 'button', class: 'ips-pf-dialog-x', 'aria-label': 'Close', text: '×', onclick: function () { stop(false); } })
|
|
791
|
+
]));
|
|
792
|
+
dialog.appendChild(body);
|
|
793
|
+
dialog.appendChild(el('div', { class: 'ips-pf-dialog-foot' }, [cancel, ' ', save]));
|
|
794
|
+
update();
|
|
795
|
+
if (!e.focused) { e.focused = true; human.focus(); }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function start(r) {
|
|
799
|
+
if (editing) { return; }
|
|
800
|
+
if (r.mode === 'resource') { ruleFields(r); }
|
|
801
|
+
editing = { r: r, msg: el('div', { class: 'ips-pf-messages' }) };
|
|
802
|
+
dirty = false;
|
|
803
|
+
drawDialog();
|
|
804
|
+
dialog.showModal();
|
|
805
|
+
}
|
|
806
|
+
// close the dialog: when there are changes, only if the user says so (unless saved)
|
|
807
|
+
function stop(saved) {
|
|
808
|
+
if (!editing) { return; }
|
|
809
|
+
if (!saved && dirty && !window.confirm('Discard your changes to this rule?')) { return; }
|
|
810
|
+
editing = null;
|
|
811
|
+
dirty = false;
|
|
812
|
+
dialog.close();
|
|
813
|
+
}
|
|
814
|
+
dialog.addEventListener('cancel', function (ev) { ev.preventDefault(); stop(false); });
|
|
815
|
+
|
|
816
|
+
function saveRow() {
|
|
817
|
+
var e = editing;
|
|
818
|
+
if (!e || saving) { return; }
|
|
819
|
+
var t = dialogExpression(e.r);
|
|
820
|
+
if (!t.expression && e.r.mode !== 'text') { showIn(e.msg, ['The rule is incomplete (the fields in red)']); return; }
|
|
821
|
+
saving = true;
|
|
822
|
+
e.save.disabled = true;
|
|
823
|
+
showIn(e.msg, ['Saving...'], 'info');
|
|
824
|
+
var url = cfg.urls.constraint + (e.r.key ? '/' + encodeURIComponent(e.r.key) : '');
|
|
825
|
+
fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
826
|
+
body: JSON.stringify({ human: e.r.human, expression: t.expression || '', _csrf: cfg.csrf }) })
|
|
827
|
+
.then(function (res) {
|
|
828
|
+
return res.json().then(function (j) { return { ok: res.ok, j: j }; }, function () { return { ok: false, j: { problems: ['The server answered ' + res.status + (res.status === 403 ? ' (has your session expired? reload the page)' : '')] } }; });
|
|
829
|
+
})
|
|
830
|
+
.then(function (a) {
|
|
831
|
+
saving = false;
|
|
832
|
+
if (a.ok) { stop(true); location.reload(); return; }
|
|
833
|
+
e.save.disabled = false;
|
|
834
|
+
showIn(e.msg, a.j.problems || ['The rule was not saved']);
|
|
835
|
+
})
|
|
836
|
+
.catch(function (x) { saving = false; e.save.disabled = false; showIn(e.msg, ['The rule was not saved: ' + x.message]); });
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
redraw = drawDialog;
|
|
840
|
+
Array.prototype.forEach.call(document.querySelectorAll('.ips-pf-edit'), function (b) {
|
|
841
|
+
b.addEventListener('click', function () {
|
|
842
|
+
var c = byKey[b.closest('tr').getAttribute('data-key')];
|
|
843
|
+
if (c) { start(rowFrom(c)); }
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
Array.prototype.forEach.call(document.querySelectorAll('.ips-pf-add'), function (b) {
|
|
847
|
+
b.addEventListener('click', function () {
|
|
848
|
+
var r = rowFrom({});
|
|
849
|
+
r.mode = MODE[b.getAttribute('data-part')];
|
|
850
|
+
start(r);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
576
855
|
window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
|
577
|
-
draw();
|
|
856
|
+
if (cfg.mode === 'rows') { rowsMode(); } else { draw(); }
|
|
578
857
|
})();
|
package/ips/readme.md
CHANGED
|
@@ -37,7 +37,9 @@ network. Without it there are no links and no analysis.
|
|
|
37
37
|
|
|
38
38
|
`/ips` lists the projects. The administrator logs in at `/ips/login` and, at
|
|
39
39
|
`/ips/admin/projects` and `/ips/admin/users`, creates and deletes projects and users, and
|
|
40
|
-
says which users manage which projects.
|
|
40
|
+
says which users manage which projects. The administrator also looks after the
|
|
41
|
+
[clinical profiles](#clinical-profiles) and the [national profiles](#national-profiles),
|
|
42
|
+
which apply across all projects. A project has a code - 2-32 lower case letters,
|
|
41
43
|
digits and hyphens, fixed once it exists - and lives at `/ips/{code}`. Deleting a project
|
|
42
44
|
deletes its documents.
|
|
43
45
|
|
|
@@ -98,14 +100,42 @@ documentation section with the endpoint's URL, the CapabilityStatement link and
|
|
|
98
100
|
the requests it answers - and `/ips/{code}/Bundle/{id}` shows the document.
|
|
99
101
|
The document page's header names the patient, the document's date, when and how it was
|
|
100
102
|
received and its size, with links to the FHIR JSON and to download it (`?download=1`, sent
|
|
101
|
-
as an attachment named `ips-{code}-{id}.json`). Below it,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
narrative
|
|
108
|
-
|
|
103
|
+
as an attachment named `ips-{code}-{id}.json`). Below it, the processing pipeline is shown
|
|
104
|
+
as one connected line: Received, then each step in the order it runs, coloured by where it
|
|
105
|
+
is (tick: done, cross: failed, gear: running, moon: waiting - the step's note, or for
|
|
106
|
+
managers the reason it failed, on hover); each step links to the view that shows its work.
|
|
107
|
+
Managers get "Process again" and "Delete" buttons. The views of a document are tabs, chosen
|
|
108
|
+
with `?view=`: `narrative` (the default; a switch, `&narrative=generated`, chooses between
|
|
109
|
+
the narrative as provided - the Composition's narrative followed by each section's, with
|
|
110
|
+
the section titles - and the narrative this server generates, below), `structure` (the
|
|
111
|
+
document's logical structure, below), `analysis` (what the analysis step found, below), `translation` (what
|
|
112
|
+
the translate step did with each concept, below), `terminology`, `validation` (below),
|
|
113
|
+
`national` (below), `profiles` (the clinical profile checks), `ai` (the AI assessment,
|
|
114
|
+
below) and `source` (the JSON, pretty-printed, monospaced). The old view names `generated`
|
|
115
|
+
and `json` still work. The actions (Edit, the Share buttons, and for managers Process again
|
|
116
|
+
and Delete) are on a line of their own below the pipeline.
|
|
117
|
+
`structure` shows the document as a tree table, in the style of the FHIR specification's
|
|
118
|
+
structure tables: columns Path, Type, Id and Details, with the tree drawn in the Path column
|
|
119
|
+
(each node expands and collapses; Expand all and Collapse all buttons; without scripts
|
|
120
|
+
every row shows). It starts at the Composition: first what the Composition refers to other
|
|
121
|
+
than its sections (subject, author, custodian, attesters, ...), then the sections, nested,
|
|
122
|
+
each with what it refers to (its entries, author, focus). Each reference's path is where it
|
|
123
|
+
is (`entry[0]`, `attester[0].party`), and the reference as given is on hover over the Id;
|
|
124
|
+
each resource expands into the references it makes, and so on (sections start open,
|
|
125
|
+
resources closed). A reference resolves as in a Bundle - by fullUrl, absolute or relative
|
|
126
|
+
to the referring entry's - and `#id` is a resource contained in the referring resource (or
|
|
127
|
+
in the resource that contains it). One that doesn't resolve is shown as external (not in
|
|
128
|
+
document). A resource is expanded once, at the shallowest place it is referred to (so one
|
|
129
|
+
the Composition refers to directly expands there, not inside the subject that also refers to
|
|
130
|
+
it); everywhere else it is shown again, greyed, saying where it is expanded, but doesn't
|
|
131
|
+
expand a second time. Details says what the resource is - a name (a person's, an organization's),
|
|
132
|
+
else its code (`code`, the medication - following `medicationReference` -, `vaccineCode`,
|
|
133
|
+
`type`...), with an Observation's value - and when: the first of the clinical dates (onset,
|
|
134
|
+
occurrence, performed, effective, ...), falling back to the record's own (`recorded`,
|
|
135
|
+
`issued`, `date`, `meta.lastUpdated`). Icons, from `static/assets/images`: the document,
|
|
136
|
+
sections, people (Patient, Practitioner, PractitionerRole, RelatedPerson), organizations,
|
|
137
|
+
medications (Medication and the Medication* resources), other clinical resources, Provenance
|
|
138
|
+
and AuditEvent, references outside the document, and anything else.
|
|
109
139
|
`terminology` lists every CodeableConcept in the document (`ips/terminology.js`): the
|
|
110
140
|
Composition's own, then each section's entries in the IPS section order (Patient Story,
|
|
111
141
|
Problems, Allergies, Medications, ... Results; sections not in that list after, in document
|
|
@@ -155,10 +185,10 @@ takes submitted documents in turn, marks each `processing`, runs the pipeline's
|
|
|
155
185
|
it (`ips/pipeline.js`), and marks it `processed` with the steps' notes, or `error` with the
|
|
156
186
|
message of the step that failed. Each step's own state (`waiting`, `running`, `done` or
|
|
157
187
|
`failed`, with its note or error) is kept in its result row as it goes, which is what the
|
|
158
|
-
document page's step line shows; processing again clears every step's result first. The pipeline, in order, will be: **translate** CodeableConcepts
|
|
159
|
-
to the international terminologies; generate the **narrative**; **validate** the document;
|
|
160
|
-
measure its quality with PIQI; check it against the **clinical profiles**; **assess** it with AI. Translation, the narrative,
|
|
161
|
-
validation, the **terminology check**, the clinical profiles check and the AI assessment exist so far (PIQI does not). A step keeps what it finds in one of two places: as a result stored against the
|
|
188
|
+
document page's step line shows; processing again clears every step's result first. The pipeline, in order, will be: determine the document's **nationality**; **analyse** each resource; **translate** CodeableConcepts
|
|
189
|
+
to the international terminologies; generate the **narrative**; **validate** the document; validate it against the
|
|
190
|
+
**national profiles**; measure its quality with PIQI; check it against the **clinical profiles**; **assess** it with AI. Translation, the narrative,
|
|
191
|
+
validation, the analysis, the national profiles, the **terminology check**, the clinical profiles check and the AI assessment exist so far (PIQI does not). A step keeps what it finds in one of two places: as a result stored against the
|
|
162
192
|
document in `document_results` (one row per step - the validation outcome), or in the
|
|
163
193
|
**analysed copy** - a second copy of the document (`documents.analysed_json`) that the steps
|
|
164
194
|
adorn with extensions, so that the master document is never changed. The analysed copy is
|
|
@@ -166,16 +196,85 @@ rebuilt from the master on every run, so a document can always be processed agai
|
|
|
166
196
|
Anything left `processing` when the server stopped is queued again at startup. Managers can
|
|
167
197
|
queue a document again from its page, though not while it is being processed.
|
|
168
198
|
|
|
199
|
+
### Nationality
|
|
200
|
+
|
|
201
|
+
The first step (`ips/nationality.js`) works out which nation the document is from, as far as the document says -
|
|
202
|
+
few national IGs give a way to tell, so this is a short list of signs, to grow as more are found:
|
|
203
|
+
|
|
204
|
+
- **US**: the Bundle claims the US Patient Summary profile (`http://hl7.org/fhir/us/pcs/StructureDefinition/Bundle-us-pcs`),
|
|
205
|
+
or a US Core extension (`http://hl7.org/fhir/us/core/StructureDefinition/...`) is used anywhere in the document;
|
|
206
|
+
- **AU**: the Bundle claims the AU Patient Summary profile (`http://hl7.org.au/fhir/ps/StructureDefinition/au-ps-bundle`).
|
|
207
|
+
|
|
208
|
+
The nation is its ISO 3166 two-letter code, or unknown when nothing points to a nation, or the signs point to more
|
|
209
|
+
than one. It is kept as the step's result (with the reasons) and in `documents.nationality`, for the list: the
|
|
210
|
+
project page has a Nation column (the code, or `??`), the document page shows the nation's flag to the left of the
|
|
211
|
+
banner (a black and white globe when it isn't known, or there is no flag for it yet -
|
|
212
|
+
`static/assets/images/flags/{code}.svg`), and the first bullet on the Composition in the analysis view says what was
|
|
213
|
+
decided, and why.
|
|
214
|
+
|
|
215
|
+
### Analysis
|
|
216
|
+
|
|
217
|
+
The first step (`ips/analysis.js`), over the document as submitted. It walks the same tree
|
|
218
|
+
as the structure view (`ips/structure.js`) and, for each resource there (once, where it is
|
|
219
|
+
first met; contained resources too) and for the Composition section by section (the
|
|
220
|
+
Composition itself without its sections), keeps a list of findings, by the node's key
|
|
221
|
+
(`e{n}` for entry n, `e{n}#id` for a contained resource, `s:{path}` for a section):
|
|
222
|
+
|
|
223
|
+
- every extension, at any depth (a primitive's extensions are at the primitive's path; an
|
|
224
|
+
extension's own parts are not listed), with its definition when the module can find one
|
|
225
|
+
- the validator is asked (fhir-validator-wrapper's `lookupCanonical()`, the service's
|
|
226
|
+
`/canonical`, which answers from the validator's context; with `autoLoadPackages`, on by
|
|
227
|
+
default, the validator finds, downloads and loads the package that defines an extension it
|
|
228
|
+
doesn't have, as it does while validating), and the answers are cached - or a note that it
|
|
229
|
+
couldn't be found;
|
|
230
|
+
modifier extensions likewise, in maroon; `textLink` extensions are left out;
|
|
231
|
+
- `implicitRules`, in maroon;
|
|
232
|
+
- in navy: `active` = false; a status of `entered-in-error` (and a Condition's or
|
|
233
|
+
AllergyIntolerance's `verificationStatus` of entered-in-error); a status of `unknown`; or no
|
|
234
|
+
status where the resource requires one (`lifecycleStatus` for a Goal);
|
|
235
|
+
- in dark green: a `subject` (or `patient`) that isn't the document's - not the same
|
|
236
|
+
resource in the document, nor the same reference, nor the same identifier - and any `focus`;
|
|
237
|
+
- dates in the future, where one isn't expected. Only elements the (R4) type model says are
|
|
238
|
+
dates (date, dateTime, instant) are looked at - so not an identifier or a note that looks
|
|
239
|
+
like a date - and not inside extensions. Some dates are about the future by their nature and
|
|
240
|
+
are never mentioned: the end of a period (a name's, address's or identifier's validity, a
|
|
241
|
+
prescription's), expiry dates, due dates, and schedules (Timing). Some resources may have
|
|
242
|
+
future dates: Appointment, AppointmentResponse, ImmunizationRecommendation, Schedule and
|
|
243
|
+
Slot always; requests and plans (ServiceRequest, MedicationRequest, DeviceRequest,
|
|
244
|
+
NutritionOrder, CommunicationRequest, SupplyRequest, VisionPrescription, CarePlan, Task, and
|
|
245
|
+
a Goal by its lifecycleStatus) until they are over (completed, revoked, cancelled,
|
|
246
|
+
entered-in-error, stopped, failed or rejected); and events still to come - a Procedure in
|
|
247
|
+
preparation, a planned Encounter, an intended MedicationStatement or DeviceUseStatement. A
|
|
248
|
+
date with no time zone is in the future only if it is everywhere;
|
|
249
|
+
- a Patient's `link`s; a Composition whose status isn't `final`;
|
|
250
|
+
- for a section: its `emptyReason`, and an author other than that of what contains it (the
|
|
251
|
+
Composition, or the containing section - which may have inherited its own);
|
|
252
|
+
- whether there is narrative.
|
|
253
|
+
|
|
254
|
+
With nothing but the narrative to say, a resource is "All OK". The `analysis` view shows the
|
|
255
|
+
structure as a flat table - each node's row as in the structure view but with its full path
|
|
256
|
+
(`Composition.section[0].entry[0].resolve().subject`) - with a row under each that lists the
|
|
257
|
+
findings; a resource already shown is "Analysed above", and a reference to a resource not
|
|
258
|
+
in the document has no rows at all.
|
|
259
|
+
|
|
169
260
|
### Translation
|
|
170
261
|
|
|
171
|
-
The
|
|
262
|
+
The second step. Every CodeableConcept the terminology view lists is checked as the
|
|
172
263
|
terminology check (below) checks it; one that fails - no coding in the element's value set,
|
|
173
264
|
or, for an element with no value set of its own, no international coding - is sent to the
|
|
174
265
|
terminology server to translate: `ConceptMap/$translate` with `sourceCodeableConcept` (the
|
|
175
266
|
concept as submitted) and `targetScope` (the element's value set, or the fallback), the
|
|
176
|
-
value set and what it draws on as `tx-resource`.
|
|
267
|
+
value set and what it draws on as `tx-resource`. A SNOMED CT coding with no version is sent as a code of
|
|
268
|
+
the edition of the document's nation: when the [nationality](#nationality) step found one, and a national profile
|
|
269
|
+
for that nation names a SNOMED CT edition, the coding goes with `version` = `http://snomed.info/sct/{edition}`
|
|
270
|
+
(the document is not changed, and the translation view says which edition was used). Which edition a code is
|
|
271
|
+
from decides which maps apply - a national edition's own concepts translate to the International Edition. The
|
|
272
|
+
terminology check does not do this: it takes the codes as they are, so a SNOMED CT code with no version passes if
|
|
273
|
+
it is fine as it is, and one whose version names another edition does not. The server chooses the concept maps. Every
|
|
177
274
|
match that comes back (whatever its relationship) is added to the concept in the analysed
|
|
178
|
-
copy as a Coding, unless the concept already has that system and code,
|
|
275
|
+
copy as a Coding, unless the concept already has that coding - the same system and code, and the same version
|
|
276
|
+
when both give one (SNOMED CT versions compared by edition: the same code in the International Edition is a
|
|
277
|
+
different coding from the one in a national edition, and is what the translation is for) - with an extension
|
|
179
278
|
saying where it came from:
|
|
180
279
|
`http://healthintersections.com.au/fhirsmith/StructureDefinition/ips-translation`, with
|
|
181
280
|
`server` (the terminology server, as configured), `conceptMap` (the match's `originMap`, when
|
|
@@ -236,16 +335,42 @@ view shows it.
|
|
|
236
335
|
`modules.ips.validator` configures a FHIR validator service, exactly as the SHL module's
|
|
237
336
|
does (`fhir-validator-wrapper`: the validator CLI jar in the data folder's `bin` directory,
|
|
238
337
|
run as a service on its own `port`, with `txServer` - tx.fhir.org, by default - and the IPS
|
|
239
|
-
package loaded).
|
|
338
|
+
package loaded). `jar` names a validator jar of its own instead - a validator built locally from
|
|
339
|
+
org.hl7.fhir.core, say: absolute, or relative to the data folder (`bin/local/validator_cli.jar`) - which
|
|
340
|
+
is used as it is, never downloaded or updated (the wrapper reads its version from the jar, so it
|
|
341
|
+
is only given the options it knows). Keep such a jar out of `bin/validator_cli.jar`: the wrapper
|
|
342
|
+
keeps that one up to date, so a different jar in its place is replaced by the next release. `autoLoadPackages` (default true) starts the validator with `-autoLoadPackages`, so
|
|
343
|
+
the analysis step's extension lookups can load the package that defines an extension (validator
|
|
344
|
+
7.0.5 and `fhir-validator-wrapper` 1.7.0 or later; older ones just don't look further). It starts in the background when the module does, and the queue waits for
|
|
240
345
|
it. Each document - its analysed copy, so the translations are seen (the validator will
|
|
241
346
|
remark on the translation extension until it is given its definition) - is validated against the IPS Bundle profile
|
|
242
347
|
(`http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips`; set `profiles` to validate
|
|
243
|
-
against something else)
|
|
244
|
-
OperationOutcome is stored with the document and shown on
|
|
348
|
+
against something else) - validator 7.0.4 dropped its IPS code checks, so `checkIpsCodes` is no longer
|
|
349
|
+
sent (the validator refuses it) - and the OperationOutcome is stored with the document and shown on
|
|
245
350
|
its `validation` view: the counts, each issue's severity, code, location and message, and
|
|
246
351
|
the raw OperationOutcome. Without a configured, running validator the step fails, and the
|
|
247
352
|
document's status is `error`.
|
|
248
353
|
|
|
354
|
+
### National profile validation
|
|
355
|
+
|
|
356
|
+
The national step runs after validation. It validates the document (the analysed copy, as validation does) against each
|
|
357
|
+
[national profile](#national-profiles) in turn: the national IG's package is loaded into the validator (once - a package
|
|
358
|
+
that has loaded, or has failed to, is remembered until the national profiles are changed), then the document is
|
|
359
|
+
validated against the IG's profile on Bundle. The national profiles' packages are also loaded when the validator
|
|
360
|
+
starts (and when the national profiles are changed), before any document is processed, so that the analysis step -
|
|
361
|
+
which runs first - finds the extensions they define; an extension that couldn't be found is looked up again once a
|
|
362
|
+
package has loaded. For each profile the result keeps
|
|
363
|
+
the OperationOutcome, its counts, and whether the document passed - it passes when the validator reports no errors (or
|
|
364
|
+
fatal issues). A profile whose package can't be loaded, or that the validator can't validate against, is kept with the
|
|
365
|
+
reason and counts as not checked; it doesn't stop the pipeline. With no national profiles there is nothing to do, and no
|
|
366
|
+
validator is needed; with some, the step fails, as validation does, without a configured, running validator.
|
|
367
|
+
|
|
368
|
+
The document's **National Profiles** view lists the profiles as they were when the document was processed, with the
|
|
369
|
+
outcome (passed, failed, or not checked) and the issue counts for each; a profile's page shows its validation as the
|
|
370
|
+
validation view shows the base validation (`?view=national&np={n}`). The packages loaded for national profiles stay
|
|
371
|
+
loaded in the validator alongside its own (`packages`), so two national IGs that define the same things differently can
|
|
372
|
+
affect each other.
|
|
373
|
+
|
|
249
374
|
### Terminology check
|
|
250
375
|
|
|
251
376
|
After validation, every CodeableConcept the terminology view lists is checked for its
|
|
@@ -403,12 +528,36 @@ The editor (`/ips/StructureDefinition/new`, or Edit on a profile) is in three pa
|
|
|
403
528
|
- **Rules about resources:** the third shape, each a row of a table with four columns (the type, which is optional, the system, the code, and the rest of the rule as FHIRPath starting with a dot).
|
|
404
529
|
- **Other rules:** any other FHIRPath.
|
|
405
530
|
|
|
531
|
+
The page that shows a profile is the same for everyone. Its first part is a table of three columns: the **Expectation** (the description), the **Section** (an IPS section by its title, or `system|code`) and the **Entry** (`Code is system|code`, or `Code is in` a value set, by its title, or its URL when the title isn't known). A code system is shown by name when it is a known one. A cardinality, `[m..n]`, follows a section or an entry unless it is `1..1` or not given (none means at least one). The second part is also a table of three columns: the **Expectation**, the **Resource Set** (the type, as `MedicationAdministrations where` or `Any resource where`, then the code as in the Entry column) and the **Rule**, which is the rest of the expression in words where it is one of these: `.exists()` is "Must be present somewhere"; `.all(sameSubject())` is "Patient must match the Document"; `.x.exists()` and `.x.y.exists()` are "Must have an x" and "Must have an x.y" (no deeper); `.conformsTo(url)` (or `.all(conformsTo(url))`) is "Conform to" the profile, named by its title when it is one of the clinical profiles or by its jurisdiction and package when it is a national profile; and `.category.hasCoding(system, code [, version])` (or inside `.all()`) is "Has category system|code". Any other rest is shown as FHIRPath, and so are the other rules. On the same page the administrator edits the rules one at a time: each row has Edit and Delete, and each part has an Add button. Add and Edit open a dialog for the one rule - the expectation, then the parts of the rule, each with what it means (for a rule about resources, what must be true of them is a choice: present somewhere, the patient matches the document's, an element, conformance to a profile, a category, or other FHIRPath), and the FHIRPath it makes - and Save stores that one rule by itself (`POST /ips/StructureDefinition/{id}/constraint` to add, `.../constraint/{key}` to change, `.../constraint/{key}/delete` to delete). An edited rule keeps its key. The full editor is still there (Edit all), for the title, or for many changes at once.
|
|
532
|
+
|
|
406
533
|
A rule moves between the parts when it is switched between table and FHIRPath, and it lands in the part whose shape it fits. A section is one of the IPS sections, or any code in any system. An entry or resource code is a code in a named code system (SNOMED CT, LOINC, ICD-10...) or in any other given by URL, and it is looked up on the terminology server (`GET /ips/lookup?system=&q=`, for the administrator only). Choosing "In a value set..." as the system matches entries or resources by value set instead. The code box becomes a value set search (`GET /ips/lookup-vs?q=`, for the administrator only): this module's own value sets that match the title come first, then the terminology server's (a `ValueSet?title=` search). A suggestion shows its title, with its URL underneath. A canonical can also be typed or pasted in, with `|version` to pin a version. When the editor opens, it looks up the title of each value set by URL (`?url=`). If the terminology server doesn't know a value set, it says so as a warning, not an error: the value set has to be known when documents are checked, not when the profile is saved. A cardinality is blank (no bounds) or `min..max`, where max may be `*`. Before a profile is saved, every expression is parsed and run once against an empty document. That catches unknown functions and wrong numbers of arguments as well as syntax errors.
|
|
407
534
|
|
|
408
535
|
A profile can be downloaded as JSON (`?download=1`), and a downloaded profile can be uploaded again as a new profile, with a new id, url and keys. An upload must be no more than a profile like this: a profile on Bundle with no snapshot, and one differential element (the root) carrying constraints that have only a key, an `error` severity, a description, an expression and a source. Anything more is rejected, with the reasons. A profile downloaded when clinical profiles were on Composition is accepted and moved onto Bundle, as profiles already stored are when the server starts: `Composition.section(...)` becomes `Bundle.section(...)`, and any other `Composition.x` becomes `Bundle.entry.resource.ofType(Composition).x`.
|
|
409
536
|
|
|
410
537
|
As FHIR: `GET /ips/StructureDefinition` is a searchset of them all, and `GET /ips/StructureDefinition/{id}` is one of them.
|
|
411
538
|
|
|
539
|
+
## National profiles
|
|
540
|
+
|
|
541
|
+
A national profile is the profile a national implementation guide defines for an IPS document. Each has five parts:
|
|
542
|
+
the jurisdiction it is for, the IG's package id (e.g. `hl7.fhir.au.ips`), the package version, the canonical URL
|
|
543
|
+
of the IG's profile on Bundle, and - optionally, since not every IG uses SNOMED CT - the SNOMED CT edition the IG takes for granted. National profiles are shared across all projects (`/ips/national-profiles`). Everyone can see them; only the
|
|
544
|
+
administrator adds, edits and deletes them. There is at most one for a package id and version. Every document is
|
|
545
|
+
validated against each of them by the [national profiles step](#national-profile-validation).
|
|
546
|
+
|
|
547
|
+
The jurisdiction is a code from `http://hl7.org/fhir/ValueSet/jurisdiction`: a country, by its ISO 3166 two-letter
|
|
548
|
+
code (`AU`; lower case is accepted, and stored in upper case), or a region, by its UN M49 three-digit code (`150` for
|
|
549
|
+
Europe, `053` for Australia and New Zealand). The pages show the country's or region's name with it, from the code
|
|
550
|
+
lists the terminology module holds (`tx/cs/cs-country.js`, `tx/cs/cs-areacode.js`); ISO's user-assigned and 'unknown' codes
|
|
551
|
+
(`XK`, `XX`, `ZZ`...) and countries' M49 numeric codes aren't accepted. National profiles made before the jurisdiction was asked
|
|
552
|
+
for show it as not given, until they are saved with one. The package id must be a FHIR package id (lower case letters, digits, `-` and `_`, in two or more parts separated by
|
|
553
|
+
dots), the version a package version (`1.0.0`, `1.1.0-ballot`), and the profile an http or https URL. `GET
|
|
554
|
+
/ips/national-profiles` with `Accept: application/json` returns them as `[{jurisdiction, packageId, version, url, snomedEdition}]`
|
|
555
|
+
(`snomedEdition` only when there is one).
|
|
556
|
+
|
|
557
|
+
The SNOMED CT edition is chosen from a list - the editions in `SNOMED_LINKABLE_EDITIONS` (`tx/sct/editions.js`, the
|
|
558
|
+
editions snomed.info can link to), by name - and kept as the edition's module id (`900000000000207008` for the
|
|
559
|
+
International Edition, `32506021000036107` for the Australian), the id in its edition URI `http://snomed.info/sct/{id}`.
|
|
560
|
+
|
|
412
561
|
## FHIRPath functions
|
|
413
562
|
|
|
414
563
|
`ips/fhirpath-ips.js` adds two functions to fhirpath.js, for expressions on IPS documents:
|
|
@@ -455,6 +604,7 @@ shared this way. The QR codes are made with `qrcode-generator`, error correction
|
|
|
455
604
|
### Validation view
|
|
456
605
|
|
|
457
606
|
The view shows everything by default; its "Show" selector (`?level=warning` or
|
|
458
|
-
`?level=error`) hides the lesser issues - the summary line still counts them all.
|
|
607
|
+
`?level=error`) hides the lesser issues - the summary line still counts them all. A national profile's
|
|
608
|
+
validation (the `national` view, one profile) shows errors only by default (`?level=information` for everything).
|
|
459
609
|
`fhir-validator-wrapper` 1.5.0 or later is needed: earlier versions sent the profiles in a
|
|
460
610
|
form the validator service refuses.
|