@lykmapipo/mongoose-common 0.38.3 → 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -0,0 +1,183 @@
1
+ import { filter, forEach, isEmpty, isFunction, map, nth } from 'lodash';
2
+ import { uniq } from '@lykmapipo/common';
3
+ import mongoose from 'mongoose-valid8';
4
+
5
+ /**
6
+ * @function isUniqueError
7
+ * @name isUniqueError
8
+ * @description Check if the given error is a unique mongodb error.
9
+ * @param {object} error valid error object to test.
10
+ * @returns {boolean} true if and only if it is an unique error.
11
+ * @version 0.2.0
12
+ * @since 0.1.0
13
+ * @private
14
+ */
15
+ const isUniqueError = (error) => {
16
+ return (
17
+ error &&
18
+ (error.name === 'BulkWriteError' ||
19
+ error.name === 'MongoError' ||
20
+ error.name === 'MongoServerError' ||
21
+ error.name === 'MongoBulkWriteError') &&
22
+ (error.code === 11000 || error.code === 11001) &&
23
+ !isEmpty(error.message)
24
+ );
25
+ };
26
+
27
+ /**
28
+ * @function parseErrorPaths
29
+ * @name parseErrorPaths
30
+ * @description Parse paths found in unique mongodb error.
31
+ * @param {object} schema valid mongoose schema.
32
+ * @param {Error} error valid mongodb error.
33
+ * @returns {string[]} found paths in error.
34
+ * @version 0.19.0
35
+ * @since 0.1.0
36
+ * @private
37
+ */
38
+ const parseErrorPaths = (schema, error) => {
39
+ // back off if no error message
40
+ if (!error.message) {
41
+ return [];
42
+ }
43
+
44
+ // obtain paths from error message
45
+ let paths = nth(error.message.match(/index: (.+?) dup key:/), 1) || '';
46
+ paths = paths.split('$').pop();
47
+
48
+ // handle compound unique paths index
49
+ paths = [].concat(paths.split('_'));
50
+
51
+ // in case for id ensure _id too and compact paths
52
+ paths = uniq([
53
+ ...paths,
54
+ ...map(paths, (pathName) => (pathName === 'id' ? '_id' : pathName)),
55
+ ]);
56
+
57
+ // ensure paths are within schema
58
+ paths = filter(paths, (pathName) => !isEmpty(schema.path(pathName)));
59
+
60
+ // return found paths
61
+ return paths;
62
+ };
63
+
64
+ /**
65
+ * @function parseErrorValues
66
+ * @name parseErrorValues
67
+ * @description Parse paths value found in unique mongodb error.
68
+ * @param {string[]} paths paths found in unique mongodb error.
69
+ * @param {Error} error valid mongodb error.
70
+ * @returns {string[]} found paths value from error.
71
+ * @version 0.19.0
72
+ * @since 0.1.0
73
+ * @private
74
+ */
75
+ const parseErrorValues = (paths, error) => {
76
+ // back off if no error message
77
+ if (!error.message) {
78
+ return [];
79
+ }
80
+
81
+ // obtain paths value
82
+ let values = nth(error.message.match(/dup key: { (.+?) }/), 1) || '';
83
+ values = uniq(
84
+ [].concat(values.match(/'(.+?)'/g)).concat(values.match(/"(.+?)"/g))
85
+ ); // enclosed with quotes
86
+ values = map(values, (v) => v.replace(/^"(.+?)"$/, '$1')); // double quotes
87
+ values = map(values, (v) => v.replace(/^'(.+?)'$/, '$1')); // single quotes
88
+ values = paths.length === 1 ? [values.join(' ')] : values;
89
+
90
+ // return parsed paths values
91
+ return values;
92
+ };
93
+
94
+ /**
95
+ * @function uniqueErrorPlugin
96
+ * @name uniqueErrorPlugin
97
+ * @description Plugin to handle mongodb unique error
98
+ * @param {object} schema valid mongoose schema
99
+ * @version 0.2.0
100
+ * @since 0.1.0
101
+ * @public
102
+ */
103
+ export default function uniqueErrorPlugin(schema) {
104
+ /**
105
+ * @function handleUniqueError
106
+ * @name handleUniqueError
107
+ * @description Handle mongodb unique error and transform to mongoose error
108
+ * @param {Error|object} error valid mongodb unique error
109
+ * @param {object} doc valid mongoose document
110
+ * @param {Function} next callback to invoke on success or error
111
+ * @returns {Error} valid mongoose error
112
+ * @version 0.2.0
113
+ * @since 0.1.0
114
+ * @private
115
+ */
116
+ function handleUniqueError(error, doc, next) {
117
+ // this: Model instance context
118
+
119
+ // obtain current instance
120
+ const instance = doc || this;
121
+
122
+ // continue if is not unique error
123
+ if (!isUniqueError(error)) {
124
+ return next(error);
125
+ }
126
+
127
+ // obtain index name
128
+ const indexName =
129
+ nth(error.message.match(/index: (.+?) dup key:/), 1) || '';
130
+
131
+ // obtain unique paths from error
132
+ const paths = parseErrorPaths(schema, error);
133
+
134
+ // obtain paths value from error
135
+ const values = parseErrorValues(paths, error);
136
+
137
+ // build mongoose validations error bag
138
+ if (!isEmpty(paths) && !isEmpty(values)) {
139
+ const errors = {};
140
+
141
+ forEach(paths, (pathName, index) => {
142
+ // construct path error properties
143
+ let pathValue = nth(values, index);
144
+ if (isFunction(instance.get)) {
145
+ pathValue = instance.get(pathName);
146
+ }
147
+ const props = {
148
+ type: 'unique',
149
+ path: pathName,
150
+ value: pathValue,
151
+ message: 'Path `{PATH}` ({VALUE}) is not unique.',
152
+ reason: error.message,
153
+ index: indexName,
154
+ };
155
+
156
+ // construct path validation error
157
+ const pathError = new mongoose.Error.ValidatorError(props);
158
+ pathError.index = indexName;
159
+ errors[pathName] = pathError;
160
+ });
161
+
162
+ // build mongoose validation error
163
+ const err = new mongoose.Error.ValidationError();
164
+ err.status = err.status || 400;
165
+ err.errors = errors;
166
+
167
+ return next(err);
168
+ }
169
+
170
+ // continue with error
171
+ return next(error);
172
+ }
173
+
174
+ // plugin unique error handler
175
+ schema.post('save', handleUniqueError);
176
+ schema.post('insertMany', handleUniqueError);
177
+ schema.post('findOneAndReplace', handleUniqueError);
178
+ schema.post('findOneAndUpdate', handleUniqueError);
179
+ schema.post('replaceOne', handleUniqueError);
180
+ schema.post('update', handleUniqueError);
181
+ schema.post('updateMany', handleUniqueError);
182
+ schema.post('updateOne', handleUniqueError);
183
+ }
@@ -0,0 +1,39 @@
1
+ import { forEach, isFunction, split } from 'lodash';
2
+
3
+ /**
4
+ * @function path
5
+ * @name path
6
+ * @description obtain schema path from model
7
+ * @param {object} schema valid mongoose schema instance
8
+ * @version 0.1.0
9
+ * @since 0.1.0
10
+ * @public
11
+ */
12
+ export default (schema) => {
13
+ // register path
14
+ const canNotGetPath = !isFunction(schema.statics.path);
15
+
16
+ if (canNotGetPath) {
17
+ // eslint-disable-next-line no-param-reassign
18
+ schema.statics.path = function path(pathName) {
19
+ // initalize path
20
+ let $path;
21
+
22
+ // tokenize path
23
+ const paths = split(pathName, '.');
24
+
25
+ // iterate on schema recursive to get path schema
26
+ forEach(paths, function getPath(part) {
27
+ // obtain schema to resolve path
28
+ const $schema = $path ? $path.schema : schema;
29
+ $path = $schema && $schema.path ? $schema.path(part) : undefined;
30
+ });
31
+
32
+ // fall back to direct path
33
+ $path = $path || schema.path(pathName);
34
+
35
+ // return found path
36
+ return $path;
37
+ };
38
+ }
39
+ };
@@ -1,52 +1,85 @@
1
- 'use strict';
1
+ import { join as joinPath, resolve as resolvePath } from 'path';
2
+ import {
3
+ compact,
4
+ filter,
5
+ forEach,
6
+ get,
7
+ isArray,
8
+ isEmpty,
9
+ isFunction,
10
+ isPlainObject,
11
+ isString,
12
+ map,
13
+ mapValues,
14
+ noop,
15
+ omit,
16
+ } from 'lodash';
17
+ import { waterfall, parallel } from 'async';
18
+ import { mergeObjects, idOf } from '@lykmapipo/common';
19
+ import { getBoolean, getString } from '@lykmapipo/env';
20
+ import mongoose from 'mongoose-valid8';
2
21
 
3
- /* dependencies */
4
- const path = require('path');
5
- const _ = require('lodash');
6
- const { waterfall, parallel } = require('async');
7
- const { mergeObjects, idOf } = require('@lykmapipo/common');
8
- const { getBoolean, getString } = require('@lykmapipo/env');
9
- const mongoose = require('mongoose');
10
-
11
- const loadPathSeeds = (collectionName) => {
22
+ /**
23
+ * @name loadPathSeeds
24
+ * @description load seeds from paths
25
+ * @param {string} collectionName valid collection name
26
+ * @returns {object|object[]} given collection seed from a path
27
+ * @since 0.21.0
28
+ * @version 0.2.0
29
+ * @private
30
+ */
31
+ function loadPathSeeds(collectionName) {
32
+ // resolve seed path
12
33
  const BASE_PATH = getString('BASE_PATH', process.cwd());
13
- let SEED_PATH = getString('SEED_PATH', path.join(BASE_PATH, 'seeds'));
14
- SEED_PATH = path.resolve(SEED_PATH, collectionName);
34
+ let SEED_PATH = getString('SEED_PATH', joinPath(BASE_PATH, 'seeds'));
35
+ SEED_PATH = resolvePath(SEED_PATH, collectionName);
36
+
37
+ // try load seeds from path
15
38
  try {
39
+ // eslint-disable-next-line import/no-dynamic-require, global-require
16
40
  let seeds = require(SEED_PATH);
17
41
  // honor es6 default exports
18
- seeds = [].concat(_.isArray(seeds.default) ? seeds.default : seeds);
42
+ seeds = [].concat(isArray(seeds.default) ? seeds.default : seeds);
19
43
  return seeds;
20
44
  } catch (e) {
21
45
  return [];
22
46
  }
23
- };
24
-
25
- const seedModel = function (data, done) {
26
- /* jshint validthis: true */
47
+ }
27
48
 
49
+ /**
50
+ * @function clearAndSeedModel
51
+ * @name clearAndSeedModel
52
+ * @description clear and seed the given model data
53
+ * @param {object|object[]|Function} data valid model data
54
+ * @param {Function} done callback to invoke on success or error
55
+ * @returns {object} seed results
56
+ * @since 0.21.0
57
+ * @version 0.2.0
58
+ * @private
59
+ */
60
+ function seedModel(data, done) {
28
61
  // this: Model static context
29
62
 
30
63
  // normalize arguments
31
64
  let seeds = [];
32
- let cb = _.noop;
33
- let filter = (val) => val;
34
- let transform = (val) => val;
35
- if (_.isFunction(data)) {
65
+ let cb = noop;
66
+ let filterFn = (val) => val;
67
+ let transformFn = (val) => val;
68
+ if (isFunction(data)) {
36
69
  cb = data;
37
70
  }
38
- if (_.isArray(data)) {
71
+ if (isArray(data)) {
39
72
  seeds = [].concat(data);
40
73
  }
41
- if (_.isPlainObject(data)) {
42
- filter = data.filter || filter;
43
- transform = data.transform || transform;
44
- seeds = data.data || _.omit(data, 'filter', 'transform');
45
- seeds = _.isArray(seeds) ? seeds : mergeObjects(seeds);
74
+ if (isPlainObject(data)) {
75
+ filterFn = data.filter || filterFn;
76
+ transformFn = data.transform || transformFn;
77
+ seeds = data.data || omit(data, 'filter', 'transform');
78
+ seeds = isArray(seeds) ? seeds : mergeObjects(seeds);
46
79
  seeds = [].concat(seeds);
47
- seeds = _.filter(seeds, (seed) => !_.isEmpty(seed));
80
+ seeds = filter(seeds, (seed) => !isEmpty(seed));
48
81
  }
49
- if (_.isFunction(done)) {
82
+ if (isFunction(done)) {
50
83
  cb = done || cb;
51
84
  }
52
85
  // let seeds = _.isFunction(data) ? [] : [].concat(data);
@@ -54,34 +87,34 @@ const seedModel = function (data, done) {
54
87
 
55
88
  // compact seeds
56
89
  const collectionName =
57
- _.get(this, 'collection.name') || _.get(this, 'collection.collectionName');
90
+ get(this, 'collection.name') || get(this, 'collection.collectionName');
58
91
 
59
92
  // ignore path seeds if seed provided
60
- if (_.isEmpty(seeds)) {
93
+ if (isEmpty(seeds)) {
61
94
  const pathSeeds = loadPathSeeds(collectionName);
62
- seeds = _.compact([...seeds, ...pathSeeds]);
63
- seeds = _.filter(seeds, (seed) => !_.isEmpty(seed));
95
+ seeds = compact([...seeds, ...pathSeeds]);
96
+ seeds = filter(seeds, (seed) => !isEmpty(seed));
64
97
  }
65
98
 
66
99
  // filter seeds
67
- seeds = _.filter(seeds, filter);
100
+ seeds = filter(seeds, filterFn);
68
101
 
69
102
  // transform seeds
70
- seeds = _.map(seeds, transform);
103
+ seeds = map(seeds, transformFn);
71
104
 
72
105
  // filter empty seeds
73
- seeds = _.filter(seeds, (seed) => !_.isEmpty(seed));
106
+ seeds = filter(seeds, (seed) => !isEmpty(seed));
74
107
 
75
108
  // find existing instance fullfill seed criteria
76
109
  const findExisting = (seed, afterFind) => {
77
110
  // map seed to criteria
78
- const canProvideCriteria = _.isFunction(this.prepareSeedCriteria);
79
- let prepareSeedCriteria = (seed) => seed;
111
+ const canProvideCriteria = isFunction(this.prepareSeedCriteria);
112
+ let prepareSeedCriteria = ($seed) => $seed;
80
113
  if (canProvideCriteria) {
81
114
  prepareSeedCriteria = this.prepareSeedCriteria;
82
115
  }
83
116
  let criteria = prepareSeedCriteria(seed);
84
- criteria = _.omit(criteria, 'populate');
117
+ criteria = omit(criteria, 'populate');
85
118
 
86
119
  // find existing data
87
120
  return this.findOne(criteria, afterFind);
@@ -93,12 +126,12 @@ const seedModel = function (data, done) {
93
126
  const { model, match, select, array } = dependency;
94
127
 
95
128
  const afterFetchDependency = (error, found) => {
96
- const result = _.isEmpty(found) ? undefined : found;
129
+ const result = isEmpty(found) ? undefined : found;
97
130
  return afterDependency(error, result);
98
131
  };
99
132
 
100
133
  // try fetch with provide options
101
- if (_.isString(model) && _.isPlainObject(match)) {
134
+ if (isString(model) && isPlainObject(match)) {
102
135
  try {
103
136
  const Model = mongoose.model(model);
104
137
  if (array) {
@@ -131,20 +164,21 @@ const seedModel = function (data, done) {
131
164
  return waterfall(
132
165
  [
133
166
  (next) => {
134
- if (_.isEmpty(ignore)) {
167
+ if (isEmpty(ignore)) {
135
168
  return next(null, []);
136
169
  }
137
- const ignoreCriteria = _.omit(ignore, 'select');
170
+ const ignoreCriteria = omit(ignore, 'select');
138
171
  return fetchDependency(ignoreCriteria, next);
139
172
  }, // fetch ignored
140
173
  (ignored, next) => {
141
174
  // use ignored
142
175
  const ignorePath = ignore.path || '_id';
143
- const ignoredIds = _.compact(
144
- _.map([].concat(ignored), (val) => idOf(val))
176
+ const ignoredIds = compact(
177
+ map([].concat(ignored), (val) => idOf(val))
145
178
  );
146
- let { model, match, select, array } = mergeObjects(dependency);
147
- if (!_.isEmpty(ignoredIds)) {
179
+ const { model, select, array } = mergeObjects(dependency);
180
+ let { match } = mergeObjects(dependency);
181
+ if (!isEmpty(ignoredIds)) {
148
182
  match = mergeObjects(
149
183
  {
150
184
  [ignorePath]: { $nin: ignoredIds },
@@ -163,8 +197,8 @@ const seedModel = function (data, done) {
163
197
  // TODO: optimize queries
164
198
  const fetchDependencies = (seed, afterDependencies) => {
165
199
  let dependencies = mergeObjects(seed.populate);
166
- if (_.isPlainObject(dependencies) && !_.isEmpty(dependencies)) {
167
- dependencies = _.mapValues(dependencies, (dependency) => {
200
+ if (isPlainObject(dependencies) && !isEmpty(dependencies)) {
201
+ dependencies = mapValues(dependencies, (dependency) => {
168
202
  return (afterDependency) => {
169
203
  return fetchDependencyExcludeIgnore(dependency, afterDependency);
170
204
  };
@@ -175,19 +209,21 @@ const seedModel = function (data, done) {
175
209
  };
176
210
 
177
211
  // merge existing with seed data
178
- const mergeOne = (found, data, afterMergeOne) => {
212
+ const mergeOne = (found, $data, afterMergeOne) => {
179
213
  if (found) {
180
214
  const SEED_FRESH = getBoolean('SEED_FRESH', false);
181
215
  let updates = {};
182
216
  if (SEED_FRESH) {
183
- updates = mergeObjects(found.toObject(), data);
217
+ updates = mergeObjects(found.toObject(), $data);
184
218
  } else {
185
- updates = mergeObjects(data, found.toObject());
219
+ updates = mergeObjects($data, found.toObject());
186
220
  }
187
221
  found.set(updates);
222
+ // eslint-disable-next-line no-param-reassign
188
223
  found.updatedAt = new Date();
189
224
  } else {
190
- found = new this(data);
225
+ // eslint-disable-next-line no-param-reassign
226
+ found = new this($data);
191
227
  }
192
228
  return found.put ? found.put(afterMergeOne) : found.save(afterMergeOne);
193
229
  };
@@ -201,7 +237,8 @@ const seedModel = function (data, done) {
201
237
  if (error) {
202
238
  return next(error);
203
239
  }
204
- _.forEach(dependencies, (value, key) => {
240
+ forEach(dependencies, (value, key) => {
241
+ // eslint-disable-next-line no-param-reassign
205
242
  seed[key] = value;
206
243
  });
207
244
  return next();
@@ -215,48 +252,61 @@ const seedModel = function (data, done) {
215
252
  };
216
253
 
217
254
  // prepare seeds
218
- seeds = _.map(seeds, (seed) => {
255
+ seeds = map(seeds, (seed) => {
219
256
  return (next) => upsertOne(seed, next);
220
257
  });
221
258
 
222
259
  // run seeds
223
260
  return parallel(seeds, cb);
224
- };
225
-
226
- const clearAndSeedModel = function (data, done) {
227
- /* jshint validthis: true */
261
+ }
228
262
 
263
+ /**
264
+ * @function clearAndSeedModel
265
+ * @name clearAndSeedModel
266
+ * @description clear and seed the given model data
267
+ * @param {object|object[]|Function} data valid model data
268
+ * @param {Function} done callback to invoke on success or error
269
+ * @returns {object} seed results
270
+ * @since 0.21.0
271
+ * @version 0.2.0
272
+ * @private
273
+ */
274
+ function clearAndSeedModel(data, done) {
229
275
  // this: Model static context
230
276
 
231
277
  // normalize callback
232
- let cb = _.isFunction(data) ? data : done;
278
+ const cb = isFunction(data) ? data : done;
233
279
 
234
280
  // clear model data
235
281
  const doClear = (next) => this.deleteMany((error) => next(error));
236
282
 
237
283
  // seed model data
238
284
  const doSeed = (next) =>
239
- _.isFunction(data) ? this.seed(next) : this.seed(data, next);
285
+ isFunction(data) ? this.seed(next) : this.seed(data, next);
240
286
 
241
287
  // run clear then seed
242
288
  return waterfall([doClear, doSeed], cb);
243
- };
289
+ }
244
290
 
245
291
  /**
246
- * @function seed
247
- * @name seed
248
- * @description extend mongoose schema with seed capability
249
- * @param {Schema} schema valid mongoose schema instance
292
+ * @function seedPlugin
293
+ * @name seedPlugin
294
+ * @description Extend mongoose schema with seed capability
295
+ * @param {object} schema valid mongoose schema instance
250
296
  * @since 0.21.0
251
- * @version 0.1.0
297
+ * @version 0.2.0
298
+ * @public
252
299
  */
253
- module.exports = exports = (schema) => {
254
- const canNotSeed = !_.isFunction(schema.statics.seed);
300
+ export default function seedPlugin(schema) {
301
+ const canNotSeed = !isFunction(schema.statics.seed);
255
302
  if (canNotSeed) {
303
+ // eslint-disable-next-line no-param-reassign
256
304
  schema.statics.seed = seedModel;
305
+
306
+ // eslint-disable-next-line no-param-reassign
257
307
  schema.statics.clearAndSeed = clearAndSeedModel;
258
308
  }
259
- };
309
+ }
260
310
 
261
311
  // TODO: async prepareSeedCriteria
262
312
  // TODO: prepareSeedCriteria(seed, code)
package/.jsbeautifyrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "js": {
3
- "jslint_happy": true,
4
- "indent_size": 2,
5
- "wrap_line_length": 80
6
- }
7
- }