gdc-common-utils-ts 2.3.17 → 2.3.18

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.
@@ -49,6 +49,7 @@ export * from './relationship-access';
49
49
  export * from './response';
50
50
  export * from './subject-identifier-ledger';
51
51
  export * from './subject-identity-binding';
52
+ export * from './terminology';
52
53
  export * from './urlPath';
53
54
  export * from './verifiable-credential';
54
55
  export * from './wallet';
@@ -49,6 +49,7 @@ export * from './relationship-access.js';
49
49
  export * from './response.js';
50
50
  export * from './subject-identifier-ledger.js';
51
51
  export * from './subject-identity-binding.js';
52
+ export * from './terminology.js';
52
53
  export * from './urlPath.js';
53
54
  export * from './verifiable-credential.js';
54
55
  export * from './wallet.js';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * One terminology block in the legacy JSON catalog format.
3
+ *
4
+ * `id` is normally the canonical coding-system URI. The compatibility alias
5
+ * `ips` is accepted for the SNOMED IPS catalog used by
6
+ * the predecessor FHIR utility package.
7
+ */
8
+ export type TerminologyCatalogResource = Readonly<{
9
+ id: string;
10
+ language?: string;
11
+ attributes: Readonly<Record<string, string>>;
12
+ meta?: Readonly<Record<string, unknown>>;
13
+ }>;
14
+ /**
15
+ * Loadable local terminology document compatible with the historic
16
+ * `data[].attributes[code] = display` JSON shape.
17
+ */
18
+ export type TerminologyCatalogDocument = Readonly<{
19
+ id?: string;
20
+ name?: string;
21
+ language?: string;
22
+ system?: string;
23
+ version?: string;
24
+ jurisdiction?: string;
25
+ data: readonly TerminologyCatalogResource[];
26
+ meta?: Readonly<Record<string, unknown>>;
27
+ }>;
28
+ /** Input for a synchronous local terminology label lookup. */
29
+ export type TerminologyLookupInput = Readonly<{
30
+ system: string;
31
+ code: string;
32
+ language: string;
33
+ jurisdiction?: string;
34
+ }>;
35
+ /** Input for an offline/local terminology text search. */
36
+ export type TerminologySearchInput = Readonly<{
37
+ text: string;
38
+ language: string;
39
+ jurisdiction?: string;
40
+ systems?: readonly string[];
41
+ limit?: number;
42
+ }>;
43
+ /** One terminology option suitable for a coded clinical form control. */
44
+ export type TerminologySearchResult = Readonly<{
45
+ system: string;
46
+ code: string;
47
+ display: string;
48
+ language: string;
49
+ }>;
50
+ /**
51
+ * Synchronous terminology contract used by cached and offline applications.
52
+ *
53
+ * A future remote terminology client should populate a local implementation
54
+ * of this contract before synchronous clinical-card rendering.
55
+ */
56
+ export interface TerminologyProvider {
57
+ lookup(input: TerminologyLookupInput): string | undefined;
58
+ search(input: TerminologySearchInput): readonly TerminologySearchResult[];
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -45,6 +45,9 @@ export type ClinicalResourceDisplayOptions = Readonly<{
45
45
  *
46
46
  * Return `undefined` when the product has no translation so the shared
47
47
  * reader can fall back to the international `Coding.display`.
48
+ * `createClinicalCodeTranslator(new LocalTerminologyProvider(catalogs))`
49
+ * provides the synchronous MVP/offline implementation. Remote terminology
50
+ * results must be fetched and cached before this render callback runs.
48
51
  */
49
52
  translateCode?: (input: ClinicalTerminologyTranslationInput) => string | undefined;
50
53
  }>;
@@ -81,6 +81,7 @@ export * from './jwt-signer';
81
81
  export * from './jwk-thumbprint';
82
82
  export * from './legal-organization-onboarding';
83
83
  export * from './legal-organization-verification-transaction';
84
+ export * from './local-terminology-provider';
84
85
  export * from './license';
85
86
  export * from './license-commercial-search';
86
87
  export * from './license-list-search';
@@ -81,6 +81,7 @@ export * from './jwt-signer.js';
81
81
  export * from './jwk-thumbprint.js';
82
82
  export * from './legal-organization-onboarding.js';
83
83
  export * from './legal-organization-verification-transaction.js';
84
+ export * from './local-terminology-provider.js';
84
85
  export * from './license.js';
85
86
  export * from './license-commercial-search.js';
86
87
  export * from './license-list-search.js';
@@ -0,0 +1,27 @@
1
+ import type { TerminologyCatalogDocument, TerminologyLookupInput, TerminologyProvider, TerminologySearchInput, TerminologySearchResult } from '../models/terminology.js';
2
+ import type { ClinicalTerminologyTranslationInput } from './clinical-resource-view.js';
3
+ /**
4
+ * In-memory terminology provider compatible with the historic JSON
5
+ * catalogs. It performs no network requests and keeps catalog loading under
6
+ * application control.
7
+ */
8
+ export declare class LocalTerminologyProvider implements TerminologyProvider {
9
+ private readonly terms;
10
+ private readonly byIdentity;
11
+ private readonly byScopedIdentity;
12
+ constructor(catalogs: readonly TerminologyCatalogDocument[]);
13
+ /** Returns the best local label for one canonical coding identity. */
14
+ lookup(input: TerminologyLookupInput): string | undefined;
15
+ /**
16
+ * Searches local labels and codes, optionally constrained to coding systems.
17
+ * Results are deterministic and contain at most 100 entries.
18
+ */
19
+ search(input: TerminologySearchInput): readonly TerminologySearchResult[];
20
+ private identityKey;
21
+ private scopedIdentityKey;
22
+ }
23
+ /**
24
+ * Adapts a synchronous local terminology provider to
25
+ * `ClinicalResourceDisplayOptions.translateCode`.
26
+ */
27
+ export declare function createClinicalCodeTranslator(provider: Pick<TerminologyProvider, 'lookup'>): (input: ClinicalTerminologyTranslationInput) => string | undefined;
@@ -0,0 +1,154 @@
1
+ const SNOMED_SYSTEM = 'http://snomed.info/sct';
2
+ const DEFAULT_LANGUAGE = 'en';
3
+ const DEFAULT_LIMIT = 20;
4
+ const MAX_LIMIT = 100;
5
+ function normalizedString(value) {
6
+ return typeof value === 'string' ? value.trim() : '';
7
+ }
8
+ function normalizeSystem(value) {
9
+ const system = normalizedString(value);
10
+ return system.toLowerCase() === 'ips' ? SNOMED_SYSTEM : system;
11
+ }
12
+ function normalizeLanguage(value) {
13
+ return normalizedString(value).replace(/_/g, '-').toLowerCase();
14
+ }
15
+ function normalizeJurisdiction(value) {
16
+ return normalizedString(value).toUpperCase();
17
+ }
18
+ function languageCandidates(language) {
19
+ const normalized = normalizeLanguage(language);
20
+ const base = normalized.split('-')[0];
21
+ return [...new Set([normalized, base, DEFAULT_LANGUAGE].filter(Boolean))];
22
+ }
23
+ function searchableText(value) {
24
+ return value
25
+ .normalize('NFD')
26
+ .replace(/[\u0300-\u036f]/g, '')
27
+ .toLocaleLowerCase();
28
+ }
29
+ function normalizedLimit(value) {
30
+ if (!Number.isFinite(value))
31
+ return DEFAULT_LIMIT;
32
+ return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(value)));
33
+ }
34
+ /**
35
+ * In-memory terminology provider compatible with the historic JSON
36
+ * catalogs. It performs no network requests and keeps catalog loading under
37
+ * application control.
38
+ */
39
+ export class LocalTerminologyProvider {
40
+ terms;
41
+ byIdentity;
42
+ byScopedIdentity;
43
+ constructor(catalogs) {
44
+ const terms = [];
45
+ const byIdentity = new Map();
46
+ const byScopedIdentity = new Map();
47
+ for (const catalog of catalogs) {
48
+ const catalogLanguage = normalizeLanguage(catalog.language) || DEFAULT_LANGUAGE;
49
+ const jurisdiction = normalizeJurisdiction(catalog.jurisdiction) || undefined;
50
+ for (const resource of catalog.data || []) {
51
+ const system = normalizeSystem(resource.id || catalog.system);
52
+ const language = normalizeLanguage(resource.language) || catalogLanguage;
53
+ if (!system || !language || !resource.attributes)
54
+ continue;
55
+ for (const [rawCode, rawDisplay] of Object.entries(resource.attributes)) {
56
+ const code = normalizedString(rawCode);
57
+ const display = normalizedString(rawDisplay);
58
+ if (!code || !display)
59
+ continue;
60
+ const term = { system, code, display, language, jurisdiction };
61
+ terms.push(term);
62
+ byIdentity.set(this.identityKey(language, system, code), display);
63
+ byScopedIdentity.set(this.scopedIdentityKey(language, jurisdiction, system, code), display);
64
+ }
65
+ }
66
+ }
67
+ this.terms = terms;
68
+ this.byIdentity = byIdentity;
69
+ this.byScopedIdentity = byScopedIdentity;
70
+ }
71
+ /** Returns the best local label for one canonical coding identity. */
72
+ lookup(input) {
73
+ const system = normalizeSystem(input.system);
74
+ const code = normalizedString(input.code);
75
+ const jurisdiction = normalizeJurisdiction(input.jurisdiction) || undefined;
76
+ if (!system || !code)
77
+ return undefined;
78
+ for (const language of languageCandidates(input.language)) {
79
+ if (jurisdiction) {
80
+ const scopedDisplay = this.byScopedIdentity.get(this.scopedIdentityKey(language, jurisdiction, system, code)) || this.byScopedIdentity.get(this.scopedIdentityKey(language, undefined, system, code));
81
+ if (scopedDisplay)
82
+ return scopedDisplay;
83
+ continue;
84
+ }
85
+ const display = this.byIdentity.get(this.identityKey(language, system, code));
86
+ if (display)
87
+ return display;
88
+ }
89
+ return undefined;
90
+ }
91
+ /**
92
+ * Searches local labels and codes, optionally constrained to coding systems.
93
+ * Results are deterministic and contain at most 100 entries.
94
+ */
95
+ search(input) {
96
+ const query = searchableText(normalizedString(input.text));
97
+ if (!query)
98
+ return [];
99
+ const allowedSystems = new Set((input.systems || []).map(normalizeSystem).filter(Boolean));
100
+ const jurisdiction = normalizeJurisdiction(input.jurisdiction);
101
+ const languages = languageCandidates(input.language);
102
+ const languageRank = new Map(languages.map((language, index) => [language, index]));
103
+ const selected = new Map();
104
+ for (const term of this.terms) {
105
+ if (allowedSystems.size > 0 && !allowedSystems.has(term.system))
106
+ continue;
107
+ if (jurisdiction && term.jurisdiction && term.jurisdiction !== jurisdiction)
108
+ continue;
109
+ const rank = languageRank.get(term.language);
110
+ if (rank === undefined)
111
+ continue;
112
+ if (!searchableText(term.code).includes(query)
113
+ && !searchableText(term.display).includes(query)) {
114
+ continue;
115
+ }
116
+ const identity = `${term.system}\u0000${term.code}`;
117
+ const current = selected.get(identity);
118
+ if (!current || rank < current.rank)
119
+ selected.set(identity, { term, rank });
120
+ }
121
+ return [...selected.values()]
122
+ .sort((left, right) => (left.rank - right.rank
123
+ || left.term.display.localeCompare(right.term.display)
124
+ || left.term.code.localeCompare(right.term.code)))
125
+ .slice(0, normalizedLimit(input.limit))
126
+ .map(({ term }) => ({
127
+ system: term.system,
128
+ code: term.code,
129
+ display: term.display,
130
+ language: term.language,
131
+ }));
132
+ }
133
+ identityKey(language, system, code) {
134
+ return `${language}\u0000${system}\u0000${code}`;
135
+ }
136
+ scopedIdentityKey(language, jurisdiction, system, code) {
137
+ return `${language}\u0000${jurisdiction || ''}\u0000${system}\u0000${code}`;
138
+ }
139
+ }
140
+ /**
141
+ * Adapts a synchronous local terminology provider to
142
+ * `ClinicalResourceDisplayOptions.translateCode`.
143
+ */
144
+ export function createClinicalCodeTranslator(provider) {
145
+ return (input) => {
146
+ if (!input.system)
147
+ return undefined;
148
+ return provider.lookup({
149
+ system: input.system,
150
+ code: input.code,
151
+ language: input.locale,
152
+ });
153
+ };
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.3.17",
3
+ "version": "2.3.18",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },