@jsreport/jsreport-core 4.2.1 → 4.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.
package/README.md CHANGED
@@ -256,7 +256,7 @@ jsreport currently support these main listeners
256
256
  - `closeListeners()` called when jsreport is about to be closed, you will usually put here some code that clean up some resource
257
257
 
258
258
  ## Studio
259
- jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details.
259
+ jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details
260
260
 
261
261
  ## Template store
262
262
  `jsreport-core` includes API for persisting and accessing report templates. This API is then used by extensions mainly in combination with jsreport [studio](#studio). `jsreport-core` implements just in-memory persistence, but you can add other persistence methods through extensions, see the [template stores](https://jsreport.net/learn/template-stores) docummentation
@@ -282,6 +282,20 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 4.3.0
286
+
287
+ - expose safe properties of `req.context.user` in sandbox
288
+ - fix component execution when wrapped with async helper
289
+ - fix jsdom require in sandbox
290
+
291
+ ### 4.2.2
292
+
293
+ - fix recursive component rendering
294
+
295
+ ### 4.2.1
296
+
297
+ - response.output.update now accepts Web ReadableStream (which is what new version of puppeteer returns)
298
+
285
299
  ### 4.2.0
286
300
 
287
301
  - 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)
@@ -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()
196
218
 
197
- context.__executionId = executionId
219
+ if (!contextExecutionChainMap.has(context)) {
220
+ contextExecutionChainMap.set(context, [])
221
+ }
222
+
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,21 +258,36 @@ 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
235
- const clonedMap = new Map(asyncResultMap)
261
+ // we need to use the cloned map, because there can be a waitForAsyncHelper pending that needs the asyncResultMap values
262
+ let clonedMap = new Map(asyncResultMap)
263
+
236
264
  while (clonedMap.size > 0) {
237
- await Promise.all([...clonedMap.keys()].map(async (k) => {
238
- resolvedResultsMap.set(k, `${await clonedMap.get(k)}`)
265
+ const keysEvaluated = [...clonedMap.keys()]
266
+
267
+ await Promise.all(keysEvaluated.map(async (k) => {
268
+ const result = await clonedMap.get(k)
269
+ asyncCallChainSet.delete(k)
270
+ resolvedResultsMap.set(k, `${result}`)
239
271
  clonedMap.delete(k)
240
272
  }))
273
+
274
+ // we need to remove the keys processed from the original map at this point
275
+ // (after the await) because during the async work the asyncResultMap will be read
276
+ for (const k of keysEvaluated) {
277
+ asyncResultMap.delete(k)
278
+ }
279
+
280
+ // we want to process the new generated pending async results
281
+ if (asyncResultMap.size > 0) {
282
+ clonedMap = new Map(asyncResultMap)
283
+ }
241
284
  }
242
- asyncResultMap.clear()
243
285
 
244
286
  while (contentResult.includes('{#asyncHelperResult')) {
245
287
  contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
246
288
  const asyncResultId = p1
247
289
  // 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
290
+ // because every evaluate uses a unique map of async results
249
291
  // example is the case when component receives as a value async thing
250
292
  // instead of returning "undefined" we let the outer eval to do the replace
251
293
  if (!resolvedResultsMap.has(asyncResultId)) {
@@ -255,9 +297,12 @@ module.exports = (reporter) => {
255
297
  return `${resolvedResultsMap.get(asyncResultId)}`
256
298
  })
257
299
  }
300
+
258
301
  contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
259
302
 
260
- await executionFinishListenersMap.get(context.__executionId).fire()
303
+ await executionFinishListenersMap.get(executionId).fire()
304
+
305
+ contextExecutionChainMap.set(context, contextExecutionChainMap.get(context).filter((id) => id !== executionId))
261
306
 
262
307
  return {
263
308
  // handlebars escapes single brackets before execution to prevent errors on {#asset}
@@ -287,30 +332,12 @@ module.exports = (reporter) => {
287
332
  templatesCache.reset()
288
333
  }
289
334
 
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
335
  try {
309
336
  return await reporter.runInSandbox({
310
337
  context: {
311
338
  ...(engine.createContext ? engine.createContext(req) : {})
312
339
  },
313
- userCode: helpersStr,
340
+ userCode: normalizedHelpers,
314
341
  initFn,
315
342
  executionFn,
316
343
  currentPath: entityPath,
@@ -363,7 +390,7 @@ module.exports = (reporter) => {
363
390
  }
364
391
  }
365
392
 
366
- function wrapHelperForAsyncSupport (fn, asyncResultMap) {
393
+ function wrapHelperForAsyncSupport (fn, asyncResultMap, asyncCallChainSet) {
367
394
  return function (...args) {
368
395
  // important to call the helper with the current this to preserve the same behavior
369
396
  const fnResult = fn.call(this, ...args)
@@ -374,6 +401,7 @@ module.exports = (reporter) => {
374
401
 
375
402
  const asyncResultId = nanoid(7)
376
403
  asyncResultMap.set(asyncResultId, fnResult)
404
+ asyncCallChainSet.add(asyncResultId)
377
405
 
378
406
  return `{#asyncHelperResult ${asyncResultId}}`
379
407
  }
@@ -253,6 +253,23 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
253
253
  shouldStoreOriginal = false
254
254
  }
255
255
 
256
+ // allow configuring parent as hidden, but child props as readonly so we expose
257
+ // just part of the parent
258
+ // const ignoreParentHidden = false
259
+ const ignoreParentHidden = (
260
+ isHidden &&
261
+ isGrouped &&
262
+ (hierarchyPropertiesConfig.inner != null || hierarchyPropertiesConfig.standalone != null)
263
+ )
264
+
265
+ if (parentOpts?.originalSandboxHidden !== true && ignoreParentHidden) {
266
+ // we store the original value for the top parent with hidden value
267
+ shouldStoreOriginal = true
268
+ } else if (parentOpts && parentOpts.originalSandboxHidden === true) {
269
+ // we don't store original value when parent was configured as hidden
270
+ shouldStoreOriginal = false
271
+ }
272
+
256
273
  // saving original value
257
274
  if (shouldStoreOriginal) {
258
275
  let exists = true
@@ -284,6 +301,19 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
284
301
  Object.keys(c.standalone).forEach((standaloneKey) => {
285
302
  const standaloneConfig = c.standalone[standaloneKey]
286
303
 
304
+ const newParentOpts = {
305
+ sandboxHidden: ignoreParentHidden ? false : isHidden,
306
+ sandboxReadOnly: isReadOnly
307
+ }
308
+
309
+ // set and inherit originalSandboxHidden if needed
310
+ if (
311
+ parentOpts?.originalSandboxHidden === true ||
312
+ (ignoreParentHidden && isHidden)
313
+ ) {
314
+ newParentOpts.originalSandboxHidden = true
315
+ }
316
+
287
317
  applyPropertiesConfig(context, standaloneConfig, {
288
318
  original,
289
319
  customProxies,
@@ -291,7 +321,7 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
291
321
  isRoot: false,
292
322
  isGrouped: false,
293
323
  onlyReadOnlyTopLevel,
294
- parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
324
+ parentOpts: newParentOpts
295
325
  }, readOnlyConfigured)
296
326
  })
297
327
  }
@@ -300,19 +330,65 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
300
330
  Object.keys(c.inner).forEach((innerKey) => {
301
331
  const innerConfig = c.inner[innerKey]
302
332
 
333
+ const newParentOpts = {
334
+ sandboxHidden: ignoreParentHidden ? false : isHidden,
335
+ sandboxReadOnly: isReadOnly
336
+ }
337
+
338
+ // set and inherit originalSandboxHidden if needed
339
+ if (
340
+ parentOpts?.originalSandboxHidden === true ||
341
+ (ignoreParentHidden && isHidden)
342
+ ) {
343
+ newParentOpts.originalSandboxHidden = true
344
+ }
345
+
303
346
  applyPropertiesConfig(context, innerConfig, {
304
347
  original,
305
348
  customProxies,
306
349
  prop: innerKey,
307
350
  isRoot: false,
308
351
  isGrouped: true,
309
- parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
352
+ parentOpts: newParentOpts
310
353
  }, readOnlyConfigured)
311
354
  })
312
355
  }
313
356
 
314
357
  if (isHidden) {
315
- omitProp(context, prop)
358
+ if (ignoreParentHidden) {
359
+ // when parent is hidden but there are configuration for child properties we just copy
360
+ // the properties listed there, we do this because we want to work with just the configured
361
+ // properties, the other properties should not be exposed
362
+ const currentValue = get(context, prop)
363
+
364
+ if (currentValue != null && typeof currentValue === 'object') {
365
+ let newValue
366
+
367
+ if (Array.isArray(currentValue)) {
368
+ newValue = []
369
+ } else {
370
+ newValue = {}
371
+ }
372
+
373
+ for (const config of [hierarchyPropertiesConfig.standalone, hierarchyPropertiesConfig.inner]) {
374
+ if (config == null) {
375
+ continue
376
+ }
377
+
378
+ for (const childProp of Object.keys(config)) {
379
+ if (hasOwn(context, childProp)) {
380
+ const childValue = get(context, childProp)
381
+ const targetProp = childProp.replace(`${prop}.`, '')
382
+ set(newValue, targetProp, childValue)
383
+ }
384
+ }
385
+ }
386
+
387
+ set(context, prop, newValue)
388
+ }
389
+ } else {
390
+ omitProp(context, prop)
391
+ }
316
392
  } else if (isReadOnly) {
317
393
  readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
318
394
  onlyTopLevel: false,
@@ -38,7 +38,7 @@ module.exports = function createSandboxRequire (safeExecution, isolateModules, m
38
38
 
39
39
  if (isolateModules) {
40
40
  const requireExtensions = Object.create(null)
41
- isolatedRequire.setDefaultRequireExtensions(requireExtensions, modulesCache, compileScript)
41
+ isolatedRequire.setDefaultRequireExtensions(requireExtensions, requireFromRootDirectory, compileScript)
42
42
  modulesMeta.requireExtensions = requireExtensions
43
43
  }
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -78,6 +78,7 @@
78
78
  },
79
79
  "devDependencies": {
80
80
  "@node-rs/jsonwebtoken": "0.2.0",
81
+ "jsdom": "17.0.0",
81
82
  "mocha": "10.1.0",
82
83
  "should": "13.2.3",
83
84
  "standard": "16.0.4",