UNPKG

12.6 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const deprecate = require('depd')('camo');
5const DB = require('./clients').getClient;
6const BaseDocument = require('./base-document');
7const isSupportedType = require('./validate').isSupportedType;
8const isArray = require('./validate').isArray;
9const isReferenceable = require('./validate').isReferenceable;
10const isEmbeddedDocument = require('./validate').isEmbeddedDocument;
11const isString = require('./validate').isString;
12
13class Document extends BaseDocument {
14 constructor(name) {
15 super();
16
17 if (name !== undefined && name !== null) {
18 deprecate('Document.constructor(name) - override Document.collectionName() instead');
19 this._meta = {
20 collection: name
21 };
22 }
23 }
24
25 // TODO: Is there a way to tell if a class is
26 // a subclass of something? Until I find out
27 // how, we'll be lazy use this.
28 static documentClass() {
29 return 'document';
30 }
31
32 documentClass() {
33 return 'document';
34 }
35
36 get meta() {
37 return this._meta;
38 }
39
40 set meta(meta) {
41 this._meta = meta;
42 }
43
44 /**
45 * Save (upsert) current document
46 *
47 * TODO: The method is too long and complex, it is necessary to divide...
48 * @returns {Promise}
49 */
50 save() {
51 const that = this;
52
53 let preValidatePromises = this._getHookPromises('preValidate');
54
55 return Promise.all(preValidatePromises).then(function() {
56
57 // Ensure we at least have defaults set
58
59 // TODO: We already do this on .create(), so
60 // should it really be done again?
61 _.keys(that._schema).forEach(function(key) {
62 if (!(key in that._schema)) {
63 that[key] = that.getDefault(key);
64 }
65 });
66
67 // Validate the assigned type, choices, and min/max
68 that.validate();
69
70 // Ensure all data types are saved in the same encodings
71 that.canonicalize();
72
73 return Promise.all(that._getHookPromises('postValidate'));
74 }).then(function() {
75 return Promise.all(that._getHookPromises('preSave'));
76 }).then(function() {
77
78 // TODO: We should instead track what has changed and
79 // only update those values. Maybe make that._changed
80 // object to do this.
81 // Also, this might be really slow for objects with
82 // lots of references. Figure out a better way.
83 let toUpdate = that._toData({_id: false});
84
85 // Reference our objects
86 _.keys(that._schema).forEach(function(key) {
87 // Never care about _id
88 if (key === '_id') return;
89
90 if (isReferenceable(that[key]) || // isReferenceable OR
91 (isArray(that[key]) && // isArray AND contains value AND value isReferenceable
92 that[key].length > 0 &&
93 isReferenceable(that[key][0]))) {
94
95 // Handle array of references (ex: { type: [MyObject] })
96 if (isArray(that[key])) {
97 toUpdate[key] = [];
98 that[key].forEach(function(v) {
99 if (DB().isNativeId(v)) {
100 toUpdate[key].push(v);
101 } else {
102 toUpdate[key].push(v._id);
103 }
104 });
105 } else {
106 if (DB().isNativeId(that[key])) {
107 toUpdate[key] = that[key];
108 } else {
109 toUpdate[key] = that[key]._id;
110 }
111 }
112
113 }
114 });
115
116 // Replace EmbeddedDocument references with just their data
117 _.keys(that._schema).forEach(function(key) {
118 if (isEmbeddedDocument(that[key]) || // isEmbeddedDocument OR
119 (isArray(that[key]) && // isArray AND contains value AND value isEmbeddedDocument
120 that[key].length > 0 &&
121 isEmbeddedDocument(that[key][0]))) {
122
123 // Handle array of references (ex: { type: [MyObject] })
124 if (isArray(that[key])) {
125 toUpdate[key] = [];
126 that[key].forEach(function(v) {
127 toUpdate[key].push(v._toData());
128 });
129 } else {
130 toUpdate[key] = that[key]._toData();
131 }
132
133 }
134 });
135
136 return DB().save(that.collectionName(), that._id, toUpdate);
137 }).then(function(id) {
138 if (that._id === null) {
139 that._id = id;
140 }
141 }).then(function() {
142 // TODO: hack?
143 let postSavePromises = that._getHookPromises('postSave');
144 return Promise.all(postSavePromises);
145 }).then(function() {
146 return that;
147 }).catch(function(error) {
148 return Promise.reject(error);
149 });
150 }
151
152 /**
153 * Delete current document
154 *
155 * @returns {Promise}
156 */
157 delete() {
158 const that = this;
159
160 let preDeletePromises = that._getHookPromises('preDelete');
161
162 return Promise.all(preDeletePromises).then(function() {
163 return DB().delete(that.collectionName(), that._id);
164 }).then(function(deleteReturn) {
165 // TODO: hack?
166 let postDeletePromises = [deleteReturn].concat(that._getHookPromises('postDelete'));
167 return Promise.all(postDeletePromises);
168 }).then(function(prevData) {
169 let deleteReturn = prevData[0];
170 return deleteReturn;
171 });
172 }
173
174 /**
175 * Delete one document in current collection
176 *
177 * @param {Object} query Query
178 * @returns {Promise}
179 */
180 static deleteOne(query) {
181 return DB().deleteOne(this.collectionName(), query);
182 }
183
184 /**
185 * Delete many documents in current collection
186 *
187 * @param {Object} query Query
188 * @returns {Promise}
189 */
190 static deleteMany(query) {
191 if (query === undefined || query === null) {
192 query = {};
193 }
194
195 return DB().deleteMany(this.collectionName(), query);
196 }
197
198 /**
199 * @deprecated Use `findOne`
200 */
201 static loadOne(query, options) {
202 deprecate('loadOne - use findOne instead');
203 return this.findOne(query, options);
204 }
205
206 /**
207 * Find one document in current collection
208 *
209 * TODO: Need options to specify whether references should be loaded
210 *
211 * @param {Object} query Query
212 * @returns {Promise}
213 */
214 static findOne(query, options) {
215 const that = this;
216
217 let populate = true;
218 if (options && options.hasOwnProperty('populate')) {
219 populate = options.populate;
220 }
221
222 return DB().findOne(this.collectionName(), query)
223 .then(function(data) {
224 if (!data) {
225 return null;
226 }
227
228 let doc = that._fromData(data);
229 if (populate === true || (isArray(populate) && populate.length > 0)) {
230 return that.populate(doc, populate);
231 }
232
233 return doc;
234 }).then(function(docs) {
235 if (docs) {
236 return docs;
237 }
238 return null;
239 });
240 }
241
242 /**
243 * @deprecated Use `findOneAndUpdate`
244 */
245 static loadOneAndUpdate(query, values, options) {
246 deprecate('loadOneAndUpdate - use findOneAndUpdate instead');
247 return this.findOneAndUpdate(query, values, options);
248 }
249
250 /**
251 * Find one document and update it in current collection
252 *
253 * @param {Object} query Query
254 * @param {Object} values
255 * @param {Object} options
256 * @returns {Promise}
257 */
258 static findOneAndUpdate(query, values, options) {
259 const that = this;
260
261 if (arguments.length < 2) {
262 throw new Error('findOneAndUpdate requires at least 2 arguments. Got ' + arguments.length + '.');
263 }
264
265 if (!options) {
266 options = {};
267 }
268
269 let populate = true;
270 if (options.hasOwnProperty('populate')) {
271 populate = options.populate;
272 }
273
274 return DB().findOneAndUpdate(this.collectionName(), query, values, options)
275 .then(function(data) {
276 if (!data) {
277 return null;
278 }
279
280 let doc = that._fromData(data);
281 if (populate) {
282 return that.populate(doc);
283 }
284
285 return doc;
286 }).then(function(doc) {
287 if (doc) {
288 return doc;
289 }
290 return null;
291 });
292 }
293
294 /**
295 * @deprecated Use `findOneAndDelete`
296 */
297 static loadOneAndDelete(query, options) {
298 deprecate('loadOneAndDelete - use findOneAndDelete instead');
299 return this.findOneAndDelete(query, options);
300 }
301
302 /**
303 * Find one document and delete it in current collection
304 *
305 * @param {Object} query Query
306 * @param {Object} options
307 * @returns {Promise}
308 */
309 static findOneAndDelete(query, options) {
310 const that = this;
311
312 if (arguments.length < 1) {
313 throw new Error('findOneAndDelete requires at least 1 argument. Got ' + arguments.length + '.');
314 }
315
316 if (!options) {
317 options = {};
318 }
319
320 return DB().findOneAndDelete(this.collectionName(), query, options);
321 }
322
323 /**
324 * @deprecated Use `find`
325 */
326 static loadMany(query, options) {
327 deprecate('loadMany - use find instead');
328 return this.find(query, options);
329 }
330
331 /**
332 * Find documents
333 *
334 * TODO: Need options to specify whether references should be loaded
335 *
336 * @param {Object} query Query
337 * @param {Object} options
338 * @returns {Promise}
339 */
340 static find(query, options) {
341 const that = this;
342
343 if (query === undefined || query === null) {
344 query = {};
345 }
346
347 if (options === undefined || options === null) {
348 // Populate by default
349 options = {populate: true};
350 }
351
352 return DB().find(this.collectionName(), query, options)
353 .then(function(datas) {
354 let docs = that._fromData(datas);
355
356 if (options.populate === true ||
357 (isArray(options.populate) && options.populate.length > 0)) {
358 return that.populate(docs, options.populate);
359 }
360
361 return docs;
362 }).then(function(docs) {
363 // Ensure we always return an array
364 return [].concat(docs);
365 });
366 }
367
368 /**
369 * Get count documents in current collection by query
370 *
371 * @param {Object} query Query
372 * @returns {Promise}
373 */
374 static count(query) {
375 const that = this;
376 return DB().count(this.collectionName(), query);
377 }
378
379 /**
380 * Create indexes
381 *
382 * @returns {Promise}
383 */
384 static createIndexes() {
385 if (this._indexesCreated) {
386 return;
387 }
388
389 const that = this;
390 let instance = this._instantiate();
391
392 _.keys(instance._schema).forEach(function(k) {
393 if (instance._schema[k].unique) {
394 DB().createIndex(that.collectionName(), k, {unique: true});
395 }
396 });
397
398 this._indexesCreated = true;
399 }
400
401 static _fromData(datas) {
402 let instances = super._fromData(datas);
403 // This way we preserve the original structure of the data. Data
404 // that was passed as an array is returned as an array, and data
405 // passes as a single object is returned as single object
406 let datasArray = [].concat(datas);
407 let instancesArray = [].concat(instances);
408
409 /*for (let i = 0; i < instancesArray.length; i++) {
410 if (datasArray[i].hasOwnProperty('_id')) {
411 instancesArray[i]._id = datasArray[i]._id;
412 } else {
413 instancesArray[i]._id = null;
414 }
415 }*/
416
417 return instances;
418 }
419
420 /**
421 * Clear current collection
422 *
423 * @returns {Promise}
424 */
425 static clearCollection() {
426 return DB().clearCollection(this.collectionName());
427 }
428
429}
430
431module.exports = Document;
\No newline at end of file