fhirsmith 0.5.6 → 0.7.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 +38 -0
- package/README.md +2 -0
- package/configurations/projector.json +21 -0
- package/configurations/readme.md +5 -0
- package/library/html-server.js +2 -1
- package/library/package-manager.js +37 -34
- package/library/utilities.js +10 -1
- package/library/version-utilities.js +85 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +144 -52
- package/packages/packages.js +15 -7
- package/publisher/publisher.js +15 -3
- package/registry/api.js +173 -191
- package/registry/crawler.js +100 -65
- package/registry/model.js +14 -8
- package/registry/registry.js +5 -0
- package/root-template.html +1 -0
- package/server.js +113 -45
- package/tx/README.md +4 -4
- package/tx/cs/cs-api.js +18 -1
- package/tx/cs/cs-base.js +1 -0
- package/tx/cs/cs-loinc.js +5 -2
- package/tx/cs/cs-provider-api.js +25 -1
- package/tx/cs/cs-provider-list.js +2 -2
- 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/canonical-resource.js +6 -1
- package/tx/library/codesystem.js +6 -1
- package/tx/library/renderer.js +81 -7
- package/tx/library.js +145 -13
- package/tx/ocl/README.md +236 -0
- package/tx/ocl/cache/cache-paths.cjs +32 -0
- package/tx/ocl/cache/cache-paths.js +2 -0
- package/tx/ocl/cache/cache-utils.cjs +43 -0
- package/tx/ocl/cache/cache-utils.js +2 -0
- package/tx/ocl/cm-ocl.cjs +531 -0
- package/tx/ocl/cm-ocl.js +1 -105
- package/tx/ocl/cs-ocl.cjs +1779 -0
- package/tx/ocl/cs-ocl.js +1 -38
- package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
- package/tx/ocl/fingerprint/fingerprint.js +2 -0
- package/tx/ocl/http/client.cjs +31 -0
- package/tx/ocl/http/client.js +2 -0
- package/tx/ocl/http/pagination.cjs +98 -0
- package/tx/ocl/http/pagination.js +2 -0
- package/tx/ocl/jobs/background-queue.cjs +200 -0
- package/tx/ocl/jobs/background-queue.js +2 -0
- package/tx/ocl/mappers/concept-mapper.cjs +66 -0
- package/tx/ocl/mappers/concept-mapper.js +2 -0
- package/tx/ocl/model/concept-filter-context.cjs +51 -0
- package/tx/ocl/model/concept-filter-context.js +2 -0
- package/tx/ocl/shared/constants.cjs +15 -0
- package/tx/ocl/shared/constants.js +2 -0
- package/tx/ocl/shared/patches.cjs +224 -0
- package/tx/ocl/shared/patches.js +2 -0
- package/tx/ocl/vs-ocl.cjs +1848 -0
- package/tx/ocl/vs-ocl.js +1 -104
- package/tx/operation-context.js +8 -1
- package/tx/params.js +24 -3
- package/tx/provider.js +51 -2
- package/tx/sct/expressions.js +20 -9
- package/tx/tx-html.js +144 -51
- package/tx/tx.js +10 -2
- package/tx/vs/vs-vsac.js +4 -3
- package/tx/workers/batch-validate.js +3 -2
- package/tx/workers/batch.js +3 -2
- package/tx/workers/expand.js +125 -18
- package/tx/workers/lookup.js +5 -4
- package/tx/workers/read.js +2 -1
- package/tx/workers/related.js +3 -2
- package/tx/workers/search.js +6 -8
- package/tx/workers/subsumes.js +3 -2
- package/tx/workers/translate.js +4 -3
- package/tx/workers/validate.js +132 -40
- package/tx/workers/worker.js +1 -7
- package/tx/xversion/xv-terminologyCapabilities.js +1 -1
|
@@ -9,10 +9,12 @@ const {XMLParser} = require('fast-xml-parser');
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const {debugLog} = require("../tx/operation-context");
|
|
12
13
|
|
|
13
14
|
class PackageCrawler {
|
|
14
15
|
log;
|
|
15
|
-
|
|
16
|
+
packages = new Set();
|
|
17
|
+
|
|
16
18
|
constructor(config, db, stats) {
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.db = db;
|
|
@@ -20,13 +22,16 @@ class PackageCrawler {
|
|
|
20
22
|
this.totalBytes = 0;
|
|
21
23
|
this.crawlerLog = {};
|
|
22
24
|
this.errors = '';
|
|
25
|
+
this.abortController = null;
|
|
23
26
|
this.db.run('PRAGMA journal_mode = WAL');
|
|
24
27
|
this.db.run('PRAGMA busy_timeout = 5000');
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
async crawl(log) {
|
|
28
31
|
this.log = log;
|
|
29
|
-
|
|
32
|
+
this.packages.clear();
|
|
33
|
+
this.abortController = new AbortController();
|
|
34
|
+
|
|
30
35
|
const startTime = Date.now();
|
|
31
36
|
this.crawlerLog = {
|
|
32
37
|
startTime: new Date().toISOString(),
|
|
@@ -52,19 +57,36 @@ class PackageCrawler {
|
|
|
52
57
|
|
|
53
58
|
// Process each feed
|
|
54
59
|
for (const feedConfig of masterResponse.feeds) {
|
|
60
|
+
if (this.abortController?.signal.aborted) break;
|
|
61
|
+
if (!feedConfig.url) {
|
|
62
|
+
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
let url = this.fixUrl(feedConfig.url)
|
|
67
|
+
if (!url.includes('simplifier')) {
|
|
68
|
+
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
69
|
+
await this.updateTheFeed(url, this.config.masterUrl,feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '', packageRestrictions);
|
|
70
|
+
}
|
|
71
|
+
} catch (feedError) {
|
|
72
|
+
this.log.error(`Failed to process feed ${feedConfig.url}: `+ feedError.message);
|
|
73
|
+
// Continue with next feed even if this one fails
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// process simplifier last
|
|
77
|
+
for (const feedConfig of masterResponse.feeds) {
|
|
78
|
+
if (this.abortController?.signal.aborted) break;
|
|
55
79
|
if (!feedConfig.url) {
|
|
56
80
|
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
57
81
|
continue;
|
|
58
82
|
}
|
|
59
|
-
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
60
83
|
|
|
61
84
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '',
|
|
66
|
-
|
|
67
|
-
);
|
|
85
|
+
let url = this.fixUrl(feedConfig.url)
|
|
86
|
+
if (url.includes('simplifier')) {
|
|
87
|
+
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
88
|
+
await this.updateTheFeed(url, this.config.masterUrl,feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '', packageRestrictions);
|
|
89
|
+
}
|
|
68
90
|
} catch (feedError) {
|
|
69
91
|
this.log.error(`Failed to process feed ${feedConfig.url}: `+ feedError.message);
|
|
70
92
|
// Continue with next feed even if this one fails
|
|
@@ -100,14 +122,21 @@ class PackageCrawler {
|
|
|
100
122
|
|
|
101
123
|
async fetchJson(url) {
|
|
102
124
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
if (url.startsWith("/")) {
|
|
126
|
+
const content = await fs.promises.readFile(url, "utf8");
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
} else {
|
|
129
|
+
const response = await axios.get(url, {
|
|
130
|
+
timeout: 30000,
|
|
131
|
+
signal: this.abortController?.signal,
|
|
132
|
+
headers: {
|
|
133
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
110
138
|
} catch (error) {
|
|
139
|
+
debugLog(error);
|
|
111
140
|
if (error.response && error.response.status === 429) {
|
|
112
141
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
113
142
|
}
|
|
@@ -117,21 +146,33 @@ class PackageCrawler {
|
|
|
117
146
|
|
|
118
147
|
async fetchXml(url) {
|
|
119
148
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
if (url.startsWith("/")) {
|
|
150
|
+
const content = await fs.promises.readFile(url, 'utf8');
|
|
151
|
+
const parser = new XMLParser({
|
|
152
|
+
ignoreAttributes: false,
|
|
153
|
+
attributeNamePrefix: '@_',
|
|
154
|
+
textNodeName: '#text'
|
|
155
|
+
});
|
|
156
|
+
return parser.parse(content);
|
|
157
|
+
} else {
|
|
158
|
+
const response = await axios.get(url, {
|
|
159
|
+
timeout: 30000,
|
|
160
|
+
signal: this.abortController?.signal,
|
|
161
|
+
headers: {
|
|
162
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
163
|
+
}
|
|
164
|
+
});
|
|
126
165
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
const parser = new XMLParser({
|
|
167
|
+
ignoreAttributes: false,
|
|
168
|
+
attributeNamePrefix: '@_',
|
|
169
|
+
textNodeName: '#text'
|
|
170
|
+
});
|
|
132
171
|
|
|
133
|
-
|
|
172
|
+
return parser.parse(response.data);
|
|
173
|
+
}
|
|
134
174
|
} catch (error) {
|
|
175
|
+
debugLog(error);
|
|
135
176
|
if (error.response && error.response.status === 429) {
|
|
136
177
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
137
178
|
}
|
|
@@ -141,17 +182,25 @@ class PackageCrawler {
|
|
|
141
182
|
|
|
142
183
|
async fetchUrl(url) {
|
|
143
184
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
185
|
+
if (url.startsWith("/")) {
|
|
186
|
+
const buffer = await fs.promises.readFile(url);
|
|
187
|
+
this.totalBytes += buffer.byteLength;
|
|
188
|
+
return buffer;
|
|
189
|
+
} else {
|
|
190
|
+
const response = await axios.get(url, {
|
|
191
|
+
timeout: 60000,
|
|
192
|
+
responseType: 'arraybuffer',
|
|
193
|
+
signal: this.abortController?.signal,
|
|
194
|
+
headers: {
|
|
195
|
+
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
151
198
|
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
this.totalBytes += response.data.byteLength;
|
|
200
|
+
return Buffer.from(response.data);
|
|
201
|
+
}
|
|
154
202
|
} catch (error) {
|
|
203
|
+
debugLog(error);
|
|
155
204
|
if (error.response && error.response.status === 429) {
|
|
156
205
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
157
206
|
}
|
|
@@ -166,7 +215,7 @@ class PackageCrawler {
|
|
|
166
215
|
};
|
|
167
216
|
this.crawlerLog.feeds.push(feedLog);
|
|
168
217
|
|
|
169
|
-
this.log.info('Processing feed: '+ url);
|
|
218
|
+
this.log.info('Processing feed: ' + url);
|
|
170
219
|
const startTime = Date.now();
|
|
171
220
|
|
|
172
221
|
try {
|
|
@@ -183,6 +232,7 @@ class PackageCrawler {
|
|
|
183
232
|
this.log.info(`Found ${items.length} items in feed`);
|
|
184
233
|
|
|
185
234
|
for (let i = 0; i < items.length; i++) {
|
|
235
|
+
if (this.abortController?.signal.aborted) break;
|
|
186
236
|
try {
|
|
187
237
|
await this.updateItem(url, items[i], i, packageRestrictions, feedLog);
|
|
188
238
|
} catch (itemError) {
|
|
@@ -195,7 +245,7 @@ class PackageCrawler {
|
|
|
195
245
|
break; // Stop processing this feed
|
|
196
246
|
}
|
|
197
247
|
// For other errors, log and continue with next item
|
|
198
|
-
this.log.error(`Error processing item ${i} from ${url}
|
|
248
|
+
this.log.error(`Error processing item ${i} from ${url}:` + itemError.message);
|
|
199
249
|
}
|
|
200
250
|
}
|
|
201
251
|
|
|
@@ -205,6 +255,7 @@ class PackageCrawler {
|
|
|
205
255
|
}
|
|
206
256
|
|
|
207
257
|
} catch (error) {
|
|
258
|
+
debugLog(error);
|
|
208
259
|
// Check if this is a 429 error on feed fetch
|
|
209
260
|
if (error.message.includes('RATE_LIMITED')) {
|
|
210
261
|
this.log.info(`Rate limited while fetching feed ${url}, skipping this feed`);
|
|
@@ -216,7 +267,7 @@ class PackageCrawler {
|
|
|
216
267
|
|
|
217
268
|
feedLog.exception = error.message;
|
|
218
269
|
feedLog.failTime = `${Date.now() - startTime}ms`;
|
|
219
|
-
this.log.error(`Exception processing feed ${url}
|
|
270
|
+
this.log.error(`Exception processing feed ${url}:` + error.message);
|
|
220
271
|
|
|
221
272
|
// TODO: Send email notification for non-rate-limit errors
|
|
222
273
|
if (email) {
|
|
@@ -262,7 +313,7 @@ class PackageCrawler {
|
|
|
262
313
|
}
|
|
263
314
|
|
|
264
315
|
// Check package restrictions
|
|
265
|
-
if (!this.isPackageAllowed(id, source, packageRestrictions)) {
|
|
316
|
+
if (!this.isPackageAllowed(id, source, packageRestrictions).allowed) {
|
|
266
317
|
if (!source.includes('simplifier.net')) {
|
|
267
318
|
const error = `The package ${id} is not allowed to come from ${source}`;
|
|
268
319
|
this.log.info(error);
|
|
@@ -275,6 +326,12 @@ class PackageCrawler {
|
|
|
275
326
|
return;
|
|
276
327
|
}
|
|
277
328
|
|
|
329
|
+
if (this.packages.has(id)) {
|
|
330
|
+
this.log.info(`Ignoring package ${id} because it's already been seen in another feed`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.packages.add(id);
|
|
334
|
+
|
|
278
335
|
// Check if already processed
|
|
279
336
|
if (await this.hasStored(guid)) {
|
|
280
337
|
itemLog.status = 'Already Processed';
|
|
@@ -283,11 +340,12 @@ class PackageCrawler {
|
|
|
283
340
|
|
|
284
341
|
// Parse publication date
|
|
285
342
|
let pubDate;
|
|
343
|
+
let pd;
|
|
286
344
|
try {
|
|
287
345
|
let pd = item.pubDate;
|
|
288
346
|
pubDate = this.parsePubDate(pd);
|
|
289
347
|
} catch (error) {
|
|
290
|
-
itemLog.error = `Invalid date format '{pd}': ${error.message}`;
|
|
348
|
+
itemLog.error = `Invalid date format '${pd}': ${error.message}`;
|
|
291
349
|
itemLog.status = 'error';
|
|
292
350
|
return;
|
|
293
351
|
}
|
|
@@ -301,7 +359,7 @@ class PackageCrawler {
|
|
|
301
359
|
}
|
|
302
360
|
|
|
303
361
|
itemLog.url = url;
|
|
304
|
-
this.log.info('Fetching package: '+ url);
|
|
362
|
+
this.log.info('Fetching package: ' + url);
|
|
305
363
|
|
|
306
364
|
const packageContent = await this.fetchUrl(url, 'application/tar+gzip');
|
|
307
365
|
await this.store(source, url, guid, pubDate, packageContent, id, itemLog);
|
|
@@ -309,13 +367,14 @@ class PackageCrawler {
|
|
|
309
367
|
itemLog.status = 'Fetched';
|
|
310
368
|
|
|
311
369
|
} catch (error) {
|
|
312
|
-
this.log.error(`Exception processing item ${itemLog.guid || index}
|
|
370
|
+
this.log.error(`Exception processing item ${itemLog.guid || index} from ${source}: `+ error.message);
|
|
313
371
|
itemLog.status = 'Exception';
|
|
314
372
|
itemLog.error = error.message;
|
|
315
373
|
if (error.message.includes('RATE_LIMITED')) {
|
|
316
374
|
throw error;
|
|
317
375
|
}
|
|
318
376
|
}
|
|
377
|
+
|
|
319
378
|
}
|
|
320
379
|
|
|
321
380
|
isPackageAllowed(packageId, source, restrictions) {
|
|
@@ -336,7 +395,7 @@ class PackageCrawler {
|
|
|
336
395
|
|
|
337
396
|
if (this.matchesPattern(fixedPackageId, fixedMask)) {
|
|
338
397
|
// This package matches a restriction - check if source is allowed
|
|
339
|
-
const allowedFeeds = restriction.feeds.map(feed => feed);
|
|
398
|
+
const allowedFeeds = restriction.feeds.map(feed => fixUrl(feed));
|
|
340
399
|
const feedList = allowedFeeds.join(', ');
|
|
341
400
|
|
|
342
401
|
for (const allowedFeed of restriction.feeds) {
|
|
@@ -416,18 +475,19 @@ class PackageCrawler {
|
|
|
416
475
|
itemLog.warning = warning;
|
|
417
476
|
}
|
|
418
477
|
|
|
478
|
+
// Validate package data
|
|
479
|
+
if (!this.isValidPackageId(id)) {
|
|
480
|
+
throw new Error(`NPM Id "${id}" is not valid from ${source}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
419
483
|
// Save to mirror if configured
|
|
420
484
|
if (this.config.mirrorPath) {
|
|
421
|
-
|
|
485
|
+
let fid = this.fixPrefix(id);
|
|
486
|
+
const filename = `${fid}-${version}.tgz`;
|
|
422
487
|
const filepath = path.join(this.config.mirrorPath, filename);
|
|
423
488
|
fs.writeFileSync(filepath, packageBuffer);
|
|
424
489
|
}
|
|
425
490
|
|
|
426
|
-
// Validate package data
|
|
427
|
-
if (!this.isValidPackageId(id)) {
|
|
428
|
-
throw new Error(`NPM Id "${id}" is not valid from ${source}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
491
|
if (!this.isValidSemVersion(version)) {
|
|
432
492
|
throw new Error(`NPM Version "${version}" is not valid from ${source}`);
|
|
433
493
|
}
|
|
@@ -437,6 +497,14 @@ class PackageCrawler {
|
|
|
437
497
|
throw new Error(`NPM Canonical "${canonical}" is not valid from ${source}`);
|
|
438
498
|
}
|
|
439
499
|
|
|
500
|
+
const isTemplate = npmPackage.kind === 2; // fhir.template
|
|
501
|
+
if (npmPackage.hasInstallScripts) {
|
|
502
|
+
throw new Error(`Package ${idver} rejected: contains install scripts (preinstall/install/postinstall)`);
|
|
503
|
+
}
|
|
504
|
+
if (npmPackage.hasJavaScript && !isTemplate) {
|
|
505
|
+
throw new Error(`Package ${idver} rejected: contains JavaScript files but is not a template package`);
|
|
506
|
+
}
|
|
507
|
+
|
|
440
508
|
// Extract URLs from package
|
|
441
509
|
const urls = this.processPackageUrls(npmPackage);
|
|
442
510
|
|
|
@@ -444,6 +512,7 @@ class PackageCrawler {
|
|
|
444
512
|
await this.commit(packageBuffer, npmPackage, date, guid, id, version, canonical, urls);
|
|
445
513
|
|
|
446
514
|
} catch (error) {
|
|
515
|
+
debugLog(error);
|
|
447
516
|
this.log.error(`Error storing package ${guid}:`+ error.message);
|
|
448
517
|
throw error;
|
|
449
518
|
}
|
|
@@ -506,6 +575,14 @@ class PackageCrawler {
|
|
|
506
575
|
}
|
|
507
576
|
|
|
508
577
|
const packageJson = JSON.parse(files['package.json']);
|
|
578
|
+
const hasInstallScripts = !!(
|
|
579
|
+
packageJson.scripts && (
|
|
580
|
+
packageJson.scripts.preinstall ||
|
|
581
|
+
packageJson.scripts.install ||
|
|
582
|
+
packageJson.scripts.postinstall
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
const hasJavaScript = Object.keys(files).some(f => f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.cjs'));
|
|
509
586
|
|
|
510
587
|
// Extract basic NPM fields
|
|
511
588
|
const id = packageJson.name || '';
|
|
@@ -615,6 +692,8 @@ class PackageCrawler {
|
|
|
615
692
|
url: homepage,
|
|
616
693
|
dependencies,
|
|
617
694
|
kind,
|
|
695
|
+
hasInstallScripts,
|
|
696
|
+
hasJavaScript,
|
|
618
697
|
notForPublication,
|
|
619
698
|
files
|
|
620
699
|
};
|
|
@@ -668,7 +747,7 @@ class PackageCrawler {
|
|
|
668
747
|
|
|
669
748
|
isValidPackageId(id) {
|
|
670
749
|
// Simple package ID validation
|
|
671
|
-
return /^[a-z0-9][a-z0-9._-]*$/.test(id);
|
|
750
|
+
return /^(@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(id);
|
|
672
751
|
}
|
|
673
752
|
|
|
674
753
|
isValidSemVersion(version) {
|
|
@@ -846,6 +925,19 @@ class PackageCrawler {
|
|
|
846
925
|
});
|
|
847
926
|
});
|
|
848
927
|
}
|
|
928
|
+
|
|
929
|
+
fixPrefix(id) {
|
|
930
|
+
if (id && id.startsWith("@") && id.includes("/")) {
|
|
931
|
+
return id.replace("@", "$$").replace("/", "$");
|
|
932
|
+
} else {
|
|
933
|
+
return id;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
shutdown() {
|
|
937
|
+
if (this.abortController) {
|
|
938
|
+
this.abortController.abort();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
849
941
|
}
|
|
850
942
|
|
|
851
943
|
module.exports = PackageCrawler;
|
package/packages/packages.js
CHANGED
|
@@ -803,6 +803,7 @@ class PackagesModule {
|
|
|
803
803
|
|
|
804
804
|
stopCrawlerJob() {
|
|
805
805
|
if (this.crawlerJob) {
|
|
806
|
+
this.crawler.shutdown();
|
|
806
807
|
this.crawlerJob.stop();
|
|
807
808
|
this.crawlerJob = null;
|
|
808
809
|
pckLog.info('Package crawler job stopped');
|
|
@@ -1067,13 +1068,13 @@ class PackagesModule {
|
|
|
1067
1068
|
const {id, version} = req.params;
|
|
1068
1069
|
|
|
1069
1070
|
if (!id || !version ||
|
|
1070
|
-
!/^[a-zA-Z0-9._-]+$/.test(id) ||
|
|
1071
|
+
!/^(@[a-z0-9._-]+\/)?[a-zA-Z0-9._-]+$/.test(id) ||
|
|
1071
1072
|
!/^[a-zA-Z0-9._-]+$/.test(version)) {
|
|
1072
|
-
return res.status(400).json({error:
|
|
1073
|
+
return res.status(400).json({error: `Invalid package id or version format: ${id}`});
|
|
1073
1074
|
}
|
|
1074
1075
|
|
|
1075
1076
|
if (id.length > 100 || version.length > 50) {
|
|
1076
|
-
return res.status(400).json({error:
|
|
1077
|
+
return res.status(400).json({error: `Package id or version too long: ${id}`});
|
|
1077
1078
|
}
|
|
1078
1079
|
|
|
1079
1080
|
next();
|
|
@@ -1517,7 +1518,7 @@ class PackagesModule {
|
|
|
1517
1518
|
// Check if we should redirect to bucket storage
|
|
1518
1519
|
if (this.config.bucketPath) {
|
|
1519
1520
|
let bucketUrl = this.getBucketUrl(secure);
|
|
1520
|
-
const redirectUrl = `${bucketUrl}${id}-${version}.tgz`;
|
|
1521
|
+
const redirectUrl = `${bucketUrl}${this.fixPrefix(id)}-${version}.tgz`;
|
|
1521
1522
|
res.redirect(redirectUrl);
|
|
1522
1523
|
return;
|
|
1523
1524
|
}
|
|
@@ -1888,7 +1889,7 @@ class PackagesModule {
|
|
|
1888
1889
|
table += `<td>${escape(this.codeForKind(pv.Kind))}</td>`;
|
|
1889
1890
|
table += `<td>${new Date(pv.PubDate).toLocaleDateString()}</td>`;
|
|
1890
1891
|
table += `<td>${(pv.DownloadCount || 0).toLocaleString()}</td>`;
|
|
1891
|
-
table += `<td><a href="/packages/${
|
|
1892
|
+
table += `<td><a href="/packages/${encodeURIComponent(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
|
|
1892
1893
|
table += '</tr>';
|
|
1893
1894
|
}
|
|
1894
1895
|
|
|
@@ -2126,8 +2127,8 @@ class PackagesModule {
|
|
|
2126
2127
|
|
|
2127
2128
|
for (const pkg of results) {
|
|
2128
2129
|
table += '<tr>';
|
|
2129
|
-
table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
|
|
2130
|
-
table += `<td>${escape(pkg.version)} (<a href="/packages/${
|
|
2130
|
+
table += `<td><a href="${escape(this.fixPrefix(pkg.url))}">${escape(pkg.name)}</a></td>`;
|
|
2131
|
+
table += `<td>${escape(pkg.version)} (<a href="/packages/${encodeURIComponent(pkg.name)}">all</a>)</td>`;
|
|
2131
2132
|
table += `<td>${escape(pkg.fhirVersion)}</td>`;
|
|
2132
2133
|
table += `<td>${escape(pkg.kind)}</td>`;
|
|
2133
2134
|
table += `<td>${pkg.date ? new Date(pkg.date).toLocaleDateString() : 'N/A'}</td>`;
|
|
@@ -2817,6 +2818,13 @@ class PackagesModule {
|
|
|
2817
2818
|
return content;
|
|
2818
2819
|
}
|
|
2819
2820
|
|
|
2821
|
+
fixPrefix(id) {
|
|
2822
|
+
if (id && id.startsWith("@") && id.includes("/")) {
|
|
2823
|
+
return id.replace("@", "$$").replace("/", "$");
|
|
2824
|
+
} else {
|
|
2825
|
+
return id;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2820
2828
|
}
|
|
2821
2829
|
|
|
2822
2830
|
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();
|