From 80318d30830922ed811e0708e0f14df29fc91d7a Mon Sep 17 00:00:00 2001 From: Ebrahim Date: Thu, 4 Aug 2022 10:16:04 +0200 Subject: [PATCH] added connection manager and id helper functions --- lib/ConnectionManager/addTenantConnection.js | 19 +++ lib/ConnectionManager/connections.js | 19 +++ lib/ConnectionManager/destroyConnections.js | 9 ++ lib/ConnectionManager/getTenantConnection.js | 9 ++ lib/ConnectionManager/index.js | 11 ++ lib/ConnectionManager/initTenants.js | 16 ++ lib/helper_functions/addTenantId.js | 3 + lib/helper_functions/getTenantFromId.js | 4 + lib/models/Tenant.js | 31 ++++ tests/database/multi-tenancy.spec.js | 157 +++++++++++++++++++ 10 files changed, 278 insertions(+) create mode 100644 lib/ConnectionManager/addTenantConnection.js create mode 100644 lib/ConnectionManager/connections.js create mode 100644 lib/ConnectionManager/destroyConnections.js create mode 100644 lib/ConnectionManager/getTenantConnection.js create mode 100644 lib/ConnectionManager/index.js create mode 100644 lib/ConnectionManager/initTenants.js create mode 100644 lib/helper_functions/addTenantId.js create mode 100644 lib/helper_functions/getTenantFromId.js create mode 100644 lib/models/Tenant.js create mode 100644 tests/database/multi-tenancy.spec.js diff --git a/lib/ConnectionManager/addTenantConnection.js b/lib/ConnectionManager/addTenantConnection.js new file mode 100644 index 0000000000..f9c8f23345 --- /dev/null +++ b/lib/ConnectionManager/addTenantConnection.js @@ -0,0 +1,19 @@ +const Tenant = require('../models/Tenant'); +const Database = require('../Database/index'); +const { setConnection, getConnection } = require('./connections'); + +module.exports = async (organizationId) => { + try { + const alreadyConnected = getConnection(organizationId); + if (alreadyConnected) return alreadyConnected; + const [tenant] = await Tenant.find({ organization: organizationId }); + if (!tenant || !tenant.url) throw new Error('Organization not found!'); + console.log('tenant', tenant); + let connection = new Database(tenant.url); + await connection.connect(); + setConnection(tenant.organization, connection); + return connection; + } catch (e) { + console.log('organization not found!'); + } +}; diff --git a/lib/ConnectionManager/connections.js b/lib/ConnectionManager/connections.js new file mode 100644 index 0000000000..55a14f75ed --- /dev/null +++ b/lib/ConnectionManager/connections.js @@ -0,0 +1,19 @@ +const connections = {}; + +const setConnection = (orgId, connection) => { + connections[`${orgId}`] = connection; +}; + +const getConnection = (orgId) => { + if (!connections[orgId]) return null; + return connections[orgId]; +}; + +const destroy = async () => { + for (let conn in connections) { + await connections[conn].disconnect(); + delete connections[conn]; + } +}; + +module.exports = { setConnection, getConnection, destroy }; diff --git a/lib/ConnectionManager/destroyConnections.js b/lib/ConnectionManager/destroyConnections.js new file mode 100644 index 0000000000..d5e6a09968 --- /dev/null +++ b/lib/ConnectionManager/destroyConnections.js @@ -0,0 +1,9 @@ +const connections = require('./connections'); + +module.exports = async () => { + try { + await connections.destroy(); + } catch (e) { + console.log(e); + } +}; diff --git a/lib/ConnectionManager/getTenantConnection.js b/lib/ConnectionManager/getTenantConnection.js new file mode 100644 index 0000000000..3e0084356b --- /dev/null +++ b/lib/ConnectionManager/getTenantConnection.js @@ -0,0 +1,9 @@ +const { getConnection } = require('./connections'); + +module.exports = (organizationId) => { + try { + return getConnection(organizationId); + } catch (e) { + console.log('organization not found!'); + } +}; diff --git a/lib/ConnectionManager/index.js b/lib/ConnectionManager/index.js new file mode 100644 index 0000000000..00ad9c365b --- /dev/null +++ b/lib/ConnectionManager/index.js @@ -0,0 +1,11 @@ +const addTenantConnection = require('./addTenantConnection'); +const getTenantConnection = require('./getTenantConnection'); +const initTenants = require('./initTenants'); +const destroyConnections = require('./destroyConnections'); + +module.exports = { + addTenantConnection, + getTenantConnection, + initTenants, + destroyConnections, +}; diff --git a/lib/ConnectionManager/initTenants.js b/lib/ConnectionManager/initTenants.js new file mode 100644 index 0000000000..274f2f5c8e --- /dev/null +++ b/lib/ConnectionManager/initTenants.js @@ -0,0 +1,16 @@ +const Tenant = require('../models/Tenant'); +const Database = require('../Database/index'); +const { setConnection } = require('./connections'); + +module.exports = async () => { + try { + const databases = await Tenant.find(); + for (let db of databases) { + let connection = new Database(db.url); + await connection.connect(); + setConnection(db.organization, connection); + } + } catch (e) { + console.log('connection failed'); + } +}; diff --git a/lib/helper_functions/addTenantId.js b/lib/helper_functions/addTenantId.js new file mode 100644 index 0000000000..7189ca14f4 --- /dev/null +++ b/lib/helper_functions/addTenantId.js @@ -0,0 +1,3 @@ +module.exports = (tenantId, id) => { + return tenantId + ' ' + id; +}; diff --git a/lib/helper_functions/getTenantFromId.js b/lib/helper_functions/getTenantFromId.js new file mode 100644 index 0000000000..75c1d59edc --- /dev/null +++ b/lib/helper_functions/getTenantFromId.js @@ -0,0 +1,4 @@ +module.exports = (fullId) => { + const [tenantId, id] = fullId.split(' '); + return { tenantId, id }; +}; diff --git a/lib/models/Tenant.js b/lib/models/Tenant.js new file mode 100644 index 0000000000..d520f09134 --- /dev/null +++ b/lib/models/Tenant.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const tenantSchema = new Schema({ + organization: { + type: Schema.Types.ObjectId, + ref: 'Organizaiton', + }, + url: { + type: String, + required: true, + }, + type: { + type: String, + enum: ['MONGO', 'POSTGRES'], + default: 'MONGO', + required: true, + }, + status: { + type: String, + required: true, + default: 'ACTIVE', + enum: ['ACTIVE', 'BLOCKED', 'DELETED'], + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +module.exports = mongoose.model('Tenant', tenantSchema); diff --git a/tests/database/multi-tenancy.spec.js b/tests/database/multi-tenancy.spec.js new file mode 100644 index 0000000000..1e322983f8 --- /dev/null +++ b/tests/database/multi-tenancy.spec.js @@ -0,0 +1,157 @@ +const shortid = require('shortid'); +const Tenant = require('../../lib/models/Tenant'); +const connectionManager = require('../../lib/ConnectionManager'); + +const database = require('../../db'); +const getUserIdFromSignUp = require('../functions/getUserIdFromSignup'); +const Organization = require('../../lib/models/Organization'); +const User = require('../../lib/models/User'); +// const Post = require('../../lib/models/Post'); +const tenantUrl = + 'mongodb://localhost:27017/org1-tenant?retryWrites=true&w=majority'; +const secondTenantUrl = + 'mongodb://localhost:27017/org2-tenant?retryWrites=true&w=majority'; + +let adminId; +let organizationId; +let secondOrganizationId; + +beforeAll(async () => { + // setting up 1 org, one user with 1 tenant record (on the main database). + require('dotenv').config(); + await database.connect(); + + const adminEmail = `${shortid.generate().toLowerCase()}@test.com`; + adminId = await getUserIdFromSignUp(adminEmail); + + const organization = new Organization({ + name: 'tenant organization', + description: 'testing org', + isPublic: true, + visibileInSearch: true, + status: 'ACTIVE', + members: [adminId], + admins: [adminId], + posts: [], + membershipRequests: [], + blockedUsers: [], + groupChats: [], + image: '', + creator: adminId, + }); + const savedOrg = await organization.save(); + organizationId = savedOrg._id; + + const admin = await User.findById(adminId); + admin.overwrite({ + ...admin._doc, + joinedOrganizations: [organizationId], + createdOrganizations: [organizationId], + adminFor: [organizationId], + }); + await admin.save(); + + const tenant = new Tenant({ + organization: organizationId, + url: tenantUrl, + }); + await tenant.save(); +}); + +afterAll(async () => { + const conn1 = connectionManager.getTenantConnection(organizationId); + const conn2 = connectionManager.getTenantConnection(secondOrganizationId); + await conn1.Post.deleteMany(); + await conn2.Post.deleteMany(); + await User.findByIdAndDelete(adminId); + await Organization.findByIdAndDelete(organizationId); + await Organization.findByIdAndDelete(secondOrganizationId); + await Tenant.deleteMany({}); + await connectionManager.destroyConnections(); + await database.disconnect(); +}); + +describe('tenant is working and transparent from main db', () => { + test('initTenants and destroyConnections', async () => { + let conn = connectionManager.getTenantConnection(organizationId); + expect(conn).toBe(null); + await connectionManager.initTenants(); + conn = connectionManager.getTenantConnection(organizationId); + expect(conn).toBeTruthy(); + await connectionManager.destroyConnections(); + conn = connectionManager.getTenantConnection(organizationId); + expect(conn).toBe(null); + await connectionManager.initTenants(); + }); + test('addConnection', async () => { + const organization = new Organization({ + name: 'second tenant organization', + description: 'testing org', + isPublic: true, + visibileInSearch: true, + status: 'ACTIVE', + members: [adminId], + admins: [adminId], + posts: [], + membershipRequests: [], + blockedUsers: [], + groupChats: [], + image: '', + creator: adminId, + }); + + const savedOrg = await organization.save(); + secondOrganizationId = savedOrg._id; + + const admin = await User.findById(adminId); + admin.overwrite({ + ...admin._doc, + joinedOrganizations: [organizationId, secondOrganizationId], + createdOrganizations: [organizationId, secondOrganizationId], + adminFor: [organizationId, secondOrganizationId], + }); + await admin.save(); + const tenant = new Tenant({ + organization: secondOrganizationId, + url: secondTenantUrl, + }); + await tenant.save(); + + const conn = await connectionManager.addTenantConnection( + secondOrganizationId + ); + expect(conn).toBeTruthy(); + const posts = await conn.Post.find(); + expect(posts).toEqual([]); + }); + + test('getConnection', async () => { + const conn = connectionManager.getTenantConnection(organizationId); + const newPost = new conn.Post({ + status: 'ACTIVE', + likedBy: [adminId], + likeCount: 1, + comments: [], + text: 'a', + title: 'a', + imageUrl: 'a.png', + videoUrl: 'a', + creator: adminId, + organization: organizationId, + }); + await newPost.save(); + const [savedPost] = await conn.Post.find(); + expect(savedPost).toBeTruthy(); + }); + + test('Isolated tenants', async () => { + const conn1 = connectionManager.getTenantConnection(organizationId); + const conn2 = connectionManager.getTenantConnection(secondOrganizationId); + + const firstOrgPosts = await conn1.Post.find(); + const secondOrgPosts = await conn2.Post.find(); + + expect(firstOrgPosts).toHaveLength(1); + expect(secondOrgPosts).toHaveLength(0); + }); +});