Skip to content

Commit

Permalink
Merge branch '6.7' into vkarpov15/gh-12368
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Oct 3, 2022
2 parents ba81950 + 964912f commit bfa835f
Show file tree
Hide file tree
Showing 17 changed files with 285 additions and 124 deletions.
29 changes: 12 additions & 17 deletions .github/workflows/test.yml
Expand Up @@ -41,14 +41,10 @@ jobs:
matrix:
node: [12, 14, 16, 18]
os: [ubuntu-18.04, ubuntu-20.04]
mongodb: [4.0.2, 5.0.2, 6.0.0]
mongodb: [4.0.28, 5.0.8, 6.0.0]
include:
- os: ubuntu-18.04
mongodb-os: ubuntu1804
- os: ubuntu-20.04
mongodb-os: ubuntu2004
- os: ubuntu-20.04 # customize on which matrix the coverage will be collected on
mongodb: 5.0.2
mongodb: 5.0.11
node: 16
coverage: true
exclude:
Expand All @@ -59,6 +55,9 @@ jobs:
- os: ubuntu-20.04 # exclude because there are no <4.4 mongodb builds for 2004
mongodb: 4.0.2
name: Node ${{ matrix.node }} MongoDB ${{ matrix.mongodb }} OS ${{ matrix.os }}
env:
MONGOMS_VERSION: ${{ matrix.mongodb }}
MONGOMS_PREFER_GLOBAL_PATH: 1
steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2

Expand All @@ -67,18 +66,14 @@ jobs:
with:
node-version: ${{ matrix.node }}

- run: npm install
- name: Load MongoDB binary cache
id: cache-mongodb-binaries
uses: actions/cache@v3
with:
path: ~/.cache/mongodb-binaries
key: ${{ matrix.os }}-${{ matrix.mongodb }}

- name: Setup
run: |
wget -q https://downloads.mongodb.org/linux/mongodb-linux-x86_64-${{ matrix.mongodb-os }}-${{ matrix.mongodb }}.tgz
tar xf mongodb-linux-x86_64-${{ matrix.mongodb-os }}-${{ matrix.mongodb }}.tgz
mkdir -p ./data/db/27017 ./data/db/27000
printf "\ntimeout: 8000" >> ./.mocharc.yml
./mongodb-linux-x86_64-${{ matrix.mongodb-os }}-${{ matrix.mongodb }}/bin/mongod --setParameter ttlMonitorSleepSecs=1 --fork --dbpath ./data/db/27017 --syslog --port 27017
sleep 2
mongod --version
echo `pwd`/mongodb-linux-x86_64-${{ matrix.mongodb-os }}-${{ matrix.mongodb }}/bin >> $GITHUB_PATH
- run: npm install
- name: NPM Test without Coverage
run: npm test
if: matrix.coverage != true
Expand Down
2 changes: 2 additions & 0 deletions .mocharc.yml
@@ -1,2 +1,4 @@
reporter: spec # better to identify failing / slow tests than "dot"
ui: bdd # explicitly setting, even though it is mocha default
require:
- test/mocha-fixtures.js
10 changes: 10 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,13 @@
6.6.4 / 2022-10-03
==================
* fix(model): avoid saving applied defaults if path is deselected #12506 #12414
* fix(types): correct DocType for auto typed query helpers #12342
* fix(types): avoid "excessively deep" type instantiation error when using bulkWrite() with type that extends from document #12277
* fix(types): avoid relying on typeof this, which isn't supported in TypeScript < 4.4 #12375
* docs(schema): correct example for Schema.prototype.discriminator() #12493
* docs(typescript): clean up query helpers examples #12342
* chore: use mongodb-memory-server for testing #12262 [hasezoey](https://github.com/hasezoey)

6.6.3 / 2022-09-30
==================
* fix(query): treat findOne(_id) as equivalent to findOne({ _id }) #12485 #12325
Expand Down
70 changes: 40 additions & 30 deletions docs/typescript/query-helpers.md
Expand Up @@ -3,6 +3,8 @@
[Query helpers](http://thecodebarbarian.com/mongoose-custom-query-methods.html) let you define custom helper methods on Mongoose queries.
Query helpers make queries more semantic using chaining syntax.

The following is an example of how query helpers work in JavaScript.

```javascript
ProjectSchema.query.byName = function(name) {
return this.find({ name: name });
Expand All @@ -14,9 +16,9 @@ var Project = mongoose.model('Project', ProjectSchema);
Project.find().where('stars').gt(1000).byName('mongoose');
```

In TypeScript, Mongoose does support manually typed and automatically typed Query Helpers.
## Manually Typed Query Helpers

1- Manually typed:
In TypeScript, you can define query helpers using a separate query helpers interface.
Mongoose's `Model` takes 3 generic parameters:

1. The `DocType`
Expand All @@ -30,23 +32,34 @@ Below is an example of creating a `ProjectModel` with a `byName` query helper.
import { HydratedDocument, Model, Query, Schema, model } from 'mongoose';

interface Project {
name: string;
stars: number;
name?: string;
stars?: number;
}

type ProjectModelType = Model<Project, ProjectQueryHelpers>;
// Query helpers should return `Query<any, Document<DocType>> & ProjectQueryHelpers`
// to enable chaining.
type ProjectModelQuery = Query<any, HydratedDocument<Project>, ProjectQueryHelpers> & ProjectQueryHelpers;
interface ProjectQueryHelpers {
byName(this: ProjectModelQuery, name: string): ProjectModelQuery;
byName(name: string): QueryWithHelpers<
HydratedDocument<Project>[],
HydratedDocument<Project>,
ProjectQueryHelpers
>
}

const schema = new Schema<Project, ProjectModelType, {}, ProjectQueryHelpers>({
name: { type: String, required: true },
stars: { type: Number, required: true }
type ProjectModelType = Model<Project, ProjectQueryHelpers>;

const ProjectSchema = new Schema<
Project,
Model<Project, ProjectQueryHelpers>,
{},
ProjectQueryHelpers
>({
name: String,
stars: Number
});
schema.query.byName = function(name: string): ProjectModelQuery {

ProjectSchema.query.byName = function byName(
this: QueryWithHelpers<any, HydratedDocument<Project>, ProjectQueryHelpers>,
name: string
) {
return this.find({ name: name });
};

Expand All @@ -63,30 +76,27 @@ async function run(): Promise<void> {
}
```

2- Automatically typed:
## Auto Typed Query Helpers

Mongoose does support auto typed Query Helpers that it are supplied in schema options.
Query Helpers functions can be defined as following:

```typescript
import { Schema, model } from 'mongoose';

const schema = new Schema({
name: { type: String, required: true },
stars: { type: Number, required: true }
}, {
query: {
byName(name) {
return this.find({ name: name });
}
const ProjectSchema = new Schema({
name: String,
stars: Number
}, {
query: {
byName(name: string) {
return this.find({ name });
}
});
}
});

const ProjectModel = model(
'Project',
schema
);
const ProjectModel = model('Project', ProjectSchema);

// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
}
// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
```
4 changes: 4 additions & 0 deletions lib/helpers/projection/isPathExcluded.js
Expand Up @@ -12,6 +12,10 @@ const isDefiningProjection = require('./isDefiningProjection');
*/

module.exports = function isPathExcluded(projection, path) {
if (projection == null) {
return false;
}

if (path === '_id') {
return projection._id === 0;
}
Expand Down
21 changes: 20 additions & 1 deletion lib/model.js
Expand Up @@ -51,11 +51,13 @@ const {
getRelatedDBIndexes,
getRelatedSchemaIndexes
} = require('./helpers/indexes/getRelatedIndexes');
const isPathExcluded = require('./helpers/projection/isPathExcluded');
const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions');
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
const leanPopulateMap = require('./helpers/populate/leanPopulateMap');
const modifiedPaths = require('./helpers/update/modifiedPaths');
const parallelLimit = require('./helpers/parallelLimit');
const parentPaths = require('./helpers/path/parentPaths');
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
Expand Down Expand Up @@ -760,6 +762,19 @@ Model.prototype.$__delta = function() {
}
}

// If this path is set to default, and either this path or one of
// its parents is excluded, don't treat this path as dirty.
if (this.$isDefault(data.path) && this.$__.selected) {
if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) {
continue;
}

const pathsToCheck = parentPaths(data.path);
if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) {
continue;
}
}

if (divergent.length) continue;
if (value === undefined) {
operand(this, where, delta, data, 1, '$unset');
Expand Down Expand Up @@ -798,6 +813,11 @@ Model.prototype.$__delta = function() {
if (this.$__.version) {
this.$__version(where, delta);
}

if (Object.keys(delta).length === 0) {
return [where, null];
}

return [where, delta];
};

Expand Down Expand Up @@ -1196,7 +1216,6 @@ Model.exists = function exists(filter, options, callback) {
*/

Model.discriminator = function(name, schema, options) {

let model;
if (typeof name === 'function') {
model = name;
Expand Down
22 changes: 9 additions & 13 deletions lib/schema.js
Expand Up @@ -507,25 +507,21 @@ Schema.prototype.defaultOptions = function(options) {
*
* #### Example:
*
* const options = { discriminatorKey: 'kind' };
* const eventSchema = new mongoose.Schema({ timestamp: Date }, { discriminatorKey: 'kind' });
*
* const clickedEventSchema = new mongoose.Schema({ element: String }, { discriminatorKey: 'kind' });
* const ClickedModel = eventSchema.discriminator('clicked', clickedEventSchema);
*
* const eventSchema = new mongoose.Schema({ time: Date }, options);
* const Event = mongoose.model('Event', eventSchema);
*
* // ClickedLinkEvent is a special type of Event that has
* // a URL.
* const ClickedLinkEvent = Event.discriminator('ClickedLink',
* new mongoose.Schema({ url: String }, options));
* Event.discriminators['clicked']; // Model { clicked }
*
* // When you create a generic event, it can't have a URL field...
* const genericEvent = new Event({ time: Date.now(), url: 'google.com' });
* assert.ok(!genericEvent.url);
* // But a ClickedLinkEvent can
* const clickedEvent = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
* assert.ok(clickedEvent.url);
* const doc = await Event.create({ kind: 'clicked', element: '#hero' });
* doc.element; // '#hero'
* doc instanceof ClickedModel; // true
*
* @param {String} name the name of the discriminator
* @param {Schema} schema the Schema of the discriminated Schema
* @param {Schema} schema the discriminated Schema
* @return {Schema} the Schema instance
* @api public
*/
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
"version": "6.6.3",
"version": "6.6.4",
"author": "Guillermo Rauch <guillermo@learnboost.com>",
"keywords": [
"mongodb",
Expand Down
67 changes: 15 additions & 52 deletions test/common.js
Expand Up @@ -114,11 +114,15 @@ module.exports = function(options) {
return conn;
};

function getUri(env, default_uri, db) {
function getUri(default_uri, db) {
const env = process.env.START_REPLICA_SET ? process.env.MONGOOSE_REPLSET_URI : process.env.MONGOOSE_TEST_URI;
const use = env ? env : default_uri;
const lastIndex = use.lastIndexOf('/');
const dbQueryString = use.slice(lastIndex);
const queryIndex = dbQueryString.indexOf('?');
const query = queryIndex === -1 ? '' : '?' + dbQueryString.slice(queryIndex + 1);
// use length if lastIndex is 9 or lower, because that would mean it found the last character of "mongodb://"
return use.slice(0, lastIndex <= 9 ? use.length : lastIndex) + `/${db}`;
return use.slice(0, lastIndex <= 9 ? use.length : lastIndex) + `/${db}` + query;
}

/**
Expand All @@ -134,13 +138,18 @@ const databases = module.exports.databases = [
* testing uri
*/

module.exports.uri = getUri(process.env.MONGOOSE_TEST_URI, 'mongodb://127.0.0.1:27017/', databases[0]);
// the following has to be done, otherwise mocha will evaluate this before running the global-setup, where it becomes the default
Object.defineProperty(module.exports, 'uri', {
get: () => getUri('mongodb://127.0.0.1:27017/', databases[0])
});

/**
* testing uri for 2nd db
*/

module.exports.uri2 = getUri(process.env.MONGOOSE_TEST_URI, 'mongodb://127.0.0.1:27017/', databases[1]);
Object.defineProperty(module.exports, 'uri2', {
get: () => getUri('mongodb://127.0.0.1:27017/', databases[1])
});

/**
* expose mongoose
Expand Down Expand Up @@ -177,28 +186,13 @@ module.exports.mongodVersion = async function() {
};

async function dropDBs() {
this.timeout(60000);

const db = await module.exports({ noErrorListener: true }).asPromise();
await db.dropDatabase();
await db.close();
}

before(async function() {
this.timeout(60000);
if (process.env.START_REPLICA_SET) {
const uri = await startReplicaSet();

module.exports.uri = uri;
module.exports.uri2 = uri.replace(databases[0], databases[1]);

process.env.REPLICA_SET = 'rs0';

const conn = mongoose.createConnection(uri);
await conn.asPromise();
await conn.db.collection('test').findOne();
await conn.close();
}
});

before(dropDBs);

after(dropDBs);
Expand All @@ -212,34 +206,3 @@ process.on('unhandledRejection', function(error, promise) {
}
throw error;
});

async function startReplicaSet() {
const ReplSet = require('mongodb-memory-server').MongoMemoryReplSet;

// Create new instance
const replSet = new ReplSet({
binary: {
version: '5.0.4'
},
instanceOpts: [
// Set the expiry job in MongoDB to run every second
{
port: 27017,
args: ['--setParameter', 'ttlMonitorSleepSecs=1']
}
],
dbName: databases[0],
replSet: {
name: 'rs0',
count: 2,
storageEngine: 'wiredTiger'
}
});

await replSet.start();
await replSet.waitUntilRunning();

await new Promise(resolve => setTimeout(resolve, 10000));

return replSet.getUri(databases[0]);
}

0 comments on commit bfa835f

Please sign in to comment.