@jsreport/jsreport-core 3.2.0 → 3.3.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.
@@ -37,14 +37,25 @@ function createLogger () {
37
37
  }
38
38
 
39
39
  function configureLogger (logger, _transports) {
40
+ const transports = _transports || {}
41
+ const transportFormatMap = new WeakMap()
42
+
43
+ // we ensure we do .format cleanup on options first before checking if the logger
44
+ // is configured or not, this ensure that options are properly cleaned up when
45
+ // configureLogger is called more than once (like when execution cli commands from extensions)
46
+ for (const [, transpOptions] of Object.entries(transports)) {
47
+ if (transpOptions.format != null) {
48
+ transportFormatMap.set(transpOptions, transpOptions.format)
49
+ delete transpOptions.format
50
+ }
51
+ }
52
+
40
53
  const configuredPreviously = logger.__configured__ === true
41
54
 
42
55
  if (configuredPreviously) {
43
56
  return
44
57
  }
45
58
 
46
- const transports = _transports || {}
47
-
48
59
  const knownTransports = {
49
60
  debug: DebugTransport,
50
61
  console: winston.transports.Console,
@@ -78,22 +89,21 @@ function configureLogger (logger, _transports) {
78
89
  continue
79
90
  }
80
91
 
92
+ let originalFormat
93
+
94
+ if (transportFormatMap.has(transpOptions)) {
95
+ originalFormat = transportFormatMap.get(transpOptions)
96
+ }
97
+
81
98
  if (
82
- transpOptions.format != null &&
83
- typeof transpOptions.format.constructor !== 'function'
99
+ originalFormat != null &&
100
+ typeof originalFormat.constructor !== 'function'
84
101
  ) {
85
102
  throw new Error(`Invalid option for transport object "${
86
103
  transpName
87
104
  }", option "format" has an incorrect value, must be an instance of loggerFormat. check your "logger" config`)
88
105
  }
89
106
 
90
- let originalFormat
91
-
92
- if (transpOptions.format != null) {
93
- originalFormat = transpOptions.format
94
- delete transpOptions.format
95
- }
96
-
97
107
  const options = Object.assign(omit(transpOptions, knownOptions), {
98
108
  name: transpName
99
109
  })
@@ -84,7 +84,8 @@ module.exports = (reporter) => {
84
84
 
85
85
  reporter.documentStore.registerEntitySet('monitoring', {
86
86
  entityType: 'jsreport.MonitoringType',
87
- exportable: false
87
+ exportable: false,
88
+ shared: true
88
89
  })
89
90
 
90
91
  reporter.monitoring = new Monitoring(reporter)
@@ -26,9 +26,13 @@ module.exports = (reporter) => {
26
26
  return
27
27
  }
28
28
 
29
+ let lastOperation
30
+
29
31
  for (const m of events) {
30
32
  if (m.type === 'log') {
31
33
  reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp })
34
+ } else {
35
+ lastOperation = m
32
36
  }
33
37
 
34
38
  if (profilersMap.has(req.context.rootId)) {
@@ -36,8 +40,15 @@ module.exports = (reporter) => {
36
40
  }
37
41
  }
38
42
 
43
+ if (lastOperation != null) {
44
+ req.context.profiling.lastOperation = lastOperation
45
+ }
46
+
39
47
  profilerAppendChain.set(req.context.rootId, profilerAppendChain.get(req.context.rootId).then(() => {
40
- return reporter.blobStorage.append(req.context.profiling.entity.blobName, Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'), req).catch(e => {
48
+ return reporter.blobStorage.append(
49
+ req.context.profiling.entity.blobName,
50
+ Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'), req
51
+ ).catch(e => {
41
52
  reporter.logger.error('Failed to append to profile blob', e)
42
53
  })
43
54
  }))
@@ -64,6 +75,7 @@ module.exports = (reporter) => {
64
75
  profilerAppendChain.set(req.context.rootId, Promise.resolve())
65
76
 
66
77
  req.context.profiling = req.context.profiling || {}
78
+ req.context.profiling.lastOperation = null
67
79
 
68
80
  let blobName = `profiles/${req.context.rootId}.log`
69
81
 
@@ -320,26 +320,29 @@ class MainReporter extends Reporter {
320
320
  throw new Error('Not initialized, you need to call jsreport.init().then before rendering')
321
321
  }
322
322
 
323
+ let options = {}
324
+ if (parentReq && !parentReq.__isJsreportRequest__) {
325
+ options = parentReq
326
+ parentReq = null
327
+ }
328
+
323
329
  req = Object.assign({}, req)
324
330
  req.context = Object.assign({}, req.context)
325
331
  req.context.rootId = req.context.rootId || generateRequestId()
326
332
  req.context.id = req.context.rootId
327
333
 
328
- const worker = await this._workersManager.allocate(req, {
334
+ const worker = options.worker || await this._workersManager.allocate(req, {
329
335
  timeout: this.options.reportTimeout
330
336
  })
331
337
 
338
+ let keepWorker
332
339
  let workerAborted
333
- if (parentReq && !parentReq.__isJsreportRequest__) {
334
- const options = parentReq
335
- parentReq = null
336
340
 
337
- if (options.abortEmitter) {
338
- options.abortEmitter.once('abort', () => {
339
- workerAborted = true
340
- worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
341
- })
342
- }
341
+ if (options.abortEmitter) {
342
+ options.abortEmitter.once('abort', () => {
343
+ workerAborted = true
344
+ worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
345
+ })
343
346
  }
344
347
 
345
348
  const res = { meta: {} }
@@ -388,12 +391,19 @@ class MainReporter extends Reporter {
388
391
  req.options &&
389
392
  req.options.timeout != null
390
393
  ) {
391
- reportTimeout = req
394
+ reportTimeout = req.options.timeout
392
395
  }
393
396
 
394
- await this.beforeRenderListeners.fire(req, res)
397
+ await this.beforeRenderListeners.fire(req, res, { worker })
395
398
 
396
- if (req.context.isFinished) {
399
+ // this is used so far just in the reports extension
400
+ // it wants to send to the client immediate response with link to the report status
401
+ // but the previous steps already allocated worker which has the parsed input request
402
+ // so we need to keep the worker active and let the subsequent real render call use it
403
+ // we cant move the main beforeRenderListener before the worker allocation, because at that point
404
+ // the request isn't parsed and we don't know the template and options
405
+ if (req.context.returnResponseAndKeepWorker) {
406
+ keepWorker = true
397
407
  res.stream = Readable.from(res.content)
398
408
  return res
399
409
  }
@@ -414,6 +424,15 @@ class MainReporter extends Reporter {
414
424
  } catch (err) {
415
425
  if (err.code === 'WORKER_TIMEOUT') {
416
426
  err.message = 'Report timeout'
427
+ if (req.context.profiling?.lastOperation != null && req.context.profiling?.entity != null) {
428
+ err.message += `. Last profiler operation: (${req.context.profiling.lastOperation.subtype}) ${req.context.profiling.lastOperation.name}`
429
+ }
430
+
431
+ if (req.context.http != null) {
432
+ const profileUrl = `${req.context.http.baseUrl}/studio/profiles/${req.context.profiling.entity._id}`
433
+ err.message += `. You can inspect and find more details here: ${profileUrl}`
434
+ }
435
+
417
436
  err.weak = true
418
437
  }
419
438
 
@@ -429,7 +448,7 @@ class MainReporter extends Reporter {
429
448
  await this.renderErrorListeners.fire(req, res, err)
430
449
  throw err
431
450
  } finally {
432
- if (!workerAborted) {
451
+ if (!workerAborted && !keepWorker) {
433
452
  await worker.release(req)
434
453
  }
435
454
  }
@@ -85,7 +85,6 @@ module.exports = (reporter) => {
85
85
 
86
86
  if (entity._id) {
87
87
  entityPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
88
- entityPath = entityPath.substring(0, entityPath.lastIndexOf('/'))
89
88
  }
90
89
 
91
90
  const registerResults = await reporter.registerHelpersListeners.fire(req)
@@ -106,12 +105,6 @@ module.exports = (reporter) => {
106
105
  const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${joinedHelpers}`
107
106
 
108
107
  const executionFn = async ({ require, console, topLevelFunctions }) => {
109
- // cached components dont call runInSandbox but share the proxy, we get to it here
110
- if (entitySet !== 'templates') {
111
- const jsreport = require('jsreport-proxy')
112
- jsreport.currentPath = entityPath
113
- }
114
-
115
108
  const asyncResultMap = new Map()
116
109
  executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions })
117
110
  const key = `template:${content}:${engine.name}`
@@ -18,16 +18,7 @@ class Profiler {
18
18
  })
19
19
 
20
20
  this.profiledRequestsMap = new Map()
21
- const profileEventsFlushInterval = setInterval(async () => {
22
- for (const id of [...this.profiledRequestsMap.keys()]) {
23
- const profilingInfo = this.profiledRequestsMap.get(id)
24
- if (profilingInfo) {
25
- const batch = profilingInfo.batch
26
- profilingInfo.batch = []
27
- await this.reporter.executeMainAction('profile', batch, profilingInfo.req).catch((e) => this.reporter.logger.error(e, profilingInfo.req))
28
- }
29
- }
30
- }, 100)
21
+ const profileEventsFlushInterval = setInterval(() => this.flush(), 100)
31
22
  profileEventsFlushInterval.unref()
32
23
 
33
24
  this.reporter.closeListeners.add('profiler', this, () => {
@@ -37,6 +28,19 @@ class Profiler {
37
28
  })
38
29
  }
39
30
 
31
+ async flush (id) {
32
+ const toProcess = id == null ? [...this.profiledRequestsMap.keys()] : [id]
33
+
34
+ for (const id of toProcess) {
35
+ const profilingInfo = this.profiledRequestsMap.get(id)
36
+ if (profilingInfo) {
37
+ const batch = profilingInfo.batch
38
+ profilingInfo.batch = []
39
+ await this.reporter.executeMainAction('profile', batch, profilingInfo.req).catch((e) => this.reporter.logger.error(e, profilingInfo.req))
40
+ }
41
+ }
42
+ }
43
+
40
44
  emit (m, req, res) {
41
45
  m.timestamp = m.timestamp || new Date().getTime()
42
46
 
@@ -143,6 +143,11 @@ class WorkerReporter extends Reporter {
143
143
  currentPath,
144
144
  errorLineNumberOffset
145
145
  }, req) {
146
+ // we flush before running code in sandbox because it can potentially
147
+ // include code that blocks the whole process (like `while (true) {}`) and we
148
+ // want to ensure that the batched messages are flushed before trying to execute the code
149
+ await this.profiler.flush(req.context.rootId)
150
+
146
151
  return this._runInSandbox({
147
152
  manager,
148
153
  context,
@@ -1,6 +1,7 @@
1
1
  const LRU = require('lru-cache')
2
- const safeSandbox = require('./safeSandbox')
2
+ const stackTrace = require('stack-trace')
3
3
  const { customAlphabet } = require('nanoid')
4
+ const safeSandbox = require('./safeSandbox')
4
5
  const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
6
 
6
7
  module.exports = (reporter) => {
@@ -27,7 +28,7 @@ module.exports = (reporter) => {
27
28
  context.__topLevelFunctions = {}
28
29
  context.__handleError = (err) => handleError(reporter, err)
29
30
 
30
- const { run, restore, contextifyValue, decontextifyValue, unproxyValue, sandbox, safeRequire } = safeSandbox(context, {
31
+ const { sourceFilesInfo, run, restore, contextifyValue, decontextifyValue, unproxyValue, sandbox, safeRequire } = safeSandbox(context, {
31
32
  onLog: (log) => {
32
33
  reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp })
33
34
  },
@@ -61,7 +62,51 @@ module.exports = (reporter) => {
61
62
  })
62
63
 
63
64
  jsreportProxy = reporter.createProxy({ req, runInSandbox: run, context: sandbox, getTopLevelFunctions, safeRequire })
64
- jsreportProxy.currentPath = currentPath
65
+
66
+ jsreportProxy.currentPath = async () => {
67
+ // we get the current path by throwing an error, which give us a stack trace
68
+ // which we analyze and see if some source file is associated to an entity
69
+ // if it is then we can properly get the path associated to it, if not we
70
+ // fallback to the current path passed as options
71
+ const filesCount = sourceFilesInfo.size
72
+ let resolvedPath = currentPath
73
+
74
+ if (filesCount > 0) {
75
+ const err = new Error('get me stack trace please')
76
+ const trace = stackTrace.parse(err)
77
+
78
+ for (let i = 0; i < trace.length; i++) {
79
+ const current = trace[i]
80
+
81
+ if (sourceFilesInfo.has(current.getFileName())) {
82
+ const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
83
+
84
+ if (entity != null && entitySet != null) {
85
+ resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
86
+ break
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ return resolvedPath
93
+ }
94
+
95
+ jsreportProxy.currentDirectoryPath = async () => {
96
+ const currentPath = await jsreportProxy.currentPath()
97
+
98
+ if (currentPath != null) {
99
+ const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
100
+
101
+ if (localPath === '') {
102
+ return '/'
103
+ }
104
+
105
+ return localPath
106
+ }
107
+
108
+ return currentPath
109
+ }
65
110
 
66
111
  // NOTE: it is important that cleanup, restore methods are not called from a function attached to the
67
112
  // sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
@@ -150,6 +150,7 @@ module.exports = (_sandbox, options = {}) => {
150
150
  return {
151
151
  sandbox: vm._context,
152
152
  console: _console,
153
+ sourceFilesInfo,
153
154
  contextifyValue: (value) => {
154
155
  return vm._internal.Contextify.value(value)
155
156
  },
@@ -163,11 +164,11 @@ module.exports = (_sandbox, options = {}) => {
163
164
  return getOriginalFromProxy(proxiesInVM, customProxies, value)
164
165
  },
165
166
  safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
166
- run: async (code, { filename, errorLineNumberOffset = 0, source, entity } = {}) => {
167
+ run: async (code, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
167
168
  const script = new VMScript(code, filename)
168
169
 
169
170
  if (filename != null && source != null) {
170
- sourceFilesInfo.set(filename, { filename, source, entity, errorLineNumberOffset })
171
+ sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
171
172
  }
172
173
 
173
174
  // NOTE: if we need to upgrade vm2 we will need to check the source of this function
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -63,7 +63,6 @@
63
63
  "triple-beam": "1.3.0",
64
64
  "unset-value": "1.0.0",
65
65
  "uuid": "8.3.2",
66
- "v8-compile-cache": "*",
67
66
  "vm2": "3.9.5",
68
67
  "winston": "3.3.3",
69
68
  "winston-transport": "4.4.0"