@jsreport/jsreport-core 3.2.0 → 3.4.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
@@ -36,7 +36,7 @@ const result = await jsreport.render({
36
36
  foo: "world"
37
37
  }
38
38
  })
39
- await fs.writeFile('out.pdf', resp.content)
39
+ await fs.writeFile('out.pdf', result.content)
40
40
  ```
41
41
 
42
42
  ## Render
@@ -282,6 +282,22 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.4.2
286
+
287
+ - update dep `vm2` to fix security vulnerability in sandbox
288
+
289
+ ### 3.4.1
290
+
291
+ - fix passing data to async report
292
+ - fix blob appends
293
+
294
+ ### 3.4.0
295
+
296
+ - fix for reports execution
297
+ - fix for render profiling
298
+ - fix for blob storage remove
299
+ - update deps to fix npm audit
300
+
285
301
  ### 3.1.0
286
302
 
287
303
  - fix blob storage append to not existing blob (mongo)
@@ -25,6 +25,7 @@ module.exports = (reporter, options) => {
25
25
  let existingBuf = Buffer.from([])
26
26
  try {
27
27
  existingBuf = await provider.read(blobName, req)
28
+ await provider.remove(blobName, req)
28
29
  } catch (e) {
29
30
  // so far blob storage throws when blob doesnt exit
30
31
  }
@@ -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)
@@ -133,15 +133,9 @@ async function loadConfig (defaults, options, loadExternal = true) {
133
133
  const currentValue = obj.value[extensionKey]
134
134
  delete obj.value[extensionKey]
135
135
 
136
- if (realExtensionName !== extensionKey && obj.value[realExtensionName]) {
137
- obj.value[realExtensionName] = extend(
138
- true,
139
- obj.value[realExtensionName],
140
- currentValue
141
- )
142
- } else {
143
- obj.value[realExtensionName] = currentValue
144
- }
136
+ // the camelCase key version should already contain all merged values
137
+ // (from both the real extension name with "-" and camel case)
138
+ obj.value[realExtensionName] = currentValue
145
139
  })
146
140
  } else if (!normalize && obj.key.startsWith('extensions')) {
147
141
  // the transform ensures that camelCase alias keys of extensions
@@ -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
 
@@ -25,7 +25,7 @@ const setupValidateShortid = require('./store/setupValidateShortid')
25
25
  const documentStoreActions = require('./store/mainActions')
26
26
  const blobStorageActions = require('./blobStorage/mainActions')
27
27
  const Reporter = require('../shared/reporter')
28
- const Request = require('../shared/request')
28
+ const Request = require('./request')
29
29
  const generateRequestId = require('../shared/generateRequestId')
30
30
  const Profiler = require('./profiler')
31
31
  const Monitoring = require('./monitoring')
@@ -315,7 +315,7 @@ class MainReporter extends Reporter {
315
315
  *
316
316
  * @public
317
317
  */
318
- async render (req, parentReq) {
318
+ async render (req, options = {}) {
319
319
  if (!this._initialized) {
320
320
  throw new Error('Not initialized, you need to call jsreport.init().then before rendering')
321
321
  }
@@ -325,21 +325,18 @@ class MainReporter extends Reporter {
325
325
  req.context.rootId = req.context.rootId || generateRequestId()
326
326
  req.context.id = req.context.rootId
327
327
 
328
- const worker = await this._workersManager.allocate(req, {
328
+ const worker = options.worker || await this._workersManager.allocate(req, {
329
329
  timeout: this.options.reportTimeout
330
330
  })
331
331
 
332
+ let keepWorker
332
333
  let workerAborted
333
- if (parentReq && !parentReq.__isJsreportRequest__) {
334
- const options = parentReq
335
- parentReq = null
336
-
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
- }
334
+
335
+ if (options.abortEmitter) {
336
+ options.abortEmitter.once('abort', () => {
337
+ workerAborted = true
338
+ worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
339
+ })
343
340
  }
344
341
 
345
342
  const res = { meta: {} }
@@ -348,10 +345,7 @@ class MainReporter extends Reporter {
348
345
  throw this.createError('Request aborted by client')
349
346
  }
350
347
 
351
- let isDataStoredInWorker = false
352
-
353
348
  if (req.rawContent) {
354
- isDataStoredInWorker = true
355
349
  const result = await worker.execute({
356
350
  actionName: 'parse',
357
351
  req,
@@ -362,13 +356,7 @@ class MainReporter extends Reporter {
362
356
  req = result
363
357
  }
364
358
 
365
- req = Request(req, parentReq)
366
-
367
- if (isDataStoredInWorker) {
368
- // we unset this because we want the Request() call in worker to evaluate the data
369
- // and determine if the original was empty or not
370
- delete req.context.originalInputDataIsEmpty
371
- }
359
+ req = Request(req)
372
360
 
373
361
  // TODO: we will probably validate in the thread
374
362
  if (this.entityTypeValidator.getSchema('TemplateType') != null) {
@@ -388,13 +376,31 @@ class MainReporter extends Reporter {
388
376
  req.options &&
389
377
  req.options.timeout != null
390
378
  ) {
391
- reportTimeout = req
379
+ reportTimeout = req.options.timeout
392
380
  }
393
381
 
394
- await this.beforeRenderListeners.fire(req, res)
382
+ await this.beforeRenderListeners.fire(req, res, { worker })
395
383
 
396
- if (req.context.isFinished) {
384
+ // this is used so far just in the reports extension
385
+ // it wants to send to the client immediate response with link to the report status
386
+ // but the previous steps already allocated worker which has the parsed input request
387
+ // so we need to keep the worker active and let the subsequent real render call use it
388
+ // we cant move the main beforeRenderListener before the worker allocation, because at that point
389
+ // the request isn't parsed and we don't know the template and options
390
+ if (req.context.returnResponseAndKeepWorker) {
391
+ keepWorker = true
397
392
  res.stream = Readable.from(res.content)
393
+
394
+ // just temporary workaround until we change how report render works
395
+ await this.documentStore.collection('profiles').update({
396
+ _id: req.context.profiling.entity._id
397
+ }, {
398
+ $set: {
399
+ state: 'success',
400
+ finishedOn: new Date(),
401
+ blobPersisted: true
402
+ }
403
+ }, req)
398
404
  return res
399
405
  }
400
406
 
@@ -414,6 +420,15 @@ class MainReporter extends Reporter {
414
420
  } catch (err) {
415
421
  if (err.code === 'WORKER_TIMEOUT') {
416
422
  err.message = 'Report timeout'
423
+ if (req.context.profiling?.lastOperation != null && req.context.profiling?.entity != null) {
424
+ err.message += `. Last profiler operation: (${req.context.profiling.lastOperation.subtype}) ${req.context.profiling.lastOperation.name}`
425
+ }
426
+
427
+ if (req.context.http != null) {
428
+ const profileUrl = `${req.context.http.baseUrl}/studio/profiles/${req.context.profiling.entity._id}`
429
+ err.message += `. You can inspect and find more details here: ${profileUrl}`
430
+ }
431
+
417
432
  err.weak = true
418
433
  }
419
434
 
@@ -429,7 +444,7 @@ class MainReporter extends Reporter {
429
444
  await this.renderErrorListeners.fire(req, res, err)
430
445
  throw err
431
446
  } finally {
432
- if (!workerAborted) {
447
+ if (!workerAborted && !keepWorker) {
433
448
  await worker.release(req)
434
449
  }
435
450
  }
@@ -0,0 +1,21 @@
1
+ const extend = require('node.extend.without.arrays')
2
+
3
+ module.exports = (obj) => {
4
+ const request = Object.create({}, {
5
+ __isJsreportRequest__: {
6
+ value: true,
7
+ writable: false,
8
+ configurable: false,
9
+ enumerable: false
10
+ }
11
+ })
12
+
13
+ request.template = extend(true, {}, obj.template)
14
+
15
+ request.options = extend(true, {}, request.options, obj.options)
16
+ request.context = extend(true, {}, request.context, obj.context)
17
+ request.context.shared = extend(true, {}, request.context.shared)
18
+ request.data = obj.data
19
+
20
+ return request
21
+ }
@@ -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}`
@@ -8,7 +8,7 @@ class Profiler {
8
8
  constructor (reporter) {
9
9
  this.reporter = reporter
10
10
 
11
- this.reporter.addRequestContextMetaConfig('profiling', { sandboxReadOnly: true })
11
+ this.reporter.addRequestContextMetaConfig('profiling', { sandboxHidden: true })
12
12
  this.reporter.addRequestContextMetaConfig('resolvedTemplate', { sandboxHidden: true })
13
13
 
14
14
  this.reporter.beforeMainActionListeners.add('profiler', (actionName, data, req) => {
@@ -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
 
@@ -89,7 +93,7 @@ class Profiler {
89
93
 
90
94
  m.req = { diff: createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0) }
91
95
 
92
- req.context.profiling.resLastVal = res.content
96
+ req.context.profiling.resLastVal = (res.content == null || isbinaryfile(res.content)) ? null : res.content.toString()
93
97
  req.context.profiling.resMetaLastVal = stringifiedResMeta
94
98
  req.context.profiling.reqLastVal = stringifiedReq
95
99
  }
@@ -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, sandbox, safeRequire } = safeSandbox(context, {
31
32
  onLog: (log) => {
32
33
  reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp })
33
34
  },
@@ -61,16 +62,57 @@ 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
68
113
  // we don't attach these methods to the sandbox, and instead share them through a "manager" object that should
69
114
  // be passed in options
70
115
  manager.restore = restore
71
- manager.contextifyValue = contextifyValue
72
- manager.decontextifyValue = decontextifyValue
73
- manager.unproxyValue = unproxyValue
74
116
 
75
117
  const functionNames = getTopLevelFunctions(userCode)
76
118
  const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
@@ -113,26 +113,12 @@ module.exports = (_sandbox, options = {}) => {
113
113
 
114
114
  const vm = new VM()
115
115
 
116
- // NOTE: we wrap the Contextify.object, Decontextify.object methods because those are the
117
- // methods that returns the proxies created by vm2 in the sandbox, we want to have a list of those
118
- // to later use them
119
- const wrapAndSaveProxyResult = (originalFn, thisArg) => {
120
- return (value, ...args) => {
121
- const result = originalFn.call(thisArg, value, ...args)
122
-
123
- if (result != null && result.isVMProxy === true) {
124
- proxiesInVM.set(result, value)
125
- }
126
-
127
- return result
128
- }
129
- }
130
-
131
- vm._internal.Contextify.object = wrapAndSaveProxyResult(vm._internal.Contextify.object, vm._internal.Contextify)
132
- vm._internal.Decontextify.object = wrapAndSaveProxyResult(vm._internal.Decontextify.object, vm._internal.Decontextify)
116
+ // delete the vm.sandbox.global because it introduces json stringify issues
117
+ // and we don't need such global in context
118
+ delete vm.sandbox.global
133
119
 
134
120
  for (const name in sandbox) {
135
- vm._internal.Contextify.setGlobal(name, sandbox[name])
121
+ vm.setGlobal(name, sandbox[name])
136
122
  }
137
123
 
138
124
  // processing top level props because getter/setter descriptors
@@ -141,33 +127,25 @@ module.exports = (_sandbox, options = {}) => {
141
127
  const currentConfig = propsConfig[key]
142
128
 
143
129
  if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
144
- readOnlyProp(vm._context, key, [], customProxies, { onlyTopLevel: true })
130
+ readOnlyProp(vm.sandbox, key, [], customProxies, { onlyTopLevel: true })
145
131
  }
146
132
  })
147
133
 
148
134
  const sourceFilesInfo = new Map()
149
135
 
150
136
  return {
151
- sandbox: vm._context,
137
+ sandbox: vm.sandbox,
152
138
  console: _console,
153
- contextifyValue: (value) => {
154
- return vm._internal.Contextify.value(value)
155
- },
156
- decontextifyValue: (value) => {
157
- return vm._internal.Decontextify.value(value)
158
- },
139
+ sourceFilesInfo,
159
140
  restore: () => {
160
- return restoreProperties(vm._context, originalValues, proxiesInVM, customProxies)
161
- },
162
- unproxyValue: (value) => {
163
- return getOriginalFromProxy(proxiesInVM, customProxies, value)
141
+ return restoreProperties(vm.sandbox, originalValues, proxiesInVM, customProxies)
164
142
  },
165
143
  safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
166
- run: async (code, { filename, errorLineNumberOffset = 0, source, entity } = {}) => {
144
+ run: async (code, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
167
145
  const script = new VMScript(code, filename)
168
146
 
169
147
  if (filename != null && source != null) {
170
- sourceFilesInfo.set(filename, { filename, source, entity, errorLineNumberOffset })
148
+ sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
171
149
  }
172
150
 
173
151
  // NOTE: if we need to upgrade vm2 we will need to check the source of this function
@@ -176,6 +154,7 @@ module.exports = (_sandbox, options = {}) => {
176
154
  // to show nice error when the compile of a script fails
177
155
  script._compile = function (prefix, suffix) {
178
156
  return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
157
+ __proto__: null,
179
158
  filename: this.filename,
180
159
  displayErrors: true,
181
160
  lineOffset: this.lineOffset,
@@ -31,6 +31,7 @@ module.exports = (userInitData, { executeMain, convertUint8ArrayToBuffer }) => {
31
31
  ...JSON.parse(req.rawContent),
32
32
  context: req.context
33
33
  }
34
+ parsedReq.context.parsedInWorker = true
34
35
 
35
36
  return omit(parsedReq, 'data')
36
37
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.2.0",
3
+ "version": "3.4.2",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -53,8 +53,8 @@
53
53
  "lodash.set": "4.3.2",
54
54
  "lru-cache": "4.1.1",
55
55
  "ms": "2.1.3",
56
- "nanoid": "3.1.16",
57
- "nconf": "0.10.0",
56
+ "nanoid": "3.2.0",
57
+ "nconf": "0.11.3",
58
58
  "node.extend.without.arrays": "1.1.6",
59
59
  "reap2": "1.0.1",
60
60
  "semver": "7.3.5",
@@ -63,8 +63,7 @@
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
- "vm2": "3.9.5",
66
+ "vm2": "3.9.7",
68
67
  "winston": "3.3.3",
69
68
  "winston-transport": "4.4.0"
70
69
  },
@@ -18,4 +18,8 @@ module.exports = (storage) => {
18
18
  const buf = await storage().read('foldera/folderb/myblob.txt')
19
19
  buf.toString().should.be.eql('hula')
20
20
  })
21
+
22
+ it('remove shouldnt fail for missing blob', async () => {
23
+ await storage().remove('foo')
24
+ })
21
25
  }