@jsreport/jsreport-core 3.8.1 → 3.10.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,21 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.10.0
286
+
287
+ - `mainReporter.executeWorkerAction` now supports cancellation with `AbortController.signal`
288
+ - add support for specifying what are the main document properties of templates entitySet
289
+
290
+ ### 3.9.0
291
+
292
+ - add more store methods `collection.findAdmin`, `collection.findOneAdmin`, `reporter.adminRequest` to easily allow execure store queries without taking into account permissions
293
+ - improve logging for child requests and user level logs
294
+ - differentiate between template not found errors and permissions related errors (it is now more clean what is the cause of specific error)
295
+ - normalize to error when non-errors are throw (like throw "string")
296
+ - improve errors in helpers (it now includes the helper name)
297
+ - improve error message when template was not found in child request
298
+ - improve error handling in sandbox
299
+
285
300
  ### 3.8.1
286
301
 
287
302
  - update vm2 for fix security issue
@@ -1,4 +1,3 @@
1
- const omit = require('lodash.omit')
2
1
  const resolveEntityPath = require('../../shared/folders/resolveEntityPath')
3
2
 
4
3
  async function findEntity (reporter, name, folder, req) {
@@ -7,18 +6,11 @@ async function findEntity (reporter, name, folder, req) {
7
6
  continue
8
7
  }
9
8
 
10
- const localReq = req ? reporter.Request(req) : req
11
-
12
- // we should validate against all entities without caring about permissions
13
- if (localReq) {
14
- localReq.context = localReq.context ? omit(localReq.context, 'user') : localReq.context
15
- }
16
-
17
- const allEntities = await reporter.documentStore.collection(c).find({
9
+ const allEntities = await reporter.documentStore.collection(c).findAdmin({
18
10
  folder
19
11
  }, {
20
12
  name: 1
21
- }, localReq)
13
+ }, req)
22
14
 
23
15
  const existingEntity = allEntities.find((entity) => {
24
16
  if (entity.name) {
@@ -211,6 +211,7 @@ module.exports = (reporter) => {
211
211
  profilerRequestMap.set(req.context.rootId, req)
212
212
 
213
213
  const template = await reporter.templates.resolveTemplate(req)
214
+
214
215
  if (template && template._id) {
215
216
  req.context.resolvedTemplate = extend(true, {}, template)
216
217
 
@@ -345,13 +345,14 @@ class MainReporter extends Reporter {
345
345
  }
346
346
 
347
347
  if (err.code === 'WORKER_ABORTED') {
348
- err.message = 'Report cancelled'
348
+ err.message = 'Report cancelled by the client or closed network'
349
349
  err.weak = true
350
350
  }
351
351
 
352
352
  if (!err.logged) {
353
353
  const logFn = err.weak ? this.logger.warn : this.logger.error
354
- logFn(`Report render failed: ${err.message}${err.stack != null ? ' ' + err.stack : ''}`, req)
354
+ const errorMessage = this.createError('Report render failed', { original: err }).message
355
+ logFn(`${errorMessage}${err.stack != null ? '\n' + err.stack : ''}`, req)
355
356
  }
356
357
  await this.renderErrorListeners.fire(req, res, err)
357
358
  throw err
@@ -556,6 +557,16 @@ class MainReporter extends Reporter {
556
557
  timeout
557
558
  })
558
559
 
560
+ const handleAbortSignal = () => {
561
+ if (worker) {
562
+ worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
563
+ }
564
+ }
565
+
566
+ if (options.signal && !options.worker) {
567
+ options.signal.addEventListener('abort', handleAbortSignal, { once: true })
568
+ }
569
+
559
570
  try {
560
571
  const result = await worker.execute({
561
572
  actionName,
@@ -578,7 +589,16 @@ class MainReporter extends Reporter {
578
589
  return result
579
590
  } finally {
580
591
  if (!options.worker) {
581
- await worker.release(req)
592
+ let shouldRelease = true
593
+
594
+ if (options.signal) {
595
+ options.signal.removeEventListener('abort', handleAbortSignal)
596
+ shouldRelease = options.signal.aborted !== true
597
+ }
598
+
599
+ if (shouldRelease) {
600
+ await worker.release(req)
601
+ }
582
602
  }
583
603
  }
584
604
  }
@@ -1,5 +1,3 @@
1
- const omit = require('lodash.omit')
2
- const Request = require('../../shared/request')
3
1
 
4
2
  module.exports = async function checkDuplicatedId (store, collectionName, idValue, req) {
5
3
  if (idValue == null) {
@@ -12,16 +10,10 @@ module.exports = async function checkDuplicatedId (store, collectionName, idValu
12
10
  }
13
11
 
14
12
  async function findEntity (store, collectionName, idValue, req) {
15
- const localReq = req ? Request(req) : req
16
-
17
13
  // we should validate without caring about permissions
18
- if (localReq) {
19
- localReq.context = localReq.context ? omit(localReq.context, 'user') : localReq.context
20
- }
21
-
22
- const existingEntity = await store.collection(collectionName).findOne({
14
+ const existingEntity = await store.collection(collectionName).findOneAdmin({
23
15
  _id: idValue
24
- }, localReq)
16
+ }, req)
25
17
 
26
18
  return existingEntity
27
19
  }
@@ -1,6 +1,8 @@
1
- const createListenerCollection = require('../../shared/listenerCollection')
2
1
  const { resolvePropDefinition } = require('./typeUtils')
2
+ const adminRequest = require('../../shared/adminRequest')
3
+ const createListenerCollection = require('../../shared/listenerCollection')
3
4
  const createError = require('../../shared/createError')
5
+ const Request = require('../../shared/request')
4
6
  const validateEntityName = require('../validateEntityName')
5
7
 
6
8
  module.exports = (entitySet, provider, model, validator, encryption, transactions) => ({
@@ -74,6 +76,41 @@ module.exports = (entitySet, provider, model, validator, encryption, transaction
74
76
  })
75
77
  },
76
78
 
79
+ findAdmin (q, p, req) {
80
+ if (p && p.__isJsreportRequest__ === true) {
81
+ req = p
82
+ p = {}
83
+ }
84
+
85
+ p = p || {}
86
+
87
+ req = adminRequest(req, Request)
88
+
89
+ return this.find(q, p, req)
90
+ },
91
+
92
+ async findOne (...args) {
93
+ const res = await this.find(...args)
94
+ if (res.length > 0) {
95
+ return res[0]
96
+ }
97
+
98
+ return null
99
+ },
100
+
101
+ findOneAdmin (q, p, req) {
102
+ if (p && p.__isJsreportRequest__ === true) {
103
+ req = p
104
+ p = {}
105
+ }
106
+
107
+ p = p || {}
108
+
109
+ req = adminRequest(req, Request)
110
+
111
+ return this.findOne(q, p, req)
112
+ },
113
+
77
114
  count (...args) {
78
115
  return this.find(...args).count()
79
116
  },
@@ -154,15 +191,6 @@ module.exports = (entitySet, provider, model, validator, encryption, transaction
154
191
  })
155
192
  },
156
193
 
157
- async findOne (...args) {
158
- const res = await this.find(...args)
159
- if (res.length > 0) {
160
- return res[0]
161
- }
162
-
163
- return null
164
- },
165
-
166
194
  async serializeProperties (docs, customTypeDef) {
167
195
  let typeDef
168
196
 
@@ -1,15 +1,34 @@
1
+
1
2
  module.exports = (reporter) => {
2
3
  reporter.registerMainAction('documentStore.collection.find', async (spec, originalReq) => {
3
4
  const localReq = reporter.Request(originalReq)
5
+
4
6
  localReq.context.userFindCall = true
5
- const res = await reporter.documentStore.collection(spec.collection).find(spec.query, localReq)
7
+
8
+ const collection = reporter.documentStore.collection(spec.collection)
9
+ let method = 'find'
10
+
11
+ if (spec.admin) {
12
+ method = 'findAdmin'
13
+ }
14
+
15
+ const res = await collection[method](spec.query, localReq)
6
16
  return res
7
17
  })
8
18
 
9
19
  reporter.registerMainAction('documentStore.collection.findOne', async (spec, originalReq) => {
10
20
  const localReq = reporter.Request(originalReq)
21
+
11
22
  localReq.context.userFindCall = true
12
- const res = await reporter.documentStore.collection(spec.collection).findOne(spec.query, localReq)
23
+
24
+ const collection = reporter.documentStore.collection(spec.collection)
25
+ let method = 'findOne'
26
+
27
+ if (spec.admin) {
28
+ method = 'findOneAdmin'
29
+ }
30
+
31
+ const res = await collection[method](spec.query, localReq)
13
32
  return res
14
33
  })
15
34
 
@@ -1,4 +1,3 @@
1
- const omit = require('lodash.omit')
2
1
 
3
2
  module.exports = (reporter) => {
4
3
  reporter.initializeListeners.add('core-validate-shortid', () => {
@@ -60,16 +59,10 @@ async function validateShortid (reporter, collectionName, doc, originalIdValue,
60
59
  }
61
60
 
62
61
  async function findEntity (reporter, collectionName, shortid, req) {
63
- const localReq = req ? reporter.Request(req) : req
64
-
65
62
  // we should validate without caring about permissions
66
- if (localReq) {
67
- localReq.context = localReq.context ? omit(localReq.context, 'user') : localReq.context
68
- }
69
-
70
- const existingEntity = await reporter.documentStore.collection(collectionName).findOne({
63
+ const existingEntity = await reporter.documentStore.collection(collectionName).findOneAdmin({
71
64
  shortid
72
- }, localReq)
65
+ }, req)
73
66
 
74
67
  return existingEntity
75
68
  }
@@ -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
 
@@ -0,0 +1,17 @@
1
+ const omit = require('lodash.omit')
2
+
3
+ module.exports = function adminRequest (req, Request) {
4
+ if (req == null) {
5
+ return req
6
+ }
7
+
8
+ let targetReq = req
9
+
10
+ if (Request != null) {
11
+ targetReq = Request(targetReq)
12
+ }
13
+
14
+ targetReq.context = targetReq.context ? omit(targetReq.context, 'user') : targetReq.context
15
+
16
+ return targetReq
17
+ }
@@ -1,4 +1,4 @@
1
- const _omit = require('lodash.omit')
1
+ const normalizeError = require('./normalizeError')
2
2
 
3
3
  module.exports = function (message, options = {}) {
4
4
  const { original } = options
@@ -10,23 +10,37 @@ module.exports = function (message, options = {}) {
10
10
  error = new Error(message)
11
11
 
12
12
  if (original != null) {
13
- error.entity = original.entity
14
- error.lineNumber = original.lineNumber
15
- error.property = original.property
13
+ // because in js you can throw <anything>, not specifically Error
14
+ const originalNormalized = normalizeError(original)
15
+
16
+ error.entity = originalNormalized.entity
17
+ error.lineNumber = originalNormalized.lineNumber
18
+ error.property = originalNormalized.property
19
+
20
+ if (originalNormalized.statusCode != null) {
21
+ error.statusCode = originalNormalized.statusCode
22
+ }
23
+
24
+ if (originalNormalized.weak != null) {
25
+ error.weak = originalNormalized.weak
26
+ }
16
27
 
17
28
  if (error.message == null || error.message === '') {
18
- error.message = `${original.message}`
29
+ error.message = `${originalNormalized.message}`
19
30
  } else {
20
- error.message += `. ${original.message}`
31
+ error.message += `\n(because) ${lowerCaseFirstLetter(originalNormalized.message)}`
21
32
  }
22
33
 
23
- if (error.stack != null && original.stack != null) {
24
- error.stack += `\ncaused by: ${original.stack}`
34
+ // stack is printed in reverse order (from cause to more high level error)
35
+ if (error.stack != null && originalNormalized.stack != null) {
36
+ error.stack = `${originalNormalized.stack}\nwrapped by:\n${error.stack}`
25
37
  }
26
38
  }
27
39
  }
28
40
 
29
- Object.assign(error, _omit(options, 'original'))
41
+ const { original: originalInOptions, ...restOfOptions } = options
42
+
43
+ Object.assign(error, restOfOptions)
30
44
 
31
45
  if ((error.statusCode === 400 || error.statusCode === 404) && error.weak == null) {
32
46
  error.weak = true
@@ -34,3 +48,11 @@ module.exports = function (message, options = {}) {
34
48
 
35
49
  return error
36
50
  }
51
+
52
+ function lowerCaseFirstLetter (str) {
53
+ if (str === '' || typeof str !== 'string') {
54
+ return str
55
+ }
56
+
57
+ return str.charAt(0).toLowerCase() + str.slice(1)
58
+ }
@@ -0,0 +1,31 @@
1
+
2
+ module.exports = function normalizeError (errValue, normalizedErrorPrefix = 'User code threw with non-Error: ') {
3
+ let newError
4
+
5
+ const isErrorObj = (
6
+ typeof errValue === 'object' &&
7
+ typeof errValue.hasOwnProperty === 'function' &&
8
+ Object.prototype.hasOwnProperty.call(errValue, 'message')
9
+ )
10
+
11
+ const isValidError = (
12
+ isErrorObj ||
13
+ typeof errValue === 'string'
14
+ )
15
+
16
+ if (!isValidError) {
17
+ if (Object.prototype.toString.call(errValue) === '[object Object]') {
18
+ newError = new Error(`${normalizedErrorPrefix}${JSON.stringify(errValue)}`)
19
+ } else {
20
+ newError = new Error(`${normalizedErrorPrefix}${errValue}`)
21
+ }
22
+ } else {
23
+ if (typeof errValue === 'string') {
24
+ newError = new Error(errValue)
25
+ } else {
26
+ newError = errValue
27
+ }
28
+ }
29
+
30
+ return newError
31
+ }
@@ -6,7 +6,8 @@ const Folders = require('./folders')
6
6
  const createOrExtendError = require('./createError')
7
7
  const tempFilesHandler = require('./tempFilesHandler')
8
8
  const encryption = require('./encryption')
9
- const generateRequestId = require('../shared/generateRequestId')
9
+ const generateRequestId = require('./generateRequestId')
10
+ const adminRequest = require('./adminRequest')
10
11
 
11
12
  class Reporter extends EventEmitter {
12
13
  constructor (options) {
@@ -14,6 +15,7 @@ class Reporter extends EventEmitter {
14
15
 
15
16
  this.options = options || {}
16
17
  this.Request = Request
18
+ this.adminRequest = adminRequest
17
19
 
18
20
  // since `reporter` instance will be used for other extensions,
19
21
  // it will quickly reach the limit of `10` listeners,
@@ -1,5 +1,6 @@
1
1
  const extend = require('node.extend.without.arrays')
2
2
  const omit = require('lodash.omit')
3
+ const createError = require('./createError')
3
4
 
4
5
  module.exports = (obj, parent) => {
5
6
  if (parent && !parent.__isJsreportRequest__) {
@@ -57,7 +58,14 @@ module.exports = (obj, parent) => {
57
58
 
58
59
  function normalizeJSONData (data) {
59
60
  if (typeof data === 'string') {
60
- return JSON.parse(data)
61
+ try {
62
+ return JSON.parse(data)
63
+ } catch (parseError) {
64
+ throw createError('Unable to parse request data', {
65
+ weak: true,
66
+ original: parseError
67
+ })
68
+ }
61
69
  }
62
70
 
63
71
  return data
@@ -2,14 +2,10 @@ module.exports = ({ model, collections }, executeMainAction) => {
2
2
  const store = {
3
3
  model,
4
4
  collection: (name) => ({
5
- find: (q, req) => executeMainAction('documentStore.collection.find', {
6
- query: q,
7
- collection: name
8
- }, req),
9
- findOne: (q, req) => executeMainAction('documentStore.collection.findOne', {
10
- query: q,
11
- collection: name
12
- }, req),
5
+ find: findMethod('documentStore.collection.find', name, false, executeMainAction),
6
+ findOne: findMethod('documentStore.collection.findOne', name, false, executeMainAction),
7
+ findAdmin: findMethod('documentStore.collection.find', name, true, executeMainAction),
8
+ findOneAdmin: findMethod('documentStore.collection.findOne', name, true, executeMainAction),
13
9
  insert: async (doc, req) => {
14
10
  const entity = await executeMainAction('documentStore.collection.insert', {
15
11
  doc,
@@ -47,3 +43,18 @@ module.exports = ({ model, collections }, executeMainAction) => {
47
43
 
48
44
  return store
49
45
  }
46
+
47
+ function findMethod (actionName, collectionName, admin, executeMainAction) {
48
+ return async (q, req) => {
49
+ const payload = {
50
+ query: q,
51
+ collection: collectionName
52
+ }
53
+
54
+ if (admin) {
55
+ payload.admin = admin
56
+ }
57
+
58
+ return executeMainAction(actionName, payload, req)
59
+ }
60
+ }
@@ -203,10 +203,13 @@ module.exports = (reporter) => {
203
203
  const wrappedTopLevelFunctions = {}
204
204
 
205
205
  for (const h of Object.keys(topLevelFunctions)) {
206
+ // extra wrapping for enhance the error with the helper name
207
+ wrappedTopLevelFunctions[h] = wrapHelperForHelperNameWhenError(topLevelFunctions[h], h)
208
+
206
209
  if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
207
- wrappedTopLevelFunctions[h] = engine.wrapHelper(topLevelFunctions[h], { context })
210
+ wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
208
211
  } else {
209
- wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(topLevelFunctions[h], asyncResultMap)
212
+ wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap)
210
213
  }
211
214
  }
212
215
 
@@ -279,10 +282,14 @@ module.exports = (reporter) => {
279
282
  const nestedErrorWithEntity = e.entity != null
280
283
 
281
284
  const templatePath = req.template._id ? await reporter.folders.resolveEntityPath(req.template, 'templates', req) : 'anonymous'
285
+
286
+ const newError = reporter.createError(`Error when evaluating engine ${engine.name} for template ${templatePath}`, { original: e })
287
+
282
288
  if (templatePath !== 'anonymous' && !nestedErrorWithEntity) {
283
289
  const templateFound = await reporter.folders.resolveEntityFromPath(templatePath, 'templates', req)
290
+
284
291
  if (templateFound != null) {
285
- e.entity = {
292
+ newError.entity = {
286
293
  shortid: templateFound.entity.shortid,
287
294
  name: templateFound.entity.name,
288
295
  content
@@ -290,24 +297,29 @@ module.exports = (reporter) => {
290
297
  }
291
298
  }
292
299
 
293
- e.message = `Error when evaluating engine ${engine.name} for template ${templatePath}\n` + e.message
294
-
295
300
  if (!nestedErrorWithEntity && e.property !== 'content') {
296
- e.property = 'helpers'
301
+ newError.property = 'helpers'
297
302
  }
298
303
 
299
304
  if (nestedErrorWithEntity) {
300
305
  // errors from nested assets evals needs an unwrap for some reason
301
- e.entity = { ...e.entity }
306
+ newError.entity = { ...e.entity }
302
307
  }
303
308
 
304
- throw e
309
+ // we remove the decoratedSuffix (created from sandbox) from the stack trace (if it is there) because it
310
+ // just creates noise and duplication when printing the error,
311
+ // we just want the decoration on the message not in the stack trace
312
+ if (e.decoratedSuffix != null && newError.stack.includes(e.decoratedSuffix)) {
313
+ newError.stack = newError.stack.replace(e.decoratedSuffix, '')
314
+ }
315
+
316
+ throw newError
305
317
  }
306
318
  }
307
319
 
308
320
  function wrapHelperForAsyncSupport (fn, asyncResultMap) {
309
321
  return function (...args) {
310
- // important to call the helper with the current this to preserve the same behavior
322
+ // important to call the helper with the current this to preserve the same behavior
311
323
  const fnResult = fn.call(this, ...args)
312
324
 
313
325
  if (fnResult == null || typeof fnResult.then !== 'function') {
@@ -320,4 +332,27 @@ module.exports = (reporter) => {
320
332
  return `{#asyncHelperResult ${asyncResultId}}`
321
333
  }
322
334
  }
335
+
336
+ function wrapHelperForHelperNameWhenError (fn, helperName) {
337
+ return function (...args) {
338
+ let fnResult
339
+
340
+ const getEnhancedHelperError = (e) => reporter.createError(`"${helperName}" helper call failed`, { original: e })
341
+
342
+ try {
343
+ // important to call the helper with the current this to preserve the same behavior
344
+ fnResult = fn.call(this, ...args)
345
+ } catch (syncError) {
346
+ throw getEnhancedHelperError(syncError)
347
+ }
348
+
349
+ if (fnResult == null || typeof fnResult.then !== 'function') {
350
+ return fnResult
351
+ }
352
+
353
+ return fnResult.catch((asyncError) => {
354
+ throw getEnhancedHelperError(asyncError)
355
+ })
356
+ }
357
+ }
323
358
  }
@@ -126,8 +126,9 @@ class Profiler {
126
126
 
127
127
  if (parentReq) {
128
128
  template = await this.reporter.templates.resolveTemplate(req)
129
+
129
130
  // store a copy to prevent side-effects
130
- req.context.resolvedTemplate = extend(true, {}, template)
131
+ req.context.resolvedTemplate = template != null ? extend(true, {}, template) : template
131
132
  } else {
132
133
  template = req.context.resolvedTemplate
133
134
  }
@@ -130,7 +130,7 @@ module.exports = (reporter) => {
130
130
  reporter.requestModulesCache.set(request.context.rootId, Object.create(null))
131
131
  }
132
132
 
133
- reporter.logger.info(`Starting rendering request ${request.context.reportCounter} (user: ${(request.context.user ? request.context.user.name : 'null')})`, request)
133
+ reporter.logger.info(`Starting rendering${childMsgDecorate(request)} request${counterMsgDecorate(request)}${userMsgDecorate(request)}`, request)
134
134
 
135
135
  // TODO
136
136
  /* if (reporter.entityTypeValidator.getSchema('TemplateType') != null) {
@@ -147,7 +147,7 @@ module.exports = (reporter) => {
147
147
  await invokeRender(reporter, request, response)
148
148
  await afterRender(reporter, request, response)
149
149
 
150
- reporter.logger.info(`Rendering request ${request.context.reportCounter} finished in ${(new Date().getTime() - request.context.startTimestamp)} ms`, request)
150
+ reporter.logger.info(`Rendering${childMsgDecorate(request)} request${counterMsgDecorate(request)} finished in ${(new Date().getTime() - request.context.startTimestamp)} ms`, request)
151
151
 
152
152
  response.meta.logs = request.context.logs
153
153
 
@@ -164,9 +164,11 @@ module.exports = (reporter) => {
164
164
 
165
165
  const logFn = e.weak ? reporter.logger.warn : reporter.logger.error
166
166
 
167
- logFn(`Error when processing render request ${request.context.reportCounter} ${e.message}${e.stack != null ? ' ' + e.stack : ''}`, request)
167
+ const errorMessage = reporter.createError(`Error when processing${childMsgDecorate(request)} render request${counterMsgDecorate(request)}`, { original: e }).message
168
168
 
169
- logFn(`Rendering request ${request.context.reportCounter} finished with error in ${(new Date().getTime() - request.context.startTimestamp)} ms`, request)
169
+ logFn(`${errorMessage}${e.stack != null ? '\n' + e.stack : ''}`, request)
170
+
171
+ logFn(`Rendering${childMsgDecorate(request)} request${counterMsgDecorate(request)} finished with error in ${(new Date().getTime() - request.context.startTimestamp)} ms`, request)
170
172
 
171
173
  if (
172
174
  parentReq &&
@@ -196,3 +198,27 @@ module.exports = (reporter) => {
196
198
  }
197
199
  }
198
200
  }
201
+
202
+ function childMsgDecorate (req) {
203
+ if (!req.context.isChildRequest) {
204
+ return ''
205
+ }
206
+
207
+ return ' (child)'
208
+ }
209
+
210
+ function counterMsgDecorate (req) {
211
+ if (req.context.isChildRequest) {
212
+ return ''
213
+ }
214
+
215
+ return ` ${req.context.reportCounter}`
216
+ }
217
+
218
+ function userMsgDecorate (req) {
219
+ if (!req.context.user) {
220
+ return ''
221
+ }
222
+
223
+ return ` (user: ${req.context.user.name})`
224
+ }
@@ -372,7 +372,11 @@ function decorateErrorMessage (e, sourceFilesInfo) {
372
372
  }
373
373
 
374
374
  if (suffix !== '') {
375
- e.message = `${e.message}\n\n${suffix}`
375
+ suffix = `\n\n${suffix}`
376
+ e.message = `${e.message}${suffix}`
377
+ // we store the suffix we added to the message so we can use it later
378
+ // to detect if we need to strip this from the stack or not
379
+ e.decoratedSuffix = suffix
376
380
  }
377
381
  }
378
382
 
@@ -1,8 +1,9 @@
1
1
  const LRU = require('lru-cache')
2
2
  const stackTrace = require('stack-trace')
3
3
  const { customAlphabet } = require('nanoid')
4
- const createSandbox = require('./createSandbox')
5
4
  const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
+ const createSandbox = require('./createSandbox')
6
+ const normalizeError = require('../../shared/normalizeError')
6
7
 
7
8
  module.exports = (reporter) => {
8
9
  const functionsCache = LRU(reporter.options.sandbox.cache)
@@ -36,7 +37,16 @@ module.exports = (reporter) => {
36
37
  onLog: (log) => {
37
38
  // we mark any log done in sandbox as userLevel: true, this allows us to detect which logs belongs to user
38
39
  // and can potentially contain sensitive information
39
- reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp, userLevel: true })
40
+
41
+ let consoleType = log.level
42
+
43
+ if (consoleType === 'debug') {
44
+ consoleType = 'log'
45
+ } else if (consoleType === 'warn') {
46
+ consoleType = 'warning'
47
+ }
48
+
49
+ reporter.logger.debug(`(console:${consoleType}) ${log.message}`, { ...req, timestamp: log.timestamp, userLevel: true })
40
50
  },
41
51
  formatError: (error, moduleName) => {
42
52
  error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
@@ -135,10 +145,14 @@ module.exports = (reporter) => {
135
145
  const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
136
146
 
137
147
  if (initScriptInfo) {
138
- await run(initScriptInfo.script, {
139
- filename: initScriptInfo.filename || 'sandbox-init.js',
140
- source: initScriptInfo.source
141
- })
148
+ try {
149
+ await run(initScriptInfo.script, {
150
+ filename: initScriptInfo.filename || 'sandbox-init.js',
151
+ source: initScriptInfo.source
152
+ })
153
+ } catch (e) {
154
+ handleError(reporter, e)
155
+ }
142
156
  }
143
157
  }
144
158
 
@@ -162,43 +176,29 @@ module.exports = (reporter) => {
162
176
  })
163
177
  }).catch(__handleError);`
164
178
 
165
- return run(executionCode, {
166
- filename: 'sandbox.js',
167
- source: userCode,
168
- errorLineNumberOffset
169
- })
179
+ try {
180
+ return await run(executionCode, {
181
+ filename: 'sandbox.js',
182
+ source: userCode,
183
+ errorLineNumberOffset
184
+ })
185
+ } catch (e) {
186
+ handleError(reporter, e)
187
+ }
170
188
  }
171
189
  }
172
190
 
173
191
  function handleError (reporter, errValue) {
174
- let newError
175
-
176
- const isErrorObj = (
177
- typeof errValue === 'object' &&
178
- typeof errValue.hasOwnProperty === 'function' &&
179
- Object.prototype.hasOwnProperty.call(errValue, 'message')
180
- )
181
-
182
- const isValidError = (
183
- isErrorObj ||
184
- typeof errValue === 'string'
185
- )
186
-
187
- if (!isValidError) {
188
- if (Object.prototype.toString.call(errValue) === '[object Object]') {
189
- newError = new Error(`User code threw with non-Error: ${JSON.stringify(errValue)}`)
190
- } else {
191
- newError = new Error(`User code threw with non-Error: ${errValue}`)
192
- }
193
- } else {
194
- if (typeof errValue === 'string') {
195
- newError = new Error(errValue)
196
- } else {
197
- newError = new Error(errValue.message)
198
- Object.assign(newError, errValue)
199
- if (errValue.stack) {
200
- newError.stack = errValue.stack
201
- }
192
+ let newError = normalizeError(errValue)
193
+
194
+ if (newError === errValue) {
195
+ // here it means the original error was valid in the first place,
196
+ // so it was not normalized
197
+ newError = new Error(errValue.message)
198
+ Object.assign(newError, errValue)
199
+
200
+ if (errValue.stack) {
201
+ newError.stack = errValue.stack
202
202
  }
203
203
  }
204
204
 
@@ -28,12 +28,43 @@ module.exports = (reporter) => {
28
28
  const template = req.context.resolvedTemplate
29
29
 
30
30
  if (!template && !req.template.content) {
31
- throw reporter.createError(`Unable to find specified template or user does not have permissions to read it: ${
32
- (req.template._id || req.template.shortid || req.template.name)
33
- }`, {
34
- weak: true,
35
- statusCode: 404
36
- })
31
+ let error
32
+
33
+ if (
34
+ req.template._id ||
35
+ req.template.shortid ||
36
+ req.template.name
37
+ ) {
38
+ const query = {}
39
+
40
+ if (req.template._id) {
41
+ query._id = req.template._id
42
+ } else if (req.template.shortid) {
43
+ query.shortid = req.template.shortid
44
+ } else if (req.template.name) {
45
+ query.name = req.template.name
46
+ }
47
+
48
+ const templateFromLocal = await reporter.documentStore.collection('templates').findOneAdmin(query, req)
49
+
50
+ if (templateFromLocal == null) {
51
+ error = reporter.createError(`Unable to find specified template (${
52
+ (req.template.name || req.template.shortid || req.template._id)
53
+ })`, {
54
+ weak: true,
55
+ statusCode: 404
56
+ })
57
+ } else {
58
+ error = reporter.createError(`User does not have permissions to read template (${
59
+ (req.template.name || req.template.shortid || req.template._id)
60
+ })`, {
61
+ weak: true,
62
+ statusCode: 403
63
+ })
64
+ }
65
+ }
66
+
67
+ throw error
37
68
  }
38
69
 
39
70
  // store a copy to prevent side-effects, we ignore name from the req.template because it can be path "/path/to/template"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.8.1",
3
+ "version": "3.10.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -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",