Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Sign In

hexojs / warehouse / 251

31 Aug 2019 - 10:36 coverage decreased (-0.3%) to 96.402%
251

Pull #52

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Merge branch 'master' into Convert-method-definitions
Pull Request #52: Convert method definitions

535 of 579 branches covered (92.4%)

Branch coverage included in aggregate %.

827 of 842 new or added lines in 17 files covered. (98.22%)

1126 of 1144 relevant lines covered (98.43%)

642.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.27
/lib/schema.js
1
'use strict';
2

3
const SchemaType = require('./schematype');
3×
4
const Types = require('./types');
3×
5
const Promise = require('bluebird');
3×
6
const util = require('./util');
3×
7
const PopulationError = require('./error/population');
3×
8
const isPlainObject = require('is-plain-object');
3×
9

10
const { getProp } = util;
3×
11
const { setProp } = util;
3×
12
const { delProp } = util;
3×
13
const { isArray } = Array;
3×
14

15
const builtinTypes = {
3×
16
  String: true,
17
  Number: true,
18
  Boolean: true,
19
  Array: true,
20
  Object: true,
21
  Date: true,
22
  Buffer: true
23
};
24

25
class Schema {
26

27
  /**
28
   * Schema constructor.
29
   *
30
   * @param {Object} schema
31
   */
32
  constructor(schema) {
33
    this.paths = {};
90×
34
    this.statics = {};
90×
35
    this.methods = {};
90×
36

37
    this.hooks = {
90×
38
      pre: {
39
        save: [],
40
        remove: []
41
      },
42
      post: {
43
        save: [],
44
        remove: []
45
      }
46
    };
47

48
    this.stacks = {
90×
49
      getter: [],
50
      setter: [],
51
      import: [],
52
      export: []
53
    };
54

55
    if (schema) {
90×
56
      this.add(schema);
33×
57
    }
58
  }
59

60
  /**
61
   * Adds paths.
62
   *
63
   * @param {Object} schema
64
   * @param {String} prefix
65
   */
66
  add(schema, prefix_) {
67
    const prefix = prefix_ || '';
45×
68
    const keys = Object.keys(schema);
45×
69
    const len = keys.length;
45×
70

71
    if (!len) return;
Branches [[2, 0]] missed. 45×
72

73
    for (let i = 0; i < len; i++) {
45×
74
      const key = keys[i];
135×
75
      const value = schema[key];
135×
76

77
      this.path(prefix + key, value);
135×
78
    }
79
  }
80

81
  /**
82
   * Gets/Sets a path.
83
   *
84
   * @param {String} name
85
   * @param {*} obj
86
   * @return {SchemaType}
87
   */
88
  path(name, obj) {
89
    if (obj == null) {
276×
90
      return this.paths[name];
69×
91
    }
92

93
    let type;
94
    let nested = false;
207×
95

96
    if (obj instanceof SchemaType) {
207×
97
      type = obj;
12×
98
    } else {
99
      switch (typeof obj) {
Branches [[5, 2]] missed. 195×
100
        case 'function':
101
          type = getSchemaType(name, {type: obj});
72×
102
          break;
72×
103

104
        case 'object':
105
          if (obj.type) {
123×
106
            type = getSchemaType(name, obj);
96×
107
          } else if (isArray(obj)) {
27×
108
            type = new Types.Array(name, {
18×
109
              child: obj.length ? getSchemaType(name, obj[0]) : new SchemaType(name)
110
            });
111
          } else {
112
            type = new Types.Object();
9×
113
            nested = Object.keys(obj).length > 0;
9×
114
          }
115

116
          break;
123×
117

118
        default:
NEW
119
          throw new TypeError(`Invalid value for schema path \`${name}\``);
!
120
      }
121
    }
122

123
    this.paths[name] = type;
207×
124
    this._updateStack(name, type);
207×
125

126
    if (nested) this.add(obj, `${name}.`);
207×
127
  }
128

129
  /**
130
   * Updates cache stacks.
131
   *
132
   * @param {String} name
133
   * @param {SchemaType} type
134
   * @private
135
   */
136
  _updateStack(name, type) {
137
    const stacks = this.stacks;
207×
138

139
    stacks.getter.push(data => {
207×
140
      const value = getProp(data, name);
13,497×
141
      const result = type.cast(value, data);
13,497×
142

143
      if (result !== undefined) {
13,497×
144
        setProp(data, name, result);
6,903×
145
      }
146
    });
147

148
    stacks.setter.push(data => {
207×
149
      const value = getProp(data, name);
7,503×
150
      const result = type.validate(value, data);
7,503×
151

152
      if (result !== undefined) {
7,503×
153
        setProp(data, name, result);
3,927×
154
      } else {
155
        delProp(data, name);
3,576×
156
      }
157
    });
158

159
    stacks.import.push(data => {
207×
160
      const value = getProp(data, name);
39×
161
      const result = type.parse(value, data);
39×
162

163
      if (result !== undefined) {
Branches [[12, 1]] missed. 39×
164
        setProp(data, name, result);
39×
165
      }
166
    });
167

168
    stacks.export.push(data => {
207×
169
      const value = getProp(data, name);
27×
170
      const result = type.value(value, data);
27×
171

172
      if (result !== undefined) {
27×
173
        setProp(data, name, result);
24×
174
      } else {
175
        delProp(data, name);
3×
176
      }
177
    });
178
  }
179

180
  /**
181
   * Adds a virtual path.
182
   *
183
   * @param {String} name
184
   * @param {Function} [getter]
185
   * @return {SchemaType.Virtual}
186
   */
187
  virtual(name, getter) {
188
    const virtual = new Types.Virtual(name, {});
12×
189
    if (getter) virtual.get(getter);
12×
190

191
    this.path(name, virtual);
12×
192

193
    return virtual;
12×
194
  }
195

196
  /**
197
   * Adds a pre-hook.
198
   *
199
   * @param {String} type Hook type. One of `save` or `remove`.
200
   * @param {Function} fn
201
   */
202
  pre(type, fn) {
203
    checkHookType(type);
27×
204
    if (typeof fn !== 'function') throw new TypeError('Hook must be a function!');
24×
205

206
    this.hooks.pre[type].push(hookWrapper(fn));
21×
207
  }
208

209
  /**
210
   * Adds a post-hook.
211
   *
212
   * @param {String} type Hook type. One of `save` or `remove`.
213
   * @param {Function} fn
214
   */
215
  post(type, fn) {
216
    checkHookType(type);
24×
217
    if (typeof fn !== 'function') throw new TypeError('Hook must be a function!');
21×
218

219
    this.hooks.post[type].push(hookWrapper(fn));
18×
220
  }
221

222
  /**
223
   * Adds a instance method.
224
   *
225
   * @param {String} name
226
   * @param {Function} fn
227
   */
228
  method(name, fn) {
229
    if (!name) throw new TypeError('Method name is required!');
12×
230

231
    if (typeof fn !== 'function') {
9×
232
      throw new TypeError('Instance method must be a function!');
3×
233
    }
234

235
    this.methods[name] = fn;
6×
236
  }
237

238
  /**
239
   * Adds a static method.
240
   *
241
   * @param {String} name
242
   * @param {Function} fn
243
   */
244
  static(name, fn) {
245
    if (!name) throw new TypeError('Method name is required!');
12×
246

247
    if (typeof fn !== 'function') {
9×
248
      throw new TypeError('Static method must be a function!');
3×
249
    }
250

251
    this.statics[name] = fn;
6×
252
  }
253

254
  /**
255
   * Apply getters.
256
   *
257
   * @param {Object} data
258
   * @return {*}
259
   * @private
260
   */
261
  _applyGetters(data) {
262
    const stack = this.stacks.getter;
2,652×
263

264
    for (let i = 0, len = stack.length; i < len; i++) {
2,652×
265
      stack[i](data);
13,497×
266
    }
267
  }
268

269
  /**
270
   * Apply setters.
271
   *
272
   * @param {Object} data
273
   * @return {*}
274
   * @private
275
   */
276
  _applySetters(data) {
277
    const stack = this.stacks.setter;
1,425×
278

279
    for (let i = 0, len = stack.length; i < len; i++) {
1,425×
280
      stack[i](data);
7,503×
281
    }
282
  }
283

284
  /**
285
   * Parses database.
286
   *
287
   * @param {Object} data
288
   * @return {Object}
289
   * @private
290
   */
291
  _parseDatabase(data) {
292
    const stack = this.stacks.import;
33×
293

294
    for (let i = 0, len = stack.length; i < len; i++) {
33×
295
      stack[i](data);
39×
296
    }
297

298
    return data;
33×
299
  }
300

301
  /**
302
   * Exports database.
303
   *
304
   * @param {Object} data
305
   * @return {Object}
306
   * @private
307
   */
308
  _exportDatabase(data) {
309
    const stack = this.stacks.export;
18×
310

311
    for (let i = 0, len = stack.length; i < len; i++) {
18×
312
      stack[i](data);
27×
313
    }
314

315
    return data;
18×
316
  }
317

318
  /**
319
   * Parses updating expressions and returns a stack.
320
   *
321
   * @param {Object} updates
322
   * @param {String} [prefix]
323
   * @return {Array}
324
   * @private
325
   */
326
  _parseUpdate(updates, prefix_) {
327
    const prefix = prefix_ || '';
48×
328
    const paths = this.paths;
48×
329
    let stack = [];
48×
330
    const keys = Object.keys(updates);
48×
331
    let path, prefixNoDot;
332

333
    if (prefix) {
48×
334
      prefixNoDot = prefix.substring(0, prefix.length - 1);
6×
335
      path = paths[prefixNoDot];
6×
336
    }
337

338
    for (let i = 0, len = keys.length; i < len; i++) {
48×
339
      const key = keys[i];
45×
340
      const update = updates[key];
45×
341
      const name = prefix + key;
45×
342

343
      // Update operators
344
      if (key[0] === '$') {
45×
345
        const ukey = `u${key}`;
18×
346

347
        // First-class update operators
348
        if (prefix) {
18×
349
          stack.push(updateStackOperator(path, ukey, prefixNoDot, update));
3×
350
        } else { // Inline update operators
351
          const fields = Object.keys(update);
15×
352
          const fieldLen = fields.length;
15×
353

354
          for (let j = 0; j < fieldLen; j++) {
15×
355
            const field = fields[i];
15×
356
            stack.push(
15×
357
              updateStackOperator(paths[field], ukey, field, update[field]));
358
          }
359
        }
360
      } else if (isPlainObject(update)) {
27×
361
        stack = stack.concat(this._parseUpdate(update, `${name}.`));
6×
362
      } else {
363
        stack.push(updateStackNormal(name, update));
21×
364
      }
365
    }
366

367
    return stack;
48×
368
  }
369

370
  /**
371
   * Parses array of query expressions and returns a stack.
372
   *
373
   * @param {Array} arr
374
   * @return {Array}
375
   * @private
376
   */
377
  _parseQueryArray(arr) {
378
    const stack = [];
18×
379

380
    for (let i = 0, len = arr.length; i < len; i++) {
18×
381
      stack.push(execQueryStack(this._parseQuery(arr[i])));
36×
382
    }
383

384
    return stack;
18×
385
  }
386

387
  /**
388
   * Parses normal query expressions and returns a stack.
389
   *
390
   * @param {Array} queries
391
   * @param {String} [prefix]
392
   * @return {Array}
393
   * @private
394
   */
395
  _parseNormalQuery(queries, prefix_) {
396
    const prefix = prefix_ || '';
Branches [[26, 1]] missed. 60×
397
    const paths = this.paths;
60×
398
    let stack = [];
60×
399
    const keys = Object.keys(queries);
60×
400
    let path, prefixNoDot;
401

402
    if (prefix) {
Branches [[27, 1]] missed. 60×
403
      prefixNoDot = prefix.substring(0, prefix.length - 1);
60×
404
      path = paths[prefixNoDot];
60×
405
    }
406

407
    for (let i = 0, len = keys.length; i < len; i++) {
60×
408
      const key = keys[i];
60×
409
      const query = queries[key];
60×
410
      const name = prefix + key;
60×
411

412
      if (key[0] === '$') {
Branches [[28, 1]] missed. 60×
413
        stack.push(queryStackOperator(path, `q${key}`, prefixNoDot, query));
60×
NEW
414
      } else if (isPlainObject(query)) {
Branches [[29, 0], [29, 1]] missed. !
NEW
415
        stack = stack.concat(this._parseNormalQuery(query, `${name}.`));
!
416
      } else {
NEW
417
        stack.push(queryStackNormal(paths[name], name, query));
!
418
      }
419
    }
420

421
    return stack;
60×
422
  }
423

424
  /**
425
   * Parses query expressions and returns a stack.
426
   *
427
   * @param {Array} queries
428
   * @return {Array}
429
   * @private
430
   */
431
  _parseQuery(queries) {
432
    let stack = [];
288×
433
    const paths = this.paths;
288×
434
    const keys = Object.keys(queries);
288×
435

436
    for (let i = 0, len = keys.length; i < len; i++) {
288×
437
      const key = keys[i];
156×
438
      const query = queries[key];
156×
439

440
      switch (key) {
156×
441
        case '$and':
442
          stack = stack.concat(this._parseQueryArray(query));
6×
443
          break;
6×
444

445
        case '$or':
446
          stack.push($or(this._parseQueryArray(query)));
6×
447
          break;
6×
448

449
        case '$nor':
450
          stack.push($nor(this._parseQueryArray(query)));
6×
451
          break;
6×
452

453
        case '$not':
454
          stack.push($not(this._parseQuery(query)));
6×
455
          break;
6×
456

457
        case '$where':
458
          stack.push($where(query));
6×
459
          break;
6×
460

461
        default:
462
          if (isPlainObject(query)) {
126×
463
            stack = stack.concat(this._parseNormalQuery(query, `${key}.`));
60×
464
          } else {
465
            stack.push(queryStackNormal(paths[key], key, query));
66×
466
          }
467
      }
468
    }
469

470
    return stack;
288×
471
  }
472

473
  /**
474
   * Returns a function for querying.
475
   *
476
   * @param {Object} query
477
   * @return {Function}
478
   * @private
479
   */
480
  _execQuery(query) {
481
    const stack = this._parseQuery(query);
246×
482
    return execQueryStack(stack);
246×
483
  }
484

485
  /**
486
   * Parses sorting expressions and returns a stack.
487
   *
488
   * @param {Object} sorts
489
   * @param {String} [prefix]
490
   * @return {Array}
491
   * @private
492
   */
493
  _parseSort(sorts, prefix_) {
494
    const prefix = prefix_ || '';
21×
495
    const paths = this.paths;
21×
496
    let stack = [];
21×
497
    const keys = Object.keys(sorts);
21×
498

499
    for (let i = 0, len = keys.length; i < len; i++) {
21×
500
      const key = keys[i];
27×
501
      const sort = sorts[key];
27×
502
      const name = prefix + key;
27×
503

504
      if (typeof sort === 'object') {
Branches [[33, 0]] missed. 27×
NEW
505
        stack = stack.concat(this._parseSort(sort, `${name}.`));
!
506
      } else {
507
        stack.push(sortStack(paths[name], name, sort));
27×
508
      }
509
    }
510

511
    return stack;
21×
512
  }
513

514
  /**
515
   * Returns a function for sorting.
516
   *
517
   * @param {Object} sorts
518
   * @return {Function}
519
   * @private
520
   */
521
  _execSort(sorts) {
522
    const stack = this._parseSort(sorts);
21×
523
    return execSortStack(stack);
21×
524
  }
525

526
  /**
527
   * Parses population expression and returns a stack.
528
   *
529
   * @param {String|Object} expr
530
   * @return {Array}
531
   * @private
532
   */
533
  _parsePopulate(expr) {
534
    const paths = this.paths;
42×
535
    let arr, path, ref;
536

537
    if (typeof expr === 'string') {
42×
538
      const split = expr.split(' ');
18×
539
      arr = [];
18×
540

541
      for (let i = 0, len = split.length; i < len; i++) {
18×
542
        arr.push({
18×
543
          path: split[i]
544
        });
545
      }
546
    } else if (isArray(expr)) {
Branches [[35, 0]] missed. 24×
NEW
547
      arr = [];
!
NEW
548
      for (let i = 0, len = expr.length; i < len; i++) {
!
NEW
549
        const item = expr[i];
!
550

NEW
551
        if (typeof item === 'string') {
Branches [[36, 0], [36, 1]] missed. !
NEW
552
          arr.push({
!
553
            path: item
554
          });
555
        } else {
NEW
556
          arr.push(item);
!
557
        }
558
      }
559
    } else {
560
      arr = [expr];
24×
561
    }
562

563
    for (let i = 0, len = arr.length; i < len; i++) {
42×
564
      const item = arr[i];
42×
565
      const key = item.path;
42×
566

567
      if (!key) {
Branches [[37, 0]] missed. 42×
NEW
568
        throw new PopulationError('path is required');
!
569
      }
570

571
      if (!item.model) {
Branches [[38, 1]] missed. 42×
572
        path = paths[key];
42×
573
        ref = path.child ? path.child.options.ref : path.options.ref;
42×
574

575
        if (ref) {
Branches [[40, 1]] missed. 42×
576
          item.model = ref;
42×
577
        } else {
NEW
578
          throw new PopulationError('model is required');
!
579
        }
580
      }
581
    }
582

583
    return arr;
42×
584
  }
585
}
586

587
function getSchemaType(name, options) {
588
  const Type = options.type || options;
183×
589
  const typeName = Type.name;
183×
590

591
  if (builtinTypes[typeName]) {
183×
592
    return new Types[typeName](name, options);
99×
593
  }
594

595
  return new Type(name, options);
84×
596
}
597

598
function checkHookType(type) {
599
  if (type !== 'save' && type !== 'remove') {
51×
600
    throw new TypeError('Hook type must be `save` or `remove`!');
6×
601
  }
602
}
603

604
function hookWrapper(fn) {
605
  if (fn.length > 1) {
Branches [[45, 0]] missed. 39×
NEW
606
    return Promise.promisify(fn);
!
607
  }
608

609
  return Promise.method(fn);
39×
610
}
611

612
function updateStackNormal(key, update) {
613
  return data => {
21×
614
    setProp(data, key, update);
27×
615
  };
616
}
617

618
function updateStackOperator(path_, ukey, key, update) {
619
  const path = path_ || new SchemaType(key);
Branches [[46, 1]] missed. 18×
620

621
  return data => {
18×
622
    const result = path[ukey](getProp(data, key), update, data);
18×
623
    setProp(data, key, result);
18×
624
  };
625
}
626

627
function queryStackNormal(path_, key, query) {
628
  const path = path_ || new SchemaType(key);
66×
629

630
  return data => path.match(getProp(data, key), query, data);
240×
631
}
632

633
function queryStackOperator(path_, qkey, key, query) {
634
  const path = path_ || new SchemaType(key);
Branches [[48, 1]] missed. 60×
635

636
  return data => path[qkey](getProp(data, key), query, data);
180×
637
}
638

639
function execQueryStack(stack) {
640
  const len = stack.length;
288×
641

642
  return data => {
288×
643
    for (let i = 0; i < len; i++) {
1,026×
644
      if (!stack[i](data)) return false;
522×
645
    }
646

647
    return true;
771×
648
  };
649
}
650

651
function $or(stack) {
652
  const len = stack.length;
6×
653

654
  return data => {
6×
655
    for (let i = 0; i < len; i++) {
18×
656
      if (stack[i](data)) return true;
30×
657
    }
658

659
    return false;
6×
660
  };
661
}
662

663
function $nor(stack) {
664
  const len = stack.length;
6×
665

666
  return data => {
6×
667
    for (let i = 0; i < len; i++) {
18×
668
      if (stack[i](data)) return false;
30×
669
    }
670

671
    return true;
6×
672
  };
673
}
674

675
function $not(stack) {
676
  const fn = execQueryStack(stack);
6×
677

678
  return data => !fn(data);
18×
679
}
680

681
function $where(fn) {
682
  return data => fn.call(data);
18×
683
}
684

685
function execSortStack(stack) {
686
  const len = stack.length;
21×
687

688
  return (a, b) => {
21×
689
    let result;
690

691
    for (let i = 0; i < len; i++) {
84×
692
      result = stack[i](a, b);
90×
693
      if (result) break;
90×
694
    }
695

696
    return result;
84×
697
  };
698
}
699

700
function sortStack(path_, key, sort) {
701
  const path = path_ || new SchemaType(key);
Branches [[53, 1]] missed. 27×
702
  const descending = sort === 'desc' || sort === -1;
27×
703

704
  return (a, b) => {
27×
705
    const result = path.compare(getProp(a, key), getProp(b, key));
90×
706
    return descending && result ? result * -1 : result;
90×
707
  };
708
}
709

710
Schema.Types = Schema.prototype.Types = Types;
3×
711

712
module.exports = Schema;
3×
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
BLOG · TWITTER · Legal & Privacy · Supported CI Services · What's a CI service? · Automated Testing

© 2019 Coveralls, LLC