A Feathers database adapter for Mongoose, an object modeling tool for MongoDB.
$ npm install --save mongoose feathers-mongooseNote: This adapter is currently tested against Mongoose 6. Mongoose 8 support is planned for a later date.
Important:
feathers-mongooseimplements the Feathers Common database adapter API and querying syntax.
This adapter also requires a running MongoDB database server.
This adapter includes TypeScript definitions. When using TypeScript, you can import and use it as follows:
import mongoose from 'mongoose';
import service from 'feathers-mongoose';
interface Message {
text: string;
createdAt: Date;
}
const Model = mongoose.model<Message>('Message', new mongoose.Schema({
text: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
}));
app.use('/messages', service({ Model }));Returns a new service instance initialized with the given options. Model has to be a Mongoose model. See the Mongoose Guide for more information on defining your model.
const mongoose = require('mongoose');
const service = require('feathers-mongoose');
// A module that exports your Mongoose model
const Model = require('./models/message');
// Make Mongoose use the ES6 promise
mongoose.Promise = global.Promise;
// Connect to a local database called `feathers`
mongoose.connect('mongodb://localhost:27017/feathers');
app.use('/messages', service({ Model }));
app.use('/messages', service({ Model, lean, id, events, paginate }));Options:
Model(required) - The Mongoose model definitionlean(optional, default:true) - Runs queries faster by returning plain objects instead of Mongoose models.id(optional, default:'_id') - The name of the id field property.events(optional) - A list of custom service events sent by this servicepaginate(optional) - A pagination object containing adefaultandmaxpage size. When pagination is disabled (paginate: false), the adapter skips the total count query for better performancewhitelist(optional) - A list of additional query parameters to allow (e..g[ '$regex', '$populate' ])multi(optional) - Allowcreatewith arrays andupdateandremovewithidnullto change multiple items. Can betruefor all methods or an array of allowed methods (e.g.[ 'remove', 'create' ])overwrite(optional, default:true) - Overwrite the document when update, making mongoose detect is new document and trigger default value for unspecified properties in mongoose schema.discriminators(optional) - A list of mongoose models that inherit fromModel.useEstimatedDocumentCount(optional, default:false) - UseestimatedDocumentCountinstead (usually not necessary)queryModifier(optional) - A function that takes in the raw mongoose Query object and params, which modifies all find and get requests unless overridden. (see Query Modifiers below)queryModifierKey(optional, default:'queryModifier') - The key to use to get the override query modifier function from the params. (see Query Modifiers below)
Important: To avoid odd error handling behaviour, always set
mongoose.Promise = global.Promise. If not available already, Feathers comes with a polyfill for native Promises.
Important: When setting
leantofalse, Mongoose models will be returned which can not be modified unless they are converted to a regular JavaScript object viatoObject.
Note: You can get access to the Mongoose model via
this.Modelinside a hook and use it as usual. See the Mongoose Guide for more information on defining your model.
When making a service method call, params can contain a mongoose property which allows you to modify the options used to run the Mongoose query. Normally, this will be set in a before hook:
app.service('messages').hooks({
before: {
patch(context) {
// Set some additional Mongoose options
// The adapter tries to use these settings by defaults
// but they can always be changed here
context.params.mongoose = {
runValidators: true,
setDefaultsOnInsert: true
}
}
}
});The mongoose property is also useful for performing upserts on a patch request. "Upserts" do an update if a matching record is found, or insert a record if there's no existing match. The following example will create a document that matches the data, or if there's already a record that matches the params.query, that record will be updated.
Using the writeResult mongoose option will return the write result of a patch operation, including the _ids of all upserted or modified documents. This can be helpful alongside the upsert flag, for detecting whether the outcome was a find or insert operation. More on write results is available in the Mongo documentation
Performance Note: When using
writeResultwith multi-document patch operations, the adapter uses an optimized path that performs the update directly without pre-fetching document IDs, resulting in better performance for write-only operations
const data = { address: '123', identifier: 'my-identifier' }
const params = {
query: { address: '123' },
mongoose: { upsert: true, writeResult: true }
}
app.service('address-meta').patch(null, data, params)Here's a complete example of a Feathers server with a messages Mongoose service.
$ npm install @feathersjs/feathers @feathersjs/errors @feathersjs/express @feathersjs/socketio mongoose feathers-mongoose
In message-model.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const MessageSchema = new Schema({
text: {
type: String,
required: true
}
});
const Model = mongoose.model('Message', MessageSchema);
module.exports = Model;Then in app.js:
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('@feathersjs/socketio');
const mongoose = require('mongoose');
const service = require('feathers-mongoose');
const Model = require('./message-model');
mongoose.Promise = global.Promise;
// Connect to your MongoDB instance(s)
mongoose.connect('mongodb://localhost:27017/feathers');
// Create an Express compatible Feathers application instance.
const app = express(feathers());
// Turn on JSON parser for REST services
app.use(express.json());
// Turn on URL-encoded parser for REST services
app.use(express.urlencoded({extended: true}));
// Enable REST services
app.configure(express.rest());
// Enable Socket.io services
app.configure(socketio());
// Connect to the db, create and register a Feathers service.
app.use('/messages', service({
Model,
lean: true, // set to false if you want Mongoose documents returned
paginate: {
default: 2,
max: 4
}
}));
app.use(express.errorHandler());
// Create a dummy Message
app.service('messages').create({
text: 'Message created on server'
}).then(function(message) {
console.log('Created message', message);
});
// Start the server.
const port = 3030;
app.listen(port, () => {
console.log(`Feathers server listening on port ${port}`);
});You can run this example by using node app and go to localhost:3030/messages.
Mongoose by default gives you the ability to add validations at the model level. Using an error handler like the one that comes with Feathers your validation errors will be formatted nicely right out of the box!
For more information on querying and validation refer to the Mongoose documentation.
For Mongoose, the special $populate query parameter can be used to allow Mongoose query population.
Important:
$populatehas to be whitelisted explicitly since it can expose protected fields in sub-documents (like the user password) which have to be removed manually.
const mongoose = require('feathers-mongoose');
app.use('/posts', mongoose({
Model,
whitelist: [ '$populate' ]
});
app.service('posts').find({
query: { $populate: 'user' }
});The $populate parameter supports various formats:
- String:
$populate: 'user' - Array of strings:
$populate: ['user', 'comments'] - Object with options:
$populate: { path: 'user', select: 'name email' } - Array of objects:
$populate: [{ path: 'user' }, { path: 'comments', options: { limit: 5 } }]
As of v7.3.0, the original Mongoose error can be retrieved on the server via:
const { ERROR } = require('feathers-mongoose');
try {
await app.service('posts').create({ value: 'invalid' });
} catch(error) {
// error is a FeathersError
// Safely retrieve the original Mongoose error
const mongooseError = error[ERROR];
}Instead of strict inheritance, Mongoose uses discriminators as their schema inheritance model.
To use them, pass in a discriminatorKey option to your schema object and use Model.discriminator('modelName', schema) instead of mongoose.model()
Feathers comes with full support for mongoose discriminators, allowing for automatic fetching of inherited types. A typical use case might look like:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var Post = require('./post');
var feathers = require('@feathersjs/feathers');
var app = feathers();
var service = require('feathers-mongoose');
// Discriminator key, we'll use this later to refer to all text posts
var options = {
discriminatorKey: '_type'
};
var TextPostSchema = new Schema({
text: { type: String, default: null }
}, options);
// Note the use of `Post.discriminator` rather than `mongoose.discriminator`.
var TextPost = Post.discriminator('text', TextPostSchema);
// Using the discriminators option, let feathers know about any inherited models you may have
// for that service
app.use('/posts', service({
Model: Post,
discriminators: [TextPost]
}))Without support for discriminators, when you perform a .get on the posts service, you'd only get back Post models, not TextPost models.
Now in your query, you can specify a value for your discriminatorKey:
{
_type: 'text'
}and Feathers will automatically swap in the correct model and execute the query it instead of its parent model.
This adapter includes support for collation and case insensitive indexes available in MongoDB v3.4. Collation parameters may be passed using the special collation parameter to the find(), remove() and patch() methods.
The example below would patch all student records with grades of 'c' or 'C' and above (a natural language ordering). Without collations this would not be as simple, since the comparison { $gt: 'c' } would not include uppercase grades of 'C' because the code point of 'C' is less than that of 'c'.
const patch = { shouldStudyMore: true };
const query = { grade: { $gte: 'c' } };
const collation = { locale: 'en', strength: 1 };
students.patch(null, patch, { query, collation }).then( ... );Similar to the above example, this would find students with a grade of 'c' or greater, in a case-insensitive manner.
const query = { grade: { $gte: 'c' } };
const collation = { locale: 'en', strength: 1 };
students.find({ query, collation }).then( ... );For more information on MongoDB's collation feature, visit the collation reference page.
This adapter includes support to enable database transaction to rollback the persisted records for any error occured for a api call. This requires Mongo-DB v4.x installed and replica-set enabled.
Start working with transaction enabled by adding the following lines in app.hooks.js or <any-service>.hooks.js.
const TransactionManager = require('feathers-mongoose').TransactionManager;
const isTransactionEnable = process.env.TRANSACTION_ENABLE || false;
const skipPath = ['login'];
let moduleExports = {
before: {
all: [],
find: [],
get: [],
create: [
when(isTransactionEnable, async hook =>
TransactionManager.beginTransaction(hook, skipPath)
)
],
update: [
when(isTransactionEnable, async hook =>
TransactionManager.beginTransaction(hook, skipPath)
)
],
patch: [],
remove: []
},
after: {
all: [],
find: [],
get: [],
create: [when(isTransactionEnable, TransactionManager.commitTransaction)],
update: [when(isTransactionEnable, TransactionManager.commitTransaction)],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [when(isTransactionEnable, TransactionManager.rollbackTransaction)],
update: [when(isTransactionEnable, TransactionManager.rollbackTransaction)],
patch: [],
remove: []
}
};
module.exports = moduleExports;Sometimes it's important to use an unusual Mongoose Query method, like specifying whether to read from a primary or secondary node, but maybe only for certain requests.
You can access the internal Mongoose Query object used for a find/get request by specifying the queryModifier function. It is also possible to override that global function by specifying the function in a requests params.
// Specify a global query modifier when creating the service
app.use('/messages', service({
Model,
queryModifier: (query, params) => {
query.read('secondaryPreferred');
}
}));
app.service('messages').find({
query: { ... },
}).then((result) => {
console.log('Result from secondary:', result)
});
// Override the modifier on a per-request basis
app.service('messages').find({
query: { ... },
queryModifier: (query, params) => {
query.read('primaryPreferred');
}
}).then((result) => {
console.log('Result from primary:', result)
});
// Disable the global modifier on a per-request basis
app.service('messages').find({
query: { ... },
queryModifier: false
}).then((result) => {
console.log('Result from default option:', result)
});Note: Due to replication lag, a secondary node can have "stale" data. You should ensure that this "staleness" will not be an issue for your application before reading from the secondary set.
This module is community maintained and open for pull requests. Features and bug fixes should contain
- The bug fix / feature code
- Tests to reproduce the bug or test the feature
- Documentation updates (if necessary)
To contribute, fork and clone the repository. This project uses Docker for local development to ensure consistency across different environments.
Option 1: Using Docker (Recommended)
The easiest way to run tests is using Docker Compose, which will set up MongoDB 7.0 with replica set configuration:
# Start MongoDB and run tests
docker-compose up --build
# Or run tests in watch mode
docker-compose up mongo # Start MongoDB in background
npm test # Run tests on hostTesting with Different Node Versions
To test with Node 12 (minimum supported version), you can use the alternative Docker setup:
# Test with Node 12 (skips linting due to ESLint compatibility)
docker-compose -f docker-compose.node12.yml up --buildNote: Node 12 testing skips linting steps due to ESLint compatibility issues. A separate GitHub workflow handles Node 12 testing in CI.
Option 2: Local MongoDB Setup
If you prefer to use a local MongoDB installation, you'll need:
- MongoDB 7.0 or later
- Replica set configuration (required for transaction tests)
# Start MongoDB with replica set
mongod --replSet rs0
# Initialize replica set (first time only)
mongosh --eval "rs.initiate()"
# Run tests
npm test