monastery 2.2.2 → 3.0.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.
package/lib/model.js CHANGED
@@ -1,13 +1,11 @@
1
- let crud = require('./model-crud.js')
2
- let rules = require('./rules.js')
3
- let util = require('./util.js')
4
- let validate = require('./model-validate.js')
1
+ const rules = require('./rules.js')
2
+ const util = require('./util.js')
5
3
 
6
- let Model = module.exports = function(name, opts, manager) {
4
+ function Model(name, opts, manager) {
7
5
  /**
8
- * Setup a model (aka monk collection)
6
+ * Setup a model
9
7
  * @param {string} name
10
- * @param {object} opts - see mongodb colleciton documentation
8
+ * @param {object} opts - see mongodb collection documentation
11
9
  * @param {boolean} opts.waitForIndexes
12
10
  * @return Promise(model) | this
13
11
  * @this model
@@ -112,26 +110,22 @@ let Model = module.exports = function(name, opts, manager) {
112
110
  this._setupFields(this.fields = Object.assign({}, this._defaultFields, this.fields))
113
111
  this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
114
112
 
115
- // Extend model with monk collection queries
116
- this._collection = manager.get? manager.get(name, { castIds: false }) : null
117
- if (!this._collection) {
118
- this.info('There is no mongodb connection, a lot of the monk/monastery methods will be unavailable')
119
- }
120
- for (let key in (this._collection || {})) {
121
- if (key.match(/^manager$|^options$|^_|^middlewares$|^name$/)) continue
122
- this['_' + key] = this._collection[key].bind(this._collection)
113
+ // Get collection, and extend model with collection methods
114
+ this.collection = this.manager.get(name, { castIds: false })
115
+ for (let key in Object.getPrototypeOf(this.collection||{})) {
116
+ this['_' + key] = this.collection[key].bind(this.collection)
123
117
  }
124
118
 
125
119
  // Add model to manager
126
- if (typeof this.manager[name] === 'undefined' || typeof this.manager.model[name] !== 'undefined') {
127
- this.manager[name] = this
128
- } else {
120
+ if (typeof this.manager[name] !== 'undefined' && typeof this.manager.models[name] === 'undefined') {
129
121
  this.warn(`Your model name '${name}' is conflicting with an builtin manager property, you are only able to
130
- access this model via \`db.model.${name}\``)
122
+ access this model via \`db.models.${name}\``)
123
+ } else {
124
+ this.manager[name] = this
131
125
  }
132
126
 
133
- // Add model to manager.model
134
- this.manager.model[name] = this
127
+ // Add model to manager.models
128
+ this.manager.models[name] = this
135
129
 
136
130
  // Setup/Ensure field indexes exist in MongoDB
137
131
  let errHandler = err => {
@@ -212,8 +206,8 @@ Model.prototype._setupFields = function(fields) {
212
206
 
213
207
  // Fields be an array
214
208
  } else if (util.isArray(field)) {
215
- let arrayDefault = this.manager.defaultObjects? () => [] : undefined
216
- let nullObject = this.manager.nullObjects
209
+ let arrayDefault = this.manager.opts.defaultObjects? () => [] : undefined
210
+ let nullObject = this.manager.opts.nullObjects
217
211
  let virtual = field.length == 1 && (field[0]||{}).virtual ? true : undefined
218
212
  field.schema = {
219
213
  type: 'array',
@@ -221,14 +215,14 @@ Model.prototype._setupFields = function(fields) {
221
215
  default: arrayDefault,
222
216
  nullObject: nullObject,
223
217
  virtual: virtual,
224
- ...(field.schema || {})
218
+ ...(field.schema || {}),
225
219
  }
226
220
  this._setupFields(field)
227
221
 
228
222
  // Fields can be a subdocument, e.g. user.pet = { name: {}, ..}
229
223
  } else if (util.isSubdocument(field)) {
230
- let objectDefault = this.manager.defaultObjects? () => ({}) : undefined
231
- let nullObject = this.manager.nullObjects
224
+ let objectDefault = this.manager.opts.defaultObjects? () => ({}) : undefined
225
+ let nullObject = this.manager.opts.nullObjects
232
226
  let index2dsphere = util.isSubdocument2dsphere(field)
233
227
  field.schema = field.schema || {}
234
228
  if (index2dsphere) {
@@ -240,21 +234,20 @@ Model.prototype._setupFields = function(fields) {
240
234
  isObject: true,
241
235
  default: objectDefault,
242
236
  nullObject: nullObject,
243
- ...field.schema
237
+ ...field.schema,
244
238
  }
245
239
  this._setupFields(field)
246
240
  }
247
241
  }, this)
248
242
  },
249
243
 
250
- Model.prototype._setupIndexes = function(fields, opts={}) {
244
+ Model.prototype._setupIndexes = async function(fields, opts={}) {
251
245
  /**
252
246
  * Creates indexes for the model (multikey, and sub-document supported)
247
+ * Note: the collection be created beforehand???
253
248
  * Note: only one text index per model(collection) is allowed due to mongodb limitations
254
- * @link https://docs.mongodb.com/manual/reference/command/createIndexes/
255
- * @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
256
249
  * @param {object} <fields>
257
- * @return Promise( {array} indexes ensured | {string} error )
250
+ * @return Promise( {array} indexes ensured ) || error
258
251
  *
259
252
  * MongoDB index structures = [
260
253
  * true = { name: 'name_1', key: { name: 1 } },
@@ -264,73 +257,61 @@ Model.prototype._setupIndexes = function(fields, opts={}) {
264
257
  * ..
265
258
  * ]
266
259
  */
267
- let collection
268
260
  let hasTextIndex = false
269
261
  let indexes = []
270
262
  let model = this
271
263
  let textIndex = { name: 'text', key: {} }
272
264
 
273
265
  // No db defined
274
- if (!(model.manager._state || '').match(/^open/)) {
275
- let error = {
276
- type: 'info',
277
- detail: `Skipping createIndex on the '${model.name}' model, no mongodb connection found.`
278
- }
279
- return Promise.reject(error)
266
+ if (!model.manager?._state?.match(/^open/)) {
267
+ throw new Error(`Skipping createIndex on the '${model.name||''}' model, no mongodb connection found.`)
280
268
  }
281
269
 
282
270
  // Find all indexes
283
271
  recurseFields(fields || model.fields, '')
284
272
  // console.log(2, indexes, fields)
285
273
  if (hasTextIndex) indexes.push(textIndex)
286
- if (opts.dryRun) return Promise.resolve(indexes || [])
287
- if (!indexes.length) return Promise.resolve([]) // No indexes defined
274
+ if (opts.dryRun) return indexes || []
275
+ if (!indexes.length) return [] // No indexes defined
288
276
 
289
277
  // Create indexes
290
- return (model.manager._state == 'open'? new Promise(res => res()) : model.manager)
291
- .then(data => {
292
- // Collection exist?
293
- collection = model.manager._db.collection(model.name)
294
- return model.manager._db.listCollections({ name: model.name }).toArray()
295
- })
296
- .then(collections => {
297
- // Get the collection's indexes
298
- if (collections.length > 0) return collection.indexes()
299
- else return Promise.resolve([])
300
- })
301
- .then(existingIndexes => {
302
- // Remove any existing text index that has different options as createIndexes will throws error about this
303
- let indexNames = []
304
- if (!existingIndexes.length) return new Promise(res => res())
305
- // console.log(0, textIndex)
306
- // console.log(1, existingIndexes, indexes)
307
- // Todo: Remove unused index names
278
+ // As of MongoDB 5 we no longer need to wait for the connection to be open
279
+ // await (model.manager._state == 'open' ? new Promise(res => res()) : model.manager).then()....
280
+
281
+ // Get collection
282
+ const collection = this.collection
283
+
284
+ // Get the collections indexes
285
+ const existingIndexes = await collection.indexes() // returns [] if collection doesn't exist
308
286
 
309
- for (let existingIndex of existingIndexes) {
310
- if (!existingIndex.textIndexVersion) continue
311
- for (let index of indexes) {
312
- let fieldsInTextIndex1 = Object.keys(existingIndex.weights).sort().join()
313
- let fieldsInTextIndex2 = Object.keys(index.key).sort().join()
314
- if (existingIndex.name == index.name && fieldsInTextIndex1 !== fieldsInTextIndex2) {
315
- model.info(`Text index options are different for '${existingIndex.name}', removing old text index`)
316
- indexNames.push(existingIndex.name)
317
- break
287
+ // Remove any existing text index that has different options as createIndexes will throws error about this
288
+ if (existingIndexes.length) {
289
+ // console.log(0, textIndex)
290
+ // console.log(1, existingIndexes, indexes)
291
+ // Todo: Remove unused index names
292
+ const textIndexNames = []
293
+ for (let existingIndex of existingIndexes) {
294
+ if (!existingIndex.textIndexVersion) continue
295
+ for (let index of indexes) {
296
+ let fieldsInTextIndex1 = Object.keys(existingIndex.weights).sort().join()
297
+ let fieldsInTextIndex2 = Object.keys(index.key).sort().join()
298
+ if (existingIndex.name == index.name && fieldsInTextIndex1 !== fieldsInTextIndex2) {
299
+ model.info(`Text index options are different for '${existingIndex.name}', removing old text index`)
300
+ if (!textIndexNames.includes(existingIndex.name)) {
301
+ textIndexNames.push(existingIndex.name)
318
302
  }
319
303
  }
320
304
  }
305
+ }
306
+ for (let name of textIndexNames) {
307
+ await collection.dropIndex(name)
308
+ }
309
+ }
321
310
 
322
- if (indexNames.length > 1) return collection.dropIndex(indexNames)
323
- else if (indexNames.length) return collection.dropIndex(indexNames[0])
324
- else return new Promise(res => res())
325
- })
326
- .then(() => {
327
- // Ensure/create indexes
328
- return collection.createIndexes(indexes)
329
- })
330
- .then(response => {
331
- model.info('db index(s) created for ' + model.name)
332
- return indexes
333
- })
311
+ // create indexes
312
+ await collection.createIndexes(indexes)
313
+ model.info('db index(s) created for ' + model.name)
314
+ return indexes
334
315
 
335
316
  function recurseFields(fields, parentPath) {
336
317
  util.forEach(fields, (field, name) => {
@@ -362,29 +343,27 @@ Model.prototype._setupIndexes = function(fields, opts={}) {
362
343
  Model.prototype._defaultFields = {
363
344
  _id: {
364
345
  insertOnly: true,
365
- type: 'id'
346
+ type: 'id',
366
347
  },
367
348
  createdAt: {
368
349
  default: function(fieldName, model) {
369
- return model.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000)
350
+ return model.manager.opts.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000)
370
351
  },
371
352
  insertOnly: true,
372
353
  timestampField: true,
373
- type: 'integer'
354
+ type: 'integer',
374
355
  },
375
356
  updatedAt: {
376
357
  default: function(fieldName, model) {
377
- return model.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000)
358
+ return model.manager.opts.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000)
378
359
  },
379
360
  timestampField: true,
380
- type: 'integer'
381
- }
361
+ type: 'integer',
362
+ },
382
363
  }
383
364
 
384
- for (let key in crud) {
385
- Model.prototype[key] = crud[key]
386
- }
365
+ module.exports = Model
387
366
 
388
- for (let key in validate) {
389
- Model.prototype[key] = validate[key]
390
- }
367
+ // Extend Model prototype
368
+ require('./model-crud.js')
369
+ require('./model-validate.js')
package/lib/rules.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Todo: remove stringnums in date/number/integer rules
2
- let ObjectId = require('mongodb').ObjectId
3
- let util = require('./util.js')
2
+ const ObjectId = require('mongodb').ObjectId
3
+ const util = require('./util.js')
4
+ const validator = require('validator')
4
5
 
5
6
  module.exports = {
6
7
 
@@ -12,7 +13,7 @@ module.exports = {
12
13
  fn: function(x) {
13
14
  if (util.isArray(x) && !x.length) return false
14
15
  return x || x === 0 || x === false? true : false
15
- }
16
+ },
16
17
  },
17
18
 
18
19
  // "Type" rules below ignore undefined and null
@@ -28,7 +29,7 @@ module.exports = {
28
29
  },
29
30
  fn: function(x) {
30
31
  return typeof x === 'boolean'
31
- }
32
+ },
32
33
  },
33
34
  isArray: {
34
35
  validateEmptyString: true,
@@ -41,7 +42,7 @@ module.exports = {
41
42
  },
42
43
  fn: function(x) {
43
44
  return Array.isArray(x)
44
- }
45
+ },
45
46
  },
46
47
  isDate: {
47
48
  validateEmptyString: true,
@@ -54,7 +55,7 @@ module.exports = {
54
55
  fn: function(x) {
55
56
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
56
57
  return typeof x === 'number'
57
- }
58
+ },
58
59
  },
59
60
  isImageObject: {
60
61
  validateEmptyString: true,
@@ -70,7 +71,7 @@ module.exports = {
70
71
  fn: function(x) {
71
72
  let isObject = x !== null && typeof x === 'object' && !(x instanceof Array)
72
73
  if (isObject && x.bucket && x.date && x.filename && typeof x.filesize != 'undefined' && x.path && x.uid) return true
73
- }
74
+ },
74
75
  },
75
76
  isInteger: {
76
77
  validateEmptyString: true,
@@ -83,7 +84,7 @@ module.exports = {
83
84
  fn: function(x) {
84
85
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
85
86
  return typeof x === 'number' && (parseInt(x) === x)
86
- }
87
+ },
87
88
  },
88
89
  isNumber: {
89
90
  validateEmptyString: true,
@@ -96,7 +97,7 @@ module.exports = {
96
97
  fn: function(x) {
97
98
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
98
99
  return typeof x === 'number'
99
- }
100
+ },
100
101
  },
101
102
  isObject: {
102
103
  validateEmptyString: true,
@@ -109,7 +110,7 @@ module.exports = {
109
110
  },
110
111
  fn: function(x) {
111
112
  return x !== null && typeof x === 'object' && !(x instanceof Array)
112
- }
113
+ },
113
114
  },
114
115
  isString: {
115
116
  validateEmptyString: true,
@@ -120,14 +121,14 @@ module.exports = {
120
121
  },
121
122
  fn: function(x) {
122
123
  return typeof x === 'string'
123
- }
124
+ },
124
125
  },
125
126
  isAny: {
126
127
  validateEmptyString: true,
127
128
  message: '',
128
129
  fn: function(x) {
129
130
  return true
130
- }
131
+ },
131
132
  },
132
133
  isId: {
133
134
  validateEmptyString: true,
@@ -135,14 +136,14 @@ module.exports = {
135
136
  tryParse: function(x) {
136
137
  // Try and parse value to a mongodb ObjectId
137
138
  if (x === '') return null
138
- if (util.isString(x) && ObjectId.isValid(x)) return ObjectId(x)
139
+ if (util.isString(x) && ObjectId.isValid(x)) return new ObjectId(x)
139
140
  else return x
140
141
  },
141
142
  fn: function(x) {
142
143
  // Must be a valid mongodb ObjectId
143
144
  if (x === null) return true
144
145
  return util.isObject(x) && ObjectId.isValid(x)/*x.get_inc*/? true : false
145
- }
146
+ },
146
147
  },
147
148
 
148
149
  /* "Number" rules below ignore undefined and null */
@@ -152,14 +153,14 @@ module.exports = {
152
153
  fn: function(x, arg) {
153
154
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
154
155
  return x <= arg
155
- }
156
+ },
156
157
  },
157
158
  min: {
158
159
  message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
159
160
  fn: function(x, arg) {
160
161
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
161
162
  return x >= arg
162
- }
163
+ },
163
164
  },
164
165
 
165
166
  /* "String" rules below ignore undefined, null, and empty strings */
@@ -170,54 +171,54 @@ module.exports = {
170
171
  for (let item of arg) {
171
172
  if (x === item + '') return true
172
173
  }
173
- }
174
+ },
174
175
  },
175
176
  isAfter: {
176
177
  message: (x, arg) => 'Value was before the configured time (' + arg + ')',
177
- fn: function(x, arg) { return require('validator').isAfter(x, arg) }
178
+ fn: function(x, arg) { return validator.isAfter(x, arg) },
178
179
  },
179
180
  isBefore: {
180
181
  message: (x, arg) => 'Value was after the configured time (' + arg + ')',
181
- fn: function(x, arg) { return require('validator').isBefore(x, arg) }
182
+ fn: function(x, arg) { return validator.isBefore(x, arg) },
182
183
  },
183
184
  isCreditCard: {
184
185
  message: 'Value was not a valid credit card.',
185
- fn: function(x, arg) { return require('validator').isCreditCard(x, arg) }
186
+ fn: function(x, arg) { return validator.isCreditCard(x, arg) },
186
187
  },
187
188
  isEmail: {
188
189
  message: 'Please enter a valid email address.',
189
- fn: function(x, arg) { return require('validator').isEmail(x, arg) }
190
+ fn: function(x, arg) { return validator.isEmail(x, arg) },
190
191
  },
191
192
  isHexColor: {
192
193
  message: 'Value was not a valid hex color.',
193
- fn: function(x, arg) { return require('validator').isHexColor(x, arg) }
194
+ fn: function(x, arg) { return validator.isHexColor(x, arg) },
194
195
  },
195
196
  isIn: {
196
197
  message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
197
- fn: function(x, arg) { return require('validator').isIn(x, arg) }
198
+ fn: function(x, arg) { return validator.isIn(x, arg) },
198
199
  },
199
200
  isIP: {
200
201
  message: 'Value was not a valid IP address.',
201
- fn: function(x, arg) { return require('validator').isIP(x, arg) }
202
+ fn: function(x, arg) { return validator.isIP(x, arg) },
202
203
  },
203
204
  isNotEmptyString: {
204
205
  validateEmptyString: true,
205
206
  message: 'Value was an empty string.',
206
207
  fn: function(x) {
207
208
  return x !== ''
208
- }
209
+ },
209
210
  },
210
211
  isNotIn: {
211
212
  message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
212
- fn: function(x, arg) { return !require('validator').isIn(x, arg) }
213
+ fn: function(x, arg) { return !validator.isIn(x, arg) },
213
214
  },
214
215
  isURL: {
215
216
  message: 'Value was not a valid URL.',
216
- fn: function(x, arg) { return require('validator').isURL(x, arg === true? undefined : arg) }
217
+ fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) },
217
218
  },
218
219
  isUUID: {
219
220
  message: 'Value was not a valid UUID.',
220
- fn: function(x, arg) { return require('validator').isUUID(x) }
221
+ fn: function(x, arg) { return validator.isUUID(x) },
221
222
  },
222
223
  minLength: {
223
224
  message: function(x, arg) {
@@ -226,9 +227,9 @@ module.exports = {
226
227
  },
227
228
  fn: function(x, arg) {
228
229
  if (typeof x !== 'string' && !util.isArray(x)) throw new Error ('Value was not a string or an array.')
229
- else if (typeof x === 'string') return require('validator').isLength(x, arg)
230
+ else if (typeof x === 'string') return validator.isLength(x, arg)
230
231
  else return x.length >= arg
231
- }
232
+ },
232
233
  },
233
234
  maxLength: {
234
235
  message: function(x, arg) {
@@ -237,16 +238,16 @@ module.exports = {
237
238
  },
238
239
  fn: function(x, arg) {
239
240
  if (typeof x !== 'string' && !util.isArray(x)) throw new Error ('Value was not a string or an array.')
240
- else if (typeof x === 'string') return require('validator').isLength(x, 0, arg)
241
+ else if (typeof x === 'string') return validator.isLength(x, 0, arg)
241
242
  else return x.length <= arg
242
- }
243
+ },
243
244
  },
244
245
  regex: {
245
246
  message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
246
247
  fn: function(x, arg) {
247
- if (util.isRegex(arg)) return require('validator').matches(x, arg)
248
+ if (util.isRegex(arg)) return validator.matches(x, arg)
248
249
  throw new Error('This rule expects a regular expression to be configured, but instead got the '
249
250
  + typeof arg + ' `' + arg + '`.')
250
- }
251
- }
251
+ },
252
+ },
252
253
  }
package/lib/util.js CHANGED
@@ -1,14 +1,41 @@
1
- let ObjectId = require('mongodb').ObjectId
1
+ const { ObjectId } = require('mongodb')
2
2
 
3
3
  module.exports = {
4
4
 
5
+ cast: function(obj) {
6
+ /**
7
+ * Applies ObjectId casting to _id fields.
8
+ * @param {Object} optional, query
9
+ * @return {Object} query
10
+ * @private
11
+ */
12
+ if (this.isArray(obj)) {
13
+ return obj.map(this.cast.bind(this))
14
+ }
15
+ if (obj && typeof obj === 'object') {
16
+ for (let k of Object.keys(obj)) {
17
+ if (k == '_id' && obj._id) {
18
+ if (obj._id.$in) obj._id.$in = obj._id.$in.map(this.id.bind(this))
19
+ else if (obj._id.$nin) obj._id.$nin = obj._id.$nin.map(this.id.bind(this))
20
+ else if (obj._id.$ne) obj._id.$ne = this.id(obj._id.$ne)
21
+ else obj._id = this.id(obj._id)
22
+ } else {
23
+ obj[k] = this.cast(obj[k])
24
+ }
25
+ }
26
+ }
27
+
28
+ return obj
29
+ },
30
+
5
31
  deepCopy: function(obj) {
6
32
  // Deep clones an object
33
+ // v3.0.0 - MongoIds now remain as objects, not strings.
7
34
  if (!obj) return obj
8
35
  let obj2 = Array.isArray(obj)? [] : {}
9
36
  for (let key in obj) {
10
37
  let v = obj[key]
11
- if (this.isId(v)) obj2[key] = v.toString()
38
+ if (this.isId(v)) obj2[key] = v//.toString()
12
39
  else obj2[key] = (typeof v === 'object')? this.deepCopy(v) : v
13
40
  }
14
41
  return obj2
@@ -31,6 +58,16 @@ module.exports = {
31
58
  forceArray: function(value) {
32
59
  return this.isArray(value)? value : [value]
33
60
  },
61
+
62
+ id: function(str) {
63
+ /**
64
+ * Casts to ObjectId
65
+ * @param {string|ObjectId} str - string = hex string
66
+ * @return {ObjectId}
67
+ */
68
+ if (str == null) return new ObjectId()
69
+ return typeof str === 'string' ? ObjectId.createFromHexString(str) : str
70
+ },
34
71
 
35
72
  inArray: (array, key, value) => {
36
73
  /**
@@ -72,8 +109,8 @@ module.exports = {
72
109
 
73
110
  isId: function(value) {
74
111
  // Called from db.isId
75
- // True if value is a MongoDB ObjectID() or a valid id string
76
- if (this.isObject(value) && value.get_inc) return true
112
+ // True if value is a MongoDB ObjectId() or a valid id string
113
+ if (this.isObject(value) && ObjectId.isValid(value)) return true
77
114
  else if (value && this.isString(value) && ObjectId.isValid(value)) return true
78
115
  else return false
79
116
  },
@@ -91,8 +128,8 @@ module.exports = {
91
128
  },
92
129
 
93
130
  isObjectAndNotID: function(value) {
94
- // A true object that is not an id, i.e. mongodb ObjectID()
95
- return this.isObject(value) && !value.get_inc
131
+ // A true object that is not an id, i.e. mongodb ObjectId()
132
+ return this.isObject(value) && !ObjectId.isValid(value) // was value.get_inc
96
133
  },
97
134
 
98
135
  isRegex: function(value) {
@@ -280,15 +317,18 @@ module.exports = {
280
317
  * @return promise
281
318
  * @source https://github.com/feross/run-series
282
319
  */
283
- var current = 0
284
- var results = []
285
- var isSync = true
320
+ let current = 0
321
+ let results = []
322
+ let isSync = true
286
323
 
287
324
  return new Promise((res, rej) => {
288
- function each(err, result) {
325
+ function each(i, err, result) { // aka next(err, data)
326
+ if (i == current + 1) {
327
+ throw new Error('Hook error: you cannot return a promise and call next()')
328
+ }
289
329
  results.push(result)
290
- if (++current >= tasks.length || err) done(err)
291
- else tasks[current](each)
330
+ if (!err && ++current < tasks.length) callTask(current)
331
+ else done(err)
292
332
  }
293
333
  function done(err) {
294
334
  if (isSync) process.nextTick(() => end(err))
@@ -299,8 +339,18 @@ module.exports = {
299
339
  if (err) rej(err)
300
340
  else res(results)
301
341
  }
302
- if (tasks.length) tasks[0](each)
303
- else done(null)
342
+ function callTask(i) {
343
+ const each2 = each.bind(null, i)
344
+ const res = tasks[i](each2)
345
+ if (res instanceof Promise) {
346
+ res.then((result) => each2(null, result)).catch(each2)
347
+ }
348
+ }
349
+
350
+ // Start
351
+ if (!tasks.length) done(null)
352
+ else callTask(current)
353
+
304
354
  isSync = false
305
355
  })
306
356
  },
@@ -356,6 +406,10 @@ module.exports = {
356
406
  ucFirst: function(string) {
357
407
  if (!string) return ''
358
408
  return string.charAt(0).toUpperCase() + string.slice(1)
359
- }
409
+ },
360
410
 
411
+ wait: async function(ms) {
412
+ return new Promise(res => setTimeout(res, ms))
413
+ },
414
+
361
415
  }