1 | 'use strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const fs = require('fs');
|
5 | const expect = require('chai').expect;
|
6 | const connect = require('../index').connect;
|
7 | const Document = require('../index').Document;
|
8 | const EmbeddedDocument = require('../index').EmbeddedDocument;
|
9 | const isDocument = require('../lib/validate').isDocument;
|
10 | const ValidationError = require('../lib/errors').ValidationError;
|
11 | const Data = require('./data');
|
12 | const getData1 = require('./util').data1;
|
13 | const getData2 = require('./util').data2;
|
14 | const validateId = require('./util').validateId;
|
15 |
|
16 | describe('Embedded', function() {
|
17 |
|
18 |
|
19 | const url = 'nedb://memory';
|
20 |
|
21 | let database = null;
|
22 |
|
23 | before(function(done) {
|
24 | connect(url).then(function(db) {
|
25 | database = db;
|
26 | return database.dropDatabase();
|
27 | }).then(function() {
|
28 | return done();
|
29 | });
|
30 | });
|
31 |
|
32 | beforeEach(function(done) {
|
33 | done();
|
34 | });
|
35 |
|
36 | afterEach(function(done) {
|
37 | database.dropDatabase().then(function() {}).then(done, done);
|
38 | });
|
39 |
|
40 | after(function(done) {
|
41 | database.dropDatabase().then(function() {}).then(done, done);
|
42 | });
|
43 |
|
44 | describe('general', function() {
|
45 | it('should not have an _id', function(done) {
|
46 |
|
47 | class EmbeddedModel extends EmbeddedDocument {
|
48 | constructor() {
|
49 | super();
|
50 | this.str = String;
|
51 | }
|
52 | }
|
53 |
|
54 | class DocumentModel extends Document {
|
55 | constructor() {
|
56 | super();
|
57 | this.mod = EmbeddedModel;
|
58 | this.num = { type: Number };
|
59 | }
|
60 | }
|
61 |
|
62 | let data = DocumentModel.create();
|
63 | data.mod = EmbeddedModel.create();
|
64 | data.mod.str = 'some data';
|
65 | data.num = 1;
|
66 |
|
67 | data.save().then(function() {
|
68 | expect(data.mod._id).to.be.undefined;
|
69 | return DocumentModel.findOne({ num: 1 });
|
70 | }).then(function(d) {
|
71 | expect(d.mod._id).to.be.undefined;
|
72 | }).then(done, done);
|
73 | });
|
74 | });
|
75 |
|
76 | describe('types', function() {
|
77 | it('should allow embedded types', function(done) {
|
78 |
|
79 | class EmbeddedModel extends EmbeddedDocument {
|
80 | constructor() {
|
81 | super();
|
82 | this.str = String;
|
83 | }
|
84 | }
|
85 |
|
86 | class DocumentModel extends Document {
|
87 | constructor() {
|
88 | super();
|
89 | this.mod = EmbeddedModel;
|
90 | this.num = { type: Number };
|
91 | }
|
92 | }
|
93 |
|
94 | let data = DocumentModel.create();
|
95 | data.mod = EmbeddedModel.create();
|
96 | data.mod.str = 'some data';
|
97 | data.num = 1;
|
98 |
|
99 | data.save().then(function() {
|
100 | validateId(data);
|
101 | return DocumentModel.findOne({ num: 1 });
|
102 | }).then(function(d) {
|
103 | validateId(d);
|
104 | expect(d.num).to.be.equal(1);
|
105 | expect(d.mod).to.be.a('object');
|
106 | expect(d.mod).to.be.an.instanceof(EmbeddedModel);
|
107 | expect(d.mod.str).to.be.equal('some data');
|
108 | }).then(done, done);
|
109 | });
|
110 |
|
111 | it('should allow array of embedded types', function(done) {
|
112 |
|
113 | class Limb extends EmbeddedDocument {
|
114 | constructor() {
|
115 | super();
|
116 | this.type = String;
|
117 | }
|
118 | }
|
119 |
|
120 | class Person extends Document {
|
121 | constructor() {
|
122 | super();
|
123 | this.limbs = [Limb];
|
124 | this.name = String;
|
125 | }
|
126 |
|
127 | static collectionName() {
|
128 | return 'people';
|
129 | }
|
130 | }
|
131 |
|
132 | let person = Person.create();
|
133 | person.name = 'Scott';
|
134 | person.limbs.push(Limb.create());
|
135 | person.limbs[0].type = 'left arm';
|
136 | person.limbs.push(Limb.create());
|
137 | person.limbs[1].type = 'right arm';
|
138 | person.limbs.push(Limb.create());
|
139 | person.limbs[2].type = 'left leg';
|
140 | person.limbs.push(Limb.create());
|
141 | person.limbs[3].type = 'right leg';
|
142 |
|
143 | person.save().then(function() {
|
144 | validateId(person);
|
145 | expect(person.limbs).to.have.length(4);
|
146 | return Person.findOne({ name: 'Scott' });
|
147 | }).then(function(p) {
|
148 | validateId(p);
|
149 | expect(p.name).to.be.equal('Scott');
|
150 | expect(p.limbs).to.be.a('array');
|
151 | expect(p.limbs).to.have.length(4);
|
152 | expect(p.limbs[0].type).to.be.equal('left arm');
|
153 | expect(p.limbs[1].type).to.be.equal('right arm');
|
154 | expect(p.limbs[2].type).to.be.equal('left leg');
|
155 | expect(p.limbs[3].type).to.be.equal('right leg');
|
156 | }).then(done, done);
|
157 | });
|
158 |
|
159 | it('should save nested array of embeddeds', function(done) {
|
160 | class Point extends EmbeddedDocument {
|
161 | constructor() {
|
162 | super();
|
163 | this.x = Number;
|
164 | this.y = Number;
|
165 | }
|
166 | }
|
167 |
|
168 | class Polygon extends EmbeddedDocument {
|
169 | constructor() {
|
170 | super();
|
171 | this.points = [Point];
|
172 | }
|
173 | }
|
174 |
|
175 | class WorldMap extends Document {
|
176 | constructor() {
|
177 | super();
|
178 | this.polygons = [Polygon];
|
179 | }
|
180 | }
|
181 |
|
182 | let map = WorldMap.create();
|
183 | let polygon1 = Polygon.create();
|
184 | let polygon2 = Polygon.create();
|
185 | let point1 = Point.create({ x: 123.45, y: 678.90 });
|
186 | let point2 = Point.create({ x: 543.21, y: 987.60 });
|
187 |
|
188 | map.polygons.push(polygon1);
|
189 | map.polygons.push(polygon2);
|
190 | polygon2.points.push(point1);
|
191 | polygon2.points.push(point2);
|
192 |
|
193 | map.save().then(function() {
|
194 | return WorldMap.findOne();
|
195 | }).then(function(m) {
|
196 | expect(m.polygons).to.have.length(2);
|
197 | expect(m.polygons[0]).to.be.instanceof(Polygon);
|
198 | expect(m.polygons[1]).to.be.instanceof(Polygon);
|
199 | expect(m.polygons[1].points).to.have.length(2);
|
200 | expect(m.polygons[1].points[0]).to.be.instanceof(Point);
|
201 | expect(m.polygons[1].points[1]).to.be.instanceof(Point);
|
202 | }).then(done, done);
|
203 | });
|
204 |
|
205 | it('should allow nested initialization of embedded types', function(done) {
|
206 |
|
207 | class Discount extends EmbeddedDocument {
|
208 | constructor() {
|
209 | super();
|
210 | this.authorized = Boolean;
|
211 | this.amount = Number;
|
212 | }
|
213 | }
|
214 |
|
215 | class Product extends Document {
|
216 | constructor() {
|
217 | super();
|
218 | this.name = String;
|
219 | this.discount = Discount;
|
220 | }
|
221 | }
|
222 |
|
223 | let product = Product.create({
|
224 | name: 'bike',
|
225 | discount: {
|
226 | authorized: true,
|
227 | amount: 9.99
|
228 | }
|
229 | });
|
230 |
|
231 | product.save().then(function() {
|
232 | validateId(product);
|
233 | expect(product.name).to.be.equal('bike');
|
234 | expect(product.discount).to.be.a('object');
|
235 | expect(product.discount instanceof Discount).to.be.true;
|
236 | expect(product.discount.authorized).to.be.equal(true);
|
237 | expect(product.discount.amount).to.be.equal(9.99);
|
238 | }).then(done, done);
|
239 | });
|
240 |
|
241 | it('should allow initialization of array of embedded documents', function(done) {
|
242 |
|
243 | class Discount extends EmbeddedDocument {
|
244 | constructor() {
|
245 | super();
|
246 | this.authorized = Boolean;
|
247 | this.amount = Number;
|
248 | }
|
249 | }
|
250 |
|
251 | class Product extends Document {
|
252 | constructor() {
|
253 | super();
|
254 | this.name = String;
|
255 | this.discounts = [Discount];
|
256 | }
|
257 | }
|
258 |
|
259 | let product = Product.create({
|
260 | name: 'bike',
|
261 | discounts: [{
|
262 | authorized: true,
|
263 | amount: 9.99
|
264 | },
|
265 | {
|
266 | authorized: false,
|
267 | amount: 187.44
|
268 | }]
|
269 | });
|
270 |
|
271 | product.save().then(function() {
|
272 | validateId(product);
|
273 | expect(product.name).to.be.equal('bike');
|
274 | expect(product.discounts).to.have.length(2);
|
275 | expect(product.discounts[0] instanceof Discount).to.be.true;
|
276 | expect(product.discounts[1] instanceof Discount).to.be.true;
|
277 | expect(product.discounts[0].authorized).to.be.equal(true);
|
278 | expect(product.discounts[0].amount).to.be.equal(9.99);
|
279 | expect(product.discounts[1].authorized).to.be.equal(false);
|
280 | expect(product.discounts[1].amount).to.be.equal(187.44);
|
281 | }).then(done, done);
|
282 | });
|
283 | });
|
284 |
|
285 | describe('defaults', function() {
|
286 | it('should assign defaults to embedded types', function(done) {
|
287 |
|
288 | class EmbeddedModel extends EmbeddedDocument {
|
289 | constructor() {
|
290 | super();
|
291 | this.str = { type: String, default: 'hello' };
|
292 | }
|
293 | }
|
294 |
|
295 | class DocumentModel extends Document {
|
296 | constructor() {
|
297 | super();
|
298 | this.emb = EmbeddedModel;
|
299 | this.num = { type: Number };
|
300 | }
|
301 | }
|
302 |
|
303 | let data = DocumentModel.create();
|
304 | data.emb = EmbeddedModel.create();
|
305 | data.num = 1;
|
306 |
|
307 | data.save().then(function() {
|
308 | validateId(data);
|
309 | return DocumentModel.findOne({ num: 1 });
|
310 | }).then(function(d) {
|
311 | validateId(d);
|
312 | expect(d.emb.str).to.be.equal('hello');
|
313 | }).then(done, done);
|
314 | });
|
315 |
|
316 | it('should assign defaults to array of embedded types', function(done) {
|
317 |
|
318 | class Money extends EmbeddedDocument {
|
319 | constructor() {
|
320 | super();
|
321 | this.value = { type: Number, default: 100 };
|
322 | }
|
323 | }
|
324 |
|
325 | class Wallet extends Document {
|
326 | constructor() {
|
327 | super();
|
328 | this.contents = [Money];
|
329 | this.owner = String;
|
330 | }
|
331 | }
|
332 |
|
333 | let wallet = Wallet.create();
|
334 | wallet.owner = 'Scott';
|
335 | wallet.contents.push(Money.create());
|
336 | wallet.contents.push(Money.create());
|
337 | wallet.contents.push(Money.create());
|
338 |
|
339 | wallet.save().then(function() {
|
340 | validateId(wallet);
|
341 | return Wallet.findOne({ owner: 'Scott' });
|
342 | }).then(function(w) {
|
343 | validateId(w);
|
344 | expect(w.owner).to.be.equal('Scott');
|
345 | expect(w.contents[0].value).to.be.equal(100);
|
346 | expect(w.contents[1].value).to.be.equal(100);
|
347 | expect(w.contents[2].value).to.be.equal(100);
|
348 | }).then(done, done);
|
349 | });
|
350 | });
|
351 |
|
352 | describe('validate', function() {
|
353 |
|
354 | it('should validate embedded values', function(done) {
|
355 |
|
356 | class EmbeddedModel extends EmbeddedDocument {
|
357 | constructor() {
|
358 | super();
|
359 | this.num = { type: Number, max: 10 };
|
360 | }
|
361 | }
|
362 |
|
363 | class DocumentModel extends Document {
|
364 | constructor() {
|
365 | super();
|
366 | this.emb = EmbeddedModel;
|
367 | }
|
368 | }
|
369 |
|
370 | let data = DocumentModel.create();
|
371 | data.emb = EmbeddedModel.create();
|
372 | data.emb.num = 26;
|
373 |
|
374 | data.save().then(function() {
|
375 | expect.fail(null, Error, 'Expected error, but got none.');
|
376 | }).catch(function(error) {
|
377 | expect(error).to.be.instanceof(ValidationError);
|
378 | expect(error.message).to.contain('max');
|
379 | }).then(done, done);
|
380 | });
|
381 |
|
382 | it('should validate array of embedded values', function(done) {
|
383 |
|
384 | class Money extends EmbeddedDocument {
|
385 | constructor() {
|
386 | super();
|
387 | this.value = { type: Number, choices: [1, 5, 10, 20, 50, 100] };
|
388 | }
|
389 | }
|
390 |
|
391 | class Wallet extends Document {
|
392 | constructor() {
|
393 | super();
|
394 | this.contents = [Money];
|
395 | }
|
396 | }
|
397 |
|
398 | let wallet = Wallet.create();
|
399 | wallet.contents.push(Money.create());
|
400 | wallet.contents[0].value = 5;
|
401 | wallet.contents.push(Money.create());
|
402 | wallet.contents[1].value = 26;
|
403 |
|
404 | wallet.save().then(function() {
|
405 | expect.fail(null, Error, 'Expected error, but got none.');
|
406 | }).catch(function(error) {
|
407 | expect(error).to.be.instanceof(ValidationError);
|
408 | expect(error.message).to.contain('choices');
|
409 | }).then(done, done);
|
410 | });
|
411 |
|
412 | });
|
413 |
|
414 | describe('canonicalize', function() {
|
415 | it('should ensure timestamp dates are converted to Date objects', function(done) {
|
416 | class Education extends EmbeddedDocument {
|
417 | constructor() {
|
418 | super();
|
419 |
|
420 | this.school = String;
|
421 | this.major = String;
|
422 | this.dateGraduated = Date;
|
423 | }
|
424 |
|
425 | static collectionName() {
|
426 | return 'people';
|
427 | }
|
428 | }
|
429 |
|
430 | class Person extends Document {
|
431 | constructor() {
|
432 | super();
|
433 |
|
434 | this.gradSchool = Education;
|
435 | }
|
436 |
|
437 | static collectionName() {
|
438 | return 'people';
|
439 | }
|
440 | }
|
441 |
|
442 | let now = new Date();
|
443 |
|
444 | let person = Person.create({
|
445 | gradSchool: {
|
446 | school: 'CMU',
|
447 | major: 'ECE',
|
448 | dateGraduated: now
|
449 | }
|
450 | });
|
451 |
|
452 | person.save().then(function() {
|
453 | validateId(person);
|
454 | expect(person.gradSchool.school).to.be.equal('CMU');
|
455 | expect(person.gradSchool.dateGraduated.getFullYear()).to.be.equal(now.getFullYear());
|
456 | expect(person.gradSchool.dateGraduated.getHours()).to.be.equal(now.getHours());
|
457 | expect(person.gradSchool.dateGraduated.getMinutes()).to.be.equal(now.getMinutes());
|
458 | expect(person.gradSchool.dateGraduated.getMonth()).to.be.equal(now.getMonth());
|
459 | expect(person.gradSchool.dateGraduated.getSeconds()).to.be.equal(now.getSeconds());
|
460 | }).then(done, done);
|
461 | });
|
462 | });
|
463 |
|
464 | describe('hooks', function() {
|
465 |
|
466 | it('should call all pre and post functions on embedded models', function(done) {
|
467 |
|
468 | let preValidateCalled = false;
|
469 | let preSaveCalled = false;
|
470 | let preDeleteCalled = false;
|
471 |
|
472 | let postValidateCalled = false;
|
473 | let postSaveCalled = false;
|
474 | let postDeleteCalled = false;
|
475 |
|
476 | class Coffee extends EmbeddedDocument {
|
477 | constructor() {
|
478 | super();
|
479 | }
|
480 |
|
481 | preValidate() {
|
482 | preValidateCalled = true;
|
483 | }
|
484 |
|
485 | postValidate() {
|
486 | postValidateCalled = true;
|
487 | }
|
488 |
|
489 | preSave() {
|
490 | preSaveCalled = true;
|
491 | }
|
492 |
|
493 | postSave() {
|
494 | postSaveCalled = true;
|
495 | }
|
496 |
|
497 | preDelete() {
|
498 | preDeleteCalled = true;
|
499 | }
|
500 |
|
501 | postDelete() {
|
502 | postDeleteCalled = true;
|
503 | }
|
504 | }
|
505 |
|
506 | class Cup extends Document {
|
507 | constructor() {
|
508 | super();
|
509 |
|
510 | this.contents = Coffee;
|
511 | }
|
512 | }
|
513 |
|
514 | let cup = Cup.create();
|
515 | cup.contents = Coffee.create();
|
516 |
|
517 | cup.save().then(function() {
|
518 | validateId(cup);
|
519 |
|
520 |
|
521 | expect(preValidateCalled).to.be.equal(true);
|
522 | expect(preSaveCalled).to.be.equal(true);
|
523 | expect(postValidateCalled).to.be.equal(true);
|
524 | expect(postSaveCalled).to.be.equal(true);
|
525 |
|
526 |
|
527 | expect(preDeleteCalled).to.be.equal(false);
|
528 | expect(postDeleteCalled).to.be.equal(false);
|
529 |
|
530 | return cup.delete();
|
531 | }).then(function(numDeleted) {
|
532 | expect(numDeleted).to.be.equal(1);
|
533 |
|
534 | expect(preDeleteCalled).to.be.equal(true);
|
535 | expect(postDeleteCalled).to.be.equal(true);
|
536 | }).then(done, done);
|
537 | });
|
538 |
|
539 | it('should call all pre and post functions on array of embedded models', function(done) {
|
540 |
|
541 | let preValidateCalled = false;
|
542 | let preSaveCalled = false;
|
543 | let preDeleteCalled = false;
|
544 |
|
545 | let postValidateCalled = false;
|
546 | let postSaveCalled = false;
|
547 | let postDeleteCalled = false;
|
548 |
|
549 | class Money extends EmbeddedDocument {
|
550 | constructor() {
|
551 | super();
|
552 | }
|
553 |
|
554 | preValidate() {
|
555 | preValidateCalled = true;
|
556 | }
|
557 |
|
558 | postValidate() {
|
559 | postValidateCalled = true;
|
560 | }
|
561 |
|
562 | preSave() {
|
563 | preSaveCalled = true;
|
564 | }
|
565 |
|
566 | postSave() {
|
567 | postSaveCalled = true;
|
568 | }
|
569 |
|
570 | preDelete() {
|
571 | preDeleteCalled = true;
|
572 | }
|
573 |
|
574 | postDelete() {
|
575 | postDeleteCalled = true;
|
576 | }
|
577 | }
|
578 |
|
579 | class Wallet extends Document {
|
580 | constructor() {
|
581 | super();
|
582 |
|
583 | this.contents = [Money];
|
584 | }
|
585 | }
|
586 |
|
587 | let wallet = Wallet.create();
|
588 | wallet.contents.push(Money.create());
|
589 | wallet.contents.push(Money.create());
|
590 |
|
591 | wallet.save().then(function() {
|
592 | validateId(wallet);
|
593 |
|
594 |
|
595 | expect(preValidateCalled).to.be.equal(true);
|
596 | expect(postValidateCalled).to.be.equal(true);
|
597 | expect(preSaveCalled).to.be.equal(true);
|
598 | expect(postSaveCalled).to.be.equal(true);
|
599 |
|
600 |
|
601 | expect(preDeleteCalled).to.be.equal(false);
|
602 | expect(postDeleteCalled).to.be.equal(false);
|
603 |
|
604 | return wallet.delete();
|
605 | }).then(function(numDeleted) {
|
606 | expect(numDeleted).to.be.equal(1);
|
607 |
|
608 | expect(preDeleteCalled).to.be.equal(true);
|
609 | expect(postDeleteCalled).to.be.equal(true);
|
610 | }).then(done, done);
|
611 | });
|
612 | });
|
613 |
|
614 | describe('serialize', function() {
|
615 | it('should serialize data to JSON', function(done) {
|
616 | class Address extends EmbeddedDocument {
|
617 | constructor() {
|
618 | super();
|
619 |
|
620 | this.street = String;
|
621 | this.city = String;
|
622 | this.zipCode = Number;
|
623 | this.isPoBox = Boolean;
|
624 | }
|
625 | }
|
626 |
|
627 | class Person extends Document {
|
628 | constructor() {
|
629 | super();
|
630 |
|
631 | this.name = String;
|
632 | this.age = Number;
|
633 | this.isAlive = Boolean;
|
634 | this.children = [String];
|
635 | this.address = Address;
|
636 | }
|
637 |
|
638 | static collectionName() {
|
639 | return 'people';
|
640 | }
|
641 | }
|
642 |
|
643 | let person = Person.create({
|
644 | name: 'Scott',
|
645 | address: {
|
646 | street: '123 Fake St.',
|
647 | city: 'Cityville',
|
648 | zipCode: 12345,
|
649 | isPoBox: false
|
650 | }
|
651 | });
|
652 |
|
653 | person.save().then(function() {
|
654 | validateId(person);
|
655 | expect(person.name).to.be.equal('Scott');
|
656 | expect(person.address).to.be.an.instanceof(Address);
|
657 | expect(person.address.street).to.be.equal('123 Fake St.');
|
658 | expect(person.address.city).to.be.equal('Cityville');
|
659 | expect(person.address.zipCode).to.be.equal(12345);
|
660 | expect(person.address.isPoBox).to.be.equal(false);
|
661 |
|
662 | let json = person.toJSON();
|
663 |
|
664 | expect(json.name).to.be.equal('Scott');
|
665 | expect(json.address).to.not.be.an.instanceof(Address);
|
666 | expect(json.address.street).to.be.equal('123 Fake St.');
|
667 | expect(json.address.city).to.be.equal('Cityville');
|
668 | expect(json.address.zipCode).to.be.equal(12345);
|
669 | expect(json.address.isPoBox).to.be.equal(false);
|
670 | }).then(done, done);
|
671 | });
|
672 |
|
673 | it('should serialize data to JSON and ignore methods', function(done) {
|
674 | class Address extends EmbeddedDocument {
|
675 | constructor() {
|
676 | super();
|
677 | this.street = String;
|
678 | }
|
679 |
|
680 | getBar() {
|
681 | return 'bar';
|
682 | }
|
683 | }
|
684 |
|
685 | class Person extends Document {
|
686 | constructor() {
|
687 | super();
|
688 |
|
689 | this.name = String;
|
690 | this.address = Address;
|
691 | }
|
692 |
|
693 | static collectionName() {
|
694 | return 'people';
|
695 | }
|
696 |
|
697 | getFoo() {
|
698 | return 'foo';
|
699 | }
|
700 | }
|
701 |
|
702 | let person = Person.create({
|
703 | name: 'Scott',
|
704 | address : {
|
705 | street : 'Bar street'
|
706 | }
|
707 | });
|
708 |
|
709 | let json = person.toJSON();
|
710 | expect(json).to.have.keys(['_id', 'name', 'address']);
|
711 | expect(json.address).to.have.keys(['street']);
|
712 |
|
713 | done();
|
714 | });
|
715 | });
|
716 | }); |
\ | No newline at end of file |