0.1.10 • Published 1 month ago

@giogaspa/fastify-multitenant v0.1.10

Weekly downloads
-
License
MIT
Repository
github
Last release
1 month ago

: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-multitenant

Using yarn:

yarn add @giogaspa/fastify-multitenant

Usage

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.

0.1.10

1 month ago

0.1.8

6 months ago

0.1.9

6 months ago

0.1.7

10 months ago

0.1.6

10 months ago

0.1.5

10 months ago

0.1.4

11 months ago

0.1.3

11 months ago

0.1.2

11 months ago

0.1.1

11 months ago

0.1.0

11 months ago