saas-vs v1.0.4
SAAS-VS (Software as a service)
Software as a service is a multi-tenant software hosting model. Using it's functions you will be able to register multiple tenants for your product and able to create separate database and product related tables for each tenant.
It can be used in two ways:
1. Single produt multitenant approach:
If you have only single product and there are multiple tenants and you want to host your product for multiple tenants then you can use this package with isMultiProduct flag as false. Where you do not need to provide any unique product id for your product. This usecase is the default behaviour of package.
Architecture diagram for single product multitenant usecase:
Sequence diagram for single product multitenant usecase
2. Multiple produts multitenant approach:
If you have multiple products and there are multiple tenants and you want to host your products for multiple tenants then you can use this package by passing isMultiProduct flag as true . Where you need to provide a unique product id for each of your products while using the package functions.
Architecture diagram for multiple products multitenant usecase:
Sequence diagram for multiple products multitenant usecase
Installation
- This is a Node.js module available through the npm
Before installing, download and install Node.js.
Installation is done using the
npm install
command:
Using npm:
# Install package with npm
$ npm install saas-vs
Import module
import { MultiTenantController } from "saas-vs";
const multiTenantController = new MultiTenantController();
Requirements:
- Create databse folder in root of your project and inside it schema folder
and place file containing schema creation queries i.e
.sql
file for a product inside it.
For example
create schema.sql
inside database/schema
folder path.
this can contain queries like this
CREATE TABLE IF NOT EXISTS
employee (
autoUniqueId INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
firstName VARCHAR(255) NOT NULL,
middleName VARCHAR(255),
lastName VARCHAR(255) NOT NULL,
company INT(11) NOT NULL,
}ENGINE = InnoDB;
- There should be one
db.config.json
file present in your root directory with below required keys. You can take reference fromdb.config.sample
file as well present inside root of this package.
For single product usecase(i.e default behaviour with isMultiProduct flag as false) db.config.json file will look like:
{
"MTS_MASTER_DB_PORT": 3306,
"MTS_MASTER_DB_HOSTNAME": "YOUR_DB_HOSTNAME",
"MTS_MASTER_DB_NAME": "YOUR_DB_NAME",
"MTS_MASTER_DB_USERNAME": "YOUR_DB_USERNAME",
"MTS_MASTER_DB_PASSWORD": "YOUR_DB_PASSWORD",
"MTS_PRODUCT_DB_SCHEMA_PATH": "PRODUCT_SCHEMA_PATH",
}
This will include your master db details in which all registered organisations information is stored. There will be one more key MTS_PRODUCT_DB_SCHEMA_PATH
which will be path of schema file stored in database/schema
folder in your project for a registered product. All the queries listed in this file will get execute during post call.
For example for your product suppose schema file name is schema.sql
(as specified in point one above) , then in db.config.json MTS_PRODUCT_DB_SCHEMA_PATH
key will look like this
"MTS_PRODUCT_DB_SCHEMA_PATH" : "schema.sql"
For multiple products usecase (i.e with isMultiProduct flag as true) db.config.json file will look like:
{
"MTS_MASTER_DB_PORT": 3306,
"MTS_MASTER_DB_HOSTNAME": "YOUR_DB_HOSTNAME",
"MTS_MASTER_DB_NAME": "YOUR_DB_NAME",
"MTS_MASTER_DB_USERNAME": "YOUR_DB_USERNAME",
"MTS_MASTER_DB_PASSWORD": "YOUR_DB_PASSWORD",
"MTS_PRODUCT_CONFIG": {
"PRODUCT_1_ID": {
"db_schema_path": "PRODUCT_1_SCHEMA_PATH"
},
"PRODUCT_2_ID": {
"db_schema_path": "PRODUCT_2_SCHEMA_PATH"
}
}
}
This will include your master db details in which all registered organisation information is stored. There will be one more key MTS_PRODUCT_CONFIG
which will be an object containing db schema path for requested products. You can specify schema path for multiple products in it.
db_schema_path
: It will be path of schema file stored in database/schema
folder in your project for a registered product. All the queries listed in this file will get execute during post call.
For example for product with id 41e27793-4d90-11ef-8910-0a9df3751e43
, suppose schema file name is schema.sql
(as specified in point one above) , then in db.config.json MTS_PRODUCT_CONFIG
key will look like this
"MTS_PRODUCT_CONFIG" : {"41e27793-4d90-11ef-8910-0a9df3751e43": {"db_schema_path": "schema.sql"}}
1. post : To register a particular organisation for a product
post(orgInformation,isMultiProduct)
function creates database , schema for registered product and also creates customers table inside master db if not exists and inserts record in it for registered organisation with newly created db details. In response it will return newly created orgId along with all the details for registered product.
post(orgInformation: IOrgInformation, isMultiProduct:boolean =false): Promise<ICustomer>;
Usage if single product multitenant approach:
customers table with below schema will be created automatically if not exists in your master db. It will contain details of all registered organisations.
CREATE TABLE `customers` (
`id` bigint NOT NULL AUTO_INCREMENT,
`org_id` varchar(40) NOT NULL DEFAULT (uuid()),
`org_name` varchar(255) NOT NULL,
`domain` varchar(255) NOT NULL,
`first_name` varchar(255) NOT NULL,
`last_name` varchar(255) DEFAULT NULL,
`contact_number` varchar(50) NOT NULL,
`contact_email` varchar(255) NOT NULL,
`phone_country_code` varchar(20) DEFAULT NULL,
`website` varchar(255) DEFAULT NULL,
`license` text,
`license_expiry` date DEFAULT NULL,
`address` text,
`db_credentials` json DEFAULT NULL,
`env_vars` json DEFAULT NULL,
`status` enum('ACTIVE','INACTIVE') NOT NULL,
`created_at` datetime NOT NULL DEFAULT (utc_timestamp()),
`updated_at` datetime NOT NULL DEFAULT (utc_timestamp()),
PRIMARY KEY (`id`),
UNIQUE KEY `unique_domain__customers` (`domain`),
UNIQUE KEY `unique_org_id__customers` (`org_id`),
INDEX `domain_and_status_idx` (`domain`,`status`)
) ENGINE=InnoDB;
const orgInformation = {
first_name: "abc",
email: "abc@org.com",
org_name: "org pvt. ltd.",
contact_number: "9999999999",
last_name: "abc", //optional
phone_country_code: "+91", //optional
website: "abc.com", //optional
license: "abc", //optional
license_expiry: "2024-11-1", //optional
address: "abc", //optional
status: "ACTIVE" //optional
env_vars: {var1:'abc'} //optional
}
const response = await multiTenantController.post(orgInformation)
or
const response = await multiTenantController.post(orgInformation, false)
// response will look like
{
"org_id": "e3afd9ae-177c-4809-b39e-39c1167bc7c3",
"id": 1,
"first_name": "abc",
"last_name": "abc",
"contact_number": "9999999999",
"contact_email": "abc@org.com",
"org_name": "org pvt. ltd.",
"phone_country_code": "+91",
"domain": "org.com",
"status": "ACTIVE",
"website": "org.com",
"license": "abc",
"license_expiry": "2024-10-31T18:30:00.000Z",
"address": "abc",
"env_vars": {var1:'abc'},
"db_credentials": {
"RDS_PORT": 3306,
"RDS_DATABASE": "org",
"RDS_HOSTNAME": "abc",
"RDS_PASSWORD": "abc",
"RDS_USERNAME": "abc"
}
}
Usage if multiple products multitenant approach:
customers table with below schema will be created automatically if not exists in your master db. It will contain details of all registered organisations.
CREATE TABLE `customers` (
`id` bigint NOT NULL AUTO_INCREMENT,
`org_id` varchar(40) NOT NULL DEFAULT (uuid()),
`org_name` varchar(255) NOT NULL,
`domain` varchar(255) NOT NULL,
`product_id` varchar(40) NOT NULL,
`first_name` varchar(255) NOT NULL,
`last_name` varchar(255) DEFAULT NULL,
`contact_number` varchar(50) NOT NULL,
`contact_email` varchar(255) NOT NULL,
`phone_country_code` varchar(20) DEFAULT NULL,
`website` varchar(255) DEFAULT NULL,
`license` text,
`license_expiry` date DEFAULT NULL,
`address` text,
`db_credentials` json DEFAULT NULL,
`env_vars` json DEFAULT NULL,
`status` enum('ACTIVE','INACTIVE') NOT NULL,
`created_at` datetime NOT NULL DEFAULT (utc_timestamp()),
`updated_at` datetime NOT NULL DEFAULT (utc_timestamp()),
PRIMARY KEY (`id`),
UNIQUE KEY `unique_domain_and_product_id__customers` (`domain`,`product_id`),
UNIQUE KEY `unique_org_id_and_product_id__customers` (`org_id`,`product_id`),
INDEX `domain_product_id_and_status_idx` (`domain`,`product_id`,`status`)
) ENGINE=InnoDB;
If domain already exists then it will create new entry with same orgId for new product. product_id is required in this case.
const orgInformation = {
product_id: "41e27793-4d90-11ef-8910-0a9df3751e43",
first_name: "abc",
email: "abc@org.com",
org_name: "org pvt. ltd.",
contact_number: "9999999999",
last_name: "abc", //optional
phone_country_code: "+91", //optional
website: "abc.com", //optional
license: "abc", //optional
license_expiry: "2024-11-1", //optional
address: "abc", //optional
status: "ACTIVE" //optional
env_vars: {var1:'abc'} //optional
}
const response = await multiTenantController.post(orgInformation, true)
// response will look like
{
"org_id": "e3afd9ae-177c-4809-b39e-39c1167bc7c3",
"id": 1,
"first_name": "abc",
"last_name": "abc",
"contact_number": "9999999999",
"contact_email": "abc@org.com",
"org_name": "org pvt. ltd.",
"phone_country_code": "+91",
"domain": "org.com",
"product_id": "41e27793-4d90-11ef-8910-0a9df3751e43",
"status": "ACTIVE",
"website": "org.com",
"license": "abc",
"license_expiry": "2024-10-31T18:30:00.000Z",
"address": "abc",
"env_vars": {var1:'abc'},
"db_credentials": {
"RDS_PORT": 3306,
"RDS_DATABASE": "org",
"RDS_HOSTNAME": "abc",
"RDS_PASSWORD": "abc",
"RDS_USERNAME": "abc"
}
}
2. get : To get the registered organisation details for a product
get(domainName,isMultiProduct,productId)
function fetches the registered organisation details. If no registered organisation is found then it will return null.
get(domainName: string, isMultiProduct: boolean = false, productId?: string): Promise<ICustomer | null>;
Usage if single product multitenant approach:
const domainName = "abc";
const response = await multiTenantController.get(domainName);
or
const response = await multiTenantController.get(domainName, false);
// response will be same as in case of post
Usage if multiple products multitenant approach:
productId is required in this case
const domainName = "abc";
const productId = "41e27793-4d90-11ef-8910-0a9df3751e43";
const response = await multiTenantController.get(domainName, true, productId);
// response will be same as in case of post
3. isValid : To check if domain has been registered for a product and it's status is currently active or not
isValid(domainName,isMultiProduct,productId)
function finds the active org on the basis of domain name and product id (if it's a multiple producs multitenant use case) from customers table. If it exists then it will return true otherwise false.
isValid(domainName: string, isMultiProduct: boolean = false, productId?: string): Promise<Boolean>;
Usage if single product multitenant approach:
const domainName = "abc";
const response = await multiTenantController.isValid(domainName);
or
const response = await multiTenantController.isValid(domainName, false);
// response will be true or false
Usage if multiple products multitenant approach:
productId is required in this case
const domainName = "abc";
const productId = "41e27793-4d90-11ef-8910-0a9df3751e43";
const response = await multiTenantController.isValid(domainName, true, productId);
// response will be true or false
4. getConnectionDetails : To get db connecton details for a registered product
getConnectionDetails(domainName,isMultiProduct,productId)
function returns the db details on the basis of domain name and product id(if it's a multiple producs multitenant use case) from customers table.
getConnectionDetails(domainName: string, isMultiProduct: boolean = false, productId?: string): Promise<IDbCredentials | null>;
Usage if single product multitenant approach:
const domainName = "abc";
const response = await multiTenantController.getConnectionDetails(domainName);
or
const response = await multiTenantController.getConnectionDetails(domainName, false);
// response will look like
{
"RDS_PORT": 3306,
"RDS_DATABASE": "abc",
"RDS_HOSTNAME": "abc",
"RDS_PASSWORD": "abc",
"RDS_USERNAME": "abc"
}
Usage if multiple products multitenant approach:
productId is required in this case
const domainName = "abc";
const productId = "41e27793-4d90-11ef-8910-0a9df3751e43";
const response = await multiTenantController.getConnectionDetails(domainName, true, productId);
// response will look like
{
"RDS_PORT": 3306,
"RDS_DATABASE": "abc",
"RDS_HOSTNAME": "abc",
"RDS_PASSWORD": "abc",
"RDS_USERNAME": "abc"
}
5. put : To update registered organisation information
put(orgInformation, isMultiProduct)
function updates the information of registered organisation of a product in customers table
put(orgInformation: IOrgUpdateInformation, isMultiProduct: boolean = false): Promise<void>;
Usage if single product multitenant approach:
const orgInformation = {
domain_name: "abc",
first_name: "abc", //optional
org_name: "org pvt. ltd.", //optional
contact_number: "9999999999", //optional
last_name: "abc", //optional
phone_country_code: "+91", //optional
website: "abc.com", //optional
license: "abc", //optional
license_expiry: "2024-11-1", //optional
address: "abc", //optional
status: "ACTIVE", //optional
env_vars: {var1:'abc'}, //optional
db_credentials: {RDS_PORT: 123, RDS_DATABASE: 'abc',RDS_HOSTNAME: 'abc', RDS_PASSWORD: 'abc',RDS_USERNAME: 'abc'} //optional
};
await multiTenantController.put(orgInformation)
or
await multiTenantController.put(orgInformation, false)
Usage if multiple products multitenant approach:
product_id is required in this case
const orgInformation = {
product_id: "41e27793-4d90-11ef-8910-0a9df3751e43",
domain_name: "abc",
first_name: "abc", //optional
org_name: "org pvt. ltd.", //optional
contact_number: "9999999999", //optional
last_name: "abc", //optional
phone_country_code: "+91", //optional
website: "abc.com", //optional
license: "abc", //optional
license_expiry: "2024-11-1", //optional
address: "abc", //optional
status: "ACTIVE", //optional
env_vars: {var1:'abc'}, //optional
db_credentials: {RDS_PORT: 123, RDS_DATABASE: 'abc',RDS_HOSTNAME: 'abc', RDS_PASSWORD: 'abc',RDS_USERNAME: 'abc'} //optional
};
await multiTenantController.put(orgInformation, true)