@jsreport/jsreport-core 3.1.1 → 3.3.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.
Files changed (80) hide show
  1. package/LICENSE +166 -166
  2. package/README.md +298 -298
  3. package/index.js +29 -29
  4. package/lib/main/blobStorage/blobStorage.js +52 -52
  5. package/lib/main/blobStorage/inMemoryProvider.js +27 -27
  6. package/lib/main/blobStorage/mainActions.js +24 -24
  7. package/lib/main/createDefaultLoggerFormat.js +17 -17
  8. package/lib/main/defaults.js +14 -14
  9. package/lib/main/extensions/discover.js +20 -20
  10. package/lib/main/extensions/extensionsManager.js +264 -264
  11. package/lib/main/extensions/fileUtils.js +56 -56
  12. package/lib/main/extensions/findVersion.js +49 -49
  13. package/lib/main/extensions/locationCache.js +103 -103
  14. package/lib/main/extensions/sorter.js +10 -10
  15. package/lib/main/extensions/validateMinimalVersion.js +50 -50
  16. package/lib/main/folders/cascadeFolderRemove.js +25 -25
  17. package/lib/main/folders/getEntitiesInFolder.js +53 -53
  18. package/lib/main/folders/index.js +42 -42
  19. package/lib/main/folders/moveBetweenFolders.js +354 -354
  20. package/lib/main/folders/validateDuplicatedName.js +107 -107
  21. package/lib/main/folders/validateReservedName.js +53 -53
  22. package/lib/main/logger.js +254 -244
  23. package/lib/main/migration/resourcesToAssets.js +230 -230
  24. package/lib/main/migration/xlsxTemplatesToAssets.js +128 -128
  25. package/lib/main/monitoring.js +92 -91
  26. package/lib/main/optionsLoad.js +237 -237
  27. package/lib/main/optionsSchema.js +237 -237
  28. package/lib/main/profiler.js +13 -1
  29. package/lib/main/reporter.js +593 -579
  30. package/lib/main/schemaValidator.js +252 -252
  31. package/lib/main/settings.js +154 -154
  32. package/lib/main/store/checkDuplicatedId.js +27 -27
  33. package/lib/main/store/collection.js +329 -329
  34. package/lib/main/store/documentStore.js +469 -469
  35. package/lib/main/store/mainActions.js +28 -28
  36. package/lib/main/store/memoryStoreProvider.js +99 -99
  37. package/lib/main/store/queue.js +48 -48
  38. package/lib/main/store/referenceUtils.js +251 -251
  39. package/lib/main/store/setupValidateId.js +43 -43
  40. package/lib/main/store/setupValidateShortid.js +71 -71
  41. package/lib/main/store/transaction.js +69 -69
  42. package/lib/main/store/typeUtils.js +180 -180
  43. package/lib/main/templates.js +34 -34
  44. package/lib/main/validateEntityName.js +62 -62
  45. package/lib/shared/createError.js +36 -36
  46. package/lib/shared/encryption.js +114 -114
  47. package/lib/shared/folders/index.js +11 -11
  48. package/lib/shared/folders/normalizeEntityPath.js +15 -15
  49. package/lib/shared/folders/resolveEntityFromPath.js +88 -88
  50. package/lib/shared/folders/resolveEntityPath.js +46 -46
  51. package/lib/shared/folders/resolveFolderFromPath.js +38 -38
  52. package/lib/shared/generateRequestId.js +4 -4
  53. package/lib/shared/listenerCollection.js +169 -169
  54. package/lib/shared/normalizeMetaFromLogs.js +30 -30
  55. package/lib/shared/reporter.js +128 -123
  56. package/lib/shared/request.js +64 -64
  57. package/lib/shared/tempFilesHandler.js +81 -81
  58. package/lib/shared/templates.js +82 -82
  59. package/lib/static/helpers.js +33 -33
  60. package/lib/worker/blobStorage.js +34 -34
  61. package/lib/worker/defaultProxyExtend.js +46 -46
  62. package/lib/worker/documentStore.js +49 -49
  63. package/lib/worker/extensionsManager.js +17 -17
  64. package/lib/worker/logger.js +48 -48
  65. package/lib/worker/render/diff.js +138 -138
  66. package/lib/worker/render/executeEngine.js +232 -207
  67. package/lib/worker/render/htmlRecipe.js +10 -10
  68. package/lib/worker/render/moduleHelper.js +45 -43
  69. package/lib/worker/render/noneEngine.js +12 -12
  70. package/lib/worker/render/profiler.js +162 -158
  71. package/lib/worker/render/render.js +202 -201
  72. package/lib/worker/render/resolveReferences.js +60 -60
  73. package/lib/worker/reporter.js +197 -191
  74. package/lib/worker/sandbox/runInSandbox.js +65 -13
  75. package/lib/worker/sandbox/safeSandbox.js +829 -828
  76. package/lib/worker/templates.js +80 -78
  77. package/lib/worker/workerHandler.js +54 -54
  78. package/package.json +91 -92
  79. package/test/blobStorage/common.js +21 -21
  80. package/test/store/common.js +1449 -1449
@@ -1,579 +1,593 @@
1
- /*!
2
- * Copyright(c) 2018 Jan Blaha
3
- *
4
- * Reporter main class including all methods jsreport-core exposes.
5
- */
6
- const path = require('path')
7
- const { Readable } = require('stream')
8
- const Reaper = require('reap2')
9
- const optionsLoad = require('./optionsLoad')
10
- const { createLogger, configureLogger, silentLogs } = require('./logger')
11
- const checkEntityName = require('./validateEntityName')
12
- const DocumentStore = require('./store/documentStore')
13
- const BlobStorage = require('./blobStorage/blobStorage')
14
- const ExtensionsManager = require('./extensions/extensionsManager')
15
- const Settings = require('./settings')
16
- const SchemaValidator = require('./schemaValidator')
17
- const { getRootSchemaOptions, extendRootSchemaOptions } = require('./optionsSchema')
18
- const Templates = require('./templates')
19
- const Folders = require('./folders')
20
- const WorkersManager = require('@jsreport/advanced-workers')
21
- const { validateDuplicatedName } = require('./folders/validateDuplicatedName')
22
- const { validateReservedName } = require('./folders/validateReservedName')
23
- const setupValidateId = require('./store/setupValidateId')
24
- const setupValidateShortid = require('./store/setupValidateShortid')
25
- const documentStoreActions = require('./store/mainActions')
26
- const blobStorageActions = require('./blobStorage/mainActions')
27
- const Reporter = require('../shared/reporter')
28
- const Request = require('../shared/request')
29
- const generateRequestId = require('../shared/generateRequestId')
30
- const Profiler = require('./profiler')
31
- const Monitoring = require('./monitoring')
32
- const migrateXlsxTemplatesToAssets = require('./migration/xlsxTemplatesToAssets')
33
- const migrateResourcesToAssets = require('./migration/resourcesToAssets')
34
- const semver = require('semver')
35
-
36
- class MainReporter extends Reporter {
37
- constructor (options, defaults) {
38
- super(options)
39
-
40
- if (!semver.satisfies(process.versions.node, '>=16.11.0')) {
41
- throw this.createError('jsreport needs at least node 16.11.0 to run.')
42
- }
43
-
44
- this.defaults = defaults || {}
45
- this._fnAfterConfigLoaded = () => {}
46
- this._reaperTimerRef = null
47
- this._extraPathsToCleanupCollection = new Set()
48
-
49
- this._initialized = false
50
- this._initializing = false
51
-
52
- this._initExecution = {}
53
-
54
- this._initExecution.promise = new Promise((resolve, reject) => {
55
- this._initExecution.resolve = resolve
56
- this._initExecution.reject = resolve
57
- })
58
-
59
- this._mainActions = new Map()
60
-
61
- this.settings = new Settings()
62
- this.extensionsManager = ExtensionsManager(this)
63
-
64
- this.optionsValidator = new SchemaValidator({
65
- rootSchema: getRootSchemaOptions()
66
- })
67
-
68
- this.entityTypeValidator = new SchemaValidator()
69
-
70
- this.logger = createLogger()
71
- this.beforeMainActionListeners = this.createListenerCollection('beforeMainAction')
72
- }
73
-
74
- discover () {
75
- this.options.discover = true
76
- return this
77
- }
78
-
79
- /**
80
- * Manual registration of the extension. Once calling `use` the auto discovery of extensions is turned off if not explicitly
81
- * turned on.
82
- * jsreport.use(require('@jsreport/jsreport-jsrender')())
83
- * @param {Object || Function} extensions
84
- * @return {Reporter} for chaining
85
- * @public
86
- */
87
- use (extension) {
88
- this.extensionsManager.use(extension)
89
- return this
90
- }
91
-
92
- async extensionsLoad (opts) {
93
- const appliedConfigFile = await optionsLoad({
94
- defaults: this.defaults,
95
- options: this.options,
96
- validator: this.optionsValidator,
97
- onConfigLoaded: async () => {
98
- await this._fnAfterConfigLoaded(this)
99
- }
100
- })
101
-
102
- configureLogger(this.logger, this.options.logger)
103
-
104
- if (this.options.logger && this.options.logger.silent === true) {
105
- silentLogs(this.logger)
106
- }
107
-
108
- this.logger.info(`Initializing jsreport (version: ${this.version}, configuration file: ${appliedConfigFile || 'none'}, nodejs: ${process.versions.node})`)
109
-
110
- await this.extensionsManager.load(opts)
111
-
112
- const newRootSchema = extendRootSchemaOptions(
113
- getRootSchemaOptions(),
114
- this.extensionsManager.availableExtensions.map(ex => ({
115
- name: ex.name,
116
- schema: ex.optionsSchema
117
- }))
118
- )
119
-
120
- this.optionsValidator.setRootSchema(newRootSchema)
121
-
122
- const rootOptionsValidation = this.optionsValidator.validateRoot(this.options, {
123
- rootPrefix: 'rootOptions',
124
- // extensions was validated already in extensions load
125
- ignore: ['properties.extensions']
126
- })
127
-
128
- if (!rootOptionsValidation.valid) {
129
- throw new Error(`options contain values that does not match the defined full root schema. ${rootOptionsValidation.fullErrorMessage}`)
130
- }
131
-
132
- return this
133
- }
134
-
135
- /**
136
- * Hook to alter configuration after it was loaded and merged
137
- * jsreport.afterConfigLoaded(function(reporter) { .. do your stuff ..})
138
- *
139
- *
140
- * @public
141
- */
142
- afterConfigLoaded (fn) {
143
- this._fnAfterConfigLoaded = fn
144
- return this
145
- }
146
-
147
- async waitForInit () {
148
- await this._initExecution.promise
149
- }
150
-
151
- /**
152
- * Required async method to be called before rendering.
153
- *
154
- * @return {Promise} initialization is done, promise value is Reporter instance for chaining
155
- * @public
156
- */
157
- async init () {
158
- this.closing = this.closed = false
159
- if (this._initialized || this._initializing) {
160
- throw new Error('jsreport already initialized or just initializing. Make sure init is called only once')
161
- }
162
-
163
- super.init()
164
-
165
- this._initializing = true
166
-
167
- if (this.compilation) {
168
- this.compilation.resource('sandbox.js', require.resolve('vm2/lib/sandbox.js'))
169
- this.compilation.resource('contextify.js', require.resolve('vm2/lib/contextify.js'))
170
- this.compilation.resource('fixasync.js', require.resolve('vm2/lib/fixasync.js'))
171
- }
172
-
173
- try {
174
- this._registerLogMainAction()
175
- await this.extensionsLoad()
176
-
177
- this.documentStore = DocumentStore(Object.assign({}, this.options, { logger: this.logger }), this.entityTypeValidator, this.encryption)
178
- documentStoreActions(this)
179
- this.blobStorage = BlobStorage(this, this.options)
180
- blobStorageActions(this)
181
- Templates(this)
182
- Profiler(this)
183
- Monitoring(this)
184
-
185
- this.folders = Object.assign(this.folders, Folders(this))
186
-
187
- this.settings.registerEntity(this.documentStore)
188
-
189
- this.options.blobStorage = this.options.blobStorage || {}
190
-
191
- if (this.options.blobStorage.provider == null) {
192
- this.options.blobStorage.provider = this.options.store.provider
193
- }
194
-
195
- if (this.options.blobStorage.provider === 'memory') {
196
- this.blobStorage.registerProvider(require('./blobStorage/inMemoryProvider.js')(this.options))
197
- }
198
-
199
- await this.extensionsManager.init()
200
-
201
- this.logger.info(`Using general timeout for rendering (reportTimeout: ${this.options.reportTimeout})`)
202
-
203
- if (this.options.store.provider === 'memory') {
204
- this.logger.info(`Using ${this.options.store.provider} provider for template store. The saved templates will be lost after restart`)
205
- } else {
206
- this.logger.info(`Using ${this.options.store.provider} provider for template store.`)
207
- }
208
-
209
- await this.documentStore.init()
210
- await this.blobStorage.init()
211
- await this.settings.init(this.documentStore, this.authorization)
212
-
213
- const extensionsForWorkers = this.extensionsManager.extensions.filter(e => e.worker)
214
-
215
- const workersManagerOptions = {
216
- ...this.options.sandbox,
217
- options: { ...this.options },
218
- // we do map and copy to unproxy the value
219
- extensionsDefs: extensionsForWorkers.map(e => Object.assign({}, e)),
220
- documentStore: {
221
- // we do copy to unproxy the value of entityTypes
222
- model: {
223
- ...this.documentStore.model,
224
- entityTypes: { ...this.documentStore.model.entityTypes }
225
- },
226
- collections: Object.keys(this.documentStore.collections)
227
- }
228
- }
229
-
230
- const workersManagerSystemOptions = {
231
- initTimeout: this.options.workers.initTimeout,
232
- numberOfWorkers: this.options.workers.numberOfWorkers,
233
- workerModule: path.join(__dirname, '../worker', 'workerHandler.js'),
234
- resourceLimits: this.options.workers.resourceLimits
235
- }
236
-
237
- // adding the validation of shortid after extensions has been loaded
238
- setupValidateId(this)
239
- setupValidateShortid(this)
240
-
241
- this.initializeListeners.insert(0, 'core-resources-migration', () => migrateResourcesToAssets(this))
242
- this.initializeListeners.insert(0, 'core-xlsxTemplates-migration', () => migrateXlsxTemplatesToAssets(this))
243
-
244
- await this.initializeListeners.fire()
245
-
246
- this._workersManager = this._workersManagerFactory
247
- ? this._workersManagerFactory(workersManagerOptions, workersManagerSystemOptions)
248
- : WorkersManager(workersManagerOptions, workersManagerSystemOptions, this.logger)
249
-
250
- const workersStart = new Date().getTime()
251
-
252
- this.logger.info('Initializing worker threads')
253
-
254
- this.logger.debug(`Extensions in workers: ${extensionsForWorkers.map((e) => e.name).join(', ')}`)
255
-
256
- await this._workersManager.init(workersManagerOptions)
257
-
258
- this.logger.info(`${this.options.workers.numberOfWorkers} worker threads initialized in ${new Date().getTime() - workersStart}ms`)
259
-
260
- this._startReaper(this.getPathsToWatchForAutoCleanup())
261
-
262
- this.extensionsManager.recipes.push({
263
- name: 'html'
264
- })
265
-
266
- this.extensionsManager.engines.push({
267
- name: 'none'
268
- })
269
-
270
- this.monitoring.init()
271
-
272
- this.logger.info('reporter initialized')
273
- this._initialized = true
274
- this._initExecution.resolve()
275
- return this
276
- } catch (e) {
277
- this.logger.error(`Error occurred during reporter init: ${e.stack}`)
278
- this._initExecution.reject(new Error(`Reporter initialization failed. ${e.message}`))
279
- throw e
280
- }
281
- }
282
-
283
- /**
284
- * @public
285
- */
286
- addPathToWatchForAutoCleanup (customPath) {
287
- this._extraPathsToCleanupCollection.add(customPath)
288
- }
289
-
290
- /**
291
- * @public
292
- */
293
- getPathsToWatchForAutoCleanup () {
294
- return [this.options.tempAutoCleanupDirectory].concat(Array.from(this._extraPathsToCleanupCollection.values()))
295
- }
296
-
297
- async checkValidEntityName (c, doc, req) {
298
- if (!this.documentStore.model.entitySets[c].entityTypeDef.name) {
299
- return
300
- }
301
-
302
- checkEntityName(doc.name)
303
-
304
- await validateDuplicatedName(this, c, doc, undefined, req)
305
-
306
- await validateReservedName(this, c, doc)
307
- }
308
-
309
- /**
310
- * Main method for invoking rendering
311
- * render({ template: { content: 'foo', engine: 'none', recipe: 'html' }, data: { foo: 'hello' } })
312
- *
313
- * @request {Object}
314
- * @return {Promise} response.content is output buffer, response.stream is output stream, response.headers contains http applicable headers
315
- *
316
- * @public
317
- */
318
- async render (req, parentReq) {
319
- if (!this._initialized) {
320
- throw new Error('Not initialized, you need to call jsreport.init().then before rendering')
321
- }
322
-
323
- req = Object.assign({}, req)
324
- req.context = Object.assign({}, req.context)
325
- req.context.rootId = req.context.rootId || generateRequestId()
326
- req.context.id = req.context.rootId
327
-
328
- const worker = await this._workersManager.allocate(req, {
329
- timeout: this.options.reportTimeout
330
- })
331
-
332
- 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
- }
343
- }
344
-
345
- const res = { meta: {} }
346
- try {
347
- if (workerAborted) {
348
- throw this.createError('Request aborted by client')
349
- }
350
-
351
- let isDataStoredInWorker = false
352
-
353
- if (req.rawContent) {
354
- isDataStoredInWorker = true
355
- const result = await worker.execute({
356
- actionName: 'parse',
357
- req,
358
- data: {}
359
- }, {
360
- timeout: this.options.reportTimeout
361
- })
362
- req = result
363
- }
364
-
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
- }
372
-
373
- // TODO: we will probably validate in the thread
374
- if (this.entityTypeValidator.getSchema('TemplateType') != null) {
375
- const templateValidationResult = this.entityTypeValidator.validate('TemplateType', req.template, { rootPrefix: 'template' })
376
-
377
- if (!templateValidationResult.valid) {
378
- throw this.createError(`template input in request contain values that does not match the defined schema. ${templateValidationResult.fullErrorMessage}`, {
379
- statusCode: 400
380
- })
381
- }
382
- }
383
-
384
- let reportTimeout = this.options.reportTimeout
385
-
386
- if (
387
- this.options.enableRequestReportTimeout &&
388
- req.options &&
389
- req.options.timeout != null
390
- ) {
391
- reportTimeout = req
392
- }
393
-
394
- await this.beforeRenderListeners.fire(req, res)
395
-
396
- if (req.context.isFinished) {
397
- res.stream = Readable.from(res.content)
398
- return res
399
- }
400
-
401
- if (workerAborted) {
402
- throw this.createError('Request aborted by client')
403
- }
404
-
405
- const responseResult = await this.executeWorkerAction('render', {}, {
406
- timeout: reportTimeout + this.options.reportTimeoutMargin,
407
- worker
408
- }, req)
409
-
410
- Object.assign(res, responseResult)
411
- await this.afterRenderListeners.fire(req, res)
412
- res.stream = Readable.from(res.content)
413
- return res
414
- } catch (err) {
415
- if (err.code === 'WORKER_TIMEOUT') {
416
- err.message = 'Report timeout'
417
- err.weak = true
418
- }
419
-
420
- if (err.code === 'WORKER_ABORTED') {
421
- err.message = 'Report cancelled'
422
- err.weak = true
423
- }
424
-
425
- if (!err.logged) {
426
- const logFn = err.weak ? this.logger.warn : this.logger.error
427
- logFn(`Report render failed: ${err.message}${err.stack != null ? ' ' + err.stack : ''}`, req)
428
- }
429
- await this.renderErrorListeners.fire(req, res, err)
430
- throw err
431
- } finally {
432
- if (!workerAborted) {
433
- await worker.release(req)
434
- }
435
- }
436
- }
437
-
438
- generateRequestId () {
439
- return generateRequestId()
440
- }
441
-
442
- registerWorkersManagerFactory (workersManagerFactory) {
443
- this._workersManagerFactory = workersManagerFactory
444
- }
445
-
446
- /**
447
- *
448
- * @public
449
- */
450
- async close () {
451
- this.closing = true
452
- this.logger.info('Closing jsreport instance')
453
-
454
- if (this.monitoring) {
455
- await this.monitoring.close()
456
- }
457
-
458
- if (this._reaperTimerRef) {
459
- clearInterval(this._reaperTimerRef)
460
- }
461
-
462
- if (this._workersManager) {
463
- await this._workersManager.close()
464
- }
465
-
466
- await this.closeListeners.fire()
467
-
468
- if (this.documentStore) {
469
- await this.documentStore.close()
470
- }
471
-
472
- this.logger.info('jsreport instance has been closed')
473
- this.closed = true
474
-
475
- return this
476
- }
477
-
478
- registerMainAction (actionName, fn) {
479
- this._mainActions.set(actionName, fn)
480
- }
481
-
482
- async _invokeMainAction (data, req) {
483
- await this.beforeMainActionListeners.fire(data.actionName, data.data, req)
484
- if (!this._mainActions.has(data.actionName)) {
485
- throw this.createError(`Main process action ${data.actionName} wasn't registered`)
486
- }
487
- return this._mainActions.get(data.actionName)(data.data, req)
488
- }
489
-
490
- _registerLogMainAction () {
491
- this.registerMainAction('log', (log, req) => {
492
- this.logger[log.level](log.message, { ...req, ...log.meta, timestamp: log.timestamp })
493
- })
494
- }
495
-
496
- async executeWorkerAction (actionName, data, options = {}, req) {
497
- req.context.rootId = req.context.rootId || generateRequestId()
498
- const timeout = options.timeout || 60000
499
-
500
- const worker = options.worker
501
- ? options.worker
502
- : await this._workersManager.allocate(req, {
503
- timeout
504
- })
505
-
506
- try {
507
- const result = await worker.execute({
508
- actionName,
509
- data,
510
- // we set just known props, to avoid cloning failures on express req properties
511
- req: {
512
- context: req.context,
513
- template: req.template,
514
- data: req.data,
515
- options: req.options
516
- }
517
- }, {
518
- // TODO add worker timeout
519
- timeout,
520
- timeoutErrorMessage: options.timeoutErrorMessage || ('Timeout during worker action ' + actionName),
521
- executeMain: async (data) => {
522
- return this._invokeMainAction(data, req)
523
- }
524
- })
525
- this._workersManager.convertUint8ArrayToBuffer(result)
526
- return result
527
- } finally {
528
- if (!options.worker) {
529
- await worker.release(req)
530
- }
531
- }
532
- }
533
-
534
- /**
535
- * Periodical cleaning of folders where recipes are storing files like source html for pdf rendering
536
- *
537
- * @private
538
- */
539
- _startReaper (dir) {
540
- const dirsToWatch = !Array.isArray(dir) ? [dir] : dir
541
-
542
- if (this.options.autoTempCleanup === false) {
543
- return
544
- }
545
-
546
- const threshold = this.options.reportTimeout > 180000 ? this.options.reportTimeout : 180000
547
-
548
- this.logger.info(`Starting temp files cleanup with ${threshold}ms threshold`)
549
-
550
- const reaper = new Reaper({ threshold })
551
-
552
- dirsToWatch.forEach(d => reaper.watch(d))
553
-
554
- reaper.start((err, files) => {
555
- if (err) {
556
- this.logger.error(`Failed to start auto cleanup: ${err.stack}`)
557
- }
558
- })
559
-
560
- this._reaperTimerRef = setInterval(() => {
561
- try {
562
- reaper.start((err, files) => {
563
- if (err) {
564
- // NOT logging the error anymore because it was confusing users that something bad was happening
565
- // this.logger.error('Failed to delete temp file: ' + err)
566
- }
567
- })
568
- } catch (e) {
569
- // NOT logging the error anymore because it was confusing users that something bad was happening
570
- // catch error in case the reaper can not read directory
571
- // this.logger.error('Failed to run reaper: ' + e)
572
- }
573
- }, 30000 /* check every 30s for old files */)
574
-
575
- this._reaperTimerRef.unref()
576
- }
577
- }
578
-
579
- module.exports = MainReporter
1
+ /*!
2
+ * Copyright(c) 2018 Jan Blaha
3
+ *
4
+ * Reporter main class including all methods jsreport-core exposes.
5
+ */
6
+ const path = require('path')
7
+ const { Readable } = require('stream')
8
+ const Reaper = require('reap2')
9
+ const optionsLoad = require('./optionsLoad')
10
+ const { createLogger, configureLogger, silentLogs } = require('./logger')
11
+ const checkEntityName = require('./validateEntityName')
12
+ const DocumentStore = require('./store/documentStore')
13
+ const BlobStorage = require('./blobStorage/blobStorage')
14
+ const ExtensionsManager = require('./extensions/extensionsManager')
15
+ const Settings = require('./settings')
16
+ const SchemaValidator = require('./schemaValidator')
17
+ const { getRootSchemaOptions, extendRootSchemaOptions } = require('./optionsSchema')
18
+ const Templates = require('./templates')
19
+ const Folders = require('./folders')
20
+ const WorkersManager = require('@jsreport/advanced-workers')
21
+ const { validateDuplicatedName } = require('./folders/validateDuplicatedName')
22
+ const { validateReservedName } = require('./folders/validateReservedName')
23
+ const setupValidateId = require('./store/setupValidateId')
24
+ const setupValidateShortid = require('./store/setupValidateShortid')
25
+ const documentStoreActions = require('./store/mainActions')
26
+ const blobStorageActions = require('./blobStorage/mainActions')
27
+ const Reporter = require('../shared/reporter')
28
+ const Request = require('../shared/request')
29
+ const generateRequestId = require('../shared/generateRequestId')
30
+ const Profiler = require('./profiler')
31
+ const Monitoring = require('./monitoring')
32
+ const migrateXlsxTemplatesToAssets = require('./migration/xlsxTemplatesToAssets')
33
+ const migrateResourcesToAssets = require('./migration/resourcesToAssets')
34
+ const semver = require('semver')
35
+
36
+ class MainReporter extends Reporter {
37
+ constructor (options, defaults) {
38
+ super(options)
39
+
40
+ if (!semver.satisfies(process.versions.node, '>=16.11.0')) {
41
+ throw this.createError('jsreport needs at least node 16.11.0 to run.')
42
+ }
43
+
44
+ this.defaults = defaults || {}
45
+ this._fnAfterConfigLoaded = () => {}
46
+ this._reaperTimerRef = null
47
+ this._extraPathsToCleanupCollection = new Set()
48
+
49
+ this._initialized = false
50
+ this._initializing = false
51
+
52
+ this._initExecution = {}
53
+
54
+ this._initExecution.promise = new Promise((resolve, reject) => {
55
+ this._initExecution.resolve = resolve
56
+ this._initExecution.reject = resolve
57
+ })
58
+
59
+ this._mainActions = new Map()
60
+
61
+ this.settings = new Settings()
62
+ this.extensionsManager = ExtensionsManager(this)
63
+
64
+ this.optionsValidator = new SchemaValidator({
65
+ rootSchema: getRootSchemaOptions()
66
+ })
67
+
68
+ this.entityTypeValidator = new SchemaValidator()
69
+
70
+ this.logger = createLogger()
71
+ this.beforeMainActionListeners = this.createListenerCollection('beforeMainAction')
72
+ }
73
+
74
+ discover () {
75
+ this.options.discover = true
76
+ return this
77
+ }
78
+
79
+ /**
80
+ * Manual registration of the extension. Once calling `use` the auto discovery of extensions is turned off if not explicitly
81
+ * turned on.
82
+ * jsreport.use(require('@jsreport/jsreport-jsrender')())
83
+ * @param {Object || Function} extensions
84
+ * @return {Reporter} for chaining
85
+ * @public
86
+ */
87
+ use (extension) {
88
+ this.extensionsManager.use(extension)
89
+ return this
90
+ }
91
+
92
+ async extensionsLoad (opts) {
93
+ const appliedConfigFile = await optionsLoad({
94
+ defaults: this.defaults,
95
+ options: this.options,
96
+ validator: this.optionsValidator,
97
+ onConfigLoaded: async () => {
98
+ await this._fnAfterConfigLoaded(this)
99
+ }
100
+ })
101
+
102
+ configureLogger(this.logger, this.options.logger)
103
+
104
+ if (this.options.logger && this.options.logger.silent === true) {
105
+ silentLogs(this.logger)
106
+ }
107
+
108
+ this.logger.info(`Initializing jsreport (version: ${this.version}, configuration file: ${appliedConfigFile || 'none'}, nodejs: ${process.versions.node})`)
109
+
110
+ await this.extensionsManager.load(opts)
111
+
112
+ const newRootSchema = extendRootSchemaOptions(
113
+ getRootSchemaOptions(),
114
+ this.extensionsManager.availableExtensions.map(ex => ({
115
+ name: ex.name,
116
+ schema: ex.optionsSchema
117
+ }))
118
+ )
119
+
120
+ this.optionsValidator.setRootSchema(newRootSchema)
121
+
122
+ const rootOptionsValidation = this.optionsValidator.validateRoot(this.options, {
123
+ rootPrefix: 'rootOptions',
124
+ // extensions was validated already in extensions load
125
+ ignore: ['properties.extensions']
126
+ })
127
+
128
+ if (!rootOptionsValidation.valid) {
129
+ throw new Error(`options contain values that does not match the defined full root schema. ${rootOptionsValidation.fullErrorMessage}`)
130
+ }
131
+
132
+ return this
133
+ }
134
+
135
+ /**
136
+ * Hook to alter configuration after it was loaded and merged
137
+ * jsreport.afterConfigLoaded(function(reporter) { .. do your stuff ..})
138
+ *
139
+ *
140
+ * @public
141
+ */
142
+ afterConfigLoaded (fn) {
143
+ this._fnAfterConfigLoaded = fn
144
+ return this
145
+ }
146
+
147
+ async waitForInit () {
148
+ await this._initExecution.promise
149
+ }
150
+
151
+ /**
152
+ * Required async method to be called before rendering.
153
+ *
154
+ * @return {Promise} initialization is done, promise value is Reporter instance for chaining
155
+ * @public
156
+ */
157
+ async init () {
158
+ this.closing = this.closed = false
159
+ if (this._initialized || this._initializing) {
160
+ throw new Error('jsreport already initialized or just initializing. Make sure init is called only once')
161
+ }
162
+
163
+ super.init()
164
+
165
+ this._initializing = true
166
+
167
+ if (this.compilation) {
168
+ this.compilation.resource('sandbox.js', require.resolve('vm2/lib/sandbox.js'))
169
+ this.compilation.resource('contextify.js', require.resolve('vm2/lib/contextify.js'))
170
+ this.compilation.resource('fixasync.js', require.resolve('vm2/lib/fixasync.js'))
171
+ }
172
+
173
+ try {
174
+ this._registerLogMainAction()
175
+ await this.extensionsLoad()
176
+
177
+ this.documentStore = DocumentStore(Object.assign({}, this.options, { logger: this.logger }), this.entityTypeValidator, this.encryption)
178
+ documentStoreActions(this)
179
+ this.blobStorage = BlobStorage(this, this.options)
180
+ blobStorageActions(this)
181
+ Templates(this)
182
+ Profiler(this)
183
+ Monitoring(this)
184
+
185
+ this.folders = Object.assign(this.folders, Folders(this))
186
+
187
+ this.settings.registerEntity(this.documentStore)
188
+
189
+ this.options.blobStorage = this.options.blobStorage || {}
190
+
191
+ if (this.options.blobStorage.provider == null) {
192
+ this.options.blobStorage.provider = this.options.store.provider
193
+ }
194
+
195
+ if (this.options.blobStorage.provider === 'memory') {
196
+ this.blobStorage.registerProvider(require('./blobStorage/inMemoryProvider.js')(this.options))
197
+ }
198
+
199
+ await this.extensionsManager.init()
200
+
201
+ this.logger.info(`Using general timeout for rendering (reportTimeout: ${this.options.reportTimeout})`)
202
+
203
+ if (this.options.store.provider === 'memory') {
204
+ this.logger.info(`Using ${this.options.store.provider} provider for template store. The saved templates will be lost after restart`)
205
+ } else {
206
+ this.logger.info(`Using ${this.options.store.provider} provider for template store.`)
207
+ }
208
+
209
+ await this.documentStore.init()
210
+ await this.blobStorage.init()
211
+ await this.settings.init(this.documentStore, this.authorization)
212
+
213
+ const extensionsForWorkers = this.extensionsManager.extensions.filter(e => e.worker)
214
+
215
+ const workersManagerOptions = {
216
+ ...this.options.sandbox,
217
+ options: { ...this.options },
218
+ // we do map and copy to unproxy the value
219
+ extensionsDefs: extensionsForWorkers.map(e => Object.assign({}, e)),
220
+ documentStore: {
221
+ // we do copy to unproxy the value of entityTypes
222
+ model: {
223
+ ...this.documentStore.model,
224
+ entityTypes: { ...this.documentStore.model.entityTypes }
225
+ },
226
+ collections: Object.keys(this.documentStore.collections)
227
+ }
228
+ }
229
+
230
+ const workersManagerSystemOptions = {
231
+ initTimeout: this.options.workers.initTimeout,
232
+ numberOfWorkers: this.options.workers.numberOfWorkers,
233
+ workerModule: path.join(__dirname, '../worker', 'workerHandler.js'),
234
+ resourceLimits: this.options.workers.resourceLimits
235
+ }
236
+
237
+ // adding the validation of shortid after extensions has been loaded
238
+ setupValidateId(this)
239
+ setupValidateShortid(this)
240
+
241
+ this.initializeListeners.insert(0, 'core-resources-migration', () => migrateResourcesToAssets(this))
242
+ this.initializeListeners.insert(0, 'core-xlsxTemplates-migration', () => migrateXlsxTemplatesToAssets(this))
243
+
244
+ await this.initializeListeners.fire()
245
+
246
+ this._workersManager = this._workersManagerFactory
247
+ ? this._workersManagerFactory(workersManagerOptions, workersManagerSystemOptions)
248
+ : WorkersManager(workersManagerOptions, workersManagerSystemOptions, this.logger)
249
+
250
+ const workersStart = new Date().getTime()
251
+
252
+ this.logger.info('Initializing worker threads')
253
+
254
+ this.logger.debug(`Extensions in workers: ${extensionsForWorkers.map((e) => e.name).join(', ')}`)
255
+
256
+ await this._workersManager.init(workersManagerOptions)
257
+
258
+ this.logger.info(`${this.options.workers.numberOfWorkers} worker threads initialized in ${new Date().getTime() - workersStart}ms`)
259
+
260
+ this._startReaper(this.getPathsToWatchForAutoCleanup())
261
+
262
+ this.extensionsManager.recipes.push({
263
+ name: 'html'
264
+ })
265
+
266
+ this.extensionsManager.engines.push({
267
+ name: 'none'
268
+ })
269
+
270
+ this.monitoring.init()
271
+
272
+ this.logger.info('reporter initialized')
273
+ this._initialized = true
274
+ this._initExecution.resolve()
275
+ return this
276
+ } catch (e) {
277
+ this.logger.error(`Error occurred during reporter init: ${e.stack}`)
278
+ this._initExecution.reject(new Error(`Reporter initialization failed. ${e.message}`))
279
+ throw e
280
+ }
281
+ }
282
+
283
+ /**
284
+ * @public
285
+ */
286
+ addPathToWatchForAutoCleanup (customPath) {
287
+ this._extraPathsToCleanupCollection.add(customPath)
288
+ }
289
+
290
+ /**
291
+ * @public
292
+ */
293
+ getPathsToWatchForAutoCleanup () {
294
+ return [this.options.tempAutoCleanupDirectory].concat(Array.from(this._extraPathsToCleanupCollection.values()))
295
+ }
296
+
297
+ async checkValidEntityName (c, doc, req) {
298
+ if (!this.documentStore.model.entitySets[c].entityTypeDef.name) {
299
+ return
300
+ }
301
+
302
+ checkEntityName(doc.name)
303
+
304
+ await validateDuplicatedName(this, c, doc, undefined, req)
305
+
306
+ await validateReservedName(this, c, doc)
307
+ }
308
+
309
+ /**
310
+ * Main method for invoking rendering
311
+ * render({ template: { content: 'foo', engine: 'none', recipe: 'html' }, data: { foo: 'hello' } })
312
+ *
313
+ * @request {Object}
314
+ * @return {Promise} response.content is output buffer, response.stream is output stream, response.headers contains http applicable headers
315
+ *
316
+ * @public
317
+ */
318
+ async render (req, parentReq) {
319
+ if (!this._initialized) {
320
+ throw new Error('Not initialized, you need to call jsreport.init().then before rendering')
321
+ }
322
+
323
+ let options = {}
324
+ if (parentReq && !parentReq.__isJsreportRequest__) {
325
+ options = parentReq
326
+ parentReq = null
327
+ }
328
+
329
+ req = Object.assign({}, req)
330
+ req.context = Object.assign({}, req.context)
331
+ req.context.rootId = req.context.rootId || generateRequestId()
332
+ req.context.id = req.context.rootId
333
+
334
+ const worker = options.worker || await this._workersManager.allocate(req, {
335
+ timeout: this.options.reportTimeout
336
+ })
337
+
338
+ let keepWorker
339
+ let workerAborted
340
+
341
+ if (options.abortEmitter) {
342
+ options.abortEmitter.once('abort', () => {
343
+ workerAborted = true
344
+ worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
345
+ })
346
+ }
347
+
348
+ const res = { meta: {} }
349
+ try {
350
+ if (workerAborted) {
351
+ throw this.createError('Request aborted by client')
352
+ }
353
+
354
+ let isDataStoredInWorker = false
355
+
356
+ if (req.rawContent) {
357
+ isDataStoredInWorker = true
358
+ const result = await worker.execute({
359
+ actionName: 'parse',
360
+ req,
361
+ data: {}
362
+ }, {
363
+ timeout: this.options.reportTimeout
364
+ })
365
+ req = result
366
+ }
367
+
368
+ req = Request(req, parentReq)
369
+
370
+ if (isDataStoredInWorker) {
371
+ // we unset this because we want the Request() call in worker to evaluate the data
372
+ // and determine if the original was empty or not
373
+ delete req.context.originalInputDataIsEmpty
374
+ }
375
+
376
+ // TODO: we will probably validate in the thread
377
+ if (this.entityTypeValidator.getSchema('TemplateType') != null) {
378
+ const templateValidationResult = this.entityTypeValidator.validate('TemplateType', req.template, { rootPrefix: 'template' })
379
+
380
+ if (!templateValidationResult.valid) {
381
+ throw this.createError(`template input in request contain values that does not match the defined schema. ${templateValidationResult.fullErrorMessage}`, {
382
+ statusCode: 400
383
+ })
384
+ }
385
+ }
386
+
387
+ let reportTimeout = this.options.reportTimeout
388
+
389
+ if (
390
+ this.options.enableRequestReportTimeout &&
391
+ req.options &&
392
+ req.options.timeout != null
393
+ ) {
394
+ reportTimeout = req.options.timeout
395
+ }
396
+
397
+ await this.beforeRenderListeners.fire(req, res, { worker })
398
+
399
+ // this is used so far just in the reports extension
400
+ // it wants to send to the client immediate response with link to the report status
401
+ // but the previous steps already allocated worker which has the parsed input request
402
+ // so we need to keep the worker active and let the subsequent real render call use it
403
+ // we cant move the main beforeRenderListener before the worker allocation, because at that point
404
+ // the request isn't parsed and we don't know the template and options
405
+ if (req.context.returnResponseAndKeepWorker) {
406
+ keepWorker = true
407
+ res.stream = Readable.from(res.content)
408
+ return res
409
+ }
410
+
411
+ if (workerAborted) {
412
+ throw this.createError('Request aborted by client')
413
+ }
414
+
415
+ const responseResult = await this.executeWorkerAction('render', {}, {
416
+ timeout: reportTimeout + this.options.reportTimeoutMargin,
417
+ worker
418
+ }, req)
419
+
420
+ Object.assign(res, responseResult)
421
+ await this.afterRenderListeners.fire(req, res)
422
+ res.stream = Readable.from(res.content)
423
+ return res
424
+ } catch (err) {
425
+ if (err.code === 'WORKER_TIMEOUT') {
426
+ err.message = 'Report timeout'
427
+ if (req.context.profiling?.lastOperation != null && req.context.profiling?.entity != null) {
428
+ err.message += `. Last profiler operation: (${req.context.profiling.lastOperation.subtype}) ${req.context.profiling.lastOperation.name}`
429
+ }
430
+
431
+ if (req.context.http != null) {
432
+ const profileUrl = `${req.context.http.baseUrl}/studio/profiles/${req.context.profiling.entity._id}`
433
+ err.message += `. You can inspect and find more details here: ${profileUrl}`
434
+ }
435
+
436
+ err.weak = true
437
+ }
438
+
439
+ if (err.code === 'WORKER_ABORTED') {
440
+ err.message = 'Report cancelled'
441
+ err.weak = true
442
+ }
443
+
444
+ if (!err.logged) {
445
+ const logFn = err.weak ? this.logger.warn : this.logger.error
446
+ logFn(`Report render failed: ${err.message}${err.stack != null ? ' ' + err.stack : ''}`, req)
447
+ }
448
+ await this.renderErrorListeners.fire(req, res, err)
449
+ throw err
450
+ } finally {
451
+ if (!workerAborted && !keepWorker) {
452
+ await worker.release(req)
453
+ }
454
+ }
455
+ }
456
+
457
+ registerWorkersManagerFactory (workersManagerFactory) {
458
+ this._workersManagerFactory = workersManagerFactory
459
+ }
460
+
461
+ /**
462
+ *
463
+ * @public
464
+ */
465
+ async close () {
466
+ this.closing = true
467
+ this.logger.info('Closing jsreport instance')
468
+
469
+ if (this.monitoring) {
470
+ await this.monitoring.close()
471
+ }
472
+
473
+ if (this._reaperTimerRef) {
474
+ clearInterval(this._reaperTimerRef)
475
+ }
476
+
477
+ await this.closeListeners.fire()
478
+
479
+ if (this._workersManager) {
480
+ await this._workersManager.close()
481
+ }
482
+
483
+ if (this.documentStore) {
484
+ await this.documentStore.close()
485
+ }
486
+
487
+ this.logger.info('jsreport instance has been closed')
488
+ this.closed = true
489
+
490
+ return this
491
+ }
492
+
493
+ registerMainAction (actionName, fn) {
494
+ this._mainActions.set(actionName, fn)
495
+ }
496
+
497
+ async _invokeMainAction (data, req) {
498
+ await this.beforeMainActionListeners.fire(data.actionName, data.data, req)
499
+ if (!this._mainActions.has(data.actionName)) {
500
+ throw this.createError(`Main process action ${data.actionName} wasn't registered`)
501
+ }
502
+ return this._mainActions.get(data.actionName)(data.data, req)
503
+ }
504
+
505
+ _registerLogMainAction () {
506
+ this.registerMainAction('log', (log, req) => {
507
+ this.logger[log.level](log.message, { ...req, ...log.meta, timestamp: log.timestamp })
508
+ })
509
+ }
510
+
511
+ async executeWorkerAction (actionName, data, options = {}, req) {
512
+ req.context.rootId = req.context.rootId || generateRequestId()
513
+ const timeout = options.timeout || 60000
514
+
515
+ const worker = options.worker
516
+ ? options.worker
517
+ : await this._workersManager.allocate(req, {
518
+ timeout
519
+ })
520
+
521
+ try {
522
+ const result = await worker.execute({
523
+ actionName,
524
+ data,
525
+ // we set just known props, to avoid cloning failures on express req properties
526
+ req: {
527
+ context: req.context,
528
+ template: req.template,
529
+ data: req.data,
530
+ options: req.options
531
+ }
532
+ }, {
533
+ timeout,
534
+ timeoutErrorMessage: options.timeoutErrorMessage || ('Timeout during worker action ' + actionName),
535
+ executeMain: async (data) => {
536
+ return this._invokeMainAction(data, req)
537
+ }
538
+ })
539
+ this._workersManager.convertUint8ArrayToBuffer(result)
540
+ return result
541
+ } finally {
542
+ if (!options.worker) {
543
+ await worker.release(req)
544
+ }
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Periodical cleaning of folders where recipes are storing files like source html for pdf rendering
550
+ *
551
+ * @private
552
+ */
553
+ _startReaper (dir) {
554
+ const dirsToWatch = !Array.isArray(dir) ? [dir] : dir
555
+
556
+ if (this.options.autoTempCleanup === false) {
557
+ return
558
+ }
559
+
560
+ const threshold = this.options.reportTimeout > 180000 ? this.options.reportTimeout : 180000
561
+
562
+ this.logger.info(`Starting temp files cleanup with ${threshold}ms threshold`)
563
+
564
+ const reaper = new Reaper({ threshold })
565
+
566
+ dirsToWatch.forEach(d => reaper.watch(d))
567
+
568
+ reaper.start((err, files) => {
569
+ if (err) {
570
+ this.logger.error(`Failed to start auto cleanup: ${err.stack}`)
571
+ }
572
+ })
573
+
574
+ this._reaperTimerRef = setInterval(() => {
575
+ try {
576
+ reaper.start((err, files) => {
577
+ if (err) {
578
+ // NOT logging the error anymore because it was confusing users that something bad was happening
579
+ // this.logger.error('Failed to delete temp file: ' + err)
580
+ }
581
+ })
582
+ } catch (e) {
583
+ // NOT logging the error anymore because it was confusing users that something bad was happening
584
+ // catch error in case the reaper can not read directory
585
+ // this.logger.error('Failed to run reaper: ' + e)
586
+ }
587
+ }, 30000 /* check every 30s for old files */)
588
+
589
+ this._reaperTimerRef.unref()
590
+ }
591
+ }
592
+
593
+ module.exports = MainReporter