Skip to content

Commit

Permalink
Merge branch '6.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Mar 21, 2023
2 parents f490627 + c240274 commit 28260a7
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 19 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
6.10.4 / 2023-03-21
===================
* fix(document): apply setters on resulting value when calling Document.prototype.$inc() #13178 #13158
* fix(model): add results property to unordered insertMany() to make it easy to identify exactly which documents were inserted #13163 #12791
* docs(guide+schematypes): add UUID to schematypes guide #13184

7.0.2 / 2023-03-15
==================
* fix: validate array elements when passing array path to validateSync() in pathsToValidate #13167 #13159
Expand Down
1 change: 1 addition & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The permitted SchemaTypes are:
* [Array](schematypes.html#arrays)
* [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
* [Map](schematypes.html#maps)
* [UUID](schematypes.html#uuid)

Read more about [SchemaTypes here](schematypes.html).

Expand Down
1 change: 0 additions & 1 deletion docs/layout.pug
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ html(lang='en')
.location #{job.location}
.button.jobs-view-more
a(href='/docs/jobs.html') View more jobs!


script(type="text/javascript" src="/docs/js/navbar-search.js")
script(type="text/javascript" src="/docs/js/mobile-navbar-toggle.js")
45 changes: 43 additions & 2 deletions docs/schematypes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<h2 id="schematypes"><a href="#schematypes">SchemaTypes</a></h2>

SchemaTypes handle definition of path
Expand Down Expand Up @@ -53,6 +54,7 @@ Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plug
- [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
- [Map](#maps)
- [Schema](#schemas)
- [UUID](#uuid)

<h4>Example</h4>

Expand Down Expand Up @@ -506,8 +508,6 @@ const Empty4 = new Schema({ any: [{}] });

<h4 id="maps">Maps</h4>

_New in Mongoose 5.1.0_

A `MongooseMap` is a subclass of [JavaScript's `Map` class](http://thecodebarbarian.com/the-80-20-guide-to-maps-in-javascript.html).
In these docs, we'll use the terms 'map' and `MongooseMap` interchangeably.
In Mongoose, maps are how you create a nested document with arbitrary keys.
Expand Down Expand Up @@ -592,6 +592,47 @@ on `socialMediaHandles.$*.oauth`:
const user = await User.findOne().populate('socialMediaHandles.$*.oauth');
```

<h4 id="uuid">UUID</h4>

Mongoose also supports a UUID type that stores UUID instances as [Node.js buffers](https://thecodebarbarian.com/an-overview-of-buffers-in-node-js.html).
We recommend using [ObjectIds](#objectids) rather than UUIDs for unique document ids in Mongoose, but you may use UUIDs if you need to.

In Node.js, a UUID is represented as an instance of `bson.Binary` type with a [getter](./tutorials/getters-setters.html) that converts the binary to a string when you access it.
Mongoose stores UUIDs as [binary data with subtype 4 in MongoDB](https://www.mongodb.com/docs/manual/reference/bson-types/#binary-data).

```javascript
const authorSchema = new Schema({
_id: Schema.Types.UUID, // Can also do `_id: 'UUID'`
name: String
});

const Author = mongoose.model('Author', authorSchema);

const bookSchema = new Schema({
authorId: { type: Schema.Types.UUID, ref: 'Author' }
});
const Book = mongoose.model('Book', bookSchema);

const author = new Author({ name: 'Martin Fowler' });
console.log(typeof author._id); // 'string'
console.log(author.toObject()._id instanceof mongoose.mongo.BSON.Binary); // true

const book = new Book({ authorId: '09190f70-3d30-11e5-8814-0f4df9a59c41' });
```

To create UUIDs, we recommend using [Node's built-in UUIDv4 generator](https://nodejs.org/api/crypto.html#cryptorandomuuidoptions).

```javascript
const { randomUUID } = require('crypto');

const schema = new mongoose.Schema({
docId: {
type: 'UUID',
default: () => randomUUID()
}
});
```

<h3 id="getters"><a href="#getters">Getters</a></h3>

Getters are like virtuals for paths defined in your schema. For example,
Expand Down
23 changes: 15 additions & 8 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const ValidationError = require('./error/validation');
const ValidatorError = require('./error/validator');
const VirtualType = require('./virtualtype');
const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren');
const castNumber = require('./cast/number');
const applyDefaults = require('./helpers/document/applyDefaults');
const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
const clone = require('./helpers/clone');
Expand Down Expand Up @@ -1747,18 +1746,26 @@ Document.prototype.$inc = function $inc(path, val) {
return this;
}

const currentValue = this.$__getValue(path) || 0;
let shouldSet = false;
let valToSet = null;
let valToInc = val;

try {
val = castNumber(val);
val = schemaType.cast(val);
valToSet = schemaType.applySetters(currentValue + val, this);
valToInc = valToSet - currentValue;
shouldSet = true;
} catch (err) {
this.invalidate(path, new MongooseError.CastError('number', val, path, err));
}

const currentValue = this.$__getValue(path) || 0;

this.$__.primitiveAtomics = this.$__.primitiveAtomics || {};
this.$__.primitiveAtomics[path] = { $inc: val };
this.markModified(path);
this.$__setValue(path, currentValue + val);
if (shouldSet) {
this.$__.primitiveAtomics = this.$__.primitiveAtomics || {};
this.$__.primitiveAtomics[path] = { $inc: valToInc };
this.markModified(path);
this.$__setValue(path, valToSet);
}

return this;
};
Expand Down
26 changes: 21 additions & 5 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2951,7 +2951,7 @@ Model.startSession = function() {
* @param {Array|Object|*} doc(s)
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany)
* @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`.
* @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` if this is an unordered `insertMany`.
* @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`.
* @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting.
* @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory.
* @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set.
Expand Down Expand Up @@ -3008,6 +3008,7 @@ Model.$__insertMany = function(arr, options, callback) {

const validationErrors = [];
const validationErrorsToOriginalOrder = new Map();
const results = ordered ? null : new Array(arr.length);
const toExecute = arr.map((doc, index) =>
callback => {
if (!(doc instanceof _this)) {
Expand All @@ -3033,8 +3034,8 @@ Model.$__insertMany = function(arr, options, callback) {
if (ordered === false) {
validationErrors.push(error);
validationErrorsToOriginalOrder.set(error, index);
callback(null, null);
return;
results[index] = error;
return callback(null, null);
}
callback(error);
}
Expand Down Expand Up @@ -3142,10 +3143,24 @@ Model.$__insertMany = function(arr, options, callback) {
const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index));

for (let i = 0; i < error.writeErrors.length; ++i) {
const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index);
error.writeErrors[i] = {
...error.writeErrors[i],
index: validDocIndexToOriginalIndex.get(error.writeErrors[i].index)
index: originalIndex
};
if (!ordered) {
results[originalIndex] = error.writeErrors[i];
}
}

if (!ordered) {
for (let i = 0; i < results.length; ++i) {
if (results[i] === void 0) {
results[i] = docs[i];
}
}

error.results = results;
}

let firstErroredIndex = -1;
Expand Down Expand Up @@ -3173,7 +3188,8 @@ Model.$__insertMany = function(arr, options, callback) {

if (rawResult && ordered === false) {
error.mongoose = {
validationErrors: validationErrors
validationErrors: validationErrors,
results: results
};
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"docs:clean:stable": "rimraf index.html && rimraf -rf ./docs/*.html && rimraf -rf ./docs/api && rimraf -rf ./docs/tutorials/*.html && rimraf -rf ./docs/typescript/*.html && rimraf -rf ./docs/*.html && rimraf -rf ./docs/source/_docs && rimraf -rf ./tmp",
"docs:clean:5x": "rimraf index.html && rimraf -rf ./docs/5.x && rimraf -rf ./docs/source/_docs && rimraf -rf ./tmp",
"docs:clean:6x": "rimraf index.html && rimraf -rf ./docs/6.x && rimraf -rf ./docs/source/_docs && rimraf -rf ./tmp",
"docs:copy:tmp": "mkdirp ./tmp/docs/css && mkdirp ./tmp/docs/js && mkdirp ./tmp/docs/images && mkdirp ./tmp/docs/tutorials && mkdirp ./tmp/docs/typescript && ncp ./docs/css ./tmp/docs/css --filter=.css$ && ncp ./docs/js ./tmp/docs/js --filter=.js$ && ncp ./docs/images ./tmp/docs/images && ncp ./docs/tutorials ./tmp/docs/tutorials && ncp ./docs/typescript ./tmp/docs/typescript && cp index.html ./tmp && cp docs/*.html ./tmp/docs/",
"docs:copy:tmp": "mkdirp ./tmp/docs/css && mkdirp ./tmp/docs/js && mkdirp ./tmp/docs/images && mkdirp ./tmp/docs/tutorials && mkdirp ./tmp/docs/typescript && mkdirp ./tmp/docs/api && ncp ./docs/css ./tmp/docs/css --filter=.css$ && ncp ./docs/js ./tmp/docs/js --filter=.js$ && ncp ./docs/images ./tmp/docs/images && ncp ./docs/tutorials ./tmp/docs/tutorials && ncp ./docs/typescript ./tmp/docs/typescript && ncp ./docs/api ./tmp/docs/api && cp index.html ./tmp && cp docs/*.html ./tmp/docs/",
"docs:copy:tmp:5x": "rimraf ./docs/5.x && ncp ./tmp ./docs/5.x",
"docs:copy:tmp:6x": "rimraf ./docs/6.x && ncp ./tmp ./docs/6.x",
"docs:checkout:gh-pages": "git checkout gh-pages",
Expand Down
45 changes: 45 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11482,6 +11482,51 @@ describe('document', function() {
assert.equal(res.counter, 2);
});

it('calls setters on the value passed to `$inc()` (gh-13158)', async function() {
const called = [];
const Test2 = db.model('Test2', Schema({
counter: {
type: Number,
set: v => { called.push(v); return v.toFixed(2); }
}
}));
const doc = await Test2.create({ counter: 2 });
assert.deepEqual(called, [2]);

doc.$inc('counter', 1.14159);
assert.deepEqual(called, [2, 3.14159]);
assert.equal(doc.counter, 3.14);
await doc.save();

const res = await Test2.findById(doc);
assert.equal(res.counter, 3.14);
});

it('avoids updating value if setter fails (gh-13158)', async function() {
const called = [];
const Test2 = db.model('Test2', Schema({
counter: {
type: Number,
set: v => {
called.push(v);
if (v > 3) {
throw new Error('Oops!');
}
return v;
}
}
}));
const doc = await Test2.create({ counter: 2 });
assert.deepEqual(called, [2]);

doc.$inc('counter', 3);
assert.deepEqual(called, [2, 5]);
assert.equal(doc.counter, 2);
const err = await doc.save().then(() => null, err => err);
assert.ok(err);
assert.ok(err.errors['counter']);
});

it('works as a $set if the document is new', async function() {
const doc = new Test({ counter: 0 });
doc.$inc('counter', 2);
Expand Down
16 changes: 16 additions & 0 deletions test/model.insertMany.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ describe('insertMany()', function() {
const error = await Movie.insertMany(arr, { ordered: false }).then(() => null, err => err);

assert.equal(error.message.indexOf('E11000'), 0);
assert.equal(error.results.length, 3);
assert.equal(error.results[0].name, 'Star Wars');
assert.ok(error.results[1].err);
assert.ok(error.results[1].err.errmsg.includes('E11000'));
assert.equal(error.results[2].name, 'The Empire Strikes Back');
const docs = await Movie.find({}).sort({ name: 1 }).exec();

assert.equal(docs.length, 2);
Expand Down Expand Up @@ -229,6 +234,11 @@ describe('insertMany()', function() {
assert.equal(err.insertedDocs[0].code, 'test');
assert.equal(err.insertedDocs[1].code, 'HARD');

assert.equal(err.results.length, 3);
assert.ok(err.results[0].err.errmsg.includes('E11000'));
assert.equal(err.results[1].code, 'test');
assert.equal(err.results[2].code, 'HARD');

await Question.deleteMany({});
await Question.create({ code: 'MEDIUM', text: '123' });
await Question.create({ code: 'HARD', text: '123' });
Expand Down Expand Up @@ -385,6 +395,12 @@ describe('insertMany()', function() {
assert.ok(!res.mongoose.validationErrors[0].errors['year']);
assert.ok(res.mongoose.validationErrors[1].errors['year']);
assert.ok(!res.mongoose.validationErrors[1].errors['name']);

assert.equal(res.mongoose.results.length, 3);
assert.ok(res.mongoose.results[0].errors['name']);
assert.ok(res.mongoose.results[1].errors['year']);
assert.ok(res.mongoose.results[2].$__);
assert.equal(res.mongoose.results[2].name, 'The Empire Strikes Back');
});

it('insertMany() validation error with ordered false and rawResult for mixed write and validation error (gh-12791)', async function() {
Expand Down
7 changes: 5 additions & 2 deletions test/types/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ async function insertManyTest() {
foo: string;
}

const TestSchema = new Schema<ITest & Document>({
const TestSchema = new Schema<ITest>({
foo: { type: String, required: true }
});

const Test = connection.model<ITest & Document>('Test', TestSchema);
const Test = connection.model<ITest>('Test', TestSchema);

Test.insertMany([{ foo: 'bar' }]).then(async res => {
res.length;
});

const res = await Test.insertMany([{ foo: 'bar' }], { rawResult: true });
expectType<ObjectId>(res.insertedIds[0]);

const res2 = await Test.insertMany([{ foo: 'bar' }], { ordered: false, rawResult: true });
expectAssignable<Error | Object | ReturnType<(typeof Test)['hydrate']>>(res2.mongoose.results[0]);
}

function gh10074() {
Expand Down
18 changes: 18 additions & 0 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,24 @@ declare module 'mongoose' {
docs: Array<DocContents | TRawDocType>,
options: InsertManyOptions & { lean: true; }
): Promise<Array<MergeType<MergeType<TRawDocType, DocContents>, Require_id<TRawDocType>>>>;
insertMany<DocContents = TRawDocType>(
doc: DocContents,
options: InsertManyOptions & { ordered: false; rawResult: true; }
): Promise<mongodb.InsertManyResult<TRawDocType> & {
mongoose: {
validationErrors: Error[];
results: Array<
Error |
Object |
HydratedDocument<
MergeType<
MergeType<TRawDocType, DocContents>,
Require_id<TRawDocType>
>
>
>
}
}>;
insertMany<DocContents = TRawDocType>(
docs: Array<DocContents | TRawDocType>,
options: InsertManyOptions & { rawResult: true; }
Expand Down

0 comments on commit 28260a7

Please sign in to comment.