@researchdatabox/sails-hook-redbox-storage-mongo 1.4.8

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