Skip to content

Commit

Permalink
feat: allow specifying error message override for duplicate key error…
Browse files Browse the repository at this point in the history
…s `unique: true`

Fix #12844
  • Loading branch information
vkarpov15 committed Nov 25, 2024
1 parent 7aba322 commit ff18818
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 7 deletions.
5 changes: 5 additions & 0 deletions lib/helpers/schema/getIndexes.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ module.exports = function getIndexes(schema) {
continue;
}

if (path._duplicateKeyErrorMessage != null) {
schema._duplicateKeyErrorMessagesByPath = schema._duplicateKeyErrorMessagesByPath || {};
schema._duplicateKeyErrorMessagesByPath[key] = path._duplicateKeyErrorMessage;
}

if (path.$isMongooseDocumentArray || path.$isSingleNested) {
if (get(path, 'options.excludeIndexes') !== true &&
get(path, 'schemaOptions.excludeIndexes') !== true &&
Expand Down
3 changes: 2 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ Model.prototype.$__handleSave = function(options, callback) {
Model.prototype.$__save = function(options, callback) {
this.$__handleSave(options, (error, result) => {
if (error) {
error = this.$__schema._transformDuplicateKeyError(error);
const hooks = this.$__schema.s.hooks;
return hooks.execPost('save:error', this, [this], { error: error }, (error) => {
callback(error, this);
Expand Down Expand Up @@ -3356,7 +3357,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
let error;
[res, error] = await this.$__collection.bulkWrite(validOps, options).
then(res => ([res, null])).
catch(err => ([null, err]));
catch(error => ([null, error]));

if (error) {
if (validationErrors.length > 0) {
Expand Down
2 changes: 2 additions & 0 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -4466,6 +4466,8 @@ Query.prototype.exec = async function exec(op) {
} else {
error = err;
}

error = this.model.schema._transformDuplicateKeyError(error);
}

res = await _executePostHooks(this, res, error);
Expand Down
28 changes: 28 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2799,6 +2799,34 @@ Schema.prototype._getPathType = function(path) {
return search(path.split('.'), _this);
};

/*!
*
*/

Schema.prototype._transformDuplicateKeyError = function _transformDuplicateKeyError(error) {
if (!this._duplicateKeyErrorMessagesByPath) {
return error;
}
if (error.code !== 11000 && error.code !== 11001) {
return error;
}

if (error.keyPattern != null) {
const keyPattern = error.keyPattern;
const keys = Object.keys(keyPattern);
if (keys.length !== 1) {
return error;
}
const firstKey = keys[0];
if (!this._duplicateKeyErrorMessagesByPath.hasOwnProperty(firstKey)) {
return error;
}
return new MongooseError(this._duplicateKeyErrorMessagesByPath[firstKey], { cause: error });
}

return error;
};

/*!
* ignore
*/
Expand Down
39 changes: 33 additions & 6 deletions lib/schemaType.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ function SchemaType(path, options, instance) {
this.options = new Options(options);
this._index = null;


if (utils.hasUserDefinedProperty(this.options, 'immutable')) {
this.$immutable = this.options.immutable;

Expand Down Expand Up @@ -447,21 +446,38 @@ SchemaType.prototype.index = function(options) {
*
* _NOTE: violating the constraint returns an `E11000` error from MongoDB when saving, not a Mongoose validation error._
*
* @param {Boolean} bool
* You can optionally specify an error message to replace MongoDB's default `E11000 duplicate key error` message.
* The following will throw a "Email must be unique" error if `save()`, `updateOne()`, `updateMany()`, `replaceOne()`,
* `findOneAndUpdate()`, or `findOneAndReplace()` throws a duplicate key error:
*
* ```javascript
* new Schema({
* email: {
* type: String,
* unique: [true, 'Email must be unique']
* }
* });
* ```
*
* Note that the above syntax does **not** work for `bulkWrite()` or `insertMany()`. `bulkWrite()` and `insertMany()`
* will still throw MongoDB's default `E11000 duplicate key error` message.
*
* @param {Boolean} value
* @param {String} message
* @return {SchemaType} this
* @api public
*/

SchemaType.prototype.unique = function(bool) {
SchemaType.prototype.unique = function unique(value, message) {
if (this._index === false) {
if (!bool) {
if (!value) {
return;
}
throw new Error('Path "' + this.path + '" may not have `index` set to ' +
'false and `unique` set to true');
}

if (!this.options.hasOwnProperty('index') && bool === false) {
if (!this.options.hasOwnProperty('index') && value === false) {
return this;
}

Expand All @@ -471,7 +487,10 @@ SchemaType.prototype.unique = function(bool) {
this._index = { type: this._index };
}

this._index.unique = bool;
this._index.unique = !!value;
if (typeof message === 'string') {
this._duplicateKeyErrorMessage = message;
}
return this;
};

Expand Down Expand Up @@ -1743,6 +1762,14 @@ SchemaType.prototype.getEmbeddedSchemaType = function getEmbeddedSchemaType() {
return this.$embeddedSchemaType;
};

/*!
* If _duplicateKeyErrorMessage is a string, replace unique index errors "E11000 duplicate key error" with this string.
*
* @api private
*/

SchemaType.prototype._duplicateKeyErrorMessage = null;

/*!
* Module exports.
*/
Expand Down
19 changes: 19 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14161,6 +14161,25 @@ describe('document', function() {
const fromDb = await ParentModel.findById(doc._id).orFail();
assert.strictEqual(fromDb.quests[0].campaign.milestones, null);
});

it('handles custom error message for duplicate key errors (gh-12844)', async function() {
const schema = new Schema({
name: String,
email: { type: String, unique: [true, 'Email must be unique'] }
});
const Model = db.model('Test', schema);
await Model.init();

await Model.create({ email: '[email protected]' });

let duplicateKeyError = await Model.create({ email: '[email protected]' }).catch(err => err);
assert.strictEqual(duplicateKeyError.message, 'Email must be unique');
assert.strictEqual(duplicateKeyError.cause.code, 11000);

duplicateKeyError = await Model.updateOne({ name: 'test' }, { email: '[email protected]' }, { upsert: true }).catch(err => err);
assert.strictEqual(duplicateKeyError.message, 'Email must be unique');
assert.strictEqual(duplicateKeyError.cause.code, 11000);
});
});

describe('Check if instance function that is supplied in schema option is available', function() {
Expand Down

0 comments on commit ff18818

Please sign in to comment.