@jsreport/jsreport-core 4.4.0 → 4.5.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,11 +282,24 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 4.5.0
286
+
287
+ - fix blobStorage failing to save reports bigger than 1gb
288
+ - decrease default value of `options.profile.maxDiffSize` to `10mb`
289
+ - fix logs metadata for main logs
290
+ - fix support for using of async helper that returns a value other than string in template engines
291
+ - improve support for `jsreport.templateEngines.waitForAsyncHelpers` when used in async helper
292
+
293
+ ### 4.4.1
294
+
295
+ - fix timestamp shown in logging
296
+
285
297
  ### 4.4.0
286
298
 
287
299
  - trigger async report using header to avoid allocating worker
288
300
  - fix require('..') and require('.') in sandbox
289
301
  - fix component rendering in loop with async work on helpers
302
+ - update @jsreport/ses to 1.1.0
290
303
 
291
304
  ### 4.3.1
292
305
 
@@ -3,13 +3,22 @@ module.exports = (reporter) => {
3
3
  const localReq = reporter.Request(originalReq)
4
4
  const res = await reporter.blobStorage.read(spec.blobName, localReq)
5
5
 
6
- return res.toString('base64')
6
+ if (res.length < 1000 * 1000 * 10) {
7
+ return { content: res.toString('base64') }
8
+ }
9
+
10
+ const { pathToFile } = await reporter.writeTempFile((uuid) => `${uuid}.blob`, res)
11
+ return { pathToFile }
7
12
  })
8
13
 
9
14
  reporter.registerMainAction('blobStorage.write', async (spec, originalReq) => {
10
15
  const localReq = reporter.Request(originalReq)
16
+ if (spec.content) {
17
+ return await reporter.blobStorage.write(spec.blobName, Buffer.from(spec.content, 'base64'), localReq)
18
+ }
11
19
 
12
- return await reporter.blobStorage.write(spec.blobName, Buffer.from(spec.content, 'base64'), localReq)
20
+ const { content } = await reporter.readTempFile(spec.pathToFile)
21
+ return await reporter.blobStorage.write(spec.blobName, content, localReq)
13
22
  })
14
23
 
15
24
  reporter.registerMainAction('blobStorage.remove', async (spec, originalReq) => {
@@ -4,8 +4,17 @@ const winston = require('winston')
4
4
 
5
5
  module.exports = (options = {}) => {
6
6
  return winston.format((info) => {
7
- const { level, message, ...meta } = info
8
- info[MESSAGE] = `${options.timestamp === true ? `${new Date().toISOString()} - ` : ''}${level}: ${info.userLevel === true ? colors.cyan(message) : message}`
7
+ const { level, message, timestamp, ...meta } = info
8
+ let logDate
9
+
10
+ if (timestamp == null) {
11
+ logDate = new Date()
12
+ info.timestamp = logDate.getTime()
13
+ } else {
14
+ logDate = new Date(timestamp)
15
+ }
16
+
17
+ info[MESSAGE] = `${options.timestamp === true ? `${logDate.toISOString()} - ` : ''}${level}: ${info.userLevel === true ? colors.cyan(message) : message}`
9
18
 
10
19
  const metaKeys = Object.keys(meta)
11
20
 
@@ -1,19 +1,32 @@
1
+ const omit = require('lodash.omit')
1
2
  const winston = require('winston')
3
+ const { LEVEL, MESSAGE, SPLAT } = require('triple-beam')
2
4
  const normalizeMetaFromLogs = require('../shared/normalizeMetaFromLogs')
3
5
 
4
6
  module.exports = () => {
5
7
  return winston.format((info) => {
6
- const { level, message, ...meta } = info
7
- const newMeta = normalizeMetaFromLogs(level, message, meta)
8
+ const { level, message, timestamp: _timestamp, ...meta } = info
8
9
 
9
- if (newMeta != null) {
10
- return {
11
- level,
12
- message,
13
- ...newMeta
10
+ const symbolProps = [LEVEL, MESSAGE, SPLAT]
11
+ const originalSymbolProps = {}
12
+
13
+ for (const symbolProp of symbolProps) {
14
+ if (info[symbolProp] != null) {
15
+ originalSymbolProps[symbolProp] = info[symbolProp]
14
16
  }
15
17
  }
16
18
 
17
- return info
19
+ const timestamp = _timestamp ?? new Date().getTime()
20
+
21
+ const targetMeta = omit(meta, symbolProps)
22
+ const newMeta = normalizeMetaFromLogs(level, message, timestamp, targetMeta)
23
+
24
+ return {
25
+ level,
26
+ message,
27
+ timestamp,
28
+ ...originalSymbolProps,
29
+ ...newMeta
30
+ }
18
31
  })
19
32
  }
@@ -171,6 +171,7 @@ function configureLogger (logger, _transports) {
171
171
  if (options.silent) {
172
172
  continue
173
173
  }
174
+
174
175
  const transportInstance = new TransportClass(options)
175
176
 
176
177
  const existingTransport = logger.transports.find((t) => t.name === transportInstance.name)
@@ -238,7 +239,6 @@ class DebugTransport extends Transport {
238
239
 
239
240
  this.format = options.format || winston.format.combine(
240
241
  winston.format.colorize(),
241
- normalizeMetaLoggerFormat(),
242
242
  defaultLoggerFormat()
243
243
  )
244
244
 
@@ -196,7 +196,7 @@ module.exports.getRootSchemaOptions = () => ({
196
196
  maxDiffSize: {
197
197
  type: ['string', 'number'],
198
198
  '$jsreport-acceptsSize': true,
199
- default: '50mb'
199
+ default: '10mb'
200
200
  }
201
201
  }
202
202
  }
@@ -1,6 +1,8 @@
1
1
  const omit = require('lodash.omit')
2
2
 
3
- module.exports = (level, msg, meta) => {
3
+ module.exports = (level, msg, timestamp, meta) => {
4
+ let result = meta
5
+
4
6
  // detecting if meta is jsreport request object
5
7
  if (meta != null && meta.context) {
6
8
  meta.context.logs = meta.context.logs || []
@@ -8,7 +10,7 @@ module.exports = (level, msg, meta) => {
8
10
  meta.context.logs.push({
9
11
  level: level,
10
12
  message: msg,
11
- timestamp: meta.timestamp || new Date().getTime()
13
+ timestamp
12
14
  })
13
15
 
14
16
  // TODO adding cancel looks bad, its before script is adding req.cancel()
@@ -23,8 +25,8 @@ module.exports = (level, msg, meta) => {
23
25
  newMeta.id = meta.context.id
24
26
  }
25
27
 
26
- return newMeta
28
+ result = newMeta
27
29
  }
28
30
 
29
- return meta
31
+ return result != null && Object.keys(result).length > 0 ? result : null
30
32
  }
@@ -3,7 +3,6 @@ const fs = require('fs/promises')
3
3
  const { Readable } = require('stream')
4
4
  const { pipeline } = require('stream/promises')
5
5
  const path = require('path')
6
- const isArrayBufferView = require('util').types.isArrayBufferView
7
6
 
8
7
  module.exports = (reporter, requestId, obj) => {
9
8
  let outputImpl = new BufferOutput(reporter)
@@ -44,7 +43,7 @@ module.exports = (reporter, requestId, obj) => {
44
43
  async getSize () { return outputImpl.getSize() },
45
44
  async writeToTempFile (...args) { return outputImpl.writeToTempFile(...args) },
46
45
  async update (bufOrStreamOrPath) {
47
- if (Buffer.isBuffer(bufOrStreamOrPath) || isArrayBufferView(bufOrStreamOrPath)) {
46
+ if (Buffer.isBuffer(bufOrStreamOrPath) || ArrayBuffer.isView(bufOrStreamOrPath)) {
48
47
  return outputImpl.setBuffer(bufOrStreamOrPath)
49
48
  }
50
49
 
@@ -1,17 +1,31 @@
1
- module.exports = (executeMainAction) => {
1
+ module.exports = (executeMainAction, { writeTempFile, readTempFile }) => {
2
2
  return {
3
3
  async read (blobName, req) {
4
4
  const r = await executeMainAction('blobStorage.read', {
5
5
  blobName
6
6
  }, req)
7
- return Buffer.from(r, 'base64')
7
+
8
+ if (r.content) {
9
+ return Buffer.from(r.content, 'base64')
10
+ }
11
+
12
+ const { content } = await readTempFile(r.pathToFile)
13
+ return content
8
14
  },
9
15
 
10
- write (blobName, content, req) {
11
- return executeMainAction('blobStorage.write', {
12
- blobName,
13
- content: Buffer.from(content).toString('base64')
14
- }, req)
16
+ async write (blobName, content, req) {
17
+ const message = {
18
+ blobName
19
+ }
20
+
21
+ if (content.length < 1000 * 1000 * 10) {
22
+ message.content = Buffer.from(content).toString('base64')
23
+ } else {
24
+ const { pathToFile } = await writeTempFile((uuid) => `${uuid}.blob`, content)
25
+ message.pathToFile = pathToFile
26
+ }
27
+
28
+ return executeMainAction('blobStorage.write', message, req)
15
29
  },
16
30
 
17
31
  remove (blobName, req) {
@@ -35,7 +35,7 @@ function logFn (level, profiler, ...args) {
35
35
  message: util.format.apply(util, msgArgs)
36
36
  }
37
37
 
38
- const meta = normalizeMetaFromLogs(level, log.message, lastArg)
38
+ const meta = normalizeMetaFromLogs(level, log.message, log.timestamp, lastArg)
39
39
 
40
40
  if (meta != null) {
41
41
  log.meta = meta
@@ -7,8 +7,10 @@
7
7
  */
8
8
  const LRU = require('lru-cache')
9
9
  const { nanoid } = require('nanoid')
10
+ const { AsyncLocalStorage } = require('node:async_hooks')
10
11
 
11
12
  module.exports = (reporter) => {
13
+ const helperCallerAsyncLocalStorage = new AsyncLocalStorage()
12
14
  const templatesCache = LRU(reporter.options.sandbox.cache)
13
15
  let systemHelpersCache
14
16
 
@@ -17,7 +19,6 @@ module.exports = (reporter) => {
17
19
  const contextExecutionChainMap = new Map()
18
20
  const executionFnParsedParamsMap = new Map()
19
21
  const executionAsyncResultsMap = new Map()
20
- const executionAsyncCallChainMap = new Map()
21
22
  const executionFinishListenersMap = new Map()
22
23
 
23
24
  const templatingEnginesEvaluate = async (mainCall, { engine, content, helpers, data }, { entity, entitySet }, req) => {
@@ -50,7 +51,6 @@ module.exports = (reporter) => {
50
51
  }
51
52
 
52
53
  executionAsyncResultsMap.delete(executionId)
53
- executionAsyncCallChainMap.delete(executionId)
54
54
  executionFinishListenersMap.delete(executionId)
55
55
  }
56
56
  }
@@ -59,6 +59,8 @@ module.exports = (reporter) => {
59
59
  return templatingEnginesEvaluate(true, executionInfo, entityInfo, req)
60
60
  }
61
61
 
62
+ reporter.closeListeners.add('engineHelperCaller', () => helperCallerAsyncLocalStorage.disable())
63
+
62
64
  reporter.extendProxy((proxy, req, {
63
65
  runInSandbox,
64
66
  context,
@@ -90,10 +92,23 @@ module.exports = (reporter) => {
90
92
  const matchedPart = matchResult[0]
91
93
  const asyncResultId = matchResult[1]
92
94
  const result = await asyncResultMap.get(asyncResultId)
93
- content = `${content.slice(0, matchResult.index)}${result}${content.slice(matchResult.index + matchedPart.length)}`
95
+ const isFullMatch = content === matchedPart
96
+
97
+ if (typeof result !== 'string' && isFullMatch) {
98
+ // this allows consuming async helper that returns a value other than string
99
+ // like an async helper that returns object and it is received as
100
+ // parameter of another helper
101
+ content = result
102
+ } else {
103
+ content = `${content.slice(0, matchResult.index)}${result}${content.slice(matchResult.index + matchedPart.length)}`
104
+ }
94
105
  }
95
106
 
96
- matchResult = content.match(asyncHelperResultRegExp)
107
+ if (typeof content === 'string') {
108
+ matchResult = content.match(asyncHelperResultRegExp)
109
+ } else {
110
+ matchResult = null
111
+ }
97
112
  } while (matchResult != null)
98
113
 
99
114
  return content
@@ -103,13 +118,14 @@ module.exports = (reporter) => {
103
118
  const executionId = executionChain[executionChain.length - 1]
104
119
 
105
120
  if (executionId != null && executionAsyncResultsMap.has(executionId)) {
106
- const asyncCallChainSet = executionAsyncCallChainMap.get(executionId)
107
- const lastAsyncCall = [...asyncCallChainSet].pop()
108
-
109
121
  const asyncResultMap = executionAsyncResultsMap.get(executionId)
110
- // we should exclude the last async call because if it exists it represents the parent
111
- // async call that called .waitForAsyncHelpers, it is not going to be resolved at this point
112
- const targetAsyncResultKeys = [...asyncResultMap.keys()].filter((key) => key !== lastAsyncCall)
122
+
123
+ const callerId = helperCallerAsyncLocalStorage.getStore()
124
+
125
+ // we must exclude the caller helper because if it exists it represents some parent
126
+ // sync/async call that called .waitForAsyncHelpers, it is not going to be resolved at this point
127
+ // so we should skip it in order for the execution to not hang
128
+ const targetAsyncResultKeys = [...asyncResultMap.keys()].filter((key) => key !== callerId)
113
129
 
114
130
  return Promise.all(targetAsyncResultKeys.map((k) => asyncResultMap.get(k)))
115
131
  }
@@ -157,7 +173,6 @@ module.exports = (reporter) => {
157
173
  } finally {
158
174
  executionFnParsedParamsMap.delete(req.context.id)
159
175
  executionAsyncResultsMap.delete(executionId)
160
- executionAsyncCallChainMap.delete(executionId)
161
176
  }
162
177
  }
163
178
 
@@ -216,7 +231,6 @@ module.exports = (reporter) => {
216
231
  const executionFn = async ({ require, console, topLevelFunctions, context }) => {
217
232
  sandboxId = context.__sandboxId
218
233
  const asyncResultMap = new Map()
219
- const asyncCallChainSet = new Set()
220
234
 
221
235
  if (!contextExecutionChainMap.has(sandboxId)) {
222
236
  contextExecutionChainMap.set(sandboxId, [])
@@ -225,7 +239,6 @@ module.exports = (reporter) => {
225
239
  contextExecutionChainMap.get(sandboxId).push(executionId)
226
240
 
227
241
  executionAsyncResultsMap.set(executionId, asyncResultMap)
228
- executionAsyncCallChainMap.set(executionId, asyncCallChainSet)
229
242
  executionFinishListenersMap.set(executionId, reporter.createListenerCollection())
230
243
  executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions, context })
231
244
 
@@ -253,7 +266,7 @@ module.exports = (reporter) => {
253
266
  if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
254
267
  wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
255
268
  } else {
256
- wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap, asyncCallChainSet)
269
+ wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], h, asyncResultMap)
257
270
  }
258
271
  }
259
272
 
@@ -269,7 +282,6 @@ module.exports = (reporter) => {
269
282
 
270
283
  await Promise.all(keysEvaluated.map(async (k) => {
271
284
  const result = await clonedMap.get(k)
272
- asyncCallChainSet.delete(k)
273
285
  resolvedResultsMap.set(k, `${result}`)
274
286
  clonedMap.delete(k)
275
287
  }))
@@ -401,20 +413,25 @@ module.exports = (reporter) => {
401
413
  }
402
414
  }
403
415
 
404
- function wrapHelperForAsyncSupport (fn, asyncResultMap, asyncCallChainSet) {
416
+ function wrapHelperForAsyncSupport (fn, helperName, asyncResultMap) {
405
417
  return function (...args) {
406
- // important to call the helper with the current this to preserve the same behavior
407
- const fnResult = fn.call(this, ...args)
418
+ const resultId = nanoid(7)
419
+
420
+ let fnResult
421
+
422
+ // make the result id available for all calls inside the helper
423
+ helperCallerAsyncLocalStorage.run(resultId, () => {
424
+ // important to call the helper with the current this to preserve the same behavior
425
+ fnResult = fn.call(this, ...args)
426
+ })
408
427
 
409
428
  if (fnResult == null || typeof fnResult.then !== 'function') {
410
429
  return fnResult
411
430
  }
412
431
 
413
- const asyncResultId = nanoid(7)
414
- asyncResultMap.set(asyncResultId, fnResult)
415
- asyncCallChainSet.add(asyncResultId)
432
+ asyncResultMap.set(resultId, fnResult)
416
433
 
417
- return `{#asyncHelperResult ${asyncResultId}}`
434
+ return `{#asyncHelperResult ${resultId}}`
418
435
  }
419
436
  }
420
437
 
@@ -54,7 +54,7 @@ class WorkerReporter extends Reporter {
54
54
  await this.extensionsManager.init()
55
55
 
56
56
  this.documentStore = DocumentStore(this._documentStoreData, this.executeMainAction.bind(this))
57
- this.blobStorage = BlobStorage(this.executeMainAction.bind(this))
57
+ this.blobStorage = BlobStorage(this.executeMainAction.bind(this), { writeTempFile: this.writeTempFile.bind(this), readTempFile: this.readTempFile.bind(this) })
58
58
 
59
59
  this.addRequestContextMetaConfig('rootId', { sandboxReadOnly: true })
60
60
  this.addRequestContextMetaConfig('id', { sandboxReadOnly: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "devDependencies": {
80
80
  "@node-rs/jsonwebtoken": "0.2.0",
81
- "jsdom": "17.0.0",
81
+ "jsdom": "24.1.0",
82
82
  "mocha": "10.1.0",
83
83
  "should": "13.2.3",
84
84
  "standard": "16.0.4",