@jsreport/jsreport-core 3.9.0 → 3.11.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/README.md CHANGED
@@ -282,6 +282,29 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.11.0
286
+
287
+ - log when worker returns bad res.content
288
+ - fix profiler leaks
289
+ - remove settings sync API and avoid loading all items to memory
290
+ - throw weak error when validating duplicated entity
291
+ - ensure we end with profiles with error state when there is server or req timeout
292
+
293
+ ### 3.10.0
294
+
295
+ - `mainReporter.executeWorkerAction` now supports cancellation with `AbortController.signal`
296
+ - add support for specifying what are the main document properties of templates entitySet
297
+
298
+ ### 3.9.0
299
+
300
+ - add more store methods `collection.findAdmin`, `collection.findOneAdmin`, `reporter.adminRequest` to easily allow execure store queries without taking into account permissions
301
+ - improve logging for child requests and user level logs
302
+ - differentiate between template not found errors and permissions related errors (it is now more clean what is the cause of specific error)
303
+ - normalize to error when non-errors are throw (like throw "string")
304
+ - improve errors in helpers (it now includes the helper name)
305
+ - improve error message when template was not found in child request
306
+ - improve error handling in sandbox
307
+
285
308
  ### 3.8.1
286
309
 
287
310
  - update vm2 for fix security issue
@@ -74,6 +74,7 @@ async function validateDuplicatedName (reporter, c, doc, originalIdValue, req) {
74
74
 
75
75
  throw reporter.createError(msg, {
76
76
  statusCode: 400,
77
+ weak: true,
77
78
  code: 'DUPLICATED_ENTITY',
78
79
  existingEntity: existingEntity.entity,
79
80
  existingEntityEntitySet: existingEntity.entitySet
@@ -1,9 +1,9 @@
1
1
  const EventEmitter = require('events')
2
- const winston = require('winston')
2
+ const Transport = require('winston-transport')
3
3
  const extend = require('node.extend.without.arrays')
4
4
  const generateRequestId = require('../shared/generateRequestId')
5
5
  const fs = require('fs/promises')
6
-
6
+ const { SPLAT } = require('triple-beam')
7
7
  module.exports = (reporter) => {
8
8
  reporter.documentStore.registerEntityType('ProfileType', {
9
9
  templateShortid: { type: 'Edm.String', referenceTo: 'templates' },
@@ -12,7 +12,8 @@ module.exports = (reporter) => {
12
12
  state: { type: 'Edm.String' },
13
13
  error: { type: 'Edm.String' },
14
14
  mode: { type: 'Edm.String', schema: { enum: ['full', 'standard', 'disabled'] } },
15
- blobName: { type: 'Edm.String' }
15
+ blobName: { type: 'Edm.String' },
16
+ timeout: { type: 'Edm.Int32' }
16
17
  })
17
18
 
18
19
  reporter.documentStore.registerEntitySet('profiles', {
@@ -23,7 +24,6 @@ module.exports = (reporter) => {
23
24
  const profilersMap = new Map()
24
25
  const profilerOperationsChainsMap = new Map()
25
26
  const profilerRequestMap = new Map()
26
- const profilerLogRequestMap = new Map()
27
27
 
28
28
  function runInProfilerChain (fnOrOptions, req) {
29
29
  if (req.context.profiling.mode === 'disabled') {
@@ -89,7 +89,7 @@ module.exports = (reporter) => {
89
89
  for (const m of events) {
90
90
  if (m.type === 'log') {
91
91
  if (log) {
92
- reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp, fromEmitProfile: true })
92
+ reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp, logged: true })
93
93
  }
94
94
  } else {
95
95
  lastOperation = m
@@ -167,7 +167,8 @@ module.exports = (reporter) => {
167
167
  _id: reporter.documentStore.generateId(),
168
168
  timestamp: new Date(),
169
169
  state: 'queued',
170
- mode: req.context.profiling.mode
170
+ mode: req.context.profiling.mode,
171
+ timeout: reporter.options.enableRequestReportTimeout && req.options.timeout ? req.options.timeout : reporter.options.reportTimeout
171
172
  }
172
173
 
173
174
  const { pathToFile } = await reporter.writeTempFile((uuid) => `${uuid}.log`, '')
@@ -325,49 +326,40 @@ module.exports = (reporter) => {
325
326
  }
326
327
  })
327
328
 
329
+ // we want to add to profiles also log messages from the main
328
330
  const configuredPreviously = reporter.logger.__profilerConfigured__ === true
329
-
330
331
  if (!configuredPreviously) {
331
- const originalLog = reporter.logger.log
332
-
333
- // we want to catch the original request
334
- reporter.logger.log = function (level, msg, ...splat) {
335
- const [meta] = splat
332
+ // we emit from winston transport, so winston formatters can still format message
333
+ class EmittingProfilesTransport extends Transport {
334
+ log (info, callback) {
335
+ setImmediate(() => {
336
+ this.emit('logged', info)
337
+ })
338
+
339
+ if (info[SPLAT]) {
340
+ const [req] = info[SPLAT]
341
+
342
+ if (req && req.context && req.logged !== true) {
343
+ emitProfiles({
344
+ events: [createProfileMessage({
345
+ type: 'log',
346
+ level: info.level,
347
+ message: info.message,
348
+ previousOperationId: req.context.profiling.lastOperationId
349
+ }, req)],
350
+ log: false
351
+ }, req)
352
+ }
353
+ }
336
354
 
337
- if (typeof meta === 'object' && meta !== null && meta.context?.rootId != null) {
338
- profilerLogRequestMap.set(meta.context.rootId, meta)
355
+ callback()
339
356
  }
340
-
341
- return originalLog.call(this, level, msg, ...splat)
342
357
  }
343
358
 
344
- const mainLogsToProfile = winston.format((info) => {
345
- // propagate the request logs occurring on main to the profile
346
- if (info.rootId != null && info.fromEmitProfile == null && profilerLogRequestMap.has(info.rootId)) {
347
- const req = profilerLogRequestMap.get(info.rootId)
348
-
349
- emitProfiles({
350
- events: [createProfileMessage({
351
- type: 'log',
352
- level: info.level,
353
- message: info.message,
354
- previousOperationId: req.context.profiling.lastOperationId
355
- }, req)],
356
- log: false
357
- }, req)
358
- }
359
-
360
- if (info.fromEmitProfile != null) {
361
- delete info.fromEmitProfile
362
- }
363
-
364
- return info
365
- })
366
-
367
- reporter.logger.format = winston.format.combine(
368
- reporter.logger.format,
369
- mainLogsToProfile()
370
- )
359
+ reporter.logger.add(new EmittingProfilesTransport({
360
+ format: reporter.logger.format,
361
+ level: 'debug'
362
+ }))
371
363
 
372
364
  reporter.logger.__profilerConfigured__ = true
373
365
  }
@@ -410,7 +402,6 @@ module.exports = (reporter) => {
410
402
  profilersMap.clear()
411
403
  profilerOperationsChainsMap.clear()
412
404
  profilerRequestMap.clear()
413
- profilerLogRequestMap.clear()
414
405
  })
415
406
 
416
407
  let profilesCleanupRunning = false
@@ -422,11 +413,13 @@ module.exports = (reporter) => {
422
413
 
423
414
  profilesCleanupRunning = true
424
415
 
425
- let lastRemoveError
416
+ let lastError
426
417
 
427
418
  try {
428
- const profiles = await reporter.documentStore.collection('profiles').find({}).sort({ timestamp: -1 })
429
- const profilesToRemove = profiles.slice(reporter.options.profiler.maxProfilesHistory)
419
+ const profilesToRemove = await reporter.documentStore.collection('profiles')
420
+ .find({}, { _id: 1 }).sort({ timestamp: -1 })
421
+ .skip(reporter.options.profiler.maxProfilesHistory)
422
+ .toArray()
430
423
 
431
424
  for (const profile of profilesToRemove) {
432
425
  if (reporter.closed || reporter.closing) {
@@ -438,7 +431,40 @@ module.exports = (reporter) => {
438
431
  _id: profile._id
439
432
  })
440
433
  } catch (e) {
441
- lastRemoveError = e
434
+ lastError = e
435
+ }
436
+ }
437
+
438
+ const notFinishedProfiles = await reporter.documentStore.collection('profiles')
439
+ .find({ $or: [{ state: 'running' }, { state: 'queued' }] }, { _id: 1, timeout: 1, timestamp: 1 })
440
+ .toArray()
441
+
442
+ for (const profile of notFinishedProfiles) {
443
+ if (reporter.closed || reporter.closing) {
444
+ return
445
+ }
446
+
447
+ if (!profile.timeout) {
448
+ continue
449
+ }
450
+
451
+ const whenShouldBeFinished = profile.timestamp + profile.timeout + reporter.options.reportTimeoutMargin * 2
452
+ if (whenShouldBeFinished < new Date().getTime()) {
453
+ continue
454
+ }
455
+
456
+ try {
457
+ await reporter.documentStore.collection('profiles').update({
458
+ _id: profile._id
459
+ }, {
460
+ $set: {
461
+ state: 'error',
462
+ finishedOn: new Date(),
463
+ error: 'The server did not update the report profile before its timeout. This can happen when the server is unexpectedly stopped.'
464
+ }
465
+ })
466
+ } catch (e) {
467
+ lastError = e
442
468
  }
443
469
  }
444
470
  } catch (e) {
@@ -447,8 +473,8 @@ module.exports = (reporter) => {
447
473
  profilesCleanupRunning = false
448
474
  }
449
475
 
450
- if (lastRemoveError) {
451
- reporter.logger.warn('Profile cleanup failed for some entities, last error:', lastRemoveError)
476
+ if (lastError) {
477
+ reporter.logger.warn('Profile cleanup failed for some entities, last error:', lastError)
452
478
  }
453
479
  }
454
480
 
@@ -461,7 +487,6 @@ module.exports = (reporter) => {
461
487
  profilersMap.delete(req.context.rootId)
462
488
  profilerOperationsChainsMap.delete(req.context.rootId)
463
489
  profilerRequestMap.delete(req.context.rootId)
464
- profilerLogRequestMap.delete(req.context.rootId)
465
490
  return
466
491
  }
467
492
 
@@ -471,7 +496,6 @@ module.exports = (reporter) => {
471
496
  profilersMap.delete(req.context.rootId)
472
497
  profilerOperationsChainsMap.delete(req.context.rootId)
473
498
  profilerRequestMap.delete(req.context.rootId)
474
- profilerLogRequestMap.delete(req.context.rootId)
475
499
  }
476
500
  }, req)
477
501
  }
@@ -449,6 +449,7 @@ class MainReporter extends Reporter {
449
449
  } catch (err) {
450
450
  await this._handleRenderError(req, res, err).catch((e) => {})
451
451
  } finally {
452
+ this._cleanProfileInRequest(req)
452
453
  if (!workerAborted) {
453
454
  await worker.release(req)
454
455
  }
@@ -477,6 +478,10 @@ class MainReporter extends Reporter {
477
478
 
478
479
  await this.afterRenderListeners.fire(req, res)
479
480
 
481
+ if (!res.content) {
482
+ this.logger.error('Worker didnt return render res.content, returned:' + JSON.stringify(responseResult), req)
483
+ }
484
+
480
485
  res.stream = Readable.from(res.content)
481
486
 
482
487
  this._cleanProfileInRequest(req)
@@ -557,6 +562,16 @@ class MainReporter extends Reporter {
557
562
  timeout
558
563
  })
559
564
 
565
+ const handleAbortSignal = () => {
566
+ if (worker) {
567
+ worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
568
+ }
569
+ }
570
+
571
+ if (options.signal && !options.worker) {
572
+ options.signal.addEventListener('abort', handleAbortSignal, { once: true })
573
+ }
574
+
560
575
  try {
561
576
  const result = await worker.execute({
562
577
  actionName,
@@ -579,7 +594,16 @@ class MainReporter extends Reporter {
579
594
  return result
580
595
  } finally {
581
596
  if (!options.worker) {
582
- await worker.release(req)
597
+ let shouldRelease = true
598
+
599
+ if (options.signal) {
600
+ options.signal.removeEventListener('abort', handleAbortSignal)
601
+ shouldRelease = options.signal.aborted !== true
602
+ }
603
+
604
+ if (shouldRelease) {
605
+ await worker.release(req)
606
+ }
583
607
  }
584
608
  }
585
609
  }
@@ -6,7 +6,7 @@
6
6
  const Request = require('../shared/request')
7
7
 
8
8
  const Settings = module.exports = function () {
9
- this._collection = []
9
+
10
10
  }
11
11
 
12
12
  Settings.prototype.add = function (key, value, req) {
@@ -15,15 +15,9 @@ Settings.prototype.add = function (key, value, req) {
15
15
  value: typeof value !== 'string' ? JSON.stringify(value) : value
16
16
  }
17
17
 
18
- this._collection.push(settingItem)
19
-
20
18
  return this.documentStore.collection('settings').insert(settingItem, localReqWithoutAuthorization(req))
21
19
  }
22
20
 
23
- Settings.prototype.get = function (key) {
24
- return this._collection.find((s) => s.key === key)
25
- }
26
-
27
21
  Settings.prototype.findValue = async function (key, req) {
28
22
  const res = await this.documentStore.collection('settings').find({ key: key }, localReqWithoutAuthorization(req))
29
23
  if (res.length !== 1) {
@@ -36,8 +30,6 @@ Settings.prototype.findValue = async function (key, req) {
36
30
  Settings.prototype.set = function (key, avalue, req) {
37
31
  const value = typeof avalue !== 'string' ? JSON.stringify(avalue) : avalue
38
32
 
39
- this.get(key).value = value
40
-
41
33
  return this.documentStore.collection('settings').update({
42
34
  key: key
43
35
  }, {
@@ -58,8 +50,6 @@ Settings.prototype.addOrSet = async function (key, avalue, req) {
58
50
  Settings.prototype.init = async function (documentStore, authorization) {
59
51
  this.documentStore = documentStore
60
52
 
61
- const incompatibleSettingsToRemove = []
62
-
63
53
  if (authorization != null) {
64
54
  const col = documentStore.collection('settings')
65
55
 
@@ -95,30 +85,6 @@ Settings.prototype.init = async function (documentStore, authorization) {
95
85
  }
96
86
  })
97
87
  }
98
-
99
- const res = await documentStore.collection('settings').find({})
100
-
101
- res.forEach((v) => {
102
- if (typeof v.value !== 'string') {
103
- return this._collection.push({
104
- key: v.key,
105
- value: v.value
106
- })
107
- }
108
-
109
- try {
110
- return this._collection.push({
111
- key: v.key,
112
- value: JSON.parse(v.value)
113
- })
114
- } catch (e) {
115
- incompatibleSettingsToRemove.push(v._id)
116
- }
117
- })
118
-
119
- if (incompatibleSettingsToRemove.length) {
120
- return documentStore.collection('settings').remove({ _id: { $in: incompatibleSettingsToRemove } })
121
- }
122
88
  }
123
89
 
124
90
  Settings.prototype.registerEntity = function (documentStore) {
@@ -2,9 +2,9 @@
2
2
  module.exports = (reporter) => {
3
3
  reporter.documentStore.registerEntityType('TemplateType', {
4
4
  name: { type: 'Edm.String' },
5
- content: { type: 'Edm.String', document: { extension: 'html', engine: true } },
5
+ content: { type: 'Edm.String', document: { main: true, extension: 'html', engine: true } },
6
6
  recipe: { type: 'Edm.String' },
7
- helpers: { type: 'Edm.String', document: { extension: 'js' }, schema: { type: 'object' } },
7
+ helpers: { type: 'Edm.String', document: { main: true, extension: 'js' }, schema: { type: 'object' } },
8
8
  engine: { type: 'Edm.String' }
9
9
  }, true)
10
10
 
@@ -13,7 +13,7 @@ module.exports = (level, msg, meta) => {
13
13
 
14
14
  // TODO adding cancel looks bad, its before script is adding req.cancel()
15
15
  // excluding non relevant properties for the log
16
- const newMeta = Object.assign({}, omit(meta, ['rawContent', 'template', 'options', 'data', 'context', 'timestamp', 'cancel']))
16
+ const newMeta = Object.assign({}, omit(meta, ['logged', 'rawContent', 'template', 'options', 'data', 'context', 'timestamp', 'cancel']))
17
17
 
18
18
  if (newMeta.rootId == null && meta.context.rootId != null) {
19
19
  newMeta.rootId = meta.context.rootId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -40,7 +40,7 @@
40
40
  "@babel/parser": "7.14.4",
41
41
  "@babel/traverse": "7.12.9",
42
42
  "@colors/colors": "1.5.0",
43
- "@jsreport/advanced-workers": "1.2.3",
43
+ "@jsreport/advanced-workers": "1.2.4",
44
44
  "@jsreport/mingo": "2.4.1",
45
45
  "@jsreport/reap": "0.1.0",
46
46
  "ajv": "6.12.6",
@@ -54,7 +54,7 @@
54
54
  "diff-match-patch": "1.0.5",
55
55
  "enhanced-resolve": "5.8.3",
56
56
  "has-own-deep": "1.1.0",
57
- "isbinaryfile": "4.0.0",
57
+ "isbinaryfile": "5.0.0",
58
58
  "listener-collection": "2.0.0",
59
59
  "lodash.get": "4.4.2",
60
60
  "lodash.groupby": "4.6.0",