@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 +19 -1
- package/lib/main/blobStorage/blobStorage.js +9 -0
- package/lib/main/optionsSchema.js +16 -1
- package/lib/main/profiler.js +166 -75
- package/lib/main/reporter.js +90 -82
- package/lib/main/request.js +21 -0
- package/lib/main/schemaValidator.js +30 -0
- package/lib/main/settings.js +1 -2
- package/lib/main/store/collection.js +10 -8
- package/lib/main/store/documentStore.js +19 -0
- package/lib/main/store/setupValidateId.js +7 -1
- package/lib/main/store/setupValidateShortid.js +4 -0
- package/lib/shared/normalizeMetaFromLogs.js +1 -1
- package/lib/shared/reporter.js +16 -0
- package/lib/worker/render/executeEngine.js +30 -7
- package/lib/worker/render/profiler.js +14 -12
- package/lib/worker/render/render.js +0 -4
- package/lib/worker/sandbox/runInSandbox.js +2 -5
- package/lib/worker/sandbox/safeSandbox.js +8 -30
- package/lib/worker/workerHandler.js +1 -0
- package/package.json +7 -7
- package/lib/main/monitoring.js +0 -92
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',
|
|
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
|
}
|
package/lib/main/profiler.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
52
|
-
|
|
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.
|
|
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
|
-
|
|
121
|
+
const blobName = `profiles/${req.context.rootId}.log`
|
|
81
122
|
|
|
82
|
-
const
|
|
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 (
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
201
|
-
const profileAppendPromise =
|
|
291
|
+
for (const key of profilerOperationsChainsMap.keys()) {
|
|
292
|
+
const profileAppendPromise = profilerOperationsChainsMap.get(key)
|
|
202
293
|
if (profileAppendPromise) {
|
|
203
294
|
await profileAppendPromise
|
|
204
295
|
}
|