@researchdatabox/sails-hook-redbox-storage-mongo 1.5.3 → 5.0.0-RC2

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 (37) hide show
  1. package/README.md +20 -5
  2. package/dist/config/record.d.ts +3 -0
  3. package/dist/config/record.js +7 -0
  4. package/dist/config/storage.d.ts +105 -0
  5. package/dist/config/storage.js +25 -0
  6. package/dist/index.d.ts +16 -0
  7. package/dist/index.js +31 -0
  8. package/dist/models/DeletedRecord.d.ts +7 -0
  9. package/dist/models/DeletedRecord.js +37 -0
  10. package/dist/models/IntegrationAudit.d.ts +22 -0
  11. package/dist/models/IntegrationAudit.js +112 -0
  12. package/dist/models/Record.d.ts +13 -0
  13. package/dist/models/Record.js +64 -0
  14. package/dist/models/RecordAudit.d.ts +9 -0
  15. package/dist/models/RecordAudit.js +47 -0
  16. package/dist/models/index.d.ts +10 -0
  17. package/dist/models/index.js +32 -0
  18. package/dist/services/MongoStorageService.d.ts +83 -0
  19. package/dist/services/MongoStorageService.js +1276 -0
  20. package/dist/services/index.d.ts +5 -0
  21. package/dist/services/index.js +51 -0
  22. package/package.json +66 -32
  23. package/.github/dependabot.yml +0 -12
  24. package/api/models/DeletedRecord.js +0 -24
  25. package/api/models/Record.js +0 -41
  26. package/api/models/RecordAudit.js +0 -18
  27. package/api/services/MongoStorageService.js +0 -905
  28. package/config/record.js +0 -3
  29. package/config/storage.js +0 -73
  30. package/index.js +0 -21
  31. package/test/unit/.mocharc.js +0 -21
  32. package/test/unit/bootstrap.js +0 -63
  33. package/test/unit/db/storage.test.js +0 -121
  34. package/transformer/ExportJSONTransformer.js +0 -47
  35. package/tsconfig.json +0 -39
  36. package/typescript/api/services/MongoStorageService.ts +0 -1005
  37. package/typescript/transformer/ExportJSONTransformer.ts +0 -65
@@ -1,1005 +0,0 @@
1
- import { Observable, of } from 'rxjs';
2
- import { mergeMap } from 'rxjs';
3
-
4
- import { Sails, Model } from "sails";
5
- import { v1 as uuidv1 } from 'uuid';
6
- import { DateTime } from 'luxon';
7
-
8
- import mongodb = require('mongodb');
9
- import util = require('util');
10
- import stream = require('stream');
11
- import * as fs from 'fs';
12
- import { Transform } from 'json2csv';
13
- import { Services as services, DatastreamService, StorageService, StorageServiceResponse, DatastreamServiceResponse, Datastream, Attachment, RecordAuditModel, RecordAuditParams } from '@researchdatabox/redbox-core-types';
14
- const { transforms: { unwind, flatten } } = require('json2csv');
15
- const ExportJSONTransformer = require('../../transformer/ExportJSONTransformer')
16
-
17
-
18
- const pipeline = util.promisify(stream.pipeline);
19
-
20
- declare var sails: Sails;
21
- declare var _;
22
- declare var Record: Model, DeletedRecord: Model, RecordTypesService, TranslationService, FormsService, RecordAudit;
23
-
24
- export module Services {
25
- /**
26
- * Stores ReDBox records in MongoDB.
27
- *
28
- * Notes:
29
- * - Primary
30
- *
31
- * Author: <a href='https://github.com/shilob' target='_blank'>Shilo Banihit</a>
32
- *
33
- */
34
- export class MongoStorageService extends services.Core.Service implements StorageService, DatastreamService {
35
- gridFsBucket: any;
36
- db: any;
37
- recordCol: any;
38
- deletedRecordCol: any;
39
-
40
- protected _exportedMethods: any = [
41
- 'create',
42
- 'updateMeta',
43
- 'getMeta',
44
- 'createBatch',
45
- 'provideUserAccessAndRemovePendingAccess',
46
- 'getRelatedRecords',
47
- 'delete',
48
- 'updateNotificationLog',
49
- 'getRecords',
50
- 'restoreRecord',
51
- 'destroyDeletedRecord',
52
- 'getDeletedRecords',
53
- 'exportAllPlans',
54
- 'addDatastreams',
55
- 'updateDatastream',
56
- 'removeDatastream',
57
- 'addDatastream',
58
- 'addAndRemoveDatastreams',
59
- 'getDatastream',
60
- 'listDatastreams',
61
- 'createRecordAudit',
62
- 'getRecordAudit',
63
- 'exists'
64
- ];
65
-
66
-
67
- constructor() {
68
- super();
69
- this.logHeader = 'MongoStorageService::';
70
- let that = this;
71
- sails.on('ready', function () {
72
- that.init();
73
- sails.emit('hook:redbox:storage:ready');
74
- sails.emit('hook:redbox:datastream:ready');
75
- sails.log.verbose(`${that.logHeader} Ready!`);
76
- });
77
- }
78
-
79
-
80
-
81
- private getUuid(): string {
82
- return uuidv1().replace(/-/g, '');
83
- }
84
-
85
- private async init() {
86
- this.db = Record.getDatastore().manager;
87
- // check if the collection exists ...
88
- try {
89
- const collectionInfo = await this.db.collection(Record.tableName, { strict: true });
90
- sails.log.verbose(`${this.logHeader} Collection '${Record.tableName}' info:`);
91
- sails.log.verbose(JSON.stringify(collectionInfo));
92
- } catch (err) {
93
- sails.log.verbose(`Collection doesn't exist, creating: ${Record.tableName}`);
94
- const uuid = this.getUuid();
95
- const initRec = { redboxOid: uuid };
96
- await Record.create(initRec);
97
- await Record.destroyOne({ redboxOid: uuid });
98
- }
99
- this.gridFsBucket = new mongodb.GridFSBucket(this.db);
100
- this.recordCol = await this.db.collection(Record.tableName);
101
- this.deletedRecordCol = await this.db.collection(DeletedRecord.tableName);
102
- await this.createIndices(this.db);
103
- }
104
-
105
- private async createIndices(db) {
106
- sails.log.verbose(`${this.logHeader} Existing indices:`);
107
- const currentIndices = await db.collection(Record.tableName).indexes();
108
- sails.log.verbose(JSON.stringify(currentIndices));
109
- // creating indices...
110
- // Version as of writing: http://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#createIndexes
111
- try {
112
- const indices = sails.config.storage.mongodb.indices;
113
- if (_.size(indices) > 0) {
114
- // TODO: check if indices already exists
115
- await db.collection(Record.tableName).createIndexes(indices);
116
- }
117
- } catch (err) {
118
- sails.log.error(`Failed to create indices:`);
119
- sails.log.error(JSON.stringify(err));
120
- }
121
- }
122
-
123
- public async create(brand, record, recordType, user?): Promise<any> {
124
- sails.log.verbose(`${this.logHeader} create() -> Begin`);
125
- let response = new StorageServiceResponse();
126
- // Create DB entry
127
- record.redboxOid = this.getUuid();
128
- response.oid = record.redboxOid;
129
-
130
- try {
131
- sails.log.verbose(`${this.logHeader} Saving to DB...`);
132
- await Record.create(record);
133
- response.success = true;
134
- sails.log.verbose(`${this.logHeader} Record created...`);
135
- } catch (err) {
136
- sails.log.error(`${this.logHeader} Failed to create Record:`);
137
- sails.log.error(JSON.stringify(err));
138
- response.success = false;
139
- response.message = err.message;
140
- return response;
141
- }
142
- sails.log.verbose(JSON.stringify(response));
143
- sails.log.verbose(`${this.logHeader} create() -> End`);
144
- return response;
145
- }
146
-
147
- public async updateMeta(brand, oid, record, user?): Promise<any> {
148
- let response = new StorageServiceResponse();
149
- response.oid = oid;
150
- try {
151
- // Fixes: https://github.com/redbox-mint/redbox-portal/issues/800
152
- _.unset(record, 'dateCreated');
153
- _.unset(record, 'lastSaveDate');
154
- _.unset(record, '_id');
155
-
156
- // Records with an 'id' property fail to save with error:
157
- // {"cause":{"name":"AdapterError","adapterMethodName":"update","modelIdentity":"record","raw":{"code":"E_CANNOT_INTERPRET_AS_OBJECTID"}},"isOperational":true}
158
- // Fix by ensuring the 'id' property is removed.
159
- _.unset(record, 'id');
160
-
161
- await Record.updateOne({ redboxOid: oid }).set(record);
162
- response.success = true;
163
- } catch (err) {
164
- sails.log.error(`${this.logHeader} Failed to save update to MongoDB: ${JSON.stringify({
165
- error: err,
166
- response: response,
167
- brand: brand,
168
- oid: oid,
169
- record: record,
170
- user: user,
171
- })}`);
172
- response.success = false;
173
- response.message = err;
174
- }
175
- return response;
176
- }
177
-
178
- public async getMeta(oid): Promise<any> {
179
- // let response = new StorageServiceResponse();
180
- // const rec = await Record.findOne({id: oid});
181
- // rec.success = true;
182
- // rec.
183
- if (_.isEmpty(oid)) {
184
- const msg = `${this.logHeader} getMeta() -> refusing to search using an empty OID`;
185
- sails.log.error(msg);
186
- throw new Error(msg);
187
- }
188
- const criteria = { redboxOid: oid };
189
- sails.log.verbose(`${this.logHeader} finding: `);
190
- sails.log.verbose(JSON.stringify(criteria));
191
- return Record.findOne(criteria);
192
- }
193
-
194
- public async createBatch(type, data, harvestIdFldName): Promise<any> {
195
- const response = new StorageServiceResponse();
196
- response.message = "";
197
- let failFlag = false;
198
- _.each(data, async (dataItem) => {
199
- dataItem.harvestId = _.get(dataItem, harvestIdFldName, '');
200
- _.set(dataItem, 'metaMetadata.type', type);
201
- try {
202
- await this.create(null, dataItem, null, null);
203
- } catch (err) {
204
- failFlag = true;
205
- sails.log.error(`${this.logHeader} Failed createBatch entry: `);
206
- sails.log.error(JSON.stringify(dataItem));
207
- sails.log.error(`${this.logHeader} Failed createBatch error: `);
208
- sails.log.error(JSON.stringify(err));
209
- response.message = `${response.message}, ${err.message}`;
210
- }
211
- });
212
- response.success = failFlag === false;
213
- return response;
214
- }
215
-
216
- /**
217
- * If pendingValue is in editPending or viewPending, remove all instances from the pending arrays and
218
- * put the userid in the matching access / non-pending array, then save the changes to the storage.
219
- *
220
- * Implementation Note: This is a sync method, but it calls async methods.
221
- * This means that the storage will not be updated when this method returns, but at some point later.
222
- *
223
- * @param oid The record identifier.
224
- * @param userid The value to put in the matching access / non-pending arrays.
225
- * @param pendingValue The value to find in the pending arrays.
226
- */
227
- public provideUserAccessAndRemovePendingAccess(oid, userid, pendingValue): void {
228
- const batchFn = async () => {
229
- const metadata = await this.getMeta(oid);
230
-
231
- // Update edit authorization - remove pending edit access and add real edit access with userid
232
- const pendingEditArray: string[] = _.get(metadata, 'authorization.editPending', []);
233
- const editArray: string[] = _.get(metadata, 'authorization.edit', []);
234
-
235
- // remove all items matching pendingValue from the pendingEditArray
236
- const pendingEditArrayFiltered = pendingEditArray.filter(value => value !== pendingValue);
237
- const pendingEditFound = pendingEditArray.length > pendingEditArrayFiltered.length;
238
-
239
- // add the item to the editArray if it existed in the pendingEditArray, and it is not in the editArray
240
- if (pendingEditFound && !editArray.includes(userid)) {
241
- editArray.push(userid);
242
- }
243
-
244
- // update the metadata with the modified arrays
245
- _.set(metadata, 'authorization.editPending', pendingEditArrayFiltered);
246
- _.set(metadata, 'authorization.edit', editArray);
247
-
248
- // Update view authorization - remove pending view access and add real view access with userid
249
- const pendingViewArray: string[] = _.get(metadata, 'authorization.viewPending', []);
250
- const viewArray: string[] = _.get(metadata, 'authorization.view', []);
251
-
252
- // remove all items matching pendingValue from the pendingViewArray
253
- const pendingViewArrayFiltered = pendingViewArray.filter(value => value !== pendingValue);
254
- const pendingViewFound = pendingViewArray.length > pendingViewArrayFiltered.length;
255
-
256
- // add the item to the viewArray if it existed in the pendingViewArray, and it is not in the viewArray
257
- if (pendingViewFound && !viewArray.includes(userid)) {
258
- viewArray.push(userid);
259
- }
260
-
261
- // update the metadata with the modified arrays
262
- _.set(metadata, 'authorization.viewPending', pendingViewArrayFiltered);
263
- _.set(metadata, 'authorization.view', viewArray);
264
-
265
- try {
266
- await this.updateMeta(null, oid, metadata);
267
- } catch (err) {
268
- sails.log.error(`${this.logHeader} Failed to update on 'provideUserAccessAndRemovePendingAccess': `);
269
- sails.log.error(JSON.stringify(err));
270
- }
271
- };
272
-
273
- // TODO: Fix this so that the method only returns after the storage is updated.
274
- batchFn();
275
- }
276
-
277
- public async getRelatedRecords(oid, brand, recordTypeName: any = null, mappingContext: any = null) {
278
- let record = await this.getMeta(oid);
279
- if (_.isEmpty(recordTypeName)) {
280
- recordTypeName = record['metaMetadata']['type'];
281
- }
282
- let recordType = await RecordTypesService.get(brand, recordTypeName).toPromise();
283
- if (_.isEmpty(mappingContext)) {
284
- mappingContext = {
285
- 'processedRelationships': [recordTypeName],
286
- 'relatedObjects': {}
287
- };
288
- // add this records so it can be updated too!
289
- mappingContext.relatedObjects[recordTypeName] = [record];
290
- }
291
- let relatedTo = recordType['relatedTo'];
292
- if (_.isArray(relatedTo) && _.size(relatedTo) > 0) {
293
- for (let relationship of relatedTo) {
294
- sails.log.verbose(`${this.logHeader} Processing relationship:`);
295
- sails.log.verbose(JSON.stringify(relationship));
296
- const targetRecordType = relationship['recordType'];
297
- // retrieve the related records from the DB...
298
- const criteria: any = {};
299
- criteria['metaMetadata.type'] = targetRecordType;
300
- criteria[relationship['foreignField']] = oid;
301
- sails.log.verbose(`${this.logHeader} Finding related records criteria:`);
302
- sails.log.verbose(JSON.stringify(criteria));
303
-
304
- const relatedRecords = await Record.find(criteria).meta({ enableExperimentalDeepTargets: true });
305
- sails.log.verbose(`${this.logHeader} Got related records:`);
306
- sails.log.verbose(JSON.stringify(relatedRecords));
307
- if (_.size(relatedRecords) > 0) {
308
- if (_.isEmpty(mappingContext.relatedObjects[targetRecordType])) {
309
- mappingContext.relatedObjects[targetRecordType] = relatedRecords;
310
- } else {
311
- mappingContext.relatedObjects[targetRecordType] = mappingContext.relatedObjects[targetRecordType].concat(relatedRecords);
312
- }
313
- for (let j = 0; j < relatedRecords.length; j++) {
314
- let recordRelationship = relatedRecords[j];
315
- mappingContext = await this.getRelatedRecords(recordRelationship.redboxOid, brand, null, mappingContext);
316
- }
317
- }
318
- if (!_.includes(mappingContext.processedRelationships, targetRecordType)) {
319
- mappingContext.processedRelationships.push(targetRecordType);
320
- }
321
- }
322
- } else {
323
- sails.log.verbose(`${this.logHeader} RecordType has no relationships: ${recordTypeName}`);
324
- }
325
- sails.log.verbose(`${this.logHeader} Current mapping context:`);
326
- sails.log.verbose(JSON.stringify(mappingContext));
327
- return mappingContext;
328
- }
329
-
330
-
331
- public async delete(oid, permanentlyDelete: boolean = false) {
332
- const response = new StorageServiceResponse();
333
-
334
- try {
335
- if (permanentlyDelete) {
336
-
337
- const datastreams = await this.listDatastreams(oid, null);
338
- if (_.size(datastreams) > 0) {
339
- _.each(datastreams, (file) => {
340
- sails.log.verbose(`Deleting:`);
341
- sails.log.verbose(JSON.stringify(file));
342
- this.gridFsBucket.delete(file['_id'], (err, res) => {
343
- if (err) {
344
- sails.log.error(`Error deleting: ${file['_id']}`);
345
- sails.log.error(JSON.stringify(err));
346
- }
347
- });
348
- });
349
- }
350
-
351
- } else {
352
- let record: any = await this.getMeta(oid);
353
- let deletedRecord = {
354
- redboxOid: record.redboxOid,
355
- deletedRecordMetadata: record
356
- }
357
- await DeletedRecord.create(deletedRecord);
358
- }
359
- await Record.destroyOne({ redboxOid: oid });
360
- response.success = true;
361
- } catch (err) {
362
- sails.log.error(`${this.logHeader} Failed to delete record: ${oid}`);
363
- sails.log.error(JSON.stringify(err));
364
- response.success = false;
365
- response.message = err.message;
366
- }
367
-
368
- return response;
369
- }
370
-
371
- public async updateNotificationLog(oid, record, options): Promise<any> {
372
- if (super.metTriggerCondition(oid, record, options) == "true") {
373
- sails.log.verbose(`${this.logHeader} Updating notification log for oid: ${oid}`);
374
- const logName = _.get(options, 'logName', null);
375
- if (logName) {
376
- let log = _.get(record, logName, null);
377
- // Use Luxon for ISO-like local timestamp without timezone offset
378
- const entry = { date: DateTime.now().toFormat("yyyy-LL-dd'T'HH:mm:ss") };
379
- if (log) {
380
- log.push(entry);
381
- } else {
382
- log = [entry];
383
- }
384
- _.set(record, logName, log);
385
- }
386
- const updateFlagName = _.get(options, 'flagName', null);
387
- if (updateFlagName) {
388
- _.set(record, updateFlagName, _.get(options, 'flagVal', null));
389
- }
390
- sails.log.verbose(`======== Notification log updates =========`);
391
- sails.log.verbose(JSON.stringify(record));
392
- sails.log.verbose(`======== End update =========`);
393
- // ready to update
394
- if (_.get(options, "saveRecord", false)) {
395
- try {
396
- const response = await this.updateMeta(null, oid, record, null);
397
- } catch (err) {
398
- sails.log.error(`${this.logHeader} Failed to update notification log of ${oid}:`);
399
- sails.log.error(JSON.stringify(err));
400
- throw err;
401
- }
402
- }
403
- } else {
404
- sails.log.verbose(`Notification log name: '${options.name}', for oid: ${oid}, not running, condition not met: ${options.triggerCondition}`);
405
- sails.log.verbose(JSON.stringify(record));
406
- }
407
- // no updates or condition not met ... just return the record
408
- return record;
409
- }
410
-
411
- public async getDeletedRecords(workflowState, recordType = undefined, start, rows = 10, username, roles, brand, editAccessOnly = undefined, packageType = undefined, sort = undefined, filterFields = undefined, filterString = undefined, filterMode:string = 'regex', secondarySort = undefined) {
412
-
413
-
414
- // BrandId ...
415
- let query = {
416
- "deletedRecordMetadata.metaMetadata.brandId": brand.id
417
- };
418
- // Paginate ...
419
- const options = {
420
- limit: _.toNumber(rows),
421
- skip: _.toNumber(start)
422
- }
423
- // Sort ...defaults to lastSaveDate
424
- if (_.isEmpty(sort)) {
425
- sort = '{"lastSaveDate": -1}';
426
- }
427
- sails.log.verbose(`Sort is: ${sort}`);
428
- if (_.indexOf(`${sort}`, '1') == -1) {
429
- // if only the field is specified, default to descending...
430
- sort = `{"${sort}":-1}`;
431
- } else {
432
- try {
433
- options['sort'] = JSON.parse(sort);
434
- } catch (error) {
435
- // trying to massage this to valid JSON
436
- options['sort'] = {};
437
- options['sort'][`${sort.substring(0, sort.indexOf(':'))}`] = _.toNumber(sort.substring(sort.indexOf(':') + 1));
438
- }
439
- }
440
-
441
- if(!_.isEmpty(secondarySort)) {
442
- options['sort'][`${secondarySort.substring(0, secondarySort.indexOf(':'))}`] = _.toNumber(secondarySort.substring(secondarySort.indexOf(':') + 1));
443
- }
444
-
445
- // Authorization ...
446
- let roleNames = this.getRoleNames(roles, brand);
447
- let andArray = [];
448
- let permissions = {
449
- "$or": [{ "deletedRecordMetadata.authorization.view": username },
450
- { "deletedRecordMetadata.authorization.edit": username },
451
- { "deletedRecordMetadata.authorization.editRoles": { "$in": roleNames } },
452
- { "deletedRecordMetadata.authorization.viewRoles": { "$in": roleNames } }]
453
- };
454
- andArray.push(permissions);
455
- // Metadata type...
456
- if (!_.isUndefined(recordType) && !_.isEmpty(recordType)) {
457
- let typeArray = [];
458
- _.each(recordType, rType => {
459
- typeArray.push({ "deletedRecordMetadata.metaMetadata.type": rType });
460
- });
461
- let types = { "$or": typeArray };
462
- andArray.push(types);
463
- }
464
- // Package type...
465
- if (!_.isUndefined(packageType) && !_.isEmpty(packageType)) {
466
- let typeArray = [];
467
- _.each(packageType, rType => {
468
- typeArray.push({ "deletedRecordMetadata.metaMetadata.packageType": rType });
469
- });
470
- let types = { "$or": typeArray };
471
- andArray.push(types);
472
- }
473
- // Workflow ...
474
- if (workflowState != undefined) {
475
- query["deletedRecordMetadata.workflow.stage"] = workflowState;
476
- }
477
- if (!_.isEmpty(filterString) && !_.isEmpty(filterFields)) {
478
- let escapedFilterString = this.escapeRegExp(filterString);
479
- sails.log.verbose('escapedFilterString ' + escapedFilterString);
480
- for (let filterField of filterFields) {
481
- let filterQuery = {};
482
- if (filterMode == 'equal') {
483
- filterQuery[filterField] = filterString;
484
- } else if (filterMode == 'regex') {
485
- filterQuery[filterField] = new RegExp(`.*${escapedFilterString}.*`);
486
- //regex expressions are printed as empty objects {} when using JSON.stringify
487
- //hence intentionally not using JSON.stringify in below logging print out
488
- sails.log.verbose(filterQuery);
489
- }
490
- andArray.push(filterQuery);
491
- }
492
- }
493
-
494
- query['$and'] = andArray;
495
-
496
- sails.log.verbose(`Query: ${JSON.stringify(query)}`);
497
- sails.log.verbose(`Options: ${JSON.stringify(options)}`);
498
- const { items, totalItems } = await this.runDeletedRecordQuery(Record.tableName, query, options);
499
- const response = new StorageServiceResponse();
500
- response.success = true;
501
- response.items = items;
502
- response.totalItems = totalItems;
503
- return response;
504
- }
505
-
506
- public async getRecords(workflowState, recordType = undefined, start, rows = 10, username, roles, brand, editAccessOnly = undefined, packageType = undefined, sort = undefined, filterFields = undefined, filterString = undefined, filterMode = undefined, secondarySort = undefined) {
507
-
508
- //Default to regex when filterMode is not set to maintain pre existing functionality
509
- if (_.isUndefined(filterMode) || _.isNull(filterMode) || _.isEmpty(filterMode)) {
510
- filterMode = 'regex';
511
- }
512
- // BrandId ...
513
- let query = {
514
- "metaMetadata.brandId": brand.id
515
- };
516
- // Paginate ...
517
- const options = {
518
- limit: _.toNumber(rows),
519
- skip: _.toNumber(start)
520
- }
521
- // Sort ...defaults to lastSaveDate
522
- if (_.isEmpty(sort)) {
523
- sort = '{"lastSaveDate": -1}';
524
- }
525
- sails.log.verbose(`Sort is: ${sort}`);
526
- if (_.indexOf(`${sort}`, '1') == -1) {
527
- // if only the field is specified, default to descending...
528
- sort = `{"${sort}":-1}`;
529
- } else {
530
- try {
531
- options['sort'] = JSON.parse(sort);
532
- } catch (error) {
533
- // trying to massage this to valid JSON
534
- options['sort'] = {};
535
- options['sort'][`${sort.substring(0, sort.indexOf(':'))}`] = _.toNumber(sort.substring(sort.indexOf(':') + 1));
536
- }
537
- }
538
-
539
- if(!_.isEmpty(secondarySort)) {
540
- options['sort'][`${secondarySort.substring(0, secondarySort.indexOf(':'))}`] = _.toNumber(secondarySort.substring(secondarySort.indexOf(':') + 1));
541
- }
542
-
543
- // Authorization ...
544
- let roleNames = this.getRoleNames(roles, brand);
545
- let andArray = [];
546
- let permissions = {
547
- "$or": [{ "authorization.view": username },
548
- { "authorization.edit": username },
549
- { "authorization.editRoles": { "$in": roleNames } },
550
- { "authorization.viewRoles": { "$in": roleNames } }]
551
- };
552
- andArray.push(permissions);
553
- // Metadata type...
554
- if (_.isArray(recordType)) {
555
- if(recordType.length > 1) {
556
- let typeArray = [];
557
- _.each(recordType, rType => {
558
- typeArray.push({ "metaMetadata.type": rType });
559
- });
560
- // Fixed incorrect "$or" condition construction: it should be top level, not nested within a field name.
561
- // let types = { "$or": typeArray };
562
- // query["metaMetadata.type"] = types;
563
- query["$or"] = typeArray;
564
- } else {
565
- let recType = recordType[0];
566
- if(!_.isUndefined(recType) && !_.isEmpty(recType)) {
567
- query["metaMetadata.type"] = recType;
568
- }
569
- }
570
- } else if(recordType != undefined && recordType != '') {
571
- query["metaMetadata.type"] = recordType;
572
- }
573
- // Package type...
574
- if (_.isArray(packageType)) {
575
- if(packageType.length > 1) {
576
- let typeArray = [];
577
- _.each(packageType, rType => {
578
- typeArray.push({ "metaMetadata.packageType": rType });
579
- });
580
- let types = { "$or": typeArray };
581
- query["metaMetadata.packageType"] = types;
582
- } else {
583
- let packType = packageType[0];
584
- if(!_.isUndefined(packType) && !_.isEmpty(packType)) {
585
- query["metaMetadata.packageType"] = packType;
586
- }
587
- }
588
- } else if(packageType != undefined && packageType != '') {
589
- query["metaMetadata.packageType"] = packageType;
590
- }
591
- // Workflow ...
592
- if (workflowState != undefined && workflowState != '') {
593
- query["workflow.stage"] = workflowState;
594
- }
595
- //Additional filter conditions
596
- if (!_.isEmpty(filterString) && !_.isEmpty(filterFields)) {
597
- let escapedFilterString = this.escapeRegExp(filterString);
598
- sails.log.verbose('escapedFilterString ' + escapedFilterString);
599
- for (let filterField of filterFields) {
600
- if (filterMode == 'equal') {
601
- query[filterField] = filterString;
602
- } else if (filterMode == 'regex') {
603
- // Improved to enable case insensitive search by default and allow for partial matches
604
- query[filterField] = new RegExp(`.*${escapedFilterString}.*`, 'i');
605
- }
606
- }
607
- }
608
-
609
- query['$and'] = andArray;
610
-
611
- //regex expressions are printed as empty objects {} when using JSON.stringify
612
- //hence intentionally not using JSON.stringify in below logging print out
613
- sails.log.verbose(query);
614
-
615
- sails.log.verbose(`Query: ${JSON.stringify(query)}`);
616
- sails.log.verbose(`Options: ${JSON.stringify(options)}`);
617
- const { items, totalItems } = await this.runRecordQuery(Record.tableName, query, options);
618
- const response = new StorageServiceResponse();
619
- response.success = true;
620
- response.items = items;
621
- response.totalItems = totalItems;
622
- return response;
623
- }
624
-
625
- private escapeRegExp(string) {
626
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
627
- }
628
-
629
- protected async runRecordQuery(colName, query, options) {
630
- return { items: await this.recordCol.find(query, options).toArray(), totalItems: await this.recordCol.count(query) };
631
- }
632
-
633
- protected async runDeletedRecordQuery(colName, query, options) {
634
- return { items: await this.deletedRecordCol.find(query, options).toArray(), totalItems: await this.deletedRecordCol.count(query) };
635
- }
636
-
637
- private async * fetchAllRecords(query, options, stringifyJSON: boolean = false) {
638
- let skip = 0;
639
- let limit = options.limit;
640
- options.skip = skip;
641
- let result = await this.recordCol.find(query, options).toArray();
642
-
643
- while (result.length > 0) {
644
- for (let record of result) {
645
- if (stringifyJSON) {
646
- yield JSON.stringify(record);
647
- } else {
648
- yield record;
649
- }
650
- }
651
- skip = skip + limit;
652
- options.skip = skip;
653
- result = await this.recordCol.find(query, options).toArray();
654
-
655
- }
656
- }
657
-
658
- public exportAllPlans(username, roles, brand, format, modBefore, modAfter, recType): stream.Readable {
659
- let andArray = [];
660
- let query = {
661
- "metaMetadata.brandId": brand.id,
662
- "metaMetadata.type": recType
663
- };
664
- let roleNames = this.getRoleNames(roles, brand);
665
- let permissions = {
666
- "$or": [{ "authorization.view": username },
667
- { "authorization.edit": username },
668
- { "authorization.editRoles": { "$in": roleNames } },
669
- { "authorization.viewRoles": { "$in": roleNames } }]
670
- };
671
- andArray.push(permissions);
672
- const options = {
673
- limit: _.toNumber(sails.config.record.export.maxRecords),
674
- sort: {
675
- lastSaveDate: -1
676
- }
677
- };
678
- if (!_.isEmpty(modAfter)) {
679
- andArray.push({
680
- lastSaveDate: {
681
- '$gte': `${modAfter}`
682
- }
683
- });
684
- }
685
- if (!_.isEmpty(modBefore)) {
686
- andArray.push({
687
- lastSaveDate: {
688
- '$lte': `${modBefore}`
689
- }
690
- });
691
- }
692
- query['$and'] = andArray;
693
- sails.log.verbose(`Query: ${JSON.stringify(query)}`);
694
- sails.log.verbose(`Options: ${JSON.stringify(options)}`);
695
- if (format == 'csv') {
696
- const opts = { transforms: [flatten()] };
697
- const transformOpts = { objectMode: true };
698
- const json2csv = new Transform(opts, transformOpts);
699
- return stream.Readable.from(this.fetchAllRecords(query, options)).pipe(json2csv);
700
- }
701
-
702
- //TODO: incorporate object mode so that JSON.stringify is handled in the Transformer rather than fetch
703
- const jsonTransformer = new ExportJSONTransformer(recType, modBefore, modAfter);
704
- return stream.Readable.from(this.fetchAllRecords(query, options, true)).pipe(jsonTransformer);
705
- }
706
-
707
- protected getRoleNames(roles, brand) {
708
- var roleNames = [];
709
-
710
- for (var i = 0; i < roles.length; i++) {
711
- var role = roles[i]
712
- if (role.branding == brand.id) {
713
- roleNames.push(roles[i].name);
714
- }
715
- }
716
-
717
- return roleNames;
718
- }
719
-
720
-
721
- public async addDatastreams(oid: string, fileIds: Datastream[]): Promise<DatastreamServiceResponse> {
722
- const response = new DatastreamServiceResponse();
723
- response.message = '';
724
- let hasFailure = false;
725
- for (const fileId of fileIds) {
726
- try {
727
- await this.addDatastream(oid, fileId);
728
- const successMessage = `Successfully uploaded: ${JSON.stringify(fileId)}`;
729
- response.message = _.isEmpty(response.message) ? successMessage : `${response.message}\n${successMessage}`;
730
- } catch (err) {
731
- hasFailure = true;
732
- const failureMessage = `Failed to upload: ${JSON.stringify(fileId)}, error is:\n${JSON.stringify(err)}`;
733
- response.message = _.isEmpty(response.message) ? failureMessage : `${response.message}\n${failureMessage}`;
734
- }
735
- }
736
- response.success = !hasFailure;
737
- return response;
738
- }
739
-
740
- public updateDatastream(oid: string, record, newMetadata, fileRoot, fileIdsAdded): any {
741
- // loop thru the attachment fields and determine if we need to add or remove
742
- return FormsService.getFormByName(record.metaMetadata.form, true)
743
- .pipe(
744
- mergeMap(form => {
745
- // For any generated, view-only forms, the form may be null, add a coalescence to avoid breaking
746
- // the attachment update process.
747
- form = form ?? { attachmentFields: [] };
748
- const typedForm = form as { attachmentFields: string[] };
749
- const reqs = [];
750
- record.metaMetadata.attachmentFields = typedForm.attachmentFields;
751
- _.each(typedForm.attachmentFields, async (attField) => {
752
- const oldAttachments = record.metadata[attField];
753
- const newAttachments = newMetadata[attField];
754
- const removeIds = [];
755
- // process removals
756
- if (!_.isUndefined(oldAttachments) && !_.isNull(oldAttachments) && !_.isNull(newAttachments)) {
757
- const toRemove = _.differenceBy(oldAttachments, newAttachments, 'fileId');
758
- _.each(toRemove, (removeAtt) => {
759
- if (removeAtt.type == 'attachment') {
760
- removeIds.push(new Datastream(removeAtt));
761
- }
762
- });
763
- }
764
- // process additions
765
- if (!_.isUndefined(newAttachments) && !_.isNull(newAttachments)) {
766
- const toAdd = _.differenceBy(newAttachments, oldAttachments, 'fileId');
767
- _.each(toAdd, (addAtt) => {
768
- if (addAtt.type == 'attachment') {
769
- fileIdsAdded.push(new Datastream(addAtt));
770
- }
771
- });
772
- }
773
- reqs.push(this.addAndRemoveDatastreams(oid, fileIdsAdded, removeIds));
774
- });
775
- if (_.isEmpty(reqs)) {
776
- reqs.push(of({ "request": "dummy" }));
777
- }
778
- return of(reqs);
779
- })
780
- );
781
- }
782
-
783
- public async removeDatastream(oid, datastream: Datastream) {
784
- const fileId = datastream.fileId;
785
- const fileName = `${oid}/${fileId}`;
786
- const fileRes = await this.getFileWithName(fileName).toArray();
787
- if (!_.isEmpty(fileRes)) {
788
- const fileDoc = fileRes[0];
789
- sails.log.verbose(`${this.logHeader} removeDatastream() -> Deleting:`);
790
- sails.log.verbose(JSON.stringify(fileDoc));
791
- this.gridFsBucket.delete(fileDoc['_id'], (err, res) => {
792
- if (err) {
793
- sails.log.error(`Error deleting: ${fileDoc['_id']}`);
794
- sails.log.error(JSON.stringify(err));
795
- }
796
- });
797
- sails.log.verbose(`${this.logHeader} removeDatastream() -> Delete successful.`);
798
- } else {
799
- sails.log.verbose(`${this.logHeader} removeDatastream() -> File not found: ${fileName}`);
800
- }
801
- }
802
-
803
- public async addDatastream(oid, datastream: Datastream) {
804
- const fileId = datastream.fileId;
805
- sails.log.verbose(`${this.logHeader} addDatastream() -> Meta: ${fileId}`);
806
- sails.log.verbose(JSON.stringify(datastream));
807
- const metadata = _.merge(datastream.metadata, { redboxOid: oid });
808
- const fpath = `${sails.config.record.attachments.stageDir}/${fileId}`;
809
- const fileName = `${oid}/${fileId}`;
810
- sails.log.verbose(`${this.logHeader} addDatastream() -> Adding: ${fileName}`);
811
-
812
- await this.streamFileToBucket(fpath, fileName, metadata);
813
- sails.log.verbose(`${this.logHeader} addDatastream() -> Successfully added: ${fileName}`);
814
- }
815
-
816
- /**
817
- *
818
- * Stream file to bucket and return a promise when it's complete
819
- *
820
- * */
821
- private streamFileToBucket(fpath: string, fileName: string, metadata: any) {
822
- const uploadStream = this.gridFsBucket.openUploadStream(fileName, { metadata: metadata });
823
- fs.createReadStream(fpath)
824
- .pipe(uploadStream)
825
-
826
- return new Promise((resolve, reject) => {
827
- uploadStream.on('finish', () => {
828
- resolve(uploadStream.gridFSFile);
829
- });
830
-
831
- uploadStream.on('error', (err) => {
832
- reject(err);
833
- });
834
- });
835
-
836
- }
837
-
838
- public async addAndRemoveDatastreams(oid, addIds: any[], removeIds: any[]) {
839
- for (const addId of addIds) {
840
- await this.addDatastream(oid, addId);
841
- }
842
- for (const removeId of removeIds) {
843
- await this.removeDatastream(oid, removeId);
844
- }
845
- }
846
-
847
- public async getDatastream(oid, fileId): Promise<any> {
848
- return this.getDatastreamAsync(oid, fileId);
849
- }
850
-
851
- private async getDatastreamAsync(oid, fileId): Promise<any> {
852
- const fileName = `${oid}/${fileId}`;
853
- const fileRes = await this.getFileWithName(fileName).toArray();
854
- if (_.isArray(fileRes) && fileRes.length === 0) {
855
- throw new Error(TranslationService.t('attachment-not-found'))
856
- }
857
- const response = new Attachment();
858
- response.readstream = this.gridFsBucket.openDownloadStreamByName(fileName)
859
- return response;
860
- }
861
-
862
- public async listDatastreams(oid, fileId) {
863
- let query: any = { "metadata.redboxOid": oid };
864
- if (!_.isEmpty(fileId)) {
865
- const fileName = `${oid}/${fileId}`;
866
- query = { filename: fileName };
867
- }
868
- sails.log.verbose(`${this.logHeader} listDatastreams() -> Listing attachments of oid: ${oid}`);
869
- sails.log.verbose(JSON.stringify(query));
870
- return this.gridFsBucket.find(query, {}).toArray();
871
- }
872
-
873
- public async createRecordAudit(recordAudit: RecordAuditModel): Promise<any> {
874
- let response = new StorageServiceResponse();
875
- try {
876
- sails.log.verbose(`${this.logHeader} Saving to DB...`);
877
- await RecordAudit.create(recordAudit);
878
- //TODO: fix type model to have the _id attribute
879
- let savedRecordAudit: any = recordAudit;
880
- response.oid = savedRecordAudit._id;
881
- response.success = true;
882
- sails.log.verbose(`${this.logHeader} Record Audit created...`);
883
- } catch (err) {
884
- sails.log.error(`${this.logHeader} Failed to create Record Audit:`);
885
- sails.log.error(JSON.stringify(err));
886
- response.success = false;
887
- response.message = err.message;
888
- return response;
889
- }
890
- sails.log.verbose(JSON.stringify(response));
891
- sails.log.verbose(`${this.logHeader} create() -> End`);
892
- return response;
893
- }
894
-
895
- public async getRecordAudit(params: RecordAuditParams): Promise<any> {
896
-
897
- const oid = params.oid;
898
- const dateFrom = params.dateFrom;
899
- const dateTo = params.dateTo;
900
-
901
- if (_.isEmpty(oid)) {
902
- const msg = `${this.logHeader} getMeta() -> refusing to search using an empty OID`;
903
- sails.log.error(msg);
904
- throw new Error(msg);
905
- }
906
-
907
- var criteria = { "redboxOid": oid };
908
-
909
- if (_.isDate(dateFrom)) {
910
- criteria['createdAt'] = { ['>=']: dateFrom };
911
- }
912
-
913
- if (_.isDate(dateTo)) {
914
- if (_.isUndefined(criteria['createdAt'])) {
915
- criteria['createdAt'] = {};
916
- }
917
- criteria['createdAt']['<='] = dateTo;
918
- sails.log.verbose(criteria);
919
- }
920
-
921
- sails.log.verbose(`${this.logHeader} finding: `);
922
- sails.log.verbose(JSON.stringify(criteria));
923
- return RecordAudit.find(criteria);
924
- }
925
-
926
- async restoreRecord(oid: any): Promise<any> {
927
- const response = new StorageServiceResponse();
928
-
929
- if (_.isEmpty(oid)) {
930
- const msg = `${this.logHeader} restoreRecord() -> refusing to search using an empty OID`;
931
- sails.log.error(msg);
932
- throw new Error(msg);
933
- }
934
-
935
- try {
936
- sails.log.verbose(`${this.logHeader} Restoring record ${oid} to DB...`);
937
- let deletedRecord = await DeletedRecord.findOne({redboxOid: oid});
938
- delete deletedRecord.deletedRecordMetadata._id;
939
-
940
- let record = await Record.create(deletedRecord.deletedRecordMetadata);
941
- response.metadata = record;
942
-
943
- await DeletedRecord.destroyOne({redboxOid:oid});
944
- response.success = true;
945
- sails.log.verbose(`${this.logHeader} Record restored...`);
946
- return response;
947
- } catch (err) {
948
- sails.log.error(`${this.logHeader} Failed to create Record:`);
949
- sails.log.error(JSON.stringify(err));
950
- response.success = false;
951
- response.message = err.message;
952
- return response;
953
- }
954
- }
955
-
956
- async destroyDeletedRecord(oid: any): Promise<any> {
957
- const response = new StorageServiceResponse();
958
-
959
- if (_.isEmpty(oid)) {
960
- const msg = `${this.logHeader} destroyRecord() -> refusing to search using an empty OID`;
961
- sails.log.error(msg);
962
- throw new Error(msg);
963
- }
964
-
965
- try {
966
- sails.log.verbose(`${this.logHeader} destroying deleted record ${oid} to DB...`);
967
- await DeletedRecord.destroyOne({redboxOid:oid});
968
- response.success = true;
969
- sails.log.verbose(`${this.logHeader} deleted record destroyed...`);
970
- return response;
971
- } catch (err) {
972
- sails.log.error(`${this.logHeader} Failed to create Record:`);
973
- sails.log.error(JSON.stringify(err));
974
- response.success = false;
975
- response.message = err.message;
976
- return response;
977
- }
978
-
979
- }
980
-
981
-
982
- /**
983
- * Returns a MongoDB cursor
984
- * @author <a target='_' href='https://github.com/shilob'>Shilo Banihit</a>
985
- * @param fileName
986
- * @param options
987
- * @return
988
- */
989
- protected getFileWithName(fileName: string, options: any = { limit: 1 }) {
990
- return this.gridFsBucket.find({ filename: fileName }, options);
991
- }
992
-
993
- /**
994
- * Returns true if record with oid exists.
995
- *
996
- * @param oid
997
- * @returns
998
- */
999
- public async exists(oid: string): Promise<boolean> {
1000
- return await Record.count({ redboxOid: oid }) > 0;
1001
- }
1002
- }
1003
-
1004
- }
1005
- module.exports = new Services.MongoStorageService().exports();