-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
163 lines (133 loc) · 7.38 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
'use strict';
const each = require('lodash.foreach');
const get = require('lodash.get');
const merge = require('lodash.merge');
// Function typecheck helper
const isFunc = (val) => typeof val === 'function';
const deepPath = function(schema, pathName) {
let path;
const paths = pathName.split('.');
if (paths.length > 1) {
pathName = paths.shift();
}
if (isFunc(schema.path)) {
path = schema.path(pathName);
}
if (path && path.schema) {
path = deepPath(path.schema, paths.join('.'));
}
return path;
};
const plugin = function(schema, options) {
options = options || {};
const type = options.type || plugin.defaults.type || 'unique';
const message = options.message || plugin.defaults.message || 'Error, expected `{PATH}` to be unique. Value: `{VALUE}`';
// Mongoose Schema objects don't describe default _id indexes
// https://github.com/Automattic/mongoose/issues/5998
const indexes = [[{ _id: 1 }, { unique: true }]].concat(schema.indexes());
// Dynamically iterate all indexes
each(indexes, (index) => {
const indexOptions = index[1];
if (indexOptions.unique) {
const paths = Object.keys(index[0]);
each(paths, (pathName) => {
// Choose error message
const pathMessage = typeof indexOptions.unique === 'string' ? indexOptions.unique : message;
// Obtain the correct path object
const path = deepPath(schema, pathName) || schema.path(pathName);
if (path) {
// Add an async validator
path.validate(function() {
return new Promise((resolve, reject) => {
const isQuery = this.constructor.name === 'Query';
const conditions = {};
let model;
if (isQuery) {
// If the doc is a query, this is a findAndUpdate.
each(paths, (name) => {
let pathValue = get(this, '_update.' + name) || get(this, '_update.$set.' + name);
// Wrap with case-insensitivity
if (get(path, 'options.uniqueCaseInsensitive') || indexOptions.uniqueCaseInsensitive) {
// Escape RegExp chars
pathValue = pathValue.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
pathValue = new RegExp('^' + pathValue + '$', 'i');
}
conditions[name] = pathValue;
});
// Use conditions the user has with find*AndUpdate
each(this._conditions, (value, key) => {
conditions[key] = { $ne: value };
});
model = this.model;
} else {
const parentDoc = this.parent();
const isNew = parentDoc.isNew;
if (!isNew && !parentDoc.isModified(pathName)) {
return resolve(true);
}
// https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths
const isSubdocument = this._id !== parentDoc._id;
const isNestedPath = isSubdocument ? false : pathName.split('.').length > 1;
each(paths, (name) => {
let pathValue;
if (isSubdocument) {
pathValue = get(this, name.split('.').pop());
} else if (isNestedPath) {
const keys = name.split('.');
pathValue = get(this, keys[0]);
for (let i = 1; i < keys.length; i++) {
const key = keys[i];
pathValue = get(pathValue, key);
}
} else {
pathValue = get(this, name);
}
// Wrap with case-insensitivity
if (get(path, 'options.uniqueCaseInsensitive') || indexOptions.uniqueCaseInsensitive) {
// Escape RegExp chars
pathValue = pathValue.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
pathValue = new RegExp('^' + pathValue + '$', 'i');
}
conditions[name] = pathValue;
});
if (!isNew && this._id) {
conditions._id = { $ne: this._id };
}
// Obtain the model depending on context
// https://github.com/Automattic/mongoose/issues/3430
// https://github.com/Automattic/mongoose/issues/3589
if (isSubdocument) {
model = this.ownerDocument().model(this.ownerDocument().constructor.modelName);
} else if (isFunc(this.model)) {
model = this.model(this.constructor.modelName);
} else {
model = this.constructor.model(this.constructor.modelName);
}
}
if (indexOptions.partialFilterExpression) {
merge(conditions, indexOptions.partialFilterExpression);
}
// Is this model a discriminator and the unique index is on the whole collection,
// not just the instances of the discriminator? If so, use the base model to query.
// https://github.com/Automattic/mongoose/issues/4965
// eslint-disable-next-line
if (model.baseModelName && (indexOptions.partialFilterExpression === null || indexOptions.partialFilterExpression === undefined)) {
model = model.db.model(model.baseModelName);
}
model.find(conditions).countDocuments()
.then((count) => {
resolve(count === 0);
})
.catch((err) => {
reject(err);
});
});
}, pathMessage, type);
}
});
}
});
};
plugin.defaults = {};
// Export the mongoose plugin
module.exports = plugin;