diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d10fdcc29aa..d362114672d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,34 @@ jobs: with: name: coverage path: coverage + + test-deno: + runs-on: ubuntu-20.04 + name: Deno tests + steps: + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 + - name: Setup + run: | + wget -q https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-6.0.0.tgz + tar xf mongodb-linux-x86_64-ubuntu2004-6.0.0.tgz + mkdir -p ./data/db/27017 ./data/db/27000 + printf "\ntimeout: 8000" >> ./.mocharc.yml + ./mongodb-linux-x86_64-ubuntu2004-6.0.0/bin/mongod --setParameter ttlMonitorSleepSecs=1 --fork --dbpath ./data/db/27017 --syslog --port 27017 + sleep 2 + mongod --version + echo `pwd`/mongodb-linux-x86_64-ubuntu2004-6.0.0/bin >> $GITHUB_PATH + - name: Setup node + uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # v3.4.1 + with: + node-version: 16 + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.28.x + - run: deno --version + - run: npm install + - name: Run Deno tests + run: npm run test-deno test-replica-sets: needs: diff --git a/README.md b/README.md index 60da3ccde9f..10d443f45ab 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Mongoose -Mongoose is a [MongoDB](https://www.mongodb.org/) object modeling tool designed to work in an asynchronous environment. Mongoose supports both promises and callbacks. +Mongoose is a [MongoDB](https://www.mongodb.org/) object modeling tool designed to work in an asynchronous environment. Mongoose supports [Node.js](https://nodejs.org/en/) and [Deno](https://deno.land/) (alpha). -[![Slack Status](https://img.shields.io/badge/slack-mongoosejsteam-34D058.svg?logo=slack )](https://mongoosejsteam.slack.com) [![Build Status](https://github.com/Automattic/mongoose/workflows/Test/badge.svg)](https://github.com/Automattic/mongoose) [![NPM version](https://badge.fury.io/js/mongoose.svg)](http://badge.fury.io/js/mongoose) +[![Deno version](https://deno.land/badge/mongoose/version)](https://deno.land/x/mongoose) +[![Deno popularity](https://deno.land/badge/mongoose/popularity)](https://deno.land/x/mongoose) [![npm](https://nodei.co/npm/mongoose.png)](https://www.npmjs.com/package/mongoose) @@ -45,6 +46,8 @@ First install [Node.js](http://nodejs.org/) and [MongoDB](https://www.mongodb.or $ npm install mongoose ``` +Mongoose 6.8.0 also includes alpha support for [Deno](https://deno.land/). + ## Importing ```javascript @@ -55,6 +58,24 @@ const mongoose = require('mongoose'); import mongoose from 'mongoose'; ``` +Or, using [Deno's `createRequire()` for CommonJS support](https://deno.land/std@0.113.0/node/README.md?source=#commonjs-modules-loading) as follows. + +```javascript +import { createRequire } from "https://deno.land/std/node/module.ts"; +const require = createRequire(import.meta.url); + +const mongoose = require('mongoose'); + +mongoose.connect('mongodb://localhost:27017/test') + .then(() => console.log('Connected!')); +``` + +You can then run the above script using the following. + +``` +deno run --allow-net --allow-read --allow-sys --allow-env mongoose-test.js +``` + ## Mongoose for Enterprise Available as part of the Tidelift Subscription diff --git a/lib/schematype.js b/lib/schematype.js index 6c1863febe5..d0c9d2382f6 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -56,7 +56,7 @@ function SchemaType(path, options, instance) { const defaultOptionsKeys = Object.keys(defaultOptions); for (const option of defaultOptionsKeys) { - if (defaultOptions.hasOwnProperty(option) && !options.hasOwnProperty(option)) { + if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) { options[option] = defaultOptions[option]; } } diff --git a/package.json b/package.json index ce001cdc64a..5018f58c812 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "release-legacy": "git pull origin 5.x && git push origin 5.x --tags && npm publish --tag legacy", "mongo": "node ./tools/repl.js", "test": "mocha --exit ./test/*.test.js", + "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys ./test/deno.js", "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js", "test-tsd": "node ./test/types/check-types-filename && tsd", "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}", diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml index 038d1bbc52f..5caa2b37c25 100644 --- a/test/.eslintrc.yml +++ b/test/.eslintrc.yml @@ -3,3 +3,5 @@ env: rules: # In `document.test.js` we sometimes use self assignment to test setters no-self-assign: off +ignorePatterns: + - deno.js \ No newline at end of file diff --git a/test/connection.test.js b/test/connection.test.js index b6fad70087d..d1edc428df1 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -271,6 +271,7 @@ describe('connections:', function() { }); it('allows passing a schema', function() { + mongoose.deleteModel(/Test/); const MyModel = mongoose.model('Test', new Schema({ name: String })); @@ -885,7 +886,11 @@ describe('connections:', function() { return Model.create({ name: 'test' }); }); - it('throws a MongooseServerSelectionError on server selection timeout (gh-8451)', () => { + it('throws a MongooseServerSelectionError on server selection timeout (gh-8451)', function() { + if (typeof Deno !== 'undefined') { + // In Deno dns throws an uncatchable error here. + return this.skip(); + } const opts = { serverSelectionTimeoutMS: 100 }; diff --git a/test/deno.js b/test/deno.js new file mode 100644 index 00000000000..922766f1b20 --- /dev/null +++ b/test/deno.js @@ -0,0 +1,44 @@ +'use strict'; + +import { createRequire } from "https://deno.land/std/node/module.ts"; + +// Workaround for Mocha getting terminal width, which currently requires `--unstable` +Object.defineProperty(process.stdout, 'getWindowSize', { + value: function() { + return [75, 40]; + } +}); + +import { parse } from "https://deno.land/std/flags/mod.ts" +const args = parse(Deno.args); + +const require = createRequire(import.meta.url); + +const Mocha = require('mocha'); +const fs = require('fs'); +const path = require('path'); + +const mocha = new Mocha({ + timeout: 8000, + ...(args.g ? { fgrep: '' + args.g } : {}) +}); + +const testDir = 'test'; +const files = fs.readdirSync(testDir). + concat(fs.readdirSync(path.join(testDir, 'docs')).map(file => path.join('docs', file))). + concat(fs.readdirSync(path.join(testDir, 'helpers')).map(file => path.join('helpers', file))); + +const ignoreFiles = new Set(['browser.test.js']); + +for (const file of files) { + if (!file.endsWith('.test.js') || ignoreFiles.has(file)) { + continue; + } + + mocha.addFile(path.join(testDir, file)); +} + +mocha.run(function(failures) { + process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures + process.exit(process.exitCode); +}); \ No newline at end of file diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index 49cd0c45aad..576d8bcdcf3 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -1,7 +1,6 @@ 'use strict'; const assert = require('assert'); -const v8Serialize = require('v8').serialize; const start = require('../common'); // This file is in `es-next` because it uses async/await for convenience @@ -23,6 +22,12 @@ describe('Lean Tutorial', function() { }); it('compare sizes lean vs not lean', async function() { + // acquit:ignore:start + if (typeof Deno !== 'undefined') { + return this.skip(); // Deno does not support v8.serialize() + } + const v8Serialize = require('v8').serialize; + // acquit:ignore:end const schema = new mongoose.Schema({ name: String }); const MyModel = mongoose.model('Test', schema); diff --git a/test/errors.validation.test.js b/test/errors.validation.test.js index b881d4bf80a..e778c126208 100644 --- a/test/errors.validation.test.js +++ b/test/errors.validation.test.js @@ -234,6 +234,11 @@ describe('ValidationError', function() { describe('when user code defines a r/o Error#toJSON', function() { it('should not fail', function(done) { + if (typeof Deno !== 'undefined') { + // Deno doesn't support child_process.fork + return this.skip(); + } + const err = []; const child = require('child_process') .fork('./test/isolated/project-has-error.toJSON.js', ['--no-warnings'], { silent: true }); diff --git a/test/index.test.js b/test/index.test.js index 723bc817234..7536ca33113 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -842,6 +842,11 @@ describe('mongoose module:', function() { }); it('connect with url doesnt cause unhandled rejection (gh-6997)', async function() { + if (typeof Deno !== 'undefined') { + // In Deno dns throws an uncatchable error here. + return this.skip(); + } + const m = new mongoose.Mongoose(); const _options = Object.assign({}, options, { serverSelectionTimeoutMS: 100 }); const error = await m.connect('mongodb://doesnotexist:27009/test', _options).then(() => null, err => err); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 0de1ef29b38..a2700930832 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -3713,7 +3713,7 @@ describe('model: populate:', function() { assert.deepEqual(arr, ['Shaq', 'Kobe', 'Horry']); }); - it('deep populate array -> array (gh-3954)', function(done) { + it('deep populate array -> array (gh-3954)', async function() { const personSchema = new Schema({ name: { type: String } }); @@ -3731,59 +3731,45 @@ describe('model: populate:', function() { const Team = db.model('Team', teamSchema); const Game = db.model('Test', gameSchema); - const people = [ + const people = await Person.create([ { name: 'Shaq' }, { name: 'Kobe' }, { name: 'Horry' }, { name: 'Duncan' }, { name: 'Robinson' }, { name: 'Johnson' } - ]; + ]); - Person.create(people, function(error, people) { - assert.ifError(error); - const lakers = { - name: 'Lakers', - members: [people[0]._id, people[1]._id, people[2]._id] - }; - const spurs = { - name: 'Spurs', - members: [people[3]._id, people[4]._id, people[5]._id] - }; - const teams = [lakers, spurs]; - Team.create(teams, function(error, teams) { - assert.ifError(error); - const game = { - teams: [teams[0]._id, teams[1]._id] - }; - Game.create(game, function(error, game) { - assert.ifError(error); - test(game._id); - }); - }); - }); + const lakers = { + name: 'Lakers', + members: [people[0]._id, people[1]._id, people[2]._id] + }; + const spurs = { + name: 'Spurs', + members: [people[3]._id, people[4]._id, people[5]._id] + }; + const teams = await Team.create([lakers, spurs]); - function test(id) { - const query = Game.findById(id).populate({ - path: 'teams', - select: 'name members', - populate: { path: 'members', select: 'name' } - }); - query.exec(function(error, doc) { - assert.ifError(error); - const players = doc.toObject().teams[0].members. - concat(doc.toObject().teams[1].members); - const arr = players.map(function(v) { - return v.name; - }); - assert.deepEqual(arr, - ['Shaq', 'Kobe', 'Horry', 'Duncan', 'Robinson', 'Johnson']); - done(); - }); - } + const game = { + teams: [teams[0]._id, teams[1]._id] + }; + const { _id } = await Game.create(game); + + const doc = await Game.findById(_id).populate({ + path: 'teams', + select: 'name members', + populate: { path: 'members', select: 'name' } + }); + const players = doc.toObject().teams[0].members. + concat(doc.toObject().teams[1].members); + const arr = players.map(function(v) { + return v.name; + }); + assert.deepEqual(arr, + ['Shaq', 'Kobe', 'Horry', 'Duncan', 'Robinson', 'Johnson']); }); - it('4 level population (gh-3973)', function(done) { + it('4 level population (gh-3973)', async function() { const level4Schema = new Schema({ name: { type: String } }); @@ -3809,39 +3795,35 @@ describe('model: populate:', function() { const level1 = db.model('Test', level1Schema); const l4docs = [{ name: 'level 4' }]; + const l4 = await level4.create(l4docs); - level4.create(l4docs, function(error, l4) { - assert.ifError(error); - const l3docs = [{ name: 'level 3', level4: l4[0]._id }]; - level3.create(l3docs, function(error, l3) { - assert.ifError(error); - const l2docs = [{ name: 'level 2', level3: l3[0]._id }]; - level2.create(l2docs, function(error, l2) { - assert.ifError(error); - const l1docs = [{ name: 'level 1', level2: l2[0]._id }]; - level1.create(l1docs, function(error, l1) { - assert.ifError(error); - const opts = { - path: 'level2', - populate: { - path: 'level3', - populate: { - path: 'level4' - } - } - }; - level1.findById(l1[0]._id).populate(opts).exec(function(error, obj) { - assert.ifError(error); - assert.equal(obj.level2[0].level3[0].level4[0].name, 'level 4'); - done(); - }); - }); - }); - }); + const l3docs = [{ name: 'level 3', level4: l4[0]._id }]; + const l3 = await level3.create(l3docs).catch(err => { + console.log(err); + throw err; }); + + const l2docs = [{ name: 'level 2', level3: l3[0]._id }]; + const l2 = await level2.create(l2docs); + + const l1docs = [{ name: 'level 1', level2: l2[0]._id }]; + const l1 = await level1.create(l1docs); + + const opts = { + path: 'level2', + populate: { + path: 'level3', + populate: { + path: 'level4' + } + } + }; + + const obj = await level1.findById(l1[0]._id).populate(opts).exec(); + assert.equal(obj.level2[0].level3[0].level4[0].name, 'level 4'); }); - it('deep populate two paths (gh-3974)', function(done) { + it('deep populate two paths (gh-3974)', async function() { const level3Schema = new Schema({ name: { type: String } }); @@ -3861,42 +3843,34 @@ describe('model: populate:', function() { const level2 = db.model('Test1', level2Schema); const level1 = db.model('Test2', level1Schema); - const l3 = [ + let l3 = [ { name: 'level 3/1' }, { name: 'level 3/2' } ]; - level3.create(l3, function(error, l3) { - assert.ifError(error); - const l2 = [ - { name: 'level 2', level31: l3[0]._id, level32: l3[1]._id } - ]; - level2.create(l2, function(error, l2) { - assert.ifError(error); - const l1 = [{ name: 'level 1', level2: l2[0]._id }]; - level1.create(l1, function(error, l1) { - assert.ifError(error); - level1.findById(l1[0]._id). - populate({ - path: 'level2', - populate: [{ - path: 'level31' - }] - }). - populate({ - path: 'level2', - populate: [{ - path: 'level32' - }] - }). - exec(function(error, obj) { - assert.ifError(error); - assert.equal(obj.level2[0].level31[0].name, 'level 3/1'); - assert.equal(obj.level2[0].level32[0].name, 'level 3/2'); - done(); - }); - }); + l3 = await level3.create(l3); + + const l2 = await level2.create([ + { name: 'level 2', level31: l3[0]._id, level32: l3[1]._id } + ]); + + const l1 = await level1.create([{ name: 'level 1', level2: l2[0]._id }]); + + const obj = await level1.findById(l1[0]._id). + populate({ + path: 'level2', + populate: [{ + path: 'level31' + }] + }). + populate({ + path: 'level2', + populate: [{ + path: 'level32' + }] }); - }); + + assert.equal(obj.level2[0].level31[0].name, 'level 3/1'); + assert.equal(obj.level2[0].level32[0].name, 'level 3/2'); }); it('out-of-order discriminators (gh-4073)', function() { diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index 575d06e79b6..6f22c0eab4d 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -27,13 +27,14 @@ describe('QueryCursor', function() { afterEach(() => require('./util').clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db)); - beforeEach(function() { + beforeEach(async function() { const schema = new Schema({ name: String }); schema.virtual('test').get(function() { return 'test'; }); Model = db.model('Test', schema); - return Model.create({ name: 'Axl' }, { name: 'Slash' }); + await Model.deleteMany({}); + await Model.create({ name: 'Axl' }, { name: 'Slash' }); }); describe('#next()', function() { diff --git a/test/query.test.js b/test/query.test.js index a5309ebba40..53e2dff58b3 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -2339,6 +2339,13 @@ describe('Query', function() { }); it('throw on sync exceptions in callbacks (gh-6178)', function(done) { + // Deno doesn't support 'uncaughtException', so there's no way to test this in Deno + // without starting a separate process. + // See: https://stackoverflow.com/questions/64871554/deno-how-to-handle-exceptions + if (typeof Deno !== 'undefined') { + return this.skip(); + } + const schema = new Schema({}); const Test = db.model('Test', schema); @@ -3468,11 +3475,11 @@ describe('Query', function() { }); it('query with top-level _bsontype (gh-8222) (gh-8268)', async function() { - const userSchema = Schema({ token: String }); + const userSchema = Schema({ token: String }, { strictQuery: true }); const User = db.model('Test', userSchema); - const original = await User.create({ token: 'rightToken' }); + let doc = await User.findOne({ token: 'wrongToken', _bsontype: 'a' }); assert.ok(!doc); diff --git a/test/schema.subdocumentpath.test.js b/test/schema.subdocumentpath.test.js index d7a131ab5c3..60d4a1e2f3c 100644 --- a/test/schema.subdocumentpath.test.js +++ b/test/schema.subdocumentpath.test.js @@ -179,7 +179,7 @@ describe('SubdocumentPath', function() { assert.ok(err); assert.ok(err.errors['nested']); - mongoose.Schema.Types.Subdocument.set('required', false); + delete mongoose.Schema.Types.Subdocument.defaultOptions.required; }); it('supports setting _id globally (gh-11541) (gh-8883)', function() { @@ -197,6 +197,5 @@ describe('SubdocumentPath', function() { assert.ok(!doc.nested._id); delete mongoose.Schema.Types.Subdocument.defaultOptions._id; - mongoose.Schema.Types.Subdocument.set('required', false); }); }); diff --git a/test/schema.test.js b/test/schema.test.js index 619df3dd7c9..b35cdf5cbbf 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1178,6 +1178,11 @@ describe('schema', function() { describe('other contexts', function() { it('work', function(done) { + if (typeof Deno !== 'undefined') { + // Deno throws "Not implemented: Script.prototype.runInNewContext" + return this.skip(); + } + const str = 'code = {' + ' name: String' + ', arr1: Array ' + diff --git a/test/schematype.test.js b/test/schematype.test.js index 6b96bde4377..6e07390a2bc 100644 --- a/test/schematype.test.js +++ b/test/schematype.test.js @@ -224,6 +224,8 @@ describe('schematype', function() { // Assert assert.equal(schema.path('test').options.someRandomOption, true); + + delete type.defaultOptions.someRandomOption; }); }); }); diff --git a/test/types.array.test.js b/test/types.array.test.js index 19b337c36b1..bf07fb58792 100644 --- a/test/types.array.test.js +++ b/test/types.array.test.js @@ -2255,5 +2255,7 @@ describe('types array', function() { // Assert assert.ifError(user.validateSync()); + + m.Schema.Types.Array.options.castNonArrays = true; }); });