@lykmapipo/mongoose-common 0.39.0 → 0.40.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.
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
- }