Skip to content

Commit

Permalink
fix: support multiple instances of niledatabase
Browse files Browse the repository at this point in the history
  • Loading branch information
jrea committed Oct 20, 2023
1 parent e0e4ab6 commit f2fa404
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 132 deletions.
7 changes: 1 addition & 6 deletions packages/server/src/Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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');
});
});
32 changes: 9 additions & 23 deletions packages/server/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,38 @@ 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;
users: Users;
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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
42 changes: 42 additions & 0 deletions packages/server/src/db/DBManager.ts
Original file line number Diff line number Diff line change
@@ -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<string, NileDatabase>;

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;
}
}
87 changes: 87 additions & 0 deletions packages/server/src/db/NileInstance.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion packages/server/src/db/db.test.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
104 changes: 2 additions & 102 deletions packages/server/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -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';
7 changes: 7 additions & 0 deletions packages/server/src/utils/Event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ enum Events {
User = 'userId',
Tenant = 'tenantId',
Token = 'token',
EvictPool = 'EvictPool',
}
class Eventer {
events: { [key: string]: EventFn[] };
Expand Down Expand Up @@ -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);
};

0 comments on commit f2fa404

Please sign in to comment.