Skip to main content

Multi Tenancy Plugin for Mongoose

 

Multi Tenancy Plugin for Mongoose

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 mongo-tenant@2.0
   */
  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:password@xyz.mlab.com:23315/mongo-tenant-test' npm run test-and-cover

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

JavaScript new features in ES2019(ES10)

The 2019 edition of the ECMAScript specification has many new features. Among them, I will summarize the ones that seem most useful to me. First, you can run these examples in  node.js ≥12 . To Install Node.js 12 on Ubuntu-Debian-Mint you can do the following: $sudo apt update $sudo apt -y upgrade $sudo apt update $sudo apt -y install curl dirmngr apt-transport-https lsb-release ca-certificates $curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $sudo apt -y install nodejs Or, in  Chrome Version ≥72,  you can try those features in the developer console(Alt +j). Array.prototype.flat && Array.prototype. flatMap The  flat()  method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth. let array1 = ['a','b', [1, 2, 3]]; let array2 = array1.flat(); //['a', 'b', 1, 2, 3] We should also note that the method excludes gaps or empty elements in the array: let array1 ...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...