@jsreport/jsreport-core 4.2.0 → 4.2.2

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,19 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 4.2.2
286
+
287
+ - fix recursive component rendering
288
+
289
+ ### 4.2.1
290
+
291
+ - response.output.update now accepts Web ReadableStream (which is what new version of puppeteer returns)
292
+
293
+ ### 4.2.0
294
+
295
+ - the response object has new structure and api, the `response.output` is new addition and contains different methods to work with the response output. the `response.content`, `response.stream` are now soft deprecated (will be removed in future versions)
296
+ - the internal architecture has been updated to support working with template content and report output as streams, this means that the render will be able to handle and produce bigger reports without getting hit by out of memory errors
297
+
285
298
  ### 4.1.0
286
299
 
287
300
  - update deps to fix npm audit
@@ -61,7 +61,7 @@ module.exports = (reporter, requestId, obj) => {
61
61
  return
62
62
  }
63
63
 
64
- if (isReadableStream(bufOrStreamOrPath)) {
64
+ if (isNodeReadableStream(bufOrStreamOrPath) || isWebReadableStream(bufOrStreamOrPath)) {
65
65
  if (outputImpl instanceof BufferOutput) {
66
66
  outputImpl = new StreamOutput(reporter, requestId)
67
67
  }
@@ -108,7 +108,7 @@ class BufferOutput {
108
108
  }
109
109
 
110
110
  setBuffer (buf) {
111
- // we need to ensure that the buffer is an actually buffer instance,
111
+ // we need to ensure that the buffer is a buffer instance,
112
112
  // so when receiving Uint8Array we convert it to a buffer
113
113
  this.buffer = Buffer.isBuffer(buf) ? buf : Buffer.from(buf)
114
114
  }
@@ -198,8 +198,11 @@ class StreamOutput {
198
198
  }
199
199
 
200
200
  async setStream (stream) {
201
+ // we need to ensure that the stream used in pipeline is node.js readable stream instance,
202
+ // so when receiving Web ReadableStream we convert it to node.js readable stream
203
+ const inputStream = isNodeReadableStream(stream) ? stream : Readable.fromWeb(stream)
201
204
  const { stream: responseFileStream } = await this.reporter.writeTempFileStream(this.filename)
202
- await pipeline(stream, responseFileStream)
205
+ await pipeline(inputStream, responseFileStream)
203
206
  }
204
207
 
205
208
  serialize () {
@@ -220,12 +223,27 @@ class StreamOutput {
220
223
  }
221
224
 
222
225
  // from https://github.com/sindresorhus/is-stream/blob/main/index.js
223
- function isReadableStream (stream) {
226
+ function isNodeReadableStream (stream) {
224
227
  return (
225
228
  stream !== null &&
226
229
  typeof stream === 'object' &&
227
230
  typeof stream.pipe === 'function' &&
228
- stream.readable !== false && typeof stream._read === 'function' &&
229
- typeof stream._readableState === 'object'
231
+ typeof stream.read === 'function' &&
232
+ typeof stream.readable === 'boolean' &&
233
+ typeof stream.readableObjectMode === 'boolean' &&
234
+ typeof stream.destroy === 'function' &&
235
+ typeof stream.destroyed === 'boolean'
236
+ )
237
+ }
238
+
239
+ function isWebReadableStream (stream) {
240
+ return (
241
+ stream !== null &&
242
+ typeof stream === 'object' &&
243
+ typeof stream.locked === 'boolean' &&
244
+ typeof stream.cancel === 'function' &&
245
+ typeof stream.getReader === 'function' &&
246
+ typeof stream.pipeTo === 'function' &&
247
+ typeof stream.pipeThrough === 'function'
230
248
  )
231
249
  }
@@ -14,8 +14,10 @@ module.exports = (reporter) => {
14
14
 
15
15
  reporter.templatingEngines = { cache: templatesCache }
16
16
 
17
+ const contextExecutionChainMap = new WeakMap()
17
18
  const executionFnParsedParamsMap = new Map()
18
19
  const executionAsyncResultsMap = new Map()
20
+ const executionAsyncCallChainMap = new Map()
19
21
  const executionFinishListenersMap = new Map()
20
22
 
21
23
  const templatingEnginesEvaluate = async (mainCall, { engine, content, helpers, data }, { entity, entitySet }, req) => {
@@ -48,11 +50,14 @@ module.exports = (reporter) => {
48
50
  }
49
51
 
50
52
  executionAsyncResultsMap.delete(executionId)
53
+ executionAsyncCallChainMap.delete(executionId)
51
54
  executionFinishListenersMap.delete(executionId)
52
55
  }
53
56
  }
54
57
 
55
- reporter.templatingEngines.evaluate = (executionInfo, entityInfo, req) => templatingEnginesEvaluate(true, executionInfo, entityInfo, req)
58
+ reporter.templatingEngines.evaluate = (executionInfo, entityInfo, req) => {
59
+ return templatingEnginesEvaluate(true, executionInfo, entityInfo, req)
60
+ }
56
61
 
57
62
  reporter.extendProxy((proxy, req, {
58
63
  runInSandbox,
@@ -64,15 +69,18 @@ module.exports = (reporter) => {
64
69
  return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
65
70
  },
66
71
  waitForAsyncHelper: async (maybeAsyncContent) => {
72
+ const executionChain = contextExecutionChainMap.get(context) || []
73
+ const executionId = executionChain[executionChain.length - 1]
74
+
67
75
  if (
68
- context.__executionId == null ||
69
- !executionAsyncResultsMap.has(context.__executionId) ||
76
+ executionId == null ||
77
+ !executionAsyncResultsMap.has(executionId) ||
70
78
  typeof maybeAsyncContent !== 'string'
71
79
  ) {
72
80
  return maybeAsyncContent
73
81
  }
74
82
 
75
- const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
83
+ const asyncResultMap = executionAsyncResultsMap.get(executionId)
76
84
  const asyncHelperResultRegExp = /{#asyncHelperResult ([^{}]+)}/
77
85
  let content = maybeAsyncContent
78
86
  let matchResult
@@ -91,18 +99,34 @@ module.exports = (reporter) => {
91
99
  return content
92
100
  },
93
101
  waitForAsyncHelpers: async () => {
94
- if (context.__executionId != null && executionAsyncResultsMap.has(context.__executionId)) {
95
- const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
96
- return Promise.all([...asyncResultMap.keys()].map((k) => asyncResultMap.get(k)))
102
+ const executionChain = contextExecutionChainMap.get(context) || []
103
+ const executionId = executionChain[executionChain.length - 1]
104
+
105
+ if (executionId != null && executionAsyncResultsMap.has(executionId)) {
106
+ const asyncCallChainSet = executionAsyncCallChainMap.get(executionId)
107
+ const lastAsyncCall = [...asyncCallChainSet].pop()
108
+
109
+ 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)
113
+
114
+ return Promise.all(targetAsyncResultKeys.map((k) => asyncResultMap.get(k)))
97
115
  }
98
116
  },
99
117
  addFinishListener: (fn) => {
100
- if (executionFinishListenersMap.has(context.__executionId)) {
101
- executionFinishListenersMap.get(context.__executionId).add('finish', fn)
118
+ const executionChain = contextExecutionChainMap.get(context) || []
119
+ const executionId = executionChain[executionChain.length - 1]
120
+
121
+ if (executionId && executionFinishListenersMap.has(executionId)) {
122
+ executionFinishListenersMap.get(executionId).add('finish', fn)
102
123
  }
103
124
  },
104
125
  createAsyncHelperResult: (v) => {
105
- const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
126
+ const executionChain = contextExecutionChainMap.get(context) || []
127
+ const executionId = executionChain[executionChain.length - 1]
128
+
129
+ const asyncResultMap = executionAsyncResultsMap.get(executionId)
106
130
  const asyncResultId = nanoid(7)
107
131
  asyncResultMap.set(asyncResultId, v)
108
132
  return `{#asyncHelperResult ${asyncResultId}}`
@@ -133,6 +157,7 @@ module.exports = (reporter) => {
133
157
  } finally {
134
158
  executionFnParsedParamsMap.delete(req.context.id)
135
159
  executionAsyncResultsMap.delete(executionId)
160
+ executionAsyncCallChainMap.delete(executionId)
136
161
  }
137
162
  }
138
163
 
@@ -147,10 +172,6 @@ module.exports = (reporter) => {
147
172
  const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
148
173
 
149
174
  const initFn = async (getTopLevelFunctions, compileScript) => {
150
- if (reporter.options.trustUserCode === false) {
151
- return null
152
- }
153
-
154
175
  if (systemHelpersCache != null) {
155
176
  return systemHelpersCache
156
177
  }
@@ -193,10 +214,16 @@ module.exports = (reporter) => {
193
214
 
194
215
  const executionFn = async ({ require, console, topLevelFunctions, context }) => {
195
216
  const asyncResultMap = new Map()
217
+ const asyncCallChainSet = new Set()
218
+
219
+ if (!contextExecutionChainMap.has(context)) {
220
+ contextExecutionChainMap.set(context, [])
221
+ }
196
222
 
197
- context.__executionId = executionId
223
+ contextExecutionChainMap.get(context).push(executionId)
198
224
 
199
225
  executionAsyncResultsMap.set(executionId, asyncResultMap)
226
+ executionAsyncCallChainMap.set(executionId, asyncCallChainSet)
200
227
  executionFinishListenersMap.set(executionId, reporter.createListenerCollection())
201
228
  executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions, context })
202
229
 
@@ -223,7 +250,7 @@ module.exports = (reporter) => {
223
250
  if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
224
251
  wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
225
252
  } else {
226
- wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap)
253
+ wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap, asyncCallChainSet)
227
254
  }
228
255
  }
229
256
 
@@ -231,11 +258,13 @@ module.exports = (reporter) => {
231
258
 
232
259
  const resolvedResultsMap = new Map()
233
260
 
234
- // we need to use the cloned map, becuase there can be a waitForAsyncHelper pending that needs the asyncResultMap values
261
+ // we need to use the cloned map, because there can be a waitForAsyncHelper pending that needs the asyncResultMap values
235
262
  const clonedMap = new Map(asyncResultMap)
236
263
  while (clonedMap.size > 0) {
237
264
  await Promise.all([...clonedMap.keys()].map(async (k) => {
238
- resolvedResultsMap.set(k, `${await clonedMap.get(k)}`)
265
+ const result = await clonedMap.get(k)
266
+ asyncCallChainSet.delete(k)
267
+ resolvedResultsMap.set(k, `${result}`)
239
268
  clonedMap.delete(k)
240
269
  }))
241
270
  }
@@ -245,7 +274,7 @@ module.exports = (reporter) => {
245
274
  contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
246
275
  const asyncResultId = p1
247
276
  // this can happen if a child jsreport.templatingEngines.evaluate receives an async value from outer scope
248
- // because every evaluate uses a unique map of async resuts
277
+ // because every evaluate uses a unique map of async results
249
278
  // example is the case when component receives as a value async thing
250
279
  // instead of returning "undefined" we let the outer eval to do the replace
251
280
  if (!resolvedResultsMap.has(asyncResultId)) {
@@ -257,7 +286,9 @@ module.exports = (reporter) => {
257
286
  }
258
287
  contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
259
288
 
260
- await executionFinishListenersMap.get(context.__executionId).fire()
289
+ await executionFinishListenersMap.get(executionId).fire()
290
+
291
+ contextExecutionChainMap.set(context, contextExecutionChainMap.get(context).filter((id) => id !== executionId))
261
292
 
262
293
  return {
263
294
  // handlebars escapes single brackets before execution to prevent errors on {#asset}
@@ -287,30 +318,12 @@ module.exports = (reporter) => {
287
318
  templatesCache.reset()
288
319
  }
289
320
 
290
- let helpersStr = normalizedHelpers
291
- if (reporter.options.trustUserCode === false) {
292
- const registerResults = await reporter.registerHelpersListeners.fire()
293
- const systemHelpers = []
294
-
295
- for (const result of registerResults) {
296
- if (result == null) {
297
- continue
298
- }
299
-
300
- if (typeof result === 'string') {
301
- systemHelpers.push(result)
302
- }
303
- }
304
- const systemHelpersStr = systemHelpers.join('\n')
305
- helpersStr = normalizedHelpers + '\n' + systemHelpersStr
306
- }
307
-
308
321
  try {
309
322
  return await reporter.runInSandbox({
310
323
  context: {
311
324
  ...(engine.createContext ? engine.createContext(req) : {})
312
325
  },
313
- userCode: helpersStr,
326
+ userCode: normalizedHelpers,
314
327
  initFn,
315
328
  executionFn,
316
329
  currentPath: entityPath,
@@ -363,7 +376,7 @@ module.exports = (reporter) => {
363
376
  }
364
377
  }
365
378
 
366
- function wrapHelperForAsyncSupport (fn, asyncResultMap) {
379
+ function wrapHelperForAsyncSupport (fn, asyncResultMap, asyncCallChainSet) {
367
380
  return function (...args) {
368
381
  // important to call the helper with the current this to preserve the same behavior
369
382
  const fnResult = fn.call(this, ...args)
@@ -374,6 +387,7 @@ module.exports = (reporter) => {
374
387
 
375
388
  const asyncResultId = nanoid(7)
376
389
  asyncResultMap.set(asyncResultId, fnResult)
390
+ asyncCallChainSet.add(asyncResultId)
377
391
 
378
392
  return `{#asyncHelperResult ${asyncResultId}}`
379
393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",