@jsreport/jsreport-core 3.4.0 → 3.5.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
@@ -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,24 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.5.0
286
+
287
+ - fix parsing issue of code with comment in the sandbox (helpers, scripts)
288
+ - improve profiling when there is big data
289
+ - make transactions support in store configurable
290
+ - improve timeout for the whole request
291
+ - fix applying req.options.timeout when enableRequestReportTimeout is true
292
+ - optimization regarding profile persistence
293
+
294
+ ### 3.4.2
295
+
296
+ - update dep `vm2` to fix security vulnerability in sandbox
297
+
298
+ ### 3.4.1
299
+
300
+ - fix passing data to async report
301
+ - fix blob appends
302
+
285
303
  ### 3.4.0
286
304
 
287
305
  - fix for reports execution
@@ -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
  }
@@ -45,6 +46,14 @@ module.exports = (reporter, options) => {
45
46
  }
46
47
  },
47
48
 
49
+ get supportsAppend () {
50
+ return provider.append instanceof Function
51
+ },
52
+
53
+ get _provider () {
54
+ return provider
55
+ },
56
+
48
57
  registerProvider (p) {
49
58
  provider = p
50
59
  }
@@ -126,7 +126,13 @@ module.exports.getRootSchemaOptions = () => ({
126
126
  store: {
127
127
  type: 'object',
128
128
  properties: {
129
- provider: { type: 'string', enum: ['memory'] }
129
+ provider: { type: 'string', enum: ['memory'] },
130
+ transactions: {
131
+ type: 'object',
132
+ properties: {
133
+ enabled: { type: 'boolean', default: true }
134
+ }
135
+ }
130
136
  }
131
137
  },
132
138
  blobStorage: {
@@ -163,6 +169,10 @@ module.exports.getRootSchemaOptions = () => ({
163
169
  type: 'object',
164
170
  default: {},
165
171
  properties: {
172
+ defaultMode: {
173
+ type: 'string',
174
+ default: 'standard'
175
+ },
166
176
  maxProfilesHistory: {
167
177
  type: 'number',
168
178
  default: 1000
@@ -171,6 +181,11 @@ module.exports.getRootSchemaOptions = () => ({
171
181
  type: ['string', 'number'],
172
182
  '$jsreport-acceptsDuration': true,
173
183
  default: '1m'
184
+ },
185
+ maxResponseSize: {
186
+ type: ['string', 'number'],
187
+ '$jsreport-acceptsSize': true,
188
+ default: '50mb'
174
189
  }
175
190
  }
176
191
  }
@@ -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
  }