@dhyasama/totem-models 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.npmignore ADDED
@@ -0,0 +1,14 @@
1
+ lib-cov
2
+ *.seed
3
+ *.log
4
+ *.csv
5
+ *.dat
6
+ *.out
7
+ *.pid
8
+ *.gz
9
+
10
+ npm-debug.log
11
+ node_modules
12
+
13
+ .DS_Store
14
+ .idea
package/index.js CHANGED
@@ -53,10 +53,12 @@ var bootstrap = function(mongoose, config) {
53
53
  require('./lib/Activity.js')(mongoose, config);
54
54
  require('./lib/CalendarEvent.js')(mongoose, config);
55
55
  require('./lib/CapTable.js')(mongoose, config);
56
+ require('./lib/Document.js')(mongoose, config);
56
57
  require('./lib/Financials.js')(mongoose, config);
57
58
  require('./lib/Interaction.js')(mongoose, config);
58
59
  require('./lib/Investment.js')(mongoose, config);
59
60
  require('./lib/List.js')(mongoose, config);
61
+ require('./lib/Message.js')(mongoose, config);
60
62
  require('./lib/News.js')(mongoose, config);
61
63
  require('./lib/Webhook.js')(mongoose, config);
62
64
 
package/lib/Document.js CHANGED
@@ -8,346 +8,126 @@ module.exports = function(mongoose, config) {
8
8
  env = process.env.NODE_ENV || 'development',
9
9
  s3 = require('@dhyasama/ffvc-s3'),
10
10
  utils = require('@dhyasama/ffvc-utilities'),
11
- Crypto = require('@dhyasama/ffvc-crypto'),
12
- crypto = new Crypto({'key': config.crypto.key}),
11
+ postFind = require('mongoose-post-find'),
13
12
  _ = require('underscore');
14
13
 
15
14
  var Document = new Schema({
16
15
 
17
- lpid: { type: Schema.Types.ObjectId, required: true },
16
+ customer: { type: Schema.ObjectId, ref: 'Organization', required: true },
18
17
 
19
- fundInvolved: {
20
- type: Schema.ObjectId,
21
- ref: 'Fund'
22
- },
23
-
24
- published: { type: Boolean, required: true },
25
-
26
- filename: { type: String, required: true },
27
-
28
- filetype: { type: String, required: true },
29
-
30
- filesize: { type: String, required: true },
18
+ createdOn: { type: Date, required: true },
31
19
 
32
- s3key: { type: String, required: true },
20
+ createdBy: { type: Schema.ObjectId, ref: 'Person', index: false, required: true },
33
21
 
34
- doctype: {
35
- type: String,
36
- required: true,
37
- enum: ['annual-audit', 'portfolio-holding', 'capital-call', 'k1', 'distribution', 'quarterly-account-statement'] },
22
+ name: { type: String, required: true },
38
23
 
39
- scope: {
40
- type: String,
41
- required: true,
42
- enum: ['lp', 'fund'] },
24
+ contentType: { type: String, required: true },
43
25
 
44
- // for fund-level docs:
45
- // upload one doc to s3 with this id in the key
46
- // e.g., an annual audit for ff Rose would be stored at documents/[scopeid] and shared by all lps in ff Rose
47
- // for lp-level docs:
48
- // used to get a batch of docs that were uploaded together
49
- // e.g., a capital call for ff Sapphire requires a separate document to be uploaded for each lp in the fund
50
- // scopeid allows retrieving all of the doc records at once for admin purposes
51
- // unlike fund level docs, these are stored at documents/[_id] so they can be retrieved on an individual basis
52
- scopeid: { type: String, required: true },
53
-
54
- year: { type: Number, required: false },
55
-
56
- quarter: {
57
- type: String,
58
- required: false,
59
- enum: [null, '', 'Q1', 'Q2', 'Q3', 'Q4'] },
60
-
61
- capitalCall: {
62
- noticeNumber: { type: Number, required: false },
63
- noticeDate: { type: Date, required: false },
64
- dueDate: { type: Date, required: false }
65
- },
26
+ contentLength: { type: String, required: true },
66
27
 
67
- description: { type: String, required: false },
68
-
69
- history: [{
70
- timestamp: { type: Date, required: true },
71
- username: { type: String, required: true },
72
- action: { type: String, required: true, enum: ['created', 'published', 'unpublished', 'replaced'] }
73
- }]
28
+ s3key: { type: String, required: true }
74
29
 
75
30
  });
76
31
 
77
32
 
78
33
 
79
- /////////////////////
80
- // INSTANCE METHODS
81
- /////////////////////
82
-
83
- Document.methods.friendlyFundName = function() {
84
- var fund = this.fundInvolved.name;
85
- return fund;
86
- };
87
-
88
- Document.methods.friendlyDocType = function() {
89
- var doctype = this.doctype ? utils.capitalizeFirstLetters(this.doctype.toLowerCase().split('-').join(' ')) : '';
90
- return doctype;
91
- };
92
-
93
- Document.methods.friendlyDocumentName = function() {
94
- var year = this.year ? this.year + ' ' : '';
95
- var quarter = this.quarter ? this.quarter + ' ' : '';
96
- var callNum = this.doctype == 'capital-call' && utils.isNumeric(this.capitalCall.noticeNumber) ? ' #' + this.capitalCall.noticeNumber : '';
97
- var desc = this.doctype == 'distribution' ? ' - ' + this.description : '';
98
-
99
- return year + quarter + this.friendlyDocType() + callNum + desc;
100
-
101
- };
34
+ ///////////////////////////////////////////////////////////////////////////////////////
35
+ // METHODS
36
+ // Methods operate on a single document that has already been returned
37
+ // Note that running a method on a document does not save it
38
+ ///////////////////////////////////////////////////////////////////////////////////////
102
39
 
103
40
  Document.methods.friendlyFilesize = function() {
104
- return utils.humanizeFilesize(this.filesize);
105
- };
106
-
107
- Document.methods.encryptId = function() {
108
- return crypto.encrypt(this.id);
41
+ return utils.humanizeFilesize(this.contentLength);
109
42
  };
110
43
 
111
44
 
112
45
 
113
- ////////////////////////
46
+ //////////////////////////////////////////////////////
114
47
  // STATICS
115
- ////////////////////////
116
-
117
- Document.statics.exists = function(meta, cb) {
118
-
119
- var model = this;
120
-
121
- var done = function(err, docs) {
122
- return cb(!err && docs.length >= 1, docs[0]);
123
- };
124
-
125
- switch(meta.doctype.toLowerCase()) {
126
- case 'annual-audit':
127
- listAnnualAudits(model, meta.fundId, meta.year, done);
128
- break;
129
- case 'capital-call':
130
- listCapitalCalls(model, meta.fundId, meta.callNumber, done);
131
- break;
132
- case 'portfolio-holding':
133
- listPortfolioHoldings(model, meta.fundId, meta.year, meta.quarter, done);
134
- break;
135
- case 'k1':
136
- listK1s(model, meta.fundId, meta.year, done);
137
- break;
138
- case 'quarterly-account-statement':
139
- listQuarterlyAccountStatements(model, meta.fundId, meta.year, meta.quarter, done);
140
- break;
141
- default:
142
- return cb(false, null);
143
- break;
144
- }
145
-
146
- };
147
-
148
- Document.statics.getById = function (cipher, cb) {
149
-
150
- var id = crypto.decrypt(cipher);
151
-
152
- this
153
- .findById(id)
154
- .populate({
155
- path: 'lpid',
156
- model: 'LimitedPartner',
157
- select: 'name shortName id'
158
- })
159
- .populate('fundInvolved')
160
- .exec(function (err, document) {
161
- if (document) { document.secureid = cipher; }
162
- document.secureid = cipher;
163
- cb(err, document);
164
- });
165
-
166
- };
48
+ // Statics operate on the entire collection
49
+ //////////////////////////////////////////////////////
167
50
 
168
- Document.statics.list = function (cb) {
169
-
170
- this
171
- .find()
172
- .populate({
173
- path: 'lpid',
174
- model: 'LimitedPartner',
175
- select: 'name shortName id'
176
- })
177
- .populate('fundInvolved')
178
- .sort({ year: -1, quarter: -1 })
179
- .exec(function (err, documents) {
180
- encryptDocumentIds(documents);
181
- cb(err, documents);
182
- });
51
+ Document.statics.createForModel = function(doc, Model, modelId, cb) {
183
52
 
184
- };
185
-
186
- Document.statics.listAllForLP = function (lpid, cb) {
187
-
188
- this
189
- .find({ lpid: lpid, published: true })
190
- .populate('fundInvolved')
191
- .sort({ year: -1, quarter: -1, fund: 1 })
192
- .exec(function (err, documents) {
193
- encryptDocumentIds(documents);
194
- return cb(err, documents);
195
- });
196
-
197
- };
198
-
199
- Document.statics.listForScopeId = function (cipher, cb) {
200
-
201
- // lp-level docs are grouped to scope id to faciliate pulling a batch all at once
202
-
203
- var id = crypto.decrypt(cipher);
204
-
205
- this
206
- .find({ 'scopeid': id })
207
- .populate({
208
- path: 'lpid',
209
- model: 'LimitedPartner',
210
- select: 'name shortName id'
211
- })
212
- .populate('fundInvolved')
213
- .exec(function (err, documents) {
214
- console.log(documents.length);
215
- encryptDocumentIds(documents);
216
- return cb(err, _.sortBy(documents, function(doc) {
217
- var lowered = doc.lpid && doc.lpid.name ? doc.lpid.name.toLowerCase() : 'Unknown';
218
- return lowered;
219
- }));
220
- });
221
-
222
- };
223
-
224
- Document.statics.listFundLevel = function (cb) {
225
-
226
- this
227
- .find({ 'scope': 'fund' })
228
- .populate({
229
- path: 'lpid',
230
- model: 'LimitedPartner',
231
- select: 'name shortName id'
232
- })
233
- .populate('fundInvolved')
234
- .sort({ year: -1, quarter: -1 })
235
- .exec(function (err, documents) {
236
- var docs = _.uniq(documents, false, function(doc) {
237
- return doc.scopeid;
53
+ var addToModel = function(err, createdDoc) {
54
+ if (err) return cb(err, null);
55
+ Model.findByIdAndUpdate(modelId, { $push: { documents: createdDoc }}, { new: true, upsert: false }, function(err, addedTo) {
56
+ return cb(err, {
57
+ document: createdDoc,
58
+ addedTo: addedTo
238
59
  });
239
- encryptDocumentIds(docs);
240
- cb(err, docs);
241
60
  });
61
+ };
242
62
 
243
- };
63
+ doc.createdOn = new Date();
64
+ doc.customer = config.CUSTOMER_ID;
244
65
 
245
- Document.statics.listLpLevel = function (cb) {
246
-
247
- this
248
- .find({ 'scope': 'lp' })
249
- .populate({
250
- path: 'lpid',
251
- model: 'LimitedPartner',
252
- select: 'name shortName id'
253
- })
254
- .populate('fundInvolved')
255
- .sort({ year: -1 })
256
- .exec(function (err, documents) {
257
- var docs = _.uniq(documents, false, function(doc) {
258
- return doc.scopeid;
259
- });
260
- encryptDocumentIds(docs);
261
- cb(err, docs);
262
- });
66
+ this.create(doc, addToModel);
263
67
 
264
68
  };
265
69
 
266
- Document.statics.upsert = function(doc, cb) {
267
- return save(doc, cb);
268
- };
70
+ Document.statics.delete = function(id, cb) {
269
71
 
72
+ var self = this;
270
73
 
74
+ // Not strictly necessary but provides verification that the document being deleted belongs to the customer doing the deleting
75
+ self.findOne({
76
+ '_id': id,
77
+ 'customer': config.CUSTOMER_ID
78
+ }, function(err, doc) {
271
79
 
272
- ////////////////////////
273
- // HELPERS
274
- ////////////////////////
80
+ if (err) return cb(err, null);
81
+ else if (!doc) return cb(null, null);
275
82
 
276
- function encryptDocumentIds(documents) {
83
+ doc.remove(cb);
277
84
 
278
- var result = _.map(documents, function(doc) {
279
- doc.secureid = crypto.encrypt(doc.id);
280
- return doc;
281
85
  });
282
86
 
283
- return result;
87
+ };
284
88
 
285
- }
89
+ Document.statics.getById = function (id, cb) {
90
+ this.findOne({
91
+ '_id': id,
92
+ 'customer': config.CUSTOMER_ID
93
+ }).exec(cb);
94
+ };
286
95
 
287
- function listAnnualAudits(model, fundId, year, cb) {
96
+ Document.plugin(postFind, {
288
97
 
289
- model
290
- .find({ doctype: 'annual-audit', fundInvolved: fundId, year: year })
291
- .sort({ year: -1 })
292
- .exec(function (err, documents) {
293
- encryptDocumentIds(documents);
294
- cb(err, documents);
295
- });
98
+ find: function(results, done) {
296
99
 
297
- };
100
+ var CUSTOMER_ID = config.CUSTOMER_ID;
298
101
 
299
- function listCapitalCalls(model, fundId, callNumber, cb) {
102
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return done(null, results);
300
103
 
301
- model
302
- .find({ doctype: 'capital-call', fundInvolved: fundId, 'capitalCall.noticeNumber': callNumber })
303
- .sort({ 'capitalCall.noticeNumber': -1 })
304
- .exec(function (err, documents) {
305
- encryptDocumentIds(documents);
306
- cb(err, documents);
104
+ // Reject any item that is for a different customer
105
+ results = _.reject(results, function(item) {
106
+ return item.customer.toString() != CUSTOMER_ID;
307
107
  });
308
108
 
309
- };
310
-
311
- function listK1s(model, fundId, year, cb) {
109
+ return done(null, results);
312
110
 
313
- model
314
- .find({ doctype: 'k1', fundInvolved: fundId, year: year })
315
- .sort({ 'year': -1 })
316
- .exec(function (err, documents) {
317
- encryptDocumentIds(documents);
318
- cb(err, documents);
319
- });
111
+ },
320
112
 
321
- };
113
+ findOne: function(result, done) {
322
114
 
323
- function listPortfolioHoldings(model, fundId, year, quarter, cb) {
115
+ var CUSTOMER_ID = config.CUSTOMER_ID;
324
116
 
325
- model
326
- .find({ doctype: 'portfolio-holding', fundInvolved: fundId, year: year, quarter: quarter })
327
- .sort({ year: -1, quarter: -1 })
328
- .exec(function (err, documents) {
329
- return cb(err, encryptDocumentIds(documents));
330
- });
117
+ if (!result) return done(null, null);
118
+ else if (CUSTOMER_ID == 'GLOBAL_PROCESS') return done(null, result);
119
+ else if (result.customer.toString() == CUSTOMER_ID) return done(null, result);
120
+ else return done(null, null);
331
121
 
332
- };
122
+ }
333
123
 
334
- function listQuarterlyAccountStatements(model, fundId, year, quarter, cb) {
124
+ });
335
125
 
336
- model
337
- .find({ doctype: 'quarterly-account-statement', fundInvolved: fundId, year: year, quarter: quarter })
338
- .sort({ year: -1, quarter: -1 })
339
- .exec(function (err, documents) {
340
- encryptDocumentIds(documents);
341
- cb(err, documents);
342
- });
343
126
 
344
- };
345
127
 
346
- function save(document, cb) {
347
- document.save(function(err, updatedDocument) {
348
- cb(err, updatedDocument);
349
- });
350
- }
128
+ ///////////////////////////////////////////////////////////////////////////////////////
129
+ // CONFIG
130
+ ///////////////////////////////////////////////////////////////////////////////////////
351
131
 
352
132
  Document.set('autoIndex', false);
353
133
  Document.on('index', function(err) { console.log('error building document indexes: ' + err); });
@@ -23,7 +23,8 @@ module.exports = function(mongoose, config) {
23
23
  internal: [{ type: Schema.ObjectId, ref: 'Person', index: true }], // people from customer
24
24
  external: [{ type: Schema.ObjectId, ref: 'Person', index: true }] // everyone else
25
25
  },
26
- notes: [ { type: Schema.ObjectId, ref: 'Note' } ]
26
+ notes: [ { type: Schema.ObjectId, ref: 'Note' } ],
27
+ documents: [ { type: Schema.ObjectId, ref: 'Document' } ]
27
28
 
28
29
  });
29
30
 
@@ -625,6 +625,7 @@ module.exports = function(mongoose, config) {
625
625
  }
626
626
  else {
627
627
  query = self.find({ 'name': new RegExp(terms, "i"), 'fundsInvested.fund': { $in : self.customerFunds }}, self.getReadFilterKeys(role));
628
+
628
629
  getLps();
629
630
  }
630
631
 
package/lib/Message.js ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+
3
+ module.exports = function(mongoose, config) {
4
+
5
+ var
6
+
7
+ Schema = mongoose.Schema,
8
+ env = process.env.NODE_ENV || 'development',
9
+ _ = require('underscore'),
10
+ async = require('async'),
11
+ moment = require('moment'),
12
+ Document = mongoose.model('Document'),
13
+ Note = mongoose.model('Note');
14
+
15
+ var Message = new Schema({
16
+
17
+ createdOn: { type: Date, required: true },
18
+ webhook: { type: Schema.ObjectId, ref: 'Webhook', required: true },
19
+ type: { type: String, required: true, enum: ['email'], default: 'email' }, // for future options
20
+ customer: { type: Schema.ObjectId, ref: 'Organization', required: true },
21
+ subject: { type: String, required: false, default: null, trim: true },
22
+ body: { type: String, required: false, default: null, trim: true },
23
+ recipients: {
24
+ internal: [{ type: Schema.ObjectId, ref: 'Person', index: true }], // people from customer
25
+ external: [{ type: Schema.ObjectId, ref: 'Person', index: true }] // everyone else
26
+ },
27
+ notes: [ { type: Schema.ObjectId, ref: 'Note' } ],
28
+ documents: [ { type: Schema.ObjectId, ref: 'Document' } ]
29
+
30
+ });
31
+
32
+ ///////////////////////////////////////
33
+
34
+ Message.statics.addDocument = function(messageId, doc, cb) {
35
+
36
+ Document.createForModel(doc, this, messageId, cb);
37
+
38
+ };
39
+
40
+ Message.statics.addNote = function(messageId, creatorPersonId, text, cb) {
41
+
42
+ Note.createForModel({
43
+ createdBy: creatorPersonId,
44
+ text: text
45
+ }, this, messageId, cb);
46
+
47
+ };
48
+
49
+ Message.statics.delete = function(messageId, customerId, cb) {
50
+
51
+ var self = this;
52
+
53
+ self.findOneAndRemove({
54
+ '_id': messageId,
55
+ 'customer': customerId
56
+ }, cb);
57
+
58
+ };
59
+
60
+ Message.statics.deleteDocument = function(docId, cb) {
61
+
62
+ // Delete the doc itself along with any references
63
+
64
+ var self = this;
65
+
66
+ var removeReferences = function removeReferences(callback) {
67
+ self.update({}, {
68
+ $pull: { 'documents' : [docId] }
69
+ }, callback);
70
+ };
71
+
72
+ async.series([
73
+ Document.delete.bind(Document, docId),
74
+ removeReferences
75
+ ], function(err, results) {
76
+ return cb(err, null);
77
+ });
78
+
79
+ };
80
+
81
+ Message.statics.deleteNote = function(noteId, cb) {
82
+
83
+ // Delete the note itself along with any references
84
+
85
+ var self = this;
86
+
87
+ var removeReferences = function removeReferences(callback) {
88
+ self.update({}, {
89
+ $pull: { 'notes' : [noteId] }
90
+ }, callback);
91
+ };
92
+
93
+ async.series([
94
+ Note.delete.bind(Note, noteId),
95
+ removeReferences
96
+ ], function(err, results) {
97
+ return cb(err, null);
98
+ });
99
+
100
+ };
101
+
102
+ Message.statics.getDocuments = function getDocuments(personIds, options, cb) {
103
+
104
+ var self = this;
105
+ var query = self.find({ customer: config.CUSTOMER_ID });
106
+
107
+ query.where({'attendees.external': { $in : personIds }});
108
+ query.populate({
109
+ path: 'documents',
110
+ match: { customer: config.CUSTOMER_ID }
111
+ });
112
+ query.deepPopulate([
113
+ 'documents.createdBy'
114
+ ], {
115
+ populate: {
116
+ 'documents.createdBy': { select: 'name avatarUrl title' }
117
+ }
118
+ });
119
+ query.exec(cb);
120
+
121
+ };
122
+
123
+ Message.statics.getForCustomer = function getForCustomer(customerId, cb) {
124
+
125
+ var self = this;
126
+ var query = self.find({ customer: customerId });
127
+ query.exec(cb);
128
+
129
+ };
130
+
131
+ Message.statics.getForPeople = function getForPeople(personIds, options, cb) {
132
+
133
+ // This filters by customer so is NOT usable by admin tools
134
+ if (config.CUSTOMER_ID == 'GLOBAL_PROCESS') return cb(new Error('Access denied. Use getForPeopleLite.'), null);
135
+
136
+ var self = this;
137
+ var query = self.find({ customer: config.CUSTOMER_ID });
138
+
139
+ query.where({'recipients.external': { $in : personIds }});
140
+ query.populate('recipients.internal', 'name title avatarUrl doNotDisplay');
141
+ query.populate('recipients.external', 'name title avatarUrl doNotDisplay');
142
+ query.populate({
143
+ path: 'notes',
144
+ match: { customer: config.CUSTOMER_ID }
145
+ });
146
+ query.populate({
147
+ path: 'documents',
148
+ match: { customer: config.CUSTOMER_ID }
149
+ });
150
+ query.deepPopulate([
151
+ 'notes.createdBy',
152
+ 'documents.createdBy'
153
+ ], {
154
+ populate: {
155
+ 'notes.createdBy': { select: 'name avatarUrl title' },
156
+ 'documents.createdBy': { select: 'name avatarUrl title' }
157
+ }
158
+ });
159
+ query.exec(cb);
160
+
161
+ };
162
+
163
+ Message.statics.getForPeopleLite = function getForPeopleLite(personIds, cb) {
164
+
165
+ // This doesn't filter by customer so is only usable by admin tools
166
+ if (config.CUSTOMER_ID != 'GLOBAL_PROCESS') return cb(new Error('Access denied'), null);
167
+
168
+ var self = this;
169
+ var query = self.find({'recipients.external': { $in : personIds }});
170
+ query.exec(cb);
171
+
172
+ };
173
+
174
+ Message.statics.getNotes = function getNotes(personIds, options, cb) {
175
+
176
+ var self = this;
177
+ var query = self.find({ customer: config.CUSTOMER_ID });
178
+
179
+ query.where({'attendees.external': { $in : personIds }});
180
+ query.populate({
181
+ path: 'notes',
182
+ match: { customer: config.CUSTOMER_ID }
183
+ });
184
+ query.deepPopulate([
185
+ 'notes.createdBy'
186
+ ], {
187
+ populate: {
188
+ 'notes.createdBy': { select: 'name avatarUrl title' }
189
+ }
190
+ });
191
+ query.exec(cb);
192
+
193
+ };
194
+
195
+ Message.statics.upsert = function(message, cb) {
196
+ message.createdOn = new Date();
197
+ message.save(cb);
198
+ };
199
+
200
+ ///////////////////////////////////////
201
+
202
+ Message.set('autoIndex', false);
203
+
204
+ var deepPopulate = require('mongoose-deep-populate')(mongoose);
205
+ Message.plugin(deepPopulate);
206
+
207
+ mongoose.model('Message', Message);
208
+
209
+ };