Coder Social home page Coder Social logo

craftup / node-mongo-tenant Goto Github PK

View Code? Open in Web Editor NEW
72.0 6.0 25.0 1.07 MB

A multi-tenancy plugin for mongoose with tenant separation on document level. It's completely customizable, provides a neat interface for model-tenant-binding and can be disabled for running in single-tenant/on premis scenarios.

License: MIT License

JavaScript 100.00%
tenant mongo-tenant multitenancy mongoose multi-tenancy-plugin nodejs

node-mongo-tenant's Introduction

Multi Tenancy Plugin for Mongoose

Subscribe to Release Notes Get Help on Gitter Build Status Coverage Status npm version GitHub license

Prelude

There are 3 ways of implementing multi-tenancy in mongoDB:

  • on document level (cheap and easy to administer but only secured by app logic)
  • on collection level (not recommended, due to breaking mongoDB concepts)
  • on database level (very flexible and secure but expensive)

About

The mongo tenant is a highly configurable mongoose plugin solving multi-tenancy problems on document level (for now...). It creates a tenant-reference field and takes care of unique indexes. Also it provides access to tenant-bound model-classes, that prohibit the exploid of the given tenant scope. Last but not least the "MAGIC" can be disabled so that shipping of the same code in single- and multi-tenancy environment (on premis vs. cloud hosted) is a question of a single line of config.

Requirements

Mongo tenant is compatible with mongoose 4 and 5.

Incompatibilities

Install

$ npm i -S mongo-tenant
// or
$ yarn add mongo-tenant

Use

Register the plugin on the relevant mongoose schema.

const mongoose = require('mongoose');
const mongoTenant = require('mongo-tenant');

const MySchema = new mongoose.Schema({});
MySchema.plugin(mongoTenant);

const MyModel = mongoose.model('MyModel', MySchema);

Retrieve the model in tenant scope with static byTenant method. This will return a new model subclass that has special tenant-scope guards. It has the exactly same interface as any other mongoose model but prevents the access to other tenant scopes.

const MyTenantBoundModel = MyModel.byTenant('some-tenant-id');

(new MyTenantBoundModel()).getTenantId() === 'some-tenant-id'; // true

// silently ignore other tenant scope
(new MyTenantBoundModel({
  tenantId: 'some-other-tenant-id'
})).getTenantId() === 'some-tenant-id'; // true

You can check for tenant context of a model class or instance by checking the hasTenantContext property. If this is truthy you may want to retrieve the bound tenant scope with getTenantId() method.

// With enabled mongo-tenant on a schema, all tenant bound models
// and there instances provide the hasTenantContext flag
if (SomeModelClassOrInstance.hasTenantContext) {
  const tenantId = SomeModelClassOrInstance.getTenantId();
  ...
}

Indexes

The mongo-tenant takes care of the tenant-reference field, so that you will be able to use your existing schema definitions and just plugin the mongo-tenant without changing a single line of schema definition.

But under the hood the mongo-tenant creates an indexed field (tenantId by default) and includes this in all defined unique indexes. So by default, all unique fields (and compound indexes) are unique for a single tenant id.

You may have use-cases where you want to archive global uniqueness. To skip the automatic unique key extension of mongo-tenant for a specific index you can set the preserveUniqueKey config option to true.

const MySchema = new mongoose.Schema({
  someField: {
    unique: true,
    preserveUniqueKey: true
  },
  anotherField: String,
  yetAnotherField: String
});

MySchema.index({
  anotherField: 1,
  yetAnotherField: 1
}, {
  unique: true,
  preserveUniqueKey: true
});

Context bound models and populate

Once a model with tenant context is created it will try to keep the context for other models created via it. Whenever it detects that a subsequent models tenant configuration is compatible to its own, it will return that model bound to the same tenant context.

const AuthorSchema = new mongoose.Schema({});
AuthorSchema.plugin(mongoTenant);
const AuthorModel = mongoose.model('author', AuthorSchema);

const BookSchema = new mongoose.Schema({
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'author' }
});
BookSchema.plugin(mongoTenant);
const BookModel = mongoose.model('book', BookSchema);

const BoundBookModel = BookModel.byTenant('some-tenant-id');
BoundBookModel.model('author'); // return author model bound to "some-tenant-id"
BoundBookModel.db.model('author'); // return author model bound to "some-tenant-id"

Configuration

The mongo tenant works out of the box, so all config options are optional. But you have the ability to adjust the behavior and api of the mongo tenant to your needs.

const config = {
  /**
   * Whether the mongo tenant plugin MAGIC is enabled. Default: true
   */
  enabled: false,

  /**
   * The name of the tenant id field. Default: tenantId
   */
  tenantIdKey: 'customerId',

  /**
   * The type of the tenant id field. Default: String
   */
  tenantIdType: Number,

  /**
   * The name of the tenant id getter method. Default: getTenantId
   */
  tenantIdGetter: 'getCustomerId',

  /**
   * The name of the tenant bound model getter method. Default: byTenant
   */
  accessorMethod: 'byCustomer',

  /**
   * Enforce tenantId field to be set. Default: false
   * NOTE: this option will become enabled by default in [email protected]
   */
  requireTenantId: true
};

SomeSchema.plugin(mongoTenant, config);

Running Tests

Some tests rely on a running mongoDB and by default the tests are performed against 'mongodb://localhost/mongo-tenant-test'. The tests can also be run against a custom mongoDB by passing the custom connection string to MONGO_URI environment variable.

# perform jshint on sources and tests
$ npm run hint

# run the tests and gather coverage report
$ npm run test-and-cover

# run tests with custom mongoDB uri
$ MONGO_URI='mongodb://user:[email protected]:23315/mongo-tenant-test' npm run test-and-cover

LICENSE

The files in this archive are released under MIT license. You can find a copy of this license in LICENSE.

node-mongo-tenant's People

Contributors

alrik avatar baranga avatar costinpahontu avatar dependabot[bot] avatar dericcain avatar felipe-augusto avatar himbaer avatar ivanseidel avatar rskncankov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

node-mongo-tenant's Issues

Add release notes

The project is currently missing release-notes.

  • add release-notes.yml file
  • publish release notes to the release notes hub
  • add badge for linking to the hosted release notes

How to implement node-mongo-tenant with node-restful? Newbie Question

Hi, I'm am trying to implement node_mongo_tenant with node-restful for my app, but I am struggling figuring out a way to make work.
When I define the byTenant method soon after the model, and before registering the route on node-restful, I must know the Tenant(user) by the time. As the user is going to be known only after login and the route with pre-determined tenant is already registered, I can't find a way to change the tenant constant so it will apply the right tenant to the route.
I thought on registering the route after login as solution, but it would have serious implications for the app. Is there any smarter idea? As u may see, it's a newbie question. Thanks

Flag to disable injection on create

Add a plugin option to disable injection of tenant property when calling model.byTenant(tenant).create(),
only enables it for find, count queries

Collection-level multi-tenancy

I am a bit curious on why collection-level multi-tenancy is not recommended. I am particularly interested in micro-services scenario where each 'function' of the business is a separate microservice having a separate database. So, each database is not going to have a very large number of collections if collection-level multi-tenancy is applied. Would like to hear from you on this. Thanks

Force TenantId to be Required

Is there a way to force the tenantId to be required? I would like for the tenant id to never be null and I'm having trouble altering the schema after the plugin has ran. If I edit the mongo-tenant code, and add required: true it works. Is there something I'm missing?

Pushing the tenantId filter to subdocument model for populate

Hi,
Thanks for the useful utility.
Is there a way to push the tenantId filter down into Subdocument populate.
I tried to add _conditions in pre hook of the 'find' method but the object has no clue at this stage about its parent. Find pre hook is called for each subdocument populate construct.

this.schema.pre('find', function(next) {
//add here
});
Another approach I tried, which seems to work but looks a bit cumbersome is in mongoose/lib/model.js code. Here I can fetch the parent object's tenantId and pass it to match clause of the subdocument.

Any recommendation on the right approach to implement this.

Best regards
Pav

support array of tenant ids to getTenant

when a user is part of multiple tenants,
to perform a mongo operation we need it to work on all tenant ids associated to the user.

currently, as a workaround, i return multiple models and execute the operation on all of them
but it will be nice to pass an array of ids

Problem with discriminator key

Hello, I'm trying to use mongo-tenant with mongoose discriminators. But I'm having a problem in this piece of the code:

// inherit all static properties from the mongoose base model
    for (let staticProperty of Object.getOwnPropertyNames(BaseModel)) {
      if (MongoTenantModel.hasOwnProperty(staticProperty)
      || ['arguments', 'caller'].indexOf(staticProperty) !== -1
      ) {
        continue;
      }

      MongoTenantModel[staticProperty] = BaseModel[staticProperty];
    }

    return MongoTenantModel;

This throws the following error:

TypeError: Cannot assign to read only property 'baseModelName' of function 'class MongoTenantModel extends BaseModel

The problem is that the discriminator sets a property baseModelName that has the following descriptor:

{ 
  value: 'baseModelName',
  writable: false,
  enumerable: false,
  configurable: true
}

And since is not settable, it throws an error.

Support for multi tenant models

In my application there are special kind of users which may have access to different tenants, that means tenantId should be an array.

Currently it's not supported, looks like the only needed change is inside installMiddleware method which intercepts mongoose queries

Error when use `Model.aggregate`: Mongoose 5.x disallows passing a spread of operators to `Model.aggregate()`. Instead of `Model.aggregate({ $match }, { $skip })`, do `Model.aggregate([{ $match }, { $skip }])`

MongooseError: Mongoose 5.x disallows passing a spread of operators to Model.aggregate(). Instead of Model.aggregate({ $match }, { $skip }), do Model.aggregate([{ $match }, { $skip }])
at Function.aggregate (/Users/johnny/projects/work/tenant-test/node_modules/mongoose/lib/model.js:3921:11)
at Function.aggregate (/Users/johnny/projects/work/tenant-test/node_modules/mongo-tenant/index.js:295:32)
at file:///Users/johnny/projects/work/tenant-test/index.js:31:49
at Layer.handle [as handle_request] (/Users/johnny/projects/work/tenant-test/node_modules/router/lib/layer.js:102:15)
at next (/Users/johnny/projects/work/tenant-test/node_modules/router/lib/route.js:144:13)
at Route.dispatch (/Users/johnny/projects/work/tenant-test/node_modules/router/lib/route.js:109:3)
at handle (/Users/johnny/projects/work/tenant-test/node_modules/router/index.js:515:11)
at Layer.handle [as handle_request] (/Users/johnny/projects/work/tenant-test/node_modules/router/lib/layer.js:102:15)
at /Users/johnny/projects/work/tenant-test/node_modules/router/index.js:291:22
at Function.process_params (/Users/johnny/projects/work/tenant-test/node_modules/router/index.js:349:12)
at next (/Users/johnny/projects/work/tenant-test/node_modules/router/index.js:285:10)
at file:///Users/johnny/projects/work/tenant-test/index.js:22:3
at Layer.handle [as handle_request] (/Users/johnny/projects/work/tenant-test/node_modules/router/lib/layer.js:102:15)
at trim_prefix (/Users/johnny/projects/work/tenant-test/node_modules/router/index.js:330:13)
at /Users/johnny/projects/work/tenant-test/node_modules/router/index.js:294:7
at Function.process_params (/Users/johnny/projects/work/tenant-test/node_modules/router/index.js:349:12)

Problem with aggregation

Hello there, we are experiencing some problems using aggregate with node-mongo-tenant, your implementation of the aggregate is changing the way we need to call the aggregate function. The standart Mongoose way is:

Model.aggregate([{ $match: something }])

But the way you implemented the aggregate, we need to remove the brackets:

Model.aggregate({ $match: something })

I dont't know if this is the intended behavior, but if is not, a quick fix is possible: instead of slicing the arguments, you just need to get the pipeline passed to aggregate and unshift it to add the $match on the tenantIdKey.

  static aggregate(pipeline) {
    
    if (this.hasTenantContext) {
      pipeline.unshift({
        $match: {
          [tenantIdKey]: this[tenantIdGetter]()
        }
      })
    }

    return super.aggregate.apply(this, pipeline);
  }

Use Plugin Multiple Times Per Schema

Hi, I've been using this plugin for a long time now but I have an interesting use-case that I'm trying to solve without actually forking this library. Basically, we already use the plugin to add a tenantId to every mongoose model in our application, but we want to take it one step further. I wanted to add another field to the same model where I could chain the accessorMethod's and continue to narrow down the search. For example:

User

{
   name: "Aaron",
   tenantId: "A",
   tenantId2: "1"
}
{
   name: "Alrik",
   tenantId: "A",
   tenantId2: "2"
}
{
   name: "Someone",
   tenantId: "B",
   tenantId2: "2"
}

Basically as it stands right now I have to use User.byTenant("A") to get the model that I need for querying. But what I'd like to be able to do is something like User.byTenant("A").byTenant2("1") which would just return the Aaron user. I know it's not a typical use-case but just wondered if there would be an easy way to implement this. Otherwise if I end up forking it, do you have any recommendations?

Using the custom config "tenantIdKey" for create

I would like to use the tenantIdKey value automatically when I create new objects instead of hard coding it. I am using custom tenantIdKey field specified as a config value.

Instead of:

  Cup.create({...body, 'tenantId': user.id})
    .then((cup) => cup.view(true))
    .then(success(res, 201))
    .catch(next)

I think it would be nice to just specify it in a similar manner to update, delete, query where it fills in the proper key:

  Cup.byTenantId(user.id).create(body)
    .then((cup) => cup.view(true))
    .then(success(res, 201))
    .catch(next)

And it could automatically fill in the tenantId field. Not sure if this is useful or not, maybe this is a bad idea?

Thanks!

Beautifully designed and simple API. Great work. Awesome!

Add support for operating on column **and** database level separated tenants.

Story

As a service operator I would like to run all my freemium tenants on a shared database with tenant separation on column level. All my premium customers data should be stored in a dedicated database, maybe even on another mongodb cluster. I want to have only one app deployment that can manage all my tenants.

Goal

The goal of this issue is to provide some kind of a per-tenant config or config resolver.

Multitenant model does not override updateMany.

Hi there,

I am using .byTenant followed by updateMany in order to execute batch update. Problem is tenant model actually ignores clientId field and execute update on documents regardless tenantId field.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.