Skip to content

Instantly share code, notes, and snippets.

@gusflopes
Forked from madrussa/example-model.js
Created May 2, 2020 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gusflopes/bae58e837cc3d94b6b9cedfbad422983 to your computer and use it in GitHub Desktop.
Save gusflopes/bae58e837cc3d94b6b9cedfbad422983 to your computer and use it in GitHub Desktop.
Adonis v4 Multi Tenancy Example
'use strict'
/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
const Model = use('Model')
class ExampleModel extends Model {
/**
* Add traits
*/
static boot () {
super.boot();
this.addTrait('Tenant');
}
}
module.exports = ExampleModel
const Customer = use ('App/Models/Customer')
const ExampleModel = use ('App/Models/ExampleModel')
const TenantManager = use('Service/TenantManager')
// Update a standard model
async function standardExample (customerId, exampleId, data) {
const customer = await Customer.find(customerId);
const tenant = await TenantManager.connect(customer);
await tenant.perform(async (TenantExampleModel) => {
const newExample = new TenantExampleModel();
newExample.merge(data);
await newExample.save();
}, ExampleModel);
}
// Do something more complicated
async function rawExample (customerId, inserts) {
const customer = await Customer.find(customerId);
const tenant = await TenantManager.connect(customer);
// Using the tenant perform the inserts
await tenant.perform(async (connection) => {
await connection.transaction(async (trx) => {
// Create raw query to do bulk inserts for timescaledb
await trx.raw(util.format(
'%s ON CONFLICT ("uniqueId") DO NOTHING',
trx
.from(ExampleModel.table)
.insert(inserts)
.toString()
));
});
});
}
'use strict'
const Tenant = require('./Tenant');
/** @typedef {import('app/Models/Customer')} Customer */
const Customer = use('App/Models/Customer');
class Manager {
/**
* Construct the manager class
*
* @param {Config} Config
* @param {Database} Database
*/
constructor (Config, Database) {
this.config = Config;
this.defaults = Config.get('database.tenant');
this.databaseManager = Database;
this.tenants = {};
}
/**
* Check for expired connections and close them
*/
expire = () => {
const now = new Date();
Object.values(this.tenants)
.filter(tenant => tenant.expires > now.getTime())
.forEach(tenant => this.disconnect(tenant.customerId));
}
/**
* Get a fresh expiry date (future date, +60 seconds)
*/
getExpiryDate () {
return (new Date()).getTime() + (60 * 1000);
}
/**
* Connect to the tenant database using the given customer
*
* @param {Customer} customer
*/
async connect (customer) {
return this.createConnection(customer);
}
/**
* Create a connection to the tenant database for the customer record
*
* @param {Customer} customer
* @param {Number} customer.id
* @param {String} customer.database
*/
createConnection ({ id: customerId, database }) {
if (this.tenants[database]) {
return this.tenants[database].tenant;
}
const { defaults } = this;
// Locate the customer record and populate the config with the customer connection information
const connectionConfig = { ...defaults };
connectionConfig.connection.database = database;
this.config.set(`database.${database}`, connectionConfig);
// Use the database manager to establish a connection to the database
const dbConnection = this.databaseManager.connection(database);
const tenant = new Tenant(database, dbConnection, this);
this.tenants[database] = {
tenant,
database,
customerId,
expires: this.getExpiryDate(),
};
return tenant;
}
/**
* Ping the connection to update the expiry time
*
* @param {String} connection
*/
ping (database) {
if (!this.tenants[database]) {
return;
}
// Update last used
this.tenants[database].expires = this.getExpiryDate();
}
/**
* Disconnect the tenant database for the given customer id
*
* @param {Number} customerId
*/
async disconnect (customerId) {
const { database } = await Customer.findOrFail(customerId);
delete this.tenants[database];
this.close(database);
}
/**
* Closes the connection
*
* @param {String} connection
*/
close (connection) {
try {
this.databaseManager.close([connection]);
} catch (err) {
// Ignore
}
}
/**
* Establishes a connection, this opens any that were closed before
*
* @param {String} connection
*/
establish (connection) {
return this.databaseManager.connection(connection);
}
}
module.exports = Manager;
class Tenant {
/**
* Create the tenant class which is responsible for performing database actions with
* tenant models
*
* @param {String} connection
* @param {Manager} manager
*/
constructor (dbName, dbConnection, manager) {
this.dbName = dbName;
this.dbConnection = dbConnection;
this.manager = manager;
}
/**
* Captures the models standard connection so it can be restored once the
* tenant database actions have been performed
*
* @param {Array} models
*/
capture (models) {
const { dbName } = this;
this.dbConnection = this.manager.establish(dbName);
models.forEach(model => {
model.previousConnection = model.connection;
model.connection = dbName;
});
}
/**
* Restores the connection on the model
*
* @param {Array} models
*/
release (models) {
const { dbName } = this;
// Put the previous connection back on the model
models.forEach(model => {
model.connection = model.previousConnection;
model.previousConnection = null;
});
this.manager.ping(dbName);
}
/**
* Captures the passed in models so they can be used on the tenant database, this will pass all of
* the given models to the callback as arguments, these will all be configured so they will work
* with the tenant database
*
* @param {Function} callback
* @param {...Model} models
*/
async perform (callback, ...models) {
this.capture(models);
const result = await callback(...models, this.dbConnection);
this.release(models);
return result;
}
/**
* Saves the model in the tenant database
*
* @param {Model} model
*/
async save (model) {
this.capture([model]);
const result = await model.save();
this.release([model]);
return result;
}
/**
* Finds the model from the tenant database, the model will not have the tenant connection when
* returned and further actions must be used with the Tenant.perform or Tenant.save methods
*
* @param {Model} model
* @param {any} value
*/
async find(model, value) {
this.capture([model]);
const found = await model.find(value);
this.release([model]);
return found;
}
/**
* Finds the model from the tenant database by key and value, the model will not have the tenant connection when
* returned and further actions must be used with the Tenant.perform or Tenant.save methods
*
* @param {Model} model
* @param {String} key
* @param {any} value
*/
async findBy(model, key, value) {
this.capture([model]);
const found = await model.findBy(key, value);
this.release([model]);
return found;
}
}
module.exports = Tenant;
'use strict'
class Tenant {
register (Model) {
Object.defineProperties(Model, {
// Handles updating the connection used by the model
connection: {
get: () => this.connection,
set: (connection) => this.connection = connection,
},
// Handles storing the previous connection
previousConnection: {
get: () => this.previousConnection,
set: (prev) => this.previousConnection = prev,
}
})
}
}
module.exports = Tenant
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment