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.
Files changed (78) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -0
  3. package/configurations/projector.json +21 -0
  4. package/configurations/readme.md +5 -0
  5. package/library/html-server.js +2 -1
  6. package/library/package-manager.js +37 -34
  7. package/library/utilities.js +10 -1
  8. package/library/version-utilities.js +85 -0
  9. package/package.json +1 -1
  10. package/packages/package-crawler.js +144 -52
  11. package/packages/packages.js +15 -7
  12. package/publisher/publisher.js +15 -3
  13. package/registry/api.js +173 -191
  14. package/registry/crawler.js +100 -65
  15. package/registry/model.js +14 -8
  16. package/registry/registry.js +5 -0
  17. package/root-template.html +1 -0
  18. package/server.js +113 -45
  19. package/tx/README.md +4 -4
  20. package/tx/cs/cs-api.js +18 -1
  21. package/tx/cs/cs-base.js +1 -0
  22. package/tx/cs/cs-loinc.js +5 -2
  23. package/tx/cs/cs-provider-api.js +25 -1
  24. package/tx/cs/cs-provider-list.js +2 -2
  25. package/tx/cs/cs-rxnorm.js +9 -2
  26. package/tx/cs/cs-snomed.js +17 -2
  27. package/tx/html/codesystem-operations.liquid +17 -24
  28. package/tx/html/valueset-operations.liquid +46 -52
  29. package/tx/library/canonical-resource.js +6 -1
  30. package/tx/library/codesystem.js +6 -1
  31. package/tx/library/renderer.js +81 -7
  32. package/tx/library.js +145 -13
  33. package/tx/ocl/README.md +236 -0
  34. package/tx/ocl/cache/cache-paths.cjs +32 -0
  35. package/tx/ocl/cache/cache-paths.js +2 -0
  36. package/tx/ocl/cache/cache-utils.cjs +43 -0
  37. package/tx/ocl/cache/cache-utils.js +2 -0
  38. package/tx/ocl/cm-ocl.cjs +531 -0
  39. package/tx/ocl/cm-ocl.js +1 -105
  40. package/tx/ocl/cs-ocl.cjs +1779 -0
  41. package/tx/ocl/cs-ocl.js +1 -38
  42. package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
  43. package/tx/ocl/fingerprint/fingerprint.js +2 -0
  44. package/tx/ocl/http/client.cjs +31 -0
  45. package/tx/ocl/http/client.js +2 -0
  46. package/tx/ocl/http/pagination.cjs +98 -0
  47. package/tx/ocl/http/pagination.js +2 -0
  48. package/tx/ocl/jobs/background-queue.cjs +200 -0
  49. package/tx/ocl/jobs/background-queue.js +2 -0
  50. package/tx/ocl/mappers/concept-mapper.cjs +66 -0
  51. package/tx/ocl/mappers/concept-mapper.js +2 -0
  52. package/tx/ocl/model/concept-filter-context.cjs +51 -0
  53. package/tx/ocl/model/concept-filter-context.js +2 -0
  54. package/tx/ocl/shared/constants.cjs +15 -0
  55. package/tx/ocl/shared/constants.js +2 -0
  56. package/tx/ocl/shared/patches.cjs +224 -0
  57. package/tx/ocl/shared/patches.js +2 -0
  58. package/tx/ocl/vs-ocl.cjs +1848 -0
  59. package/tx/ocl/vs-ocl.js +1 -104
  60. package/tx/operation-context.js +8 -1
  61. package/tx/params.js +24 -3
  62. package/tx/provider.js +51 -2
  63. package/tx/sct/expressions.js +20 -9
  64. package/tx/tx-html.js +144 -51
  65. package/tx/tx.js +10 -2
  66. package/tx/vs/vs-vsac.js +4 -3
  67. package/tx/workers/batch-validate.js +3 -2
  68. package/tx/workers/batch.js +3 -2
  69. package/tx/workers/expand.js +125 -18
  70. package/tx/workers/lookup.js +5 -4
  71. package/tx/workers/read.js +2 -1
  72. package/tx/workers/related.js +3 -2
  73. package/tx/workers/search.js +6 -8
  74. package/tx/workers/subsumes.js +3 -2
  75. package/tx/workers/translate.js +4 -3
  76. package/tx/workers/validate.js +132 -40
  77. package/tx/workers/worker.js +1 -7
  78. 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
- await this.updateTheFeed(
63
- this.fixUrl(feedConfig.url),
64
- this.config.masterUrl,
65
- feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '',
66
- packageRestrictions
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
- const response = await axios.get(url, {
104
- timeout: 30000,
105
- headers: {
106
- 'User-Agent': 'FHIR Package Crawler/1.0'
107
- }
108
- });
109
- return response.data;
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
- const response = await axios.get(url, {
121
- timeout: 30000,
122
- headers: {
123
- 'User-Agent': 'FHIR Package Crawler/1.0'
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
- const parser = new XMLParser({
128
- ignoreAttributes: false,
129
- attributeNamePrefix: '@_',
130
- textNodeName: '#text'
131
- });
166
+ const parser = new XMLParser({
167
+ ignoreAttributes: false,
168
+ attributeNamePrefix: '@_',
169
+ textNodeName: '#text'
170
+ });
132
171
 
133
- return parser.parse(response.data);
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
- const response = await axios.get(url, {
145
- timeout: 60000,
146
- responseType: 'arraybuffer',
147
- headers: {
148
- 'User-Agent': 'FHIR Package Crawler/1.0'
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
- this.totalBytes += response.data.byteLength;
153
- return Buffer.from(response.data);
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}:`+ itemError.message);
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}:`+ error.message);
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}:`+ error.message);
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
- const filename = `${id}-${version}.tgz`;
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;
@@ -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: 'Invalid package id or version format'});
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: 'Package id or version too long'});
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/${escape(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
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/${escape(pkg.name)}">all</a>)</td>`;
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;
@@ -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();