From f2fa40411e036137343291db146dd8a4fb3d4ca9 Mon Sep 17 00:00:00 2001 From: jrea Date: Fri, 20 Oct 2023 14:32:07 -0400 Subject: [PATCH] fix: support multiple instances of niledatabase --- packages/server/src/Server.test.ts | 7 +- packages/server/src/Server.ts | 32 ++----- packages/server/src/db/DBManager.ts | 42 +++++++++ packages/server/src/db/NileInstance.ts | 87 +++++++++++++++++++ packages/server/src/db/db.test.ts | 2 +- packages/server/src/db/index.ts | 104 +---------------------- packages/server/src/utils/Event/index.ts | 7 ++ 7 files changed, 149 insertions(+), 132 deletions(-) create mode 100644 packages/server/src/db/DBManager.ts create mode 100644 packages/server/src/db/NileInstance.ts diff --git a/packages/server/src/Server.test.ts b/packages/server/src/Server.test.ts index 90d69681..c327f59d 100644 --- a/packages/server/src/Server.test.ts +++ b/packages/server/src/Server.test.ts @@ -14,7 +14,7 @@ describe('server', () => { }); expect(server.config.api.basePath).toEqual('https://api.thenile.dev'); }); - it('sets a tenant id everywhere when set', async () => { + it('sets a tenant id everywhere when set', () => { const config = { database: 'database', workspace: 'workspace', @@ -27,10 +27,5 @@ describe('server', () => { const _api = nile.api[api]; expect(_api.tenantId).toEqual('tenantId'); } - - // @ts-expect-error - checking db - expect(nile._db.tenantId).toEqual('tenantId'); - // @ts-expect-error - checking db - expect(nile._db.userId).toEqual('userId'); }); }); diff --git a/packages/server/src/Server.ts b/packages/server/src/Server.ts index e3ed1894..b208541b 100644 --- a/packages/server/src/Server.ts +++ b/packages/server/src/Server.ts @@ -4,7 +4,8 @@ import Auth from './auth'; import Users from './users'; import Tenants from './tenants'; import { watchTenantId, watchToken, watchUserId } from './utils/Event'; -import NileDatabase, { NileDatabaseI } from './db'; +import DbManager, { NileDatabaseI } from './db'; +import DBManager from './db/DBManager'; type Api = { auth: Auth; @@ -12,31 +13,29 @@ type Api = { tenants: Tenants; }; -const init = (config: Config): [Api, NileDatabase] => { +const init = (config: Config): [Api] => { const auth = new Auth(config); const users = new Users(config); const tenants = new Tenants(config); - const db = new NileDatabase(config); return [ { auth, users, tenants, }, - db, ]; }; class Server { config: Config; api: Api; - private _db: NileDatabase; + private manager: DbManager; constructor(config?: ServerConfig) { this.config = new Config(config); - const [api, knex] = init(this.config); + const [api] = init(this.config); this.api = api; - this._db = knex; + this.manager = new DBManager(this.config); watchTenantId((tenantId) => { this.tenantId = tenantId; @@ -50,15 +49,7 @@ class Server { } setConfig(cfg: Config) { - if (cfg.db.connection) { - this.config.db.connection = cfg.db.connection; - } - if (cfg.database && this.database !== cfg.workspace) { - this.database = cfg.database; - } - if (cfg.workspace && this.workspace !== cfg.workspace) { - this.workspace = cfg.workspace; - } + this.config = new Config(cfg); } set database(val: string | void) { @@ -89,9 +80,6 @@ class Server { this.config.userId = userId; - // update the db with config values - this._db.setConfig(this.config); - if (this.api) { this.api.auth.userId = this.config.userId; this.api.users.userId = this.config.userId; @@ -106,8 +94,6 @@ class Server { set tenantId(tenantId: string | undefined | null) { this.database = this.config.database; this.config.tenantId = tenantId; - // update the db with config values - this._db.setConfig(this.config); if (this.api) { this.api.auth.tenantId = tenantId; @@ -133,13 +119,13 @@ class Server { get db(): NileDatabaseI { // only need to interact with the knex object //@ts-expect-error - because that's where it is in the proxy - return this._db.db.db; + return this.manager.getConnection(this.config).knex; } } // export default Server; export default function Nile(config: ServerConfig) { - const server = new Server(); + const server = new Server(config); server.setConfig(new Config(config as ServerConfig)); return server; } diff --git a/packages/server/src/db/DBManager.ts b/packages/server/src/db/DBManager.ts new file mode 100644 index 00000000..ef31babb --- /dev/null +++ b/packages/server/src/db/DBManager.ts @@ -0,0 +1,42 @@ +import { Config } from '../utils/Config'; +import { watchEvictPool } from '../utils/Event'; + +import NileDatabase, { NileDatabaseI } from './NileInstance'; + +export default class DBManager { + connections: Map; + + private makeId( + tenantId?: string | undefined | null, + userId?: string | undefined | null + ) { + if (tenantId && userId) { + return `${tenantId}:${userId}`; + } + if (tenantId) { + return `${tenantId}`; + } + return 'base'; + } + constructor(config: Config) { + this.connections = new Map(); + // add the base one, so you can at least query + const id = this.makeId(); + this.connections.set(id, new NileDatabase(new Config(config), id)); + watchEvictPool((id) => { + if (id && this.connections.has(id)) { + this.connections.delete(id); + } + }); + } + + getConnection(config: Config): NileDatabaseI { + const id = this.makeId(config.tenantId, config.userId); + const existing = this.connections.get(id); + if (existing) { + return existing as unknown as NileDatabaseI; + } + this.connections.set(id, new NileDatabase(new Config(config), id)); + return this.connections.get(id) as unknown as NileDatabaseI; + } +} diff --git a/packages/server/src/db/NileInstance.ts b/packages/server/src/db/NileInstance.ts new file mode 100644 index 00000000..3134b869 --- /dev/null +++ b/packages/server/src/db/NileInstance.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import knex, { Knex } from 'knex'; + +import { Config } from '../utils/Config'; +import { evictPool } from '../utils/Event'; + +// doing this now, to provide flexibility later +class NileDatabase { + knex: Knex; + // db: Knex; + tenantId?: undefined | null | string; + userId?: undefined | null | string; + id: string; + config: any; + + constructor(config: Config, id: string) { + this.id = id; + let poolConfig = {}; + const afterCreate = ( + conn: { + on: any; + query: (query: string, cb: (err: unknown) => void) => void; + }, + done: (err: unknown, conn: unknown) => void + ) => { + const query = [`SET nile.tenant_id = '${config.tenantId}'`]; + if (config.userId) { + if (!config.tenantId) { + // eslint-disable-next-line no-console + console.warn( + 'A user id cannot be set in context without a tenant id' + ); + } + query.push(`SET nile.user_id = '${config.userId}'`); + } + // in this example we use pg driver's connection API + conn.query(query.join(';'), function (err: unknown) { + done(err, conn); + }); + }; + if (config.tenantId) { + if (config.db.pool?.afterCreate) { + // eslint-disable-next-line no-console + console.log( + 'Providing an pool configuration will stop automatic tenant context setting.' + ); + } else if (config.db.pool) { + poolConfig = { + ...config.db.pool, + afterCreate, + }; + } else if (!config.db.pool) { + poolConfig = { + afterCreate, + }; + } + } + + this.config = { + ...config, + db: { + ...config.db, + connection: { + ...config.db.connection, + database: config.db.connection.database ?? config.database, + }, + pool: poolConfig, + }, + }; + const knexConfig = { ...this.config.db, client: 'pg' }; + + // start the timer for cleanup + this.startTimeout(); + + this.knex = knex(knexConfig); + } + + startTimeout() { + setTimeout(() => { + this.knex.destroy(); + evictPool(this.id); + }, this.config.db.pool.idleTimeoutMillis ?? 30000); + } +} + +export type NileDatabaseI = (table?: string) => Knex; +export default NileDatabase; diff --git a/packages/server/src/db/db.test.ts b/packages/server/src/db/db.test.ts index f5494db5..558f60c7 100644 --- a/packages/server/src/db/db.test.ts +++ b/packages/server/src/db/db.test.ts @@ -1,6 +1,6 @@ import NileDB from './index'; -const properties = ['knex', 'tenantId', 'userId', 'db', 'config']; +const properties = ['connections']; describe('db', () => { it('has expected properties', () => { const db = new NileDB({ diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index b4a24824..b2b40b5a 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -1,102 +1,2 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import knex, { Knex } from 'knex'; - -import { Config } from '../utils/Config'; - -// doing this now, to provide flexibility later -class NileDatabase { - knex: Knex; - db: Knex; - tenantId?: undefined | null | string; - userId?: undefined | null | string; - config: any; - - constructor(config: Config) { - this.config = { ...config, client: 'pg' }; - this.knex = knex(this.config); - // Create a proxy to intercept method calls - // @ts-expect-error - proxy, but knex - this.db = new Proxy(this, { - get: (target, method) => { - if (method === 'tenantId') { - return this.tenantId; - } - if (method === 'userId') { - return this.userId; - } - if (method === 'db') { - return (...args: any) => { - //@ts-expect-error - its a string - return target.knex.table(...args); - }; - } - //@ts-expect-error - its a string - if (typeof target.knex[method] === 'function') { - return (...args: any) => { - //@ts-expect-error - its a string - return target.knex[method](...args); - }; - } else { - return target.knex; - } - }, - }); - } - ensureUpToDate() { - // Close the existing pool connections and update the Knex instance with the latest config - this.knex.destroy(); - this.knex = knex({ ...this.config.db, client: 'pg' }); - } - setConfig(newConfig: Config) { - const { tenantId, userId } = newConfig; - this.tenantId = tenantId; - this.userId = userId; - let poolConfig = {}; - const afterCreate = ( - conn: { - on: any; - query: (query: string, cb: (err: unknown) => void) => void; - }, - done: (err: unknown, conn: unknown) => void - ) => { - // console.log(this.tenantId, this.userId, 'in create'); - const query = [`SET nile.tenant_id = '${this.tenantId}'`]; - if (this.userId) { - if (!this.tenantId) { - // eslint-disable-next-line no-console - console.warn( - 'A user id cannot be set in context without a tenant id' - ); - } - query.push(`SET nile.user_id = '${this.userId}'`); - } - // in this example we use pg driver's connection API - conn.query(query.join(';'), function (err: unknown) { - done(err, conn); - }); - }; - if (this.tenantId) { - if (newConfig.db.pool?.afterCreate) { - // eslint-disable-next-line no-console - console.log( - 'Providing an pool configuration will stop automatic tenant context setting.' - ); - } else if (newConfig.db.pool) { - poolConfig = { - ...newConfig.db.pool, - afterCreate, - }; - } else if (!newConfig.db.pool) { - poolConfig = { - afterCreate, - }; - } - } - - this.config = { ...newConfig, db: { ...newConfig.db, pool: poolConfig } }; - this.ensureUpToDate(); - } -} - -export type NileDatabaseI = (table?: string) => Knex; -export default NileDatabase; +export { default } from './DBManager'; +export { NileDatabaseI } from './NileInstance'; diff --git a/packages/server/src/utils/Event/index.ts b/packages/server/src/utils/Event/index.ts index 0f2f11a6..3a467676 100644 --- a/packages/server/src/utils/Event/index.ts +++ b/packages/server/src/utils/Event/index.ts @@ -4,6 +4,7 @@ enum Events { User = 'userId', Tenant = 'tenantId', Token = 'token', + EvictPool = 'EvictPool', } class Eventer { events: { [key: string]: EventFn[] }; @@ -54,3 +55,9 @@ export const updateToken = (val: BusValues) => { }; export const watchToken = (cb: EventFn) => eventer.subscribe(Events.Token, cb); + +export const watchEvictPool = (cb: EventFn) => + eventer.subscribe(Events.EvictPool, cb); +export const evictPool = (val: BusValues) => { + eventer.publish(Events.EvictPool, val); +};