@giogaspa/fastify-multitenant v0.1.10
:construction: @giogaspa/fastify-multitenant :construction:
Multitenant plugin for Fastify.
Supports Fastify versions ^4.26.2.
Install also @giogaspa/fastify-multitenant-cli in order to manage setup of admin and tenant DB and to manange migrations.
Status
Pre-alpha.
This is a work in progress and api could change quickly!!!
The plugin is not ready for production, use at your own risk!
Please look at the develop branch for ongoing development.
Install
Using npm:
npm i @giogaspa/fastify-multitenantUsing yarn:
yarn add @giogaspa/fastify-multitenantUsage
Require @giogaspa/fastify-multitenant and register as fastify plugin.
This will decorate your fastify instance with tenantsRepository and fastify request
is decorate with tenant, tenantDB and isTenantAdmin.
Tenant information is managed through an object implementing the TenantsRepository interface.
Currently the plugin provides implementation of these repositories:
- InMemoryRepository (useful for testing)
- JsonRepository
- PostgreSQLRepository
- MySQLRepository (under development)
If you want you can create your own custom repository, you just need to implement TenantsRepository
and pass the created repository in the plugin configuration object FastifyMultitenantPluginOption.
To determine the current tenant, an array of tenant resolvers must be passed (at least one resolver is needed)
in the plugin configuration.
Currently the plugin provides these resolvers:
- HostnameResolver
- HttpHeaderResolver
If you want to implement your own resolver extend the Resolver class.
To interact with the tenant database of the current request, you can use the request.tenantDB object,
use getRequestTenantDB() function or implement a repository that extends the RequestTenantRepository class.
RequestTenantRepository has the property db which is the db client of the current tenant.
const fastify = require('fastify')();
const { PostgreSQLRepository, HostnameResolver, HttpHeaderResolver } = require("@giogaspa/fastify-multitenant");
// Instantiate admin repository
const adminRepository = new PostgreSQLRepository({ clientConfig: { connectionString: "postgresql://postgres:1234@localhost:5432/postgres?schema=public" } });
//const adminRepository = new JsonRepository(join(__dirname, '..','.tenants.json'));
//const adminRepository = new InMemoryRepository();
fastify.register(require('@giogaspa/fastify-multitenant'), {
tenantsRepository: adminRepository, // Repository to retrieve tenant connection information.
resolverStrategies: [ // Strategies to recognize the tenant
HostnameResolver, // Hostname strategy
{
classConstructor: HttpHeaderResolver, // Header parameter strategy
config: {
admin: 'admin' // admin tenant identifier
header: 'x-tenant-id',
}
}
],
ignoreRoutePattern: /^\/auth\// //Regexp. Not mandatory
})
fastify.listen({ port: 3000 })fastify.tenantsRepository
Repository class to interact with tenants
interface TenantsRepository {
has(tenantId: any): Promise<boolean>
get(tenantId: any): Promise<Tenant | undefined>
getByHostname(hostname: string): Promise<Tenant | undefined>
add(tenant: Tenant): Promise<Tenant | undefined>
update(tenant: Tenant): Promise<Tenant | undefined>
delete(tenantId: any): Promise<boolean>
shutdown(): Promise<void>
}request.tenant
Get tenant of current request
export type Tenant = {
id: string,
hostname: string,
connectionString: string
}getRequestTenantDB()
Get resolved tenant of current request.
If the Tenant was not found, or we are not using the getRequestTenantDB within a request, the CantResolveTenant error is thrown.
import { getRequestTenant } from '@giogaspa/fastify-multitenant'
const currentTenant: Tenant = getRequestTenant()request.tenantDB
DB client of resolved tenant. The type of client depends on repository configuration.pg library clients are supported for now, but clients for mysql and other db's will also be supported in the future.
request.isTenantAdmin
Return true if request is for admin.
Custom Tenant Repository
Create class that implements TenantsRepository interface.
See also PostgreSQLRepository, JsonRepository or InMemoryRepository for real examples.
export interface TenantsRepository {
has(tenantId: any): Promise<boolean>
get(tenantId: any): Promise<Tenant | undefined>
getByHostname(hostname: string): Promise<Tenant | undefined>
add(tenant: Tenant): Promise<Tenant | undefined>
update(tenant: Tenant): Promise<Tenant | undefined>
delete(tenantId: any): Promise<boolean>
init(): Promise<void>
shutdown(): Promise<void>
}Custom Resolver
Create a class that extends Resolver and implements resolve(request: FastifyRequest) and getIdentifierFrom(request: FastifyRequest) methods.
See also HostnameResolver, or HttpHeaderResolver for real examples.
export abstract class Resolver {
repository: TenantsRepository;
config: ResolverConstructorConfigType;
constructor(repository: TenantsRepository, config: ResolverConstructorConfigType = {}) {
this.repository = repository;
this.config = config;
}
abstract resolve(request: FastifyRequest): Promise<Tenant | undefined>
abstract getIdentifierFrom(request: FastifyRequest): string | undefined
isAdmin(request: FastifyRequest): boolean {
return this.config.admin
&& this.getIdentifierFrom(request) === this.config.admin;
}
}How to interact with Tenant DB
There are two ways to interact with the tenant database:
the first is through request.tenantDB and the second is to extend the RequestTenantRepository as example below:
class UserRepository extends RequestTenantRepository {
private name = 'users';
async create(user: User): Promise<boolean>{
const query = SQL`
INSERT INTO ${SQL.quoteIdent(this.name)} (id, first_name, last_name, email)
VALUES (${user.id},${user.firstName},${user.lastName},${user.email})
`;
const r = await this.db.query(query);
return r.rowCount === 1;
}
async all(): Promise<any> {
const query = SQL`SELECT * FROM ${SQL.quoteIdent(this.name)}`;
const r = await this.db.query(query);
return r.rows;
}
...
}How to exclude a route/s from tenant resolution
Add ignoreRoutePattern regexp into plugin configuration object in order to exclude matching routes:
fastify.register(require('@giogaspa/fastify-multitenant'), {
tenantsRepository: repository,
resolverStrategies: [
...
],
ignoreRoutePattern: /^\/auth\// //Regexp
})Or add { multitenant: { exclude: true } } to route config object:
fastify
.get('/without_tenant',
{
config: {
multitenant: {
exclude: true
}
},
},
async function routeHandler() {
return 'Route without tenant resolver'
})License
Licensed under MIT.