fhirsmith 0.5.6 → 0.6.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.
@@ -8,6 +8,7 @@ const {
8
8
  ServerInformation,
9
9
  ServerVersionInformation,
10
10
  } = require('./model');
11
+ const {Extensions} = require("../tx/library/extensions");
11
12
 
12
13
  const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
13
14
 
@@ -36,32 +37,6 @@ class RegistryCrawler {
36
37
  this.log = logv;
37
38
  }
38
39
 
39
- // /**
40
- // * Start the crawler with periodic updates
41
- // */
42
- // start() {
43
- // if (this.crawlTimer) {
44
- // return; // Already running
45
- // }
46
- //
47
- // // Initial crawl
48
- // this.crawl();
49
- //
50
- // // Set up periodic crawling
51
- // this.crawlTimer = setInterval(() => {
52
- // this.crawl();
53
- // }, this.config.crawlInterval);
54
- // }
55
- //
56
- // /**
57
- // * Stop the crawler
58
- // */
59
- // stop() {
60
- // if (this.crawlTimer) {
61
- // clearInterval(this.crawlTimer);
62
- // this.crawlTimer = null;
63
- // }
64
- // }
65
40
 
66
41
  /**
67
42
  * Main entry point - crawl the registry starting from the master URL
@@ -111,6 +86,7 @@ class RegistryCrawler {
111
86
  // Update the current data
112
87
  this.currentData = newData;
113
88
  } catch (error) {
89
+ console.log(error);
114
90
  this.addLogEntry('error', 'Exception Scanning:', error);
115
91
  this.currentData.outcome = `Error: ${error.message}`;
116
92
  this.errors.push({
@@ -165,6 +141,7 @@ class RegistryCrawler {
165
141
  }
166
142
 
167
143
  } catch (error) {
144
+ console.log(error);
168
145
  registry.error = error.message;
169
146
  this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
170
147
  }
@@ -181,7 +158,7 @@ class RegistryCrawler {
181
158
  server.name = serverConfig.name;
182
159
  server.address = serverConfig.url || '';
183
160
  server.accessInfo = serverConfig.access_info || '';
184
-
161
+
185
162
  if (!server.name) {
186
163
  this.addLogEntry('error', 'No name provided for server', source);
187
164
  return server;
@@ -200,7 +177,7 @@ class RegistryCrawler {
200
177
  // Process each FHIR version
201
178
  const fhirVersions = serverConfig.fhirVersions || [];
202
179
  for (const versionConfig of fhirVersions) {
203
- const version = await this.processServerVersion(versionConfig, server);
180
+ const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
204
181
  if (version) {
205
182
  server.versions.push(version);
206
183
  }
@@ -212,7 +189,7 @@ class RegistryCrawler {
212
189
  /**
213
190
  * Process a single server version
214
191
  */
215
- async processServerVersion(versionConfig, server) {
192
+ async processServerVersion(versionConfig, server, exclusions) {
216
193
  const version = new ServerVersionInformation();
217
194
  version.version = versionConfig.version;
218
195
  version.address = versionConfig.url;
@@ -233,20 +210,20 @@ class RegistryCrawler {
233
210
 
234
211
  switch (majorVersion) {
235
212
  case 3:
236
- await this.processServerVersionR3(version, server);
213
+ await this.processServerVersionR3(version, server, exclusions);
237
214
  break;
238
215
  case 4:
239
- await this.processServerVersionR4(version, server);
216
+ await this.processServerVersionR4or5(version, server, '4.0.1', exclusions);
240
217
  break;
241
218
  case 5:
242
- await this.processServerVersionR5(version, server);
219
+ await this.processServerVersionR4or5(version, server, '5.0.0', exclusions);
243
220
  break;
244
221
  default:
245
222
  throw new Error(`Version ${version.version} not supported`);
246
223
  }
247
224
 
248
225
  // Sort and deduplicate
249
- version.codeSystems = [...new Set(version.codeSystems)].sort();
226
+ version.codeSystems.sort((a, b) => this.compareCS(a, b));
250
227
  version.valueSets = [...new Set(version.valueSets)].sort();
251
228
  version.lastSuccess = new Date();
252
229
  version.lastTat = `${Date.now() - startTime}ms`;
@@ -254,6 +231,7 @@ class RegistryCrawler {
254
231
  this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
255
232
 
256
233
  } catch (error) {
234
+ console.log(error);
257
235
  const elapsed = Date.now() - startTime;
258
236
  this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
259
237
  version.error = error.message;
@@ -266,7 +244,7 @@ class RegistryCrawler {
266
244
  /**
267
245
  * Process an R3 server
268
246
  */
269
- async processServerVersionR3(version, server) {
247
+ async processServerVersionR3(version, server, exclusions) {
270
248
  // Get capability statement
271
249
  const capabilityUrl = `${version.address}/metadata`;
272
250
  const capability = await this.fetchJson(capabilityUrl, server.name);
@@ -283,12 +261,12 @@ class RegistryCrawler {
283
261
  termCap.parameter.forEach(param => {
284
262
  if (param.name === 'system') {
285
263
  const uri = param.valueUri || param.valueString;
286
- if (uri) {
264
+ if (uri && !this.isExcluded(uri, exclusions)) {
287
265
  version.codeSystems.push(uri);
288
266
  // Look for version parts
289
267
  if (param.part) {
290
268
  param.part.forEach(part => {
291
- if (part.name === 'version' && part.valueString) {
269
+ if (part.name === 'version' && part.valueString && !this.isExcluded(uri+'|'+part.valueString, exclusions)) {
292
270
  version.codeSystems.push(`${uri}|${part.valueString}`);
293
271
  }
294
272
  });
@@ -298,24 +276,27 @@ class RegistryCrawler {
298
276
  });
299
277
  }
300
278
  } catch (error) {
279
+ console.log(error);
301
280
  this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
302
281
  }
303
282
 
304
283
  // Search for value sets
305
- await this.fetchValueSets(version, server);
284
+ await this.fetchValueSets(version, server, exclusions);
306
285
  }
307
286
 
308
287
  /**
309
288
  * Process an R4 server
310
289
  */
311
- async processServerVersionR4(version, server) {
290
+ async processServerVersionR4or5(version, server, defVersion, exclusions) {
312
291
  // Get capability statement
313
292
  const capabilityUrl = `${version.address}/metadata`;
314
293
  const capability = await this.fetchJson(capabilityUrl, server.code);
315
294
 
316
- version.version = capability.fhirVersion || '4.0.1';
295
+ version.version = capability.fhirVersion || defVersion;
317
296
  version.software = capability.software ? capability.software.name : "unknown";
318
-
297
+
298
+ let set = new Set();
299
+
319
300
  // Get terminology capabilities
320
301
  try {
321
302
  const termCapUrl = `${version.address}/metadata?mode=terminology`;
@@ -323,12 +304,19 @@ class RegistryCrawler {
323
304
 
324
305
  if (termCap.codeSystem) {
325
306
  termCap.codeSystem.forEach(cs => {
326
- if (cs.uri) {
327
- version.codeSystems.push(cs.uri);
307
+ let content = cs.content || Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
308
+ if (cs.uri && !this.isExcluded(cs.uri, exclusions)) {
309
+ if (!set.has(cs.uri)) {
310
+ set.add(cs.uri);
311
+ version.codeSystems.push(this.addContent({uri: cs.uri}, content));
312
+ }
328
313
  if (cs.version) {
329
314
  cs.version.forEach(v => {
330
- if (v.code) {
331
- version.codeSystems.push(`${cs.uri}|${v.code}`);
315
+ if (v.code && !this.isExcluded(cs.uri+"|"+v.code, exclusions)) {
316
+ if (!set.has(cs.uri+"|"+v.code)) {
317
+ version.codeSystems.push(this.addContent({uri: cs.uri, version: v.code}, content));
318
+ set.add(cs.uri+"|"+v.code);
319
+ }
332
320
  }
333
321
  });
334
322
  }
@@ -336,20 +324,12 @@ class RegistryCrawler {
336
324
  });
337
325
  }
338
326
  } catch (error) {
327
+ console.log(error);
339
328
  this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
340
329
  }
341
330
 
342
331
  // Search for value sets
343
- await this.fetchValueSets(version, server);
344
- }
345
-
346
- /**
347
- * Process an R5 server
348
- */
349
- async processServerVersionR5(version, server) {
350
- // R5 is essentially the same as R4 for our purposes
351
- await this.processServerVersionR4(version, server);
352
- version.version = version.version || '5.0.0';
332
+ await this.fetchValueSets(version, server, exclusions);
353
333
  }
354
334
 
355
335
  /**
@@ -360,9 +340,9 @@ class RegistryCrawler {
360
340
  * @param {Object} version - The server version information
361
341
  * @param {Object} server - The server information
362
342
  */
363
- async fetchValueSets(version, server) {
343
+ async fetchValueSets(version, server, exclusions) {
364
344
  // Initial search URL
365
- let searchUrl = `${version.address}/ValueSet?_elements=url,version`;
345
+ let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
366
346
  try {
367
347
  // Set of URLs to avoid duplicates
368
348
  const valueSetUrls = new Set();
@@ -378,9 +358,9 @@ class RegistryCrawler {
378
358
  bundle.entry.forEach(entry => {
379
359
  if (entry.resource) {
380
360
  const vs = entry.resource;
381
- if (vs.url) {
361
+ if (vs.url && !this.isExcluded(vs.url, exclusions)) {
382
362
  valueSetUrls.add(vs.url);
383
- if (vs.version) {
363
+ if (vs.version && !this.isExcluded(vs.url+'|'+vs.version, exclusions)) {
384
364
  valueSetUrls.add(`${vs.url}|${vs.version}`);
385
365
  }
386
366
  }
@@ -402,6 +382,7 @@ class RegistryCrawler {
402
382
  version.valueSets = Array.from(valueSetUrls).sort();
403
383
 
404
384
  } catch (error) {
385
+ console.log(error);
405
386
  this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
406
387
  }
407
388
  }
@@ -478,6 +459,7 @@ class RegistryCrawler {
478
459
  return response.data;
479
460
 
480
461
  } catch (error) {
462
+ console.log(error);
481
463
  if (error.response) {
482
464
  throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
483
465
  } else if (error.request) {
@@ -634,6 +616,38 @@ class RegistryCrawler {
634
616
  return filteredLogs.slice(-limit);
635
617
  }
636
618
 
619
+ addContent(param, content) {
620
+ if (content) {
621
+ param.content = content;
622
+ }
623
+ return param;
624
+ }
625
+
626
+ compareCS(a, b) {
627
+ if (a.version || b.version) {
628
+ let s = (a.uri+'|'+a.version) || '';
629
+ return s.localeCompare(b.uri+'|'+b.version);
630
+ } else {
631
+ return (a.uri || '').localeCompare(b.uri);
632
+ }
633
+ }
634
+
635
+ isExcluded(url, exclusions) {
636
+ for (let exclusion of exclusions || []) {
637
+ let match = false;
638
+ if (exclusion.endsWith('*')) {
639
+ const prefix = exclusion.slice(0, -1);
640
+ match = url.startsWith(prefix);
641
+ } else {
642
+ // Otherwise do exact matching on both full and base URL
643
+ match = url === exclusion;
644
+ }
645
+ if (match) {
646
+ return true;
647
+ }
648
+ }
649
+ return false;
650
+ }
637
651
  }
638
652
 
639
653
  module.exports = RegistryCrawler;
package/registry/model.js CHANGED
@@ -37,7 +37,7 @@ class ServerVersionInformation {
37
37
  getCsListHtml() {
38
38
  if (this.codeSystems.length === 0) return '<ul></ul>';
39
39
  return '<ul>' + this.codeSystems.map(cs =>
40
- `<li>${escape(cs)}</li>`
40
+ `<li>${escape(cs.uri+(cs.version ? '|'+cs.version : ''))}</li>`
41
41
  ).join('') + '</ul>';
42
42
  }
43
43
 
@@ -392,7 +392,7 @@ class ServerRegistryUtilities {
392
392
  return value === mask;
393
393
  }
394
394
 
395
- static hasMatchingCodeSystem(cs, list, supportMask) {
395
+ static hasMatchingCodeSystem(cs, list, supportMask, content) {
396
396
  if (!cs || list.length === 0) return false;
397
397
 
398
398
  // Handle URLs with pipes - extract base URL
@@ -403,13 +403,19 @@ class ServerRegistryUtilities {
403
403
 
404
404
  return list.some(item => {
405
405
  // If we support wildcards (masks) and the item ends with "*", do prefix matching
406
- if (supportMask && item.endsWith('*')) {
407
- const prefix = item.slice(0, -1);
408
- return cs.startsWith(prefix) || baseCs.startsWith(prefix);
406
+ let vurl = item.uri ? item.version ? item.uri+"|"+item.version : item.uri : item;
407
+ let ok = false;
408
+ if (supportMask && vurl.endsWith('*')) {
409
+ const prefix = vurl.slice(0, -1);
410
+ ok = cs.startsWith(prefix) || baseCs.startsWith(prefix);
411
+ } else {
412
+ // Otherwise do exact matching on both full and base URL
413
+ ok = vurl === cs || vurl === baseCs;
409
414
  }
410
-
411
- // Otherwise do exact matching on both full and base URL
412
- return item === cs || item === baseCs;
415
+ if (ok && content) {
416
+ content.content = item.content;
417
+ }
418
+ return ok;
413
419
  });
414
420
  }
415
421
 
@@ -1155,6 +1155,7 @@ class RegistryModule {
1155
1155
  html += '<th>URL</th>';
1156
1156
  html += '<th>Security</th>';
1157
1157
  html += '<th>Access Info</th>';
1158
+ html += '<th>Content</th>';
1158
1159
  html += '</tr>';
1159
1160
  html += '</thead>';
1160
1161
  html += '<tbody>';
@@ -1165,6 +1166,7 @@ class RegistryModule {
1165
1166
  html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
1166
1167
  html += `<td>${this.renderSecurityTags(server)}</td>`;
1167
1168
  html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
1169
+ html += `<td>${server.content ? escape(server.content) : ''}</td>`;
1168
1170
  html += '</tr>';
1169
1171
  });
1170
1172
 
@@ -74,6 +74,7 @@
74
74
 
75
75
  [%content%]
76
76
 
77
+ [%about%]
77
78
 
78
79
  </div>
79
80
 
package/server.js CHANGED
@@ -395,6 +395,14 @@ process.on('uncaughtException', (error) => {
395
395
  });
396
396
 
397
397
  app.get('/', async (req, res) => {
398
+ // If an override index.html exists, serve it instead of the FHIRsmith home page
399
+ if (config.server?.webBase) {
400
+ const overrideIndex = path.join(path.resolve(config.server.webBase), 'index.html');
401
+ if (fs.existsSync(overrideIndex)) {
402
+ return res.sendFile(overrideIndex);
403
+ }
404
+ }
405
+
398
406
  // Check if client wants HTML response
399
407
  const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
400
408
 
@@ -410,11 +418,19 @@ app.get('/', async (req, res) => {
410
418
 
411
419
  const content = await buildRootPageContent();
412
420
 
421
+ // Load optional about box fragment from data directory
422
+ let about = '';
423
+ const aboutPath = path.join(folders.dataDir(), 'about.html');
424
+ if (fs.existsSync(aboutPath)) {
425
+ about = fs.readFileSync(aboutPath, 'utf8');
426
+ }
427
+
413
428
  // Build basic stats for root page
414
429
  const stats = {
415
430
  version: packageJson.version,
416
431
  enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
417
- processingTime: Date.now() - startTime
432
+ processingTime: Date.now() - startTime,
433
+ about
418
434
  };
419
435
 
420
436
  const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
@@ -424,55 +440,26 @@ app.get('/', async (req, res) => {
424
440
  serverLog.error('Error rendering root page:', error);
425
441
  htmlServer.sendErrorResponse(res, 'root', error);
426
442
  }
427
- } else {
428
- // Return JSON response for API clients
429
- const enabledModules = {};
430
- Object.keys(config.modules).forEach(moduleName => {
431
- if (config.modules[moduleName].enabled) {
432
- if (moduleName === 'tx') {
433
- // TX module has multiple endpoints
434
- enabledModules[moduleName] = {
435
- enabled: true,
436
- endpoints: config.modules.tx.endpoints.map(e => ({
437
- path: e.path,
438
- fhirVersion: e.fhirVersion,
439
- context: e.context || null
440
- }))
441
- };
442
- } else {
443
- enabledModules[moduleName] = {
444
- enabled: true,
445
- endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
446
- };
447
- }
448
- }
449
- });
450
-
451
- res.json({
452
- message: 'FHIR Development Server',
453
- version: '1.0.0',
454
- modules: enabledModules,
455
- endpoints: {
456
- health: '/health',
457
- ...Object.fromEntries(
458
- Object.keys(enabledModules)
459
- .filter(m => m !== 'tx')
460
- .map(m => [
461
- m,
462
- m === 'vcl' ? '/VCL' : `/${m}`
463
- ])
464
- ),
465
- // Add TX endpoints separately
466
- ...(enabledModules.tx ? {
467
- tx: config.modules.tx.endpoints.map(e => e.path)
468
- } : {})
469
- }
470
- });
471
443
  }
444
+ return serveFhirsmithHome(req, res);
472
445
  });
473
446
 
447
+ app.get('/fhirsmith', (req, res) => serveFhirsmithHome(req, res));
474
448
 
475
449
  // Serve static files
450
+ if (config.server?.webBase) {
451
+ const overrideDir = path.resolve(config.server.webBase);
452
+ app.use((req, res, next) => {
453
+ const filePath = path.join(overrideDir, req.path);
454
+ fs.access(filePath, fs.constants.F_OK, (err) => {
455
+ if (!err) {
456
+ res.sendFile(filePath);
457
+ } else {
458
+ next();
459
+ }
460
+ });
461
+ });
462
+ }
476
463
  app.use(express.static(path.join(__dirname, 'static')));
477
464
 
478
465
  // Health check endpoint
@@ -610,5 +597,82 @@ process.on('SIGINT', async () => {
610
597
  process.exit(0);
611
598
  });
612
599
 
600
+ async function serveFhirsmithHome(req, res) {
601
+ // Check if client wants HTML response
602
+ const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
603
+
604
+ if (acceptsHtml) {
605
+ try {
606
+ const startTime = Date.now();
607
+
608
+ // Load template if not already loaded
609
+ if (!htmlServer.hasTemplate('root')) {
610
+ const templatePath = path.join(__dirname, 'root-template.html');
611
+ htmlServer.loadTemplate('root', templatePath);
612
+ }
613
+
614
+ const content = await buildRootPageContent();
615
+
616
+ // Build basic stats for root page
617
+ const stats = {
618
+ version: packageJson.version,
619
+ enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
620
+ processingTime: Date.now() - startTime
621
+ };
622
+
623
+ const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
624
+ res.setHeader('Content-Type', 'text/html');
625
+ res.send(html);
626
+ } catch (error) {
627
+ serverLog.error('Error rendering root page:', error);
628
+ htmlServer.sendErrorResponse(res, 'root', error);
629
+ }
630
+ } else {
631
+ // Return JSON response for API clients
632
+ const enabledModules = {};
633
+ Object.keys(config.modules).forEach(moduleName => {
634
+ if (config.modules[moduleName].enabled) {
635
+ if (moduleName === 'tx') {
636
+ // TX module has multiple endpoints
637
+ enabledModules[moduleName] = {
638
+ enabled: true,
639
+ endpoints: config.modules.tx.endpoints.map(e => ({
640
+ path: e.path,
641
+ fhirVersion: e.fhirVersion,
642
+ context: e.context || null
643
+ }))
644
+ };
645
+ } else {
646
+ enabledModules[moduleName] = {
647
+ enabled: true,
648
+ endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
649
+ };
650
+ }
651
+ }
652
+ });
653
+
654
+ res.json({
655
+ message: 'FHIR Development Server',
656
+ version: '1.0.0',
657
+ modules: enabledModules,
658
+ endpoints: {
659
+ health: '/health',
660
+ ...Object.fromEntries(
661
+ Object.keys(enabledModules)
662
+ .filter(m => m !== 'tx')
663
+ .map(m => [
664
+ m,
665
+ m === 'vcl' ? '/VCL' : `/${m}`
666
+ ])
667
+ ),
668
+ // Add TX endpoints separately
669
+ ...(enabledModules.tx ? {
670
+ tx: config.modules.tx.endpoints.map(e => e.path)
671
+ } : {})
672
+ }
673
+ });
674
+ }
675
+ }
676
+
613
677
  // Start the server
614
678
  startServer();
package/tx/cs/cs-api.js CHANGED
@@ -774,12 +774,19 @@ class CodeSystemFactoryProvider {
774
774
  }
775
775
 
776
776
  /**
777
- * @returns {string} uri for the code system
777
+ * @returns {string} name for the code system
778
778
  */
779
779
  name() {
780
780
  throw new Error("Must override");
781
781
  }
782
782
 
783
+ /**
784
+ * @returns {string} name for the code system, without version information
785
+ */
786
+ nameBase() {
787
+ return this.name();
788
+ }
789
+
783
790
  /**
784
791
  * @returns {string} version for the code system
785
792
  */
@@ -796,6 +803,16 @@ class CodeSystemFactoryProvider {
796
803
  }
797
804
  return ver;
798
805
  }
806
+
807
+ /**
808
+ * the version parameter might not be the same as version() once
809
+ * all matching rules are done
810
+ * @param version
811
+ */
812
+ describeVersion(version) {
813
+ return "v"+version;
814
+ }
815
+
799
816
  /**
800
817
  * @returns {number} how many times the factory has been asked to construct a provider
801
818
  */
package/tx/cs/cs-base.js CHANGED
@@ -72,6 +72,7 @@ class BaseCSServices extends CodeSystemProvider {
72
72
  };
73
73
  }
74
74
 
75
+
75
76
  module.exports = {
76
77
  BaseCSServices
77
78
  };
@@ -3,7 +3,7 @@ const assert = require('assert');
3
3
  const { CodeSystem } = require('../library/codesystem');
4
4
  const { CodeSystemProvider, CodeSystemFactoryProvider } = require('./cs-api');
5
5
  const {Designations} = require("../library/designations");
6
- const {validateArrayParameter} = require("../../library/utilities");
6
+ const {validateArrayParameter, formatDateMMDDYYYY} = require("../../library/utilities");
7
7
 
8
8
  // Context for RxNorm concepts
9
9
  class RxNormConcept {
@@ -806,9 +806,16 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
806
806
  }
807
807
 
808
808
  id() {
809
- return this.name();
809
+ return this.name()+"-"+this.version();
810
810
  }
811
811
 
812
+ describeVersion(version) {
813
+ try {
814
+ return formatDateMMDDYYYY(version);
815
+ } catch (error) {
816
+ return "v" + version;
817
+ }
818
+ }
812
819
  }
813
820
 
814
821
  // Specific RxNorm implementation
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../sct/expressions');
12
12
  const {DesignationUse} = require("../library/designations");
13
13
  const {BaseCSServices} = require("./cs-base");
14
+ const {formatDateMMDDYYYY} = require("../../library/utilities");
14
15
 
15
16
  // Context kinds matching Pascal enum
16
17
  const SnomedProviderContextKind = {
@@ -1262,11 +1263,25 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
1262
1263
  return `SCT ${getEditionCode(this._sharedData.edition)}`;
1263
1264
  }
1264
1265
 
1266
+ nameBase() {
1267
+ return `SCT`;
1268
+ }
1269
+
1265
1270
  id() {
1266
- return "SCT"+this.version();
1271
+ const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
1272
+ return "SCT-"+match[1]+"-"+match[2];
1267
1273
  }
1268
- }
1269
1274
 
1275
+ describeVersion(version) {
1276
+ const match = version.match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
1277
+ if (!match) return version;
1278
+
1279
+ const edition = getEditionName(match[1]);
1280
+ if (!match[2]) return edition;
1281
+
1282
+ return edition + ' ' + formatDateMMDDYYYY(match[2].substring(4, 6) + match[2].substring(6, 8) + match[2].substring(0, 4));
1283
+ }
1284
+ }
1270
1285
 
1271
1286
  function getEditionName(edition) {
1272
1287
  const editionMap = {