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.
- package/CHANGELOG.md +18 -0
- package/library/html-server.js +2 -1
- package/library/package-manager.js +37 -32
- package/library/utilities.js +10 -1
- package/package.json +1 -1
- package/packages/package-crawler.js +103 -46
- package/packages/packages.js +14 -7
- package/publisher/publisher.js +15 -3
- package/registry/api.js +173 -191
- package/registry/crawler.js +72 -58
- package/registry/model.js +14 -8
- package/registry/registry.js +2 -0
- package/root-template.html +1 -0
- package/server.js +109 -45
- package/tx/cs/cs-api.js +18 -1
- package/tx/cs/cs-base.js +1 -0
- package/tx/cs/cs-rxnorm.js +9 -2
- package/tx/cs/cs-snomed.js +17 -2
- package/tx/html/codesystem-operations.liquid +17 -24
- package/tx/html/valueset-operations.liquid +46 -52
- package/tx/library/codesystem.js +6 -1
- package/tx/library/renderer.js +81 -7
- package/tx/library.js +18 -3
- package/tx/provider.js +4 -2
- package/tx/sct/expressions.js +20 -9
- package/tx/tx-html.js +143 -50
- package/tx/tx.js +2 -2
- package/tx/workers/expand.js +61 -9
- package/tx/workers/search.js +5 -2
- package/tx/xversion/xv-terminologyCapabilities.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to the Health Intersections Node Server will be documented i
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [v0.6.0] - 2026-03-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Add support to packages server for scoped packages
|
|
12
|
+
- Add support for exclusions and content tracking in tx-registry
|
|
13
|
+
- Add support for serving a host
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- fix error in SCT expression validation
|
|
17
|
+
- fix null error in search
|
|
18
|
+
- fix search for code systems with uppercase letters in their name
|
|
19
|
+
- rework html interface for CodeSystem and ValueSet
|
|
20
|
+
- further work on publisehr
|
|
21
|
+
|
|
22
|
+
### Tx Conformance Statement
|
|
23
|
+
|
|
24
|
+
FHIRsmith passed all 1382 HL7 terminology service tests (modes tx.fhir.org,omop,general,snomed, tests v1.9.0, runner v6.8.2)
|
|
25
|
+
|
|
8
26
|
## [v0.5.6] - 2026-02-26
|
|
9
27
|
|
|
10
28
|
### Changed
|
package/library/html-server.js
CHANGED
|
@@ -72,7 +72,8 @@ class HtmlServer {
|
|
|
72
72
|
.replace(/\[%total-packages%\]/g, escape(renderOptions.totalPackages.toLocaleString()))
|
|
73
73
|
.replace(/\[%endpoint-path%\]/g, escape(renderOptions.endpointpath))
|
|
74
74
|
.replace(/\[%fhir-version%\]/g, escape(renderOptions.fhirversion))
|
|
75
|
-
.replace(/\[%ms%\]/g, escape(renderOptions.processingTime.toString()))
|
|
75
|
+
.replace(/\[%ms%\]/g, escape(renderOptions.processingTime.toString()))
|
|
76
|
+
.replace(/\[%about%\]/g, renderOptions.about || '');
|
|
76
77
|
|
|
77
78
|
// Handle any custom template variables
|
|
78
79
|
if (options.templateVars) {
|
|
@@ -594,41 +594,46 @@ class PackageManager {
|
|
|
594
594
|
*/
|
|
595
595
|
async fetchUrl(url) {
|
|
596
596
|
console.log("Fetch Package from URL: " + url);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
597
|
+
try {
|
|
598
|
+
const client = new CIBuildClient();
|
|
599
|
+
const packageData = await client.fetchFromUrlSpecific(url);
|
|
600
|
+
|
|
601
|
+
// Extract to a temp location to read package.json for name and version
|
|
602
|
+
const tempKey = `_url_temp_${Date.now()}`;
|
|
603
|
+
const tempPath = await this.extractToCache(tempKey, 'url', packageData);
|
|
604
|
+
const tempFullPath = path.join(this.cacheFolder, tempPath);
|
|
605
|
+
|
|
606
|
+
// Read package name and version from the extracted package
|
|
607
|
+
const pkgJsonPath = path.join(tempFullPath, 'package', 'package.json');
|
|
608
|
+
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
609
|
+
const packageId = pkgJson.name;
|
|
610
|
+
const version = pkgJson.version;
|
|
611
|
+
|
|
612
|
+
if (!packageId || !version) {
|
|
613
|
+
throw new Error(`Package at ${url} has no name or version in package.json`);
|
|
614
|
+
}
|
|
614
615
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
616
|
+
// Use the same cache key format as npm packages
|
|
617
|
+
const finalName = `${packageId}#${version}`;
|
|
618
|
+
const finalPath = path.join(this.cacheFolder, finalName);
|
|
618
619
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
620
|
+
// If it already exists, the same package is already loaded - that's a config error
|
|
621
|
+
try {
|
|
622
|
+
await fs.access(finalPath);
|
|
623
|
+
await fs.rm(tempFullPath, { recursive: true, force: true });
|
|
624
|
+
throw new Error(`Package ${finalName} already exists in cache. Check library config for duplicates (url: ${url})`);
|
|
625
|
+
} catch (e) {
|
|
626
|
+
if (e.message.includes('already exists')) throw e;
|
|
627
|
+
// Doesn't exist yet, rename temp to final
|
|
628
|
+
await fs.rename(tempFullPath, finalPath);
|
|
629
|
+
}
|
|
629
630
|
|
|
630
|
-
|
|
631
|
-
|
|
631
|
+
this.totalDownloaded = this.totalDownloaded + packageData.length;
|
|
632
|
+
return finalName;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error(`Error fetching package from URL ${url}: ${error.message}`);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
632
637
|
}
|
|
633
638
|
|
|
634
639
|
/**
|
package/library/utilities.js
CHANGED
|
@@ -274,4 +274,13 @@ class ArrayMatcher {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
278
|
+
|
|
279
|
+
function formatDateMMDDYYYY(s) {
|
|
280
|
+
const mm = parseInt(s.substring(0, 2), 10);
|
|
281
|
+
const dd = s.substring(2, 4);
|
|
282
|
+
const yyyy = s.substring(4, 8);
|
|
283
|
+
return dd + '-' + months[mm - 1] + ' ' + yyyy;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = { Utilities, ArrayMatcher, validateParameter, validateOptionalParameter, validateArrayParameter, validateResource, strToBool, getValuePrimitive, getValueDT, getValueName, isAbsoluteUrl, formatDateMMDDYYYY };
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ const path = require('path');
|
|
|
12
12
|
|
|
13
13
|
class PackageCrawler {
|
|
14
14
|
log;
|
|
15
|
+
packages = new Set();
|
|
15
16
|
|
|
16
17
|
constructor(config, db, stats) {
|
|
17
18
|
this.config = config;
|
|
@@ -26,6 +27,7 @@ class PackageCrawler {
|
|
|
26
27
|
|
|
27
28
|
async crawl(log) {
|
|
28
29
|
this.log = log;
|
|
30
|
+
this.packages.clear();
|
|
29
31
|
|
|
30
32
|
const startTime = Date.now();
|
|
31
33
|
this.crawlerLog = {
|
|
@@ -56,15 +58,30 @@ class PackageCrawler {
|
|
|
56
58
|
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
57
59
|
continue;
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
try {
|
|
62
|
+
let url = this.fixUrl(feedConfig.url)
|
|
63
|
+
if (!url.includes('simplifier')) {
|
|
64
|
+
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
65
|
+
await this.updateTheFeed(url, this.config.masterUrl,feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '', packageRestrictions);
|
|
66
|
+
}
|
|
67
|
+
} catch (feedError) {
|
|
68
|
+
this.log.error(`Failed to process feed ${feedConfig.url}: `+ feedError.message);
|
|
69
|
+
// Continue with next feed even if this one fails
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// process simplifier last
|
|
73
|
+
for (const feedConfig of masterResponse.feeds) {
|
|
74
|
+
if (!feedConfig.url) {
|
|
75
|
+
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
60
78
|
|
|
61
79
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '',
|
|
66
|
-
|
|
67
|
-
);
|
|
80
|
+
let url = this.fixUrl(feedConfig.url)
|
|
81
|
+
if (url.includes('simplifier')) {
|
|
82
|
+
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
83
|
+
await this.updateTheFeed(url, this.config.masterUrl,feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '', packageRestrictions);
|
|
84
|
+
}
|
|
68
85
|
} catch (feedError) {
|
|
69
86
|
this.log.error(`Failed to process feed ${feedConfig.url}: `+ feedError.message);
|
|
70
87
|
// Continue with next feed even if this one fails
|
|
@@ -100,14 +117,20 @@ class PackageCrawler {
|
|
|
100
117
|
|
|
101
118
|
async fetchJson(url) {
|
|
102
119
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
if (url.startsWith("/")) {
|
|
121
|
+
const content = await fs.promises.readFile(url, "utf8");
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
} else {
|
|
124
|
+
const response = await axios.get(url, {
|
|
125
|
+
timeout: 30000,
|
|
126
|
+
headers: {
|
|
127
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return response.data;
|
|
131
|
+
}
|
|
110
132
|
} catch (error) {
|
|
133
|
+
console.log(error);
|
|
111
134
|
if (error.response && error.response.status === 429) {
|
|
112
135
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
113
136
|
}
|
|
@@ -117,20 +140,30 @@ class PackageCrawler {
|
|
|
117
140
|
|
|
118
141
|
async fetchXml(url) {
|
|
119
142
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
if (url.startsWith("/")) {
|
|
144
|
+
const content = await fs.promises.readFile(url, 'utf8');
|
|
145
|
+
const parser = new XMLParser({
|
|
146
|
+
ignoreAttributes: false,
|
|
147
|
+
attributeNamePrefix: '@_',
|
|
148
|
+
textNodeName: '#text'
|
|
149
|
+
});
|
|
150
|
+
return parser.parse(content);
|
|
151
|
+
} else {
|
|
152
|
+
const response = await axios.get(url, {
|
|
153
|
+
timeout: 30000,
|
|
154
|
+
headers: {
|
|
155
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
156
|
+
}
|
|
157
|
+
});
|
|
126
158
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
const parser = new XMLParser({
|
|
160
|
+
ignoreAttributes: false,
|
|
161
|
+
attributeNamePrefix: '@_',
|
|
162
|
+
textNodeName: '#text'
|
|
163
|
+
});
|
|
132
164
|
|
|
133
|
-
|
|
165
|
+
return parser.parse(response.data);
|
|
166
|
+
}
|
|
134
167
|
} catch (error) {
|
|
135
168
|
if (error.response && error.response.status === 429) {
|
|
136
169
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
@@ -141,16 +174,22 @@ class PackageCrawler {
|
|
|
141
174
|
|
|
142
175
|
async fetchUrl(url) {
|
|
143
176
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
if (url.startsWith("/")) {
|
|
178
|
+
const buffer = await fs.promises.readFile(url);
|
|
179
|
+
this.totalBytes += buffer.byteLength;
|
|
180
|
+
return buffer;
|
|
181
|
+
} else {
|
|
182
|
+
const response = await axios.get(url, {
|
|
183
|
+
timeout: 60000,
|
|
184
|
+
responseType: 'arraybuffer',
|
|
185
|
+
headers: {
|
|
186
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
187
|
+
}
|
|
188
|
+
});
|
|
151
189
|
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
this.totalBytes += response.data.byteLength;
|
|
191
|
+
return Buffer.from(response.data);
|
|
192
|
+
}
|
|
154
193
|
} catch (error) {
|
|
155
194
|
if (error.response && error.response.status === 429) {
|
|
156
195
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
@@ -166,7 +205,7 @@ class PackageCrawler {
|
|
|
166
205
|
};
|
|
167
206
|
this.crawlerLog.feeds.push(feedLog);
|
|
168
207
|
|
|
169
|
-
this.log.info('Processing feed: '+ url);
|
|
208
|
+
this.log.info('Processing feed: ' + url);
|
|
170
209
|
const startTime = Date.now();
|
|
171
210
|
|
|
172
211
|
try {
|
|
@@ -195,7 +234,7 @@ class PackageCrawler {
|
|
|
195
234
|
break; // Stop processing this feed
|
|
196
235
|
}
|
|
197
236
|
// For other errors, log and continue with next item
|
|
198
|
-
this.log.error(`Error processing item ${i} from ${url}
|
|
237
|
+
this.log.error(`Error processing item ${i} from ${url}:` + itemError.message);
|
|
199
238
|
}
|
|
200
239
|
}
|
|
201
240
|
|
|
@@ -205,6 +244,7 @@ class PackageCrawler {
|
|
|
205
244
|
}
|
|
206
245
|
|
|
207
246
|
} catch (error) {
|
|
247
|
+
console.log(error);
|
|
208
248
|
// Check if this is a 429 error on feed fetch
|
|
209
249
|
if (error.message.includes('RATE_LIMITED')) {
|
|
210
250
|
this.log.info(`Rate limited while fetching feed ${url}, skipping this feed`);
|
|
@@ -216,7 +256,7 @@ class PackageCrawler {
|
|
|
216
256
|
|
|
217
257
|
feedLog.exception = error.message;
|
|
218
258
|
feedLog.failTime = `${Date.now() - startTime}ms`;
|
|
219
|
-
this.log.error(`Exception processing feed ${url}
|
|
259
|
+
this.log.error(`Exception processing feed ${url}:` + error.message);
|
|
220
260
|
|
|
221
261
|
// TODO: Send email notification for non-rate-limit errors
|
|
222
262
|
if (email) {
|
|
@@ -275,6 +315,12 @@ class PackageCrawler {
|
|
|
275
315
|
return;
|
|
276
316
|
}
|
|
277
317
|
|
|
318
|
+
if (this.packages.has(id)) {
|
|
319
|
+
this.log.info(`Ignoring package ${id} because it's already been seen in another feed`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.packages.add(id);
|
|
323
|
+
|
|
278
324
|
// Check if already processed
|
|
279
325
|
if (await this.hasStored(guid)) {
|
|
280
326
|
itemLog.status = 'Already Processed';
|
|
@@ -301,7 +347,7 @@ class PackageCrawler {
|
|
|
301
347
|
}
|
|
302
348
|
|
|
303
349
|
itemLog.url = url;
|
|
304
|
-
this.log.info('Fetching package: '+ url);
|
|
350
|
+
this.log.info('Fetching package: ' + url);
|
|
305
351
|
|
|
306
352
|
const packageContent = await this.fetchUrl(url, 'application/tar+gzip');
|
|
307
353
|
await this.store(source, url, guid, pubDate, packageContent, id, itemLog);
|
|
@@ -316,6 +362,7 @@ class PackageCrawler {
|
|
|
316
362
|
throw error;
|
|
317
363
|
}
|
|
318
364
|
}
|
|
365
|
+
|
|
319
366
|
}
|
|
320
367
|
|
|
321
368
|
isPackageAllowed(packageId, source, restrictions) {
|
|
@@ -416,18 +463,19 @@ class PackageCrawler {
|
|
|
416
463
|
itemLog.warning = warning;
|
|
417
464
|
}
|
|
418
465
|
|
|
466
|
+
// Validate package data
|
|
467
|
+
if (!this.isValidPackageId(id)) {
|
|
468
|
+
throw new Error(`NPM Id "${id}" is not valid from ${source}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
419
471
|
// Save to mirror if configured
|
|
420
472
|
if (this.config.mirrorPath) {
|
|
421
|
-
|
|
473
|
+
let fid = this.fixPrefix(id);
|
|
474
|
+
const filename = `${fid}-${version}.tgz`;
|
|
422
475
|
const filepath = path.join(this.config.mirrorPath, filename);
|
|
423
476
|
fs.writeFileSync(filepath, packageBuffer);
|
|
424
477
|
}
|
|
425
478
|
|
|
426
|
-
// Validate package data
|
|
427
|
-
if (!this.isValidPackageId(id)) {
|
|
428
|
-
throw new Error(`NPM Id "${id}" is not valid from ${source}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
479
|
if (!this.isValidSemVersion(version)) {
|
|
432
480
|
throw new Error(`NPM Version "${version}" is not valid from ${source}`);
|
|
433
481
|
}
|
|
@@ -444,6 +492,7 @@ class PackageCrawler {
|
|
|
444
492
|
await this.commit(packageBuffer, npmPackage, date, guid, id, version, canonical, urls);
|
|
445
493
|
|
|
446
494
|
} catch (error) {
|
|
495
|
+
console.log(error);
|
|
447
496
|
this.log.error(`Error storing package ${guid}:`+ error.message);
|
|
448
497
|
throw error;
|
|
449
498
|
}
|
|
@@ -668,7 +717,7 @@ class PackageCrawler {
|
|
|
668
717
|
|
|
669
718
|
isValidPackageId(id) {
|
|
670
719
|
// Simple package ID validation
|
|
671
|
-
return /^[a-z0-9][a-z0-9._-]*$/.test(id);
|
|
720
|
+
return /^(@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(id);
|
|
672
721
|
}
|
|
673
722
|
|
|
674
723
|
isValidSemVersion(version) {
|
|
@@ -846,6 +895,14 @@ class PackageCrawler {
|
|
|
846
895
|
});
|
|
847
896
|
});
|
|
848
897
|
}
|
|
898
|
+
|
|
899
|
+
fixPrefix(id) {
|
|
900
|
+
if (id && id.startsWith("@") && id.includes("/")) {
|
|
901
|
+
return id.replace("@", "$$").replace("/", "$");
|
|
902
|
+
} else {
|
|
903
|
+
return id;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
849
906
|
}
|
|
850
907
|
|
|
851
908
|
module.exports = PackageCrawler;
|
package/packages/packages.js
CHANGED
|
@@ -1067,13 +1067,13 @@ class PackagesModule {
|
|
|
1067
1067
|
const {id, version} = req.params;
|
|
1068
1068
|
|
|
1069
1069
|
if (!id || !version ||
|
|
1070
|
-
!/^[a-zA-Z0-9._-]+$/.test(id) ||
|
|
1070
|
+
!/^(@[a-z0-9._-]+\/)?[a-zA-Z0-9._-]+$/.test(id) ||
|
|
1071
1071
|
!/^[a-zA-Z0-9._-]+$/.test(version)) {
|
|
1072
|
-
return res.status(400).json({error:
|
|
1072
|
+
return res.status(400).json({error: `Invalid package id or version format: ${id}`});
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
1075
|
if (id.length > 100 || version.length > 50) {
|
|
1076
|
-
return res.status(400).json({error:
|
|
1076
|
+
return res.status(400).json({error: `Package id or version too long: ${id}`});
|
|
1077
1077
|
}
|
|
1078
1078
|
|
|
1079
1079
|
next();
|
|
@@ -1517,7 +1517,7 @@ class PackagesModule {
|
|
|
1517
1517
|
// Check if we should redirect to bucket storage
|
|
1518
1518
|
if (this.config.bucketPath) {
|
|
1519
1519
|
let bucketUrl = this.getBucketUrl(secure);
|
|
1520
|
-
const redirectUrl = `${bucketUrl}${id}-${version}.tgz`;
|
|
1520
|
+
const redirectUrl = `${bucketUrl}${this.fixPrefix(id)}-${version}.tgz`;
|
|
1521
1521
|
res.redirect(redirectUrl);
|
|
1522
1522
|
return;
|
|
1523
1523
|
}
|
|
@@ -1888,7 +1888,7 @@ class PackagesModule {
|
|
|
1888
1888
|
table += `<td>${escape(this.codeForKind(pv.Kind))}</td>`;
|
|
1889
1889
|
table += `<td>${new Date(pv.PubDate).toLocaleDateString()}</td>`;
|
|
1890
1890
|
table += `<td>${(pv.DownloadCount || 0).toLocaleString()}</td>`;
|
|
1891
|
-
table += `<td><a href="/packages/${
|
|
1891
|
+
table += `<td><a href="/packages/${encodeURIComponent(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
|
|
1892
1892
|
table += '</tr>';
|
|
1893
1893
|
}
|
|
1894
1894
|
|
|
@@ -2126,8 +2126,8 @@ class PackagesModule {
|
|
|
2126
2126
|
|
|
2127
2127
|
for (const pkg of results) {
|
|
2128
2128
|
table += '<tr>';
|
|
2129
|
-
table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
|
|
2130
|
-
table += `<td>${escape(pkg.version)} (<a href="/packages/${
|
|
2129
|
+
table += `<td><a href="${escape(this.fixPrefix(pkg.url))}">${escape(pkg.name)}</a></td>`;
|
|
2130
|
+
table += `<td>${escape(pkg.version)} (<a href="/packages/${encodeURIComponent(pkg.name)}">all</a>)</td>`;
|
|
2131
2131
|
table += `<td>${escape(pkg.fhirVersion)}</td>`;
|
|
2132
2132
|
table += `<td>${escape(pkg.kind)}</td>`;
|
|
2133
2133
|
table += `<td>${pkg.date ? new Date(pkg.date).toLocaleDateString() : 'N/A'}</td>`;
|
|
@@ -2817,6 +2817,13 @@ class PackagesModule {
|
|
|
2817
2817
|
return content;
|
|
2818
2818
|
}
|
|
2819
2819
|
|
|
2820
|
+
fixPrefix(id) {
|
|
2821
|
+
if (id && id.startsWith("@") && id.includes("/")) {
|
|
2822
|
+
return id.replace("@", "$$").replace("/", "$");
|
|
2823
|
+
} else {
|
|
2824
|
+
return id;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2820
2827
|
}
|
|
2821
2828
|
|
|
2822
2829
|
module.exports = PackagesModule;
|
package/publisher/publisher.js
CHANGED
|
@@ -91,6 +91,8 @@ class PublisherModule {
|
|
|
91
91
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
92
|
name TEXT NOT NULL,
|
|
93
93
|
local_folder TEXT NOT NULL,
|
|
94
|
+
history_templates TEXT NOT NULL,
|
|
95
|
+
web_templates TEXT NOT NULL,
|
|
94
96
|
server_update_script TEXT NOT NULL,
|
|
95
97
|
is_active BOOLEAN DEFAULT 1,
|
|
96
98
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
@@ -1605,6 +1607,14 @@ class PublisherModule {
|
|
|
1605
1607
|
content += '<input type="text" class="form-control" id="local_folder" name="local_folder" required>';
|
|
1606
1608
|
content += '</div>';
|
|
1607
1609
|
content += '<div class="col-md-4">';
|
|
1610
|
+
content += '<label for="history_templates" class="form-label">History Templates</label>';
|
|
1611
|
+
content += '<input type="text" class="form-control" id="history_templates" name="history_templates" required>';
|
|
1612
|
+
content += '</div>';
|
|
1613
|
+
content += '<div class="col-md-4">';
|
|
1614
|
+
content += '<label for="web_templates" class="form-label">Web Templates</label>';
|
|
1615
|
+
content += '<input type="text" class="form-control" id="web_templates" name="web_templates" required>';
|
|
1616
|
+
content += '</div>';
|
|
1617
|
+
content += '<div class="col-md-4">';
|
|
1608
1618
|
content += '<label for="server_update_script" class="form-label">Update Script</label>';
|
|
1609
1619
|
content += '<input type="text" class="form-control" id="server_update_script" name="server_update_script" required>';
|
|
1610
1620
|
content += '</div>';
|
|
@@ -1631,6 +1641,8 @@ class PublisherModule {
|
|
|
1631
1641
|
content += '<tr>';
|
|
1632
1642
|
content += '<td>' + website.name + '</td>';
|
|
1633
1643
|
content += '<td><code>' + website.local_folder + '</code></td>';
|
|
1644
|
+
content += '<td><code>' + website.history_templates + '</code></td>';
|
|
1645
|
+
content += '<td><code>' + website.web_templates + '</code></td>';
|
|
1634
1646
|
content += '<td><code>' + website.server_update_script + '</code></td>';
|
|
1635
1647
|
content += '<td>' + (website.is_active ? '✓' : '✗') + '</td>';
|
|
1636
1648
|
content += '<td>' + new Date(website.created_at).toLocaleString() + '</td>';
|
|
@@ -1665,12 +1677,12 @@ class PublisherModule {
|
|
|
1665
1677
|
const start = Date.now();
|
|
1666
1678
|
try {
|
|
1667
1679
|
try {
|
|
1668
|
-
const {name, local_folder, server_update_script} = req.body;
|
|
1680
|
+
const {name, local_folder, history_templates, web_templates, server_update_script} = req.body;
|
|
1669
1681
|
|
|
1670
1682
|
await new Promise((resolve, reject) => {
|
|
1671
1683
|
this.db.run(
|
|
1672
|
-
'INSERT INTO websites (name, local_folder, server_update_script) VALUES (?, ?, ?)',
|
|
1673
|
-
[name, local_folder, server_update_script],
|
|
1684
|
+
'INSERT INTO websites (name, local_folder, history_templates, web_templates, server_update_script) VALUES (?, ?, ?, ?, ?)',
|
|
1685
|
+
[name, local_folder, history_templates, web_templates, server_update_script],
|
|
1674
1686
|
function (err) {
|
|
1675
1687
|
if (err) reject(err);
|
|
1676
1688
|
else resolve();
|