@jsreport/jsreport-core 3.4.2 → 3.6.1

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,24 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.6.0
286
+
287
+ - improve handling of worker exit
288
+ - add a way to disable the safe execution and prevent the performance penalty
289
+ - cache system helpers function
290
+ - `profiler.maxDiffSize` applies to both req and res
291
+ - improved the support for running jsreport as serverless function
292
+ - added new apis in `jsreport-proxy` for better working with async helpers
293
+
294
+ ### 3.5.0
295
+
296
+ - fix parsing issue of code with comment in the sandbox (helpers, scripts)
297
+ - improve profiling when there is big data
298
+ - make transactions support in store configurable
299
+ - improve timeout for the whole request
300
+ - fix applying req.options.timeout when enableRequestReportTimeout is true
301
+ - optimization regarding profile persistence
302
+
285
303
  ### 3.4.2
286
304
 
287
305
  - update dep `vm2` to fix security vulnerability in sandbox
@@ -46,6 +46,14 @@ module.exports = (reporter, options) => {
46
46
  }
47
47
  },
48
48
 
49
+ get supportsAppend () {
50
+ return provider.append instanceof Function
51
+ },
52
+
53
+ get _provider () {
54
+ return provider
55
+ },
56
+
49
57
  registerProvider (p) {
50
58
  provider = p
51
59
  }
@@ -166,6 +166,9 @@ function configureLogger (logger, _transports) {
166
166
  }
167
167
 
168
168
  for (const { TransportClass, options } of transportsToAdd) {
169
+ if (options.silent) {
170
+ continue
171
+ }
169
172
  const transportInstance = new TransportClass(options)
170
173
 
171
174
  const existingTransport = logger.transports.find((t) => t.name === transportInstance.name)
@@ -39,6 +39,7 @@ async function optionsLoad ({
39
39
  loadConfigResult = await loadConfig(defaults, options, false)
40
40
  }
41
41
 
42
+ const explicitOptions = loadConfigResult[0]
42
43
  const appliedConfigFile = loadConfigResult[1]
43
44
 
44
45
  options.loadConfig = shouldLoadExternalConfig
@@ -79,9 +80,16 @@ async function optionsLoad ({
79
80
  options.store = options.store || { provider: 'memory' }
80
81
 
81
82
  options.sandbox = options.sandbox || {}
82
- if (options.allowLocalFilesAccess === true) {
83
+
84
+ // NOTE: handling back-compatible introduction of "trustUserCode" option, "allowLocalFilesAccess" is deprecated
85
+ if (explicitOptions.allowLocalFilesAccess === true && explicitOptions.trustUserCode == null) {
86
+ options.trustUserCode = true
87
+ }
88
+
89
+ if (options.trustUserCode === true) {
83
90
  options.sandbox.allowedModules = '*'
84
91
  }
92
+
85
93
  options.sandbox.nativeModules = options.sandbox.nativeModules || []
86
94
  options.sandbox.modules = options.sandbox.modules || []
87
95
  options.sandbox.allowedModules = options.sandbox.allowedModules || []
@@ -98,7 +106,7 @@ async function optionsLoad ({
98
106
  fs.mkdirSync(options.tempCoreDirectory, { recursive: true })
99
107
  }
100
108
 
101
- return appliedConfigFile
109
+ return [explicitOptions, appliedConfigFile]
102
110
  }
103
111
 
104
112
  /**
@@ -59,6 +59,7 @@ module.exports.getRootSchemaOptions = () => ({
59
59
  default: '2s'
60
60
  },
61
61
  enableRequestReportTimeout: { type: 'boolean', default: false, description: 'option that enables passing a custom report timeout per request using req.options.timeout. this enables that the caller of the report generation control the report timeout so enable it only when you trust the caller' },
62
+ trustUserCode: { type: 'boolean', default: false, description: 'option that control whether code sandboxing is enabled or not, code sandboxing has an impact on performance when rendering large reports. when true code sandboxing will be disabled meaning that users can potentially penetrate the local system if you allow code from external users to be part of your reports' },
62
63
  allowLocalFilesAccess: { type: 'boolean', default: false },
63
64
  encryption: {
64
65
  type: 'object',
@@ -77,6 +78,7 @@ module.exports.getRootSchemaOptions = () => ({
77
78
  },
78
79
  sandbox: {
79
80
  type: 'object',
81
+ default: {},
80
82
  properties: {
81
83
  allowedModules: {
82
84
  anyOf: [{
@@ -89,9 +91,10 @@ module.exports.getRootSchemaOptions = () => ({
89
91
  },
90
92
  cache: {
91
93
  type: 'object',
94
+ default: {},
92
95
  properties: {
93
- max: { type: 'number' },
94
- enabled: { type: 'boolean' }
96
+ max: { type: 'number', default: 100 },
97
+ enabled: { type: 'boolean', default: true }
95
98
  }
96
99
  }
97
100
  }
@@ -126,7 +129,13 @@ module.exports.getRootSchemaOptions = () => ({
126
129
  store: {
127
130
  type: 'object',
128
131
  properties: {
129
- provider: { type: 'string', enum: ['memory'] }
132
+ provider: { type: 'string', enum: ['memory'] },
133
+ transactions: {
134
+ type: 'object',
135
+ properties: {
136
+ enabled: { type: 'boolean', default: true }
137
+ }
138
+ }
130
139
  }
131
140
  },
132
141
  blobStorage: {
@@ -163,6 +172,10 @@ module.exports.getRootSchemaOptions = () => ({
163
172
  type: 'object',
164
173
  default: {},
165
174
  properties: {
175
+ defaultMode: {
176
+ type: 'string',
177
+ default: 'standard'
178
+ },
166
179
  maxProfilesHistory: {
167
180
  type: 'number',
168
181
  default: 1000
@@ -171,6 +184,11 @@ module.exports.getRootSchemaOptions = () => ({
171
184
  type: ['string', 'number'],
172
185
  '$jsreport-acceptsDuration': true,
173
186
  default: '1m'
187
+ },
188
+ maxDiffSize: {
189
+ type: ['string', 'number'],
190
+ '$jsreport-acceptsSize': true,
191
+ default: '50mb'
174
192
  }
175
193
  }
176
194
  }
@@ -1,6 +1,7 @@
1
1
  const EventEmitter = require('events')
2
2
  const extend = require('node.extend.without.arrays')
3
3
  const generateRequestId = require('../shared/generateRequestId')
4
+ const fs = require('fs/promises')
4
5
 
5
6
  module.exports = (reporter) => {
6
7
  reporter.documentStore.registerEntityType('ProfileType', {
@@ -8,9 +9,9 @@ module.exports = (reporter) => {
8
9
  timestamp: { type: 'Edm.DateTimeOffset', schema: { type: 'null' } },
9
10
  finishedOn: { type: 'Edm.DateTimeOffset', schema: { type: 'null' } },
10
11
  state: { type: 'Edm.String' },
11
- blobPersisted: { type: 'Edm.Boolean' },
12
- blobName: { type: 'Edm.String' },
13
- error: { type: 'Edm.String' }
12
+ error: { type: 'Edm.String' },
13
+ mode: { type: 'Edm.String', schema: { enum: ['full', 'standard', 'disabled'] } },
14
+ blobName: { type: 'Edm.String' }
14
15
  })
15
16
 
16
17
  reporter.documentStore.registerEntitySet('profiles', {
@@ -19,9 +20,41 @@ module.exports = (reporter) => {
19
20
  })
20
21
 
21
22
  const profilersMap = new Map()
22
- const profilerAppendChain = new Map()
23
23
 
24
- async function emitProfiles (events, req) {
24
+ const profilerOperationsChainsMap = new Map()
25
+ function runInProfilerChain (fn, req) {
26
+ if (req.context.profiling.mode === 'disabled') {
27
+ return
28
+ }
29
+
30
+ profilerOperationsChainsMap.set(req.context.rootId, profilerOperationsChainsMap.get(req.context.rootId).then(async () => {
31
+ if (req.context.profiling.chainFailed) {
32
+ return
33
+ }
34
+
35
+ try {
36
+ await fn()
37
+ } catch (e) {
38
+ reporter.logger.warn('Failed persist profile', e)
39
+ req.context.profiling.chainFailed = true
40
+ }
41
+ }))
42
+ }
43
+
44
+ function createProfileMessage (m, req) {
45
+ m.timestamp = new Date().getTime()
46
+ m.id = generateRequestId()
47
+ m.previousOperationId = m.previousOperationId || null
48
+ if (m.type !== 'log') {
49
+ m.operationId = m.operationId || generateRequestId()
50
+ req.context.profiling.lastOperationId = m.operationId
51
+ req.context.profiling.lastEventId = m.id
52
+ }
53
+
54
+ return m
55
+ }
56
+
57
+ function emitProfiles (events, req) {
25
58
  if (events.length === 0) {
26
59
  return
27
60
  }
@@ -44,14 +77,16 @@ module.exports = (reporter) => {
44
77
  req.context.profiling.lastOperation = lastOperation
45
78
  }
46
79
 
47
- profilerAppendChain.set(req.context.rootId, profilerAppendChain.get(req.context.rootId).then(() => {
80
+ runInProfilerChain(() => {
81
+ if (req.context.profiling.logFilePath) {
82
+ return fs.appendFile(req.context.profiling.logFilePath, Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'))
83
+ }
84
+
48
85
  return reporter.blobStorage.append(
49
86
  req.context.profiling.entity.blobName,
50
87
  Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'), req
51
- ).catch(e => {
52
- reporter.logger.error('Failed to append to profile blob', e)
53
- })
54
- }))
88
+ )
89
+ }, req)
55
90
  }
56
91
 
57
92
  reporter.registerMainAction('profile', async (events, req) => {
@@ -62,8 +97,7 @@ module.exports = (reporter) => {
62
97
  req.context = req.context || {}
63
98
  req.context.rootId = reporter.generateRequestId()
64
99
  req.context.profiling = {
65
- mode: profileMode == null ? 'full' : profileMode,
66
- isAttached: true
100
+ mode: profileMode == null ? 'full' : profileMode
67
101
  }
68
102
  const profiler = new EventEmitter()
69
103
  profilersMap.set(req.context.rootId, profiler)
@@ -71,88 +105,136 @@ module.exports = (reporter) => {
71
105
  return profiler
72
106
  }
73
107
 
74
- reporter.beforeRenderListeners.add('profiler', async (req, res) => {
75
- profilerAppendChain.set(req.context.rootId, Promise.resolve())
76
-
108
+ reporter.beforeRenderWorkerAllocatedListeners.add('profiler', async (req) => {
77
109
  req.context.profiling = req.context.profiling || {}
110
+
111
+ if (req.context.profiling.mode == null) {
112
+ const profilerSettings = await reporter.settings.findValue('profiler', req)
113
+ const defaultMode = reporter.options.profiler.defaultMode || 'standard'
114
+ req.context.profiling.mode = (profilerSettings != null && profilerSettings.mode != null) ? profilerSettings.mode : defaultMode
115
+ }
116
+
117
+ profilerOperationsChainsMap.set(req.context.rootId, Promise.resolve())
118
+
78
119
  req.context.profiling.lastOperation = null
79
120
 
80
- let blobName = `profiles/${req.context.rootId}.log`
121
+ const blobName = `profiles/${req.context.rootId}.log`
81
122
 
82
- const template = await reporter.templates.resolveTemplate(req)
123
+ const profile = {
124
+ _id: reporter.documentStore.generateId(),
125
+ timestamp: new Date(),
126
+ state: 'queued',
127
+ mode: req.context.profiling.mode,
128
+ blobName
129
+ }
83
130
 
84
- if (template && template._id) {
85
- const templatePath = await reporter.folders.resolveEntityPath(template, 'templates', req)
86
- blobName = `profiles/${templatePath.substring(1)}/${req.context.rootId}.log`
87
- // store a copy to prevent side-effects
88
- req.context.resolvedTemplate = extend(true, {}, template)
131
+ if (!reporter.blobStorage.supportsAppend) {
132
+ const { pathToFile } = await reporter.writeTempFile((uuid) => `${uuid}.log`, '')
133
+ req.context.profiling.logFilePath = pathToFile
89
134
  }
90
135
 
91
- if (!req.context.profiling.isAttached) {
92
- const setting = await reporter.documentStore.collection('settings').findOne({ key: 'fullProfilerRunning' }, req)
93
- if (setting && JSON.parse(setting.value)) {
94
- req.context.profiling.isAttached = true
95
- req.context.profiling.mode = 'full'
96
- }
136
+ runInProfilerChain(async () => {
137
+ req.context.skipValidationFor = profile
138
+ await reporter.documentStore.collection('profiles').insert(profile, req)
139
+ }, req)
140
+
141
+ req.context.profiling.entity = profile
142
+
143
+ const profileStartOperation = createProfileMessage({
144
+ type: 'operationStart',
145
+ subtype: 'profile',
146
+ data: profile,
147
+ doDiffs: false
148
+ }, req)
149
+
150
+ req.context.profiling.profileStartOperationId = profileStartOperation.operationId
151
+
152
+ emitProfiles([profileStartOperation], req)
153
+
154
+ emitProfiles([createProfileMessage({
155
+ type: 'log',
156
+ level: 'info',
157
+ message: `Render request ${req.context.reportCounter} queued for execution and waiting for availible worker`,
158
+ previousOperationId: profileStartOperation.operationId
159
+ }, req)], req)
160
+ })
161
+
162
+ reporter.beforeRenderListeners.add('profiler', async (req, res) => {
163
+ const update = {
164
+ state: 'running'
97
165
  }
98
166
 
99
- if (req.context.profiling.mode == null) {
100
- req.context.profiling.mode = 'standard'
167
+ const template = await reporter.templates.resolveTemplate(req)
168
+ if (template && template._id) {
169
+ req.context.resolvedTemplate = extend(true, {}, template)
170
+ const templatePath = await reporter.folders.resolveEntityPath(template, 'templates', req)
171
+ const blobName = `profiles/${templatePath.substring(1)}/${req.context.rootId}.log`
172
+ update.templateShortid = template.shortid
173
+
174
+ const originalBlobName = req.context.profiling.entity.blobName
175
+ // we want to store the profile into blobName path reflecting the template path so we need to copy the blob to new path now
176
+ runInProfilerChain(async () => {
177
+ if (req.context.profiling.logFilePath == null) {
178
+ const content = await reporter.blobStorage.read(originalBlobName, req)
179
+ await reporter.blobStorage.write(blobName, content, req)
180
+ return reporter.blobStorage.remove(originalBlobName, req)
181
+ }
182
+ }, req)
183
+
184
+ update.blobName = blobName
101
185
  }
102
186
 
103
- const profile = await reporter.documentStore.collection('profiles').insert({
104
- templateShortid: template != null ? template.shortid : null,
105
- timestamp: new Date(),
106
- state: 'running',
107
- blobName,
108
- fullRequestProfiling: req.context.profiling.mode === 'full'
187
+ runInProfilerChain(() => {
188
+ req.context.skipValidationFor = update
189
+ return reporter.documentStore.collection('profiles').update({
190
+ _id: req.context.profiling.entity._id
191
+ }, {
192
+ $set: update
193
+ }, req)
109
194
  }, req)
110
195
 
111
- req.context.profiling.entity = profile
196
+ Object.assign(req.context.profiling.entity, update)
112
197
  })
113
198
 
114
199
  reporter.afterRenderListeners.add('profiler', async (req, res) => {
200
+ emitProfiles([createProfileMessage({
201
+ type: 'operationEnd',
202
+ doDiffs: false,
203
+ previousEventId: req.context.profiling.lastEventId,
204
+ previousOperationId: req.context.profiling.lastOperationId,
205
+ operationId: req.context.profiling.profileStartOperationId
206
+ }, req)], req)
207
+
115
208
  res.meta.profileId = req.context.profiling?.entity?._id
116
- profilersMap.delete(req.context.rootId)
117
- const profilerBlobPersistPromise = profilerAppendChain.get(req.context.rootId)
118
- profilerAppendChain.delete(req.context.rootId)
119
-
120
- await reporter.documentStore.collection('profiles').update({
121
- _id: req.context.profiling.entity._id
122
- }, {
123
- $set: {
209
+
210
+ runInProfilerChain(async () => {
211
+ if (req.context.profiling.logFilePath != null) {
212
+ const content = await fs.readFile(req.context.profiling.logFilePath)
213
+ await reporter.blobStorage.write(req.context.profiling.entity.blobName, content, req)
214
+ await fs.unlink(req.context.profiling.logFilePath)
215
+ }
216
+
217
+ const update = {
124
218
  state: 'success',
125
219
  finishedOn: new Date()
126
220
  }
127
- }, req)
128
- profilerBlobPersistPromise.finally(() => {
129
- reporter.documentStore.collection('profiles').update({
221
+ req.context.skipValidationFor = update
222
+ await reporter.documentStore.collection('profiles').update({
130
223
  _id: req.context.profiling.entity._id
131
224
  }, {
132
- $set: {
133
- blobPersisted: true
134
- }
135
- }, req).catch((e) => reporter.logger.error('Failed to update profile blobPersisted', e))
136
- })
225
+ $set: update
226
+ }, req)
227
+ }, req)
228
+
229
+ // we don't remove from profiler requests map, because the renderErrorListeners are invoked if the afterRenderListener fails
137
230
  })
138
231
 
139
232
  reporter.renderErrorListeners.add('profiler', async (req, res, e) => {
140
233
  try {
141
234
  res.meta.profileId = req.context.profiling?.entity?._id
142
- const profilerBlobPersistPromise = profilerAppendChain.get(req.context.rootId)
143
235
 
144
236
  if (req.context.profiling?.entity != null) {
145
- await reporter.documentStore.collection('profiles').update({
146
- _id: req.context.profiling.entity._id
147
- }, {
148
- $set: {
149
- state: 'error',
150
- finishedOn: new Date(),
151
- error: e.toString()
152
- }
153
- }, req)
154
-
155
- await emitProfiles([{
237
+ emitProfiles([{
156
238
  type: 'error',
157
239
  timestamp: new Date().getTime(),
158
240
  ...e,
@@ -160,20 +242,29 @@ module.exports = (reporter) => {
160
242
  stack: e.stack,
161
243
  message: e.message
162
244
  }], req)
245
+ runInProfilerChain(async () => {
246
+ if (req.context.profiling.logFilePath != null) {
247
+ const content = await fs.readFile(req.context.profiling.logFilePath, 'utf8')
248
+ await reporter.blobStorage.write(req.context.profiling.entity.blobName, content, req)
249
+ await fs.unlink(req.context.profiling.logFilePath)
250
+ }
163
251
 
164
- profilerBlobPersistPromise.finally(() => {
165
- reporter.documentStore.collection('profiles').update({
252
+ const update = {
253
+ state: 'error',
254
+ finishedOn: new Date(),
255
+ error: e.toString()
256
+ }
257
+ req.context.skipValidationFor = update
258
+ await reporter.documentStore.collection('profiles').update({
166
259
  _id: req.context.profiling.entity._id
167
260
  }, {
168
- $set: {
169
- blobPersisted: true
170
- }
171
- }, req).catch((e) => reporter.logger.error('Failed to update profile blobPersisted', e))
172
- })
261
+ $set: update
262
+ }, req)
263
+ }, req)
173
264
  }
174
265
  } finally {
175
266
  profilersMap.delete(req.context.rootId)
176
- profilerAppendChain.delete(req.context.rootId)
267
+ profilerOperationsChainsMap.delete(req.context.rootId)
177
268
  }
178
269
  })
179
270
 
@@ -197,8 +288,8 @@ module.exports = (reporter) => {
197
288
  clearInterval(profilesCleanupInterval)
198
289
  }
199
290
 
200
- for (const key of profilerAppendChain.keys()) {
201
- const profileAppendPromise = profilerAppendChain.get(key)
291
+ for (const key of profilerOperationsChainsMap.keys()) {
292
+ const profileAppendPromise = profilerOperationsChainsMap.get(key)
202
293
  if (profileAppendPromise) {
203
294
  await profileAppendPromise
204
295
  }