@medicus.ai/medicus-report-pdf-generator 1.3.7 → 1.3.9

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/index.js CHANGED
@@ -7,10 +7,18 @@ const {
7
7
  } = require("./lib/wellbeing_report_generator");
8
8
  const { generateSanusXReport, generateHTMLSanusXReport } = require("./lib/sanusx_report_generator");
9
9
  const fs = require('fs');
10
+ const os = require('os');
11
+ const crypto = require('crypto');
10
12
  const { sendNascoEmail, sendEmailNotification } = require('./lib/sendEmail')
11
13
  const path = require('path');
12
14
  const { generatePDFCorporateReport, generateHTMLCorporateReport } = require('./lib/corporate_report_generator')
13
15
 
16
+ // Best-effort fire-and-forget cleanup for per-call temp files. Never throws.
17
+ const safeUnlink = (filePath) => {
18
+ if (!filePath) return Promise.resolve();
19
+ return fs.promises.unlink(filePath).catch(() => { });
20
+ };
21
+
14
22
  module.exports = {
15
23
 
16
24
  generateHTMLStaging,
@@ -187,10 +195,19 @@ module.exports = {
187
195
  let reportData = JSON.parse(data);
188
196
  let base64Object = reportData.data;
189
197
 
198
+ // Per-call unique identifiers so concurrent callers never share a path.
199
+ // Even when two requests overlap, each one owns its own input/output
200
+ // files and email attachment, eliminating cross-user PDF swapping.
201
+ const callId = crypto.randomUUID();
202
+ const tmpDir = os.tmpdir();
203
+ const tmpInputPath = path.join(tmpDir, `wb-${callId}-in.pdf`);
204
+ const tmpEncryptedPath = path.join(tmpDir, `wb-${callId}-enc.pdf`);
205
+ let tmpEmailAttachmentPath = null;
206
+
190
207
  let LOGS = '';
191
- const LOGS_FILE_PATH = path.join(__dirname, 'output', 'LOGS.txt');
192
- const PDF_FILE_PATH = path.join(__dirname, 'output', 'nasco-sample.pdf');
193
- const ENCRYPTED_PDF_PATH = path.join(__dirname, 'output', 'nasco-sample-encrypted.pdf');
208
+ const LOGS_FILE_PATH = isDebugging
209
+ ? path.join(__dirname, 'output', `LOGS-${callId}.txt`)
210
+ : null;
194
211
 
195
212
  const mailConfig = {
196
213
  host: reportData.host,
@@ -201,12 +218,6 @@ module.exports = {
201
218
  secure: reportData.secure,
202
219
  };
203
220
 
204
- if (isDebugging) {
205
- if (fs.existsSync(LOGS_FILE_PATH)) fs.unlinkSync(LOGS_FILE_PATH);
206
- if (fs.existsSync(PDF_FILE_PATH)) fs.unlinkSync(PDF_FILE_PATH);
207
- if (fs.existsSync(ENCRYPTED_PDF_PATH)) fs.unlinkSync(ENCRYPTED_PDF_PATH);
208
- }
209
-
210
221
  if (isDebugging) {
211
222
  console.log('2: save json string');
212
223
  LOGS += '2: save json string\n===============\n' + base64Object + '\n\n\n';
@@ -242,45 +253,64 @@ module.exports = {
242
253
  LOGS += '3: save PDF buffer\n===============\n' + '[binary content omitted]' + '\n\n\n';
243
254
  }
244
255
 
245
- // Write original PDF (unencrypted)
246
- fs.writeFileSync(PDF_FILE_PATH, fileBuffer);
247
-
256
+ try {
257
+ // Write the unencrypted PDF to a per-call temp file.
258
+ // qpdf needs a real input path; using a uuid-suffixed file in
259
+ // os.tmpdir() guarantees no concurrent caller can overwrite it.
260
+ await fs.promises.writeFile(tmpInputPath, fileBuffer);
248
261
 
249
- return await (async () => {
250
262
  const qpdf = await import('node-qpdf2');
251
263
 
264
+ // Encrypt to a per-call output file. We can't use qpdf's stdout
265
+ // mode (output omitted) because node-qpdf2's spawn helper joins
266
+ // stdout chunks as strings, which corrupts binary PDF bytes.
252
267
  await qpdf.encrypt({
253
- input: PDF_FILE_PATH,
254
- output: ENCRYPTED_PDF_PATH,
268
+ input: tmpInputPath,
269
+ output: tmpEncryptedPath,
255
270
  keyLength: 256,
256
271
  password: reportData.pdfPassword,
257
- })
258
- const encryptedBuffer = fs.readFileSync(ENCRYPTED_PDF_PATH);
272
+ });
273
+
274
+ const encryptedBuffer = await fs.promises.readFile(tmpEncryptedPath);
259
275
 
260
- // Save encoded base64 (of encrypted)
261
- const base64data = Buffer.from(encryptedBuffer).toString('base64');
276
+ const base64data = encryptedBuffer.toString('base64');
262
277
  if (isDebugging) {
263
278
  console.log('4: save encoded base64');
264
279
  LOGS += '4: save encoded base64\n===============\n' + base64data + '\n\n\n';
265
280
  }
266
281
 
267
- // Write logs
268
- fs.writeFileSync(LOGS_FILE_PATH, LOGS, 'utf8');
269
- if (isDebugging) console.log('5: save logs to file');
282
+ if (isDebugging && LOGS_FILE_PATH) {
283
+ await fs.promises.writeFile(LOGS_FILE_PATH, LOGS, 'utf8');
284
+ console.log('5: save logs to file');
285
+ }
270
286
 
271
287
  if (isDownloadable) {
272
288
  return encryptedBuffer;
273
- } else {
274
- if (shouldSendEmail) {
275
- return await sendNascoEmail(decodedJSON.wellbeing, ENCRYPTED_PDF_PATH, mailConfig, reportData.client);
276
- } else {
277
- return base64data;
278
- }
279
289
  }
280
290
 
281
- })();
282
-
291
+ if (shouldSendEmail) {
292
+ // Write the encrypted buffer to a dedicated per-call file just
293
+ // for the email attachment. sendNascoEmail expects a path
294
+ // (nodemailer reads it from disk), so we materialize it once,
295
+ // pass it in, then unlink in the finally block.
296
+ tmpEmailAttachmentPath = path.join(tmpDir, `wb-${callId}-mail.pdf`);
297
+ await fs.promises.writeFile(tmpEmailAttachmentPath, encryptedBuffer);
298
+ return await sendNascoEmail(
299
+ decodedJSON.wellbeing,
300
+ tmpEmailAttachmentPath,
301
+ mailConfig,
302
+ reportData.client
303
+ );
304
+ }
283
305
 
306
+ return base64data;
307
+ } finally {
308
+ await Promise.all([
309
+ safeUnlink(tmpInputPath),
310
+ safeUnlink(tmpEncryptedPath),
311
+ safeUnlink(tmpEmailAttachmentPath),
312
+ ]);
313
+ }
284
314
  },
285
315
 
286
316
  generateSanuxPDF: async (data, isDebugging, isDownloadable, shouldSendEmail) => {
package/lib/template.js CHANGED
@@ -200,7 +200,8 @@ function renderOneBiomarker(biomarker, isRtl, nestingLevel, isChildBiomarker, is
200
200
  if (biomarker.showInDetailsView === true) {
201
201
  const biomarkerCssId = "dtl-bio-" + "db" + biomarker.id;
202
202
  let customBiomarkerValueFontSize = '';
203
- const formattedValue = biomarker.formattedValue.replace(/,/g, "");
203
+ const rawFormattedValue = biomarker.formattedValue || biomarker.lingualReading || '';
204
+ const formattedValue = rawFormattedValue.replace(/,/g, "");
204
205
  if (formattedValue.length > 35) {
205
206
  customBiomarkerValueFontSize = 'font-size:12px;';
206
207
  }
@@ -231,13 +232,13 @@ function renderOneBiomarker(biomarker, isRtl, nestingLevel, isChildBiomarker, is
231
232
 
232
233
  // IMC special case (large biomarker value)
233
234
  let biomarkerClass = '';
234
- if (biomarker.formattedValue.length >= 125) {
235
+ if (rawFormattedValue.length >= 125) {
235
236
  biomarkerClass = 'large-value-class';
236
237
  firstGridColClass = '';
237
238
  // firstGridColstyle = 'width:100%';
238
239
  }
239
240
 
240
- const biomarkerValue = biomarker.formattedValue.replace(/\r\n/g, "<br/>");
241
+ const biomarkerValue = rawFormattedValue.replace(/\r\n/g, "<br/>");
241
242
 
242
243
  if (!biomarker.showScale) {
243
244
  biomarkerClass += ' hide-scale ';
@@ -1060,7 +1061,7 @@ function renderBiomarkerV3(biomarker, nestingLevel, isChildBiomarker, isCompactV
1060
1061
  if (isCompactView && biomarker.showInCompactView) {
1061
1062
  let biomarkerName = biomarker.fullName || '';
1062
1063
  let biomarkerUnit = biomarker.unit;
1063
- let biomarkerValue = biomarker.formattedValue || '';
1064
+ let biomarkerValue = biomarker.formattedValue || biomarker.lingualReading || '';
1064
1065
  let rangePlaceholder = '';
1065
1066
  let referenceRange = '';
1066
1067
  let biomarkerLast = '-';