UNPKG

multibridge

Version:

A multi-database connection framework with centralized configuration

2,002 lines (1,669 loc) 48.7 kB
# MultiBridge - Comprehensive Usage Guide This guide demonstrates how to use the `multibridge` package in a Node.js backend project for multi-tenant database operations. It covers controllers, services, custom queries, ORM integration, and model organization. ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Project Structure](#project-structure) 3. [Basic Setup](#basic-setup) 4. [Controller Layer](#controller-layer) 5. [Service Layer](#service-layer) 6. [Custom Queries with executeQuery](#custom-queries-with-executequery) 7. [ORM Integration](#orm-integration) - [Sequelize](#sequelize) - [TypeORM](#typeorm) - [Mongoose](#mongoose) - [Cassandra](#cassandra) 8. [Model Organization](#model-organization) 9. [Error Handling](#error-handling) 10. [Best Practices](#best-practices) --- ## Prerequisites 1. **Central Database**: A PostgreSQL database that stores tenant configuration mapping `appid`, `orgid`, and `appdbname` to actual database connection details. 2. **Environment Variables**: Create a `.env` file in your project root: ```env # Central Database Configuration CENTRAL_DB_HOST=localhost CENTRAL_DB_PORT=5432 CENTRAL_DB_USER=admin CENTRAL_DB_PASSWORD=password CENTRAL_DB_NAME=central_db CENTRAL_DB_TABLE=connections_config # Logging LOG_LEVEL=info # Optional: Connection Pooling & Performance CONNECTION_CACHE_MAX_SIZE=100 CONNECTION_CACHE_TTL_MS=3600000 POSTGRES_POOL_MAX=20 POSTGRES_POOL_MIN=5 MYSQL_POOL_MAX=10 QUERY_TIMEOUT_MS=30000 ``` 3. **Install Dependencies**: ```bash npm install multibridge # For ORM support (optional, install as needed) npm install sequelize pg mysql2 # For Sequelize npm install typeorm pg mysql2 mongodb # For TypeORM npm install mongoose # For Mongoose npm install cassandra-driver # For Cassandra ``` --- ## Project Structure ``` . ├── src ├── controllers ├── authController.ts ├── todoController.ts └── userController.ts ├── services ├── authService.ts ├── todoService.ts └── userService.ts ├── models ├── sequelize ├── User.ts └── Todo.ts ├── typeorm ├── User.entity.ts └── Todo.entity.ts ├── mongoose ├── userSchema.ts └── todoSchema.ts └── cassandra ├── userModel.ts └── todoModel.ts ├── types └── tenant.ts ├── middleware └── tenantMiddleware.ts ├── routes ├── authRoutes.ts ├── todoRoutes.ts └── userRoutes.ts └── server.ts ├── .env └── package.json ``` --- ## Basic Setup ### Type Definitions **`src/types/tenant.ts`** ```typescript import { ConnectVo } from "multibridge"; export interface TenantInfo extends ConnectVo { appid: string; orgid: string; appdbname: string; } // Helper to extract tenant from request (e.g., from headers, JWT, etc.) export function getTenantFromRequest(req: any): TenantInfo { // Example: Extract from headers return { appid: req.headers["x-app-id"] || req.body.appid, orgid: req.headers["x-org-id"] || req.body.orgid, appdbname: req.headers["x-app-db-name"] || req.body.appdbname, }; } ``` ### Tenant Middleware **`src/middleware/tenantMiddleware.ts`** ```typescript import { Request, Response, NextFunction } from "express"; import { getTenantFromRequest, TenantInfo } from "../types/tenant"; // Middleware to extract and validate tenant information export function tenantMiddleware(req: Request, res: Response, next: NextFunction) { try { const tenant = getTenantFromRequest(req); if (!tenant.appid || !tenant.orgid || !tenant.appdbname) { return res.status(400).json({ error: "Missing tenant information. Required: appid, orgid, appdbname", }); } // Attach tenant to request object for use in controllers (req as any).tenant = tenant; next(); } catch (error) { res.status(500).json({ error: "Failed to extract tenant information" }); } } ``` --- ## Controller Layer Controllers handle HTTP requests and delegate business logic to services. They use `runWithTenant` to establish the tenant context. ### Example: Authentication Controller **`src/controllers/authController.ts`** ```typescript import { Request, Response } from "express"; import { runWithTenant } from "multibridge"; import { authService } from "../services/authService"; import { TenantInfo } from "../types/tenant"; export class AuthController { /** * User Signup * POST /auth/signup */ static async signup(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { username, email, password } = req.body; try { await runWithTenant(tenant, async () => { const user = await authService.createUser({ username, email, password }); res.status(201).json({ success: true, data: { userId: user.id, username: user.username }, }); }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Signup failed", }); } } /** * User Login * POST /auth/login */ static async login(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { username, password } = req.body; try { await runWithTenant(tenant, async () => { const user = await authService.authenticateUser(username, password); if (user) { res.status(200).json({ success: true, data: { userId: user.id, username: user.username }, }); } else { res.status(401).json({ success: false, error: "Invalid credentials", }); } }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Login failed", }); } } } ``` ### Example: Todo Controller **`src/controllers/todoController.ts`** ```typescript import { Request, Response } from "express"; import { runWithTenant } from "multibridge"; import { todoService } from "../services/todoService"; import { TenantInfo } from "../types/tenant"; export class TodoController { /** * Create Todo * POST /api/todos */ static async createTodo(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { title, description, priority } = req.body; try { await runWithTenant(tenant, async () => { const todo = await todoService.createTodo({ title, description, priority }); res.status(201).json({ success: true, data: todo }); }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Failed to create todo", }); } } /** * Get All Todos * GET /api/todos */ static async getTodos(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { status, priority } = req.query; try { await runWithTenant(tenant, async () => { const todos = await todoService.getTodos({ status: status as string, priority: priority as string, }); res.status(200).json({ success: true, data: todos }); }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Failed to fetch todos", }); } } /** * Get Todo by ID * GET /api/todos/:id */ static async getTodoById(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { id } = req.params; try { await runWithTenant(tenant, async () => { const todo = await todoService.getTodoById(id); if (todo) { res.status(200).json({ success: true, data: todo }); } else { res.status(404).json({ success: false, error: "Todo not found" }); } }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Failed to fetch todo", }); } } /** * Update Todo * PUT /api/todos/:id */ static async updateTodo(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { id } = req.params; const updates = req.body; try { await runWithTenant(tenant, async () => { const todo = await todoService.updateTodo(id, updates); res.status(200).json({ success: true, data: todo }); }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Failed to update todo", }); } } /** * Delete Todo * DELETE /api/todos/:id */ static async deleteTodo(req: Request, res: Response): Promise<void> { const tenant: TenantInfo = (req as any).tenant; const { id } = req.params; try { await runWithTenant(tenant, async () => { await todoService.deleteTodo(id); res.status(200).json({ success: true, message: "Todo deleted successfully" }); }); } catch (error: any) { res.status(500).json({ success: false, error: error.message || "Failed to delete todo", }); } } } ``` ### Routes Setup **`src/routes/authRoutes.ts`** ```typescript import { Router } from "express"; import { AuthController } from "../controllers/authController"; import { tenantMiddleware } from "../middleware/tenantMiddleware"; const router = Router(); router.post("/signup", tenantMiddleware, AuthController.signup); router.post("/login", tenantMiddleware, AuthController.login); export default router; ``` **`src/routes/todoRoutes.ts`** ```typescript import { Router } from "express"; import { TodoController } from "../controllers/todoController"; import { tenantMiddleware } from "../middleware/tenantMiddleware"; const router = Router(); router.post("/todos", tenantMiddleware, TodoController.createTodo); router.get("/todos", tenantMiddleware, TodoController.getTodos); router.get("/todos/:id", tenantMiddleware, TodoController.getTodoById); router.put("/todos/:id", tenantMiddleware, TodoController.updateTodo); router.delete("/todos/:id", tenantMiddleware, TodoController.deleteTodo); export default router; ``` --- ## Service Layer Services contain business logic and interact with the database. They are called within `runWithTenant` context, so they can use `executeQuery` or ORM adapters directly. ### Example: Auth Service (Using Custom Queries) **`src/services/authService.ts`** ```typescript import { executeQuery } from "multibridge"; import bcrypt from "bcrypt"; export const authService = { /** * Create a new user */ async createUser(data: { username: string; email: string; password: string }) { // Hash password const hashedPassword = await bcrypt.hash(data.password, 10); // Insert user using custom SQL query const query = "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?) RETURNING id, username, email"; const result = await executeQuery(query, [data.username, data.email, hashedPassword]); // PostgreSQL returns rows array if (Array.isArray(result) && result.length > 0) { return result[0]; } // MySQL returns [rows, fields] if (Array.isArray(result) && result[0] && Array.isArray(result[0]) && result[0].length > 0) { return result[0][0]; } throw new Error("Failed to create user"); }, /** * Authenticate user */ async authenticateUser(username: string, password: string) { // Find user by username const query = "SELECT id, username, email, password_hash FROM users WHERE username = ? LIMIT 1"; const result = await executeQuery(query, [username]); let user: any; // Handle different database result formats if (Array.isArray(result) && result.length > 0) { user = result[0]; } else if (Array.isArray(result) && result[0] && Array.isArray(result[0]) && result[0].length > 0) { user = result[0][0]; } if (!user) { return null; } // Verify password const isValid = await bcrypt.compare(password, user.password_hash); if (!isValid) { return null; } // Return user without password delete user.password_hash; return user; }, }; ``` ### Example: Todo Service (Using Custom Queries) **`src/services/todoService.ts`** ```typescript import { executeQuery } from "multibridge"; export const todoService = { /** * Create a new todo */ async createTodo(data: { title: string; description?: string; priority?: string }) { const query = ` INSERT INTO todos (title, description, priority, status, created_at) VALUES (?, ?, ?, 'pending', NOW()) RETURNING id, title, description, priority, status, created_at `; const result = await executeQuery(query, [ data.title, data.description || null, data.priority || "medium", ]); if (Array.isArray(result) && result.length > 0) { return result[0]; } if (Array.isArray(result) && result[0] && Array.isArray(result[0]) && result[0].length > 0) { return result[0][0]; } throw new Error("Failed to create todo"); }, /** * Get all todos with optional filters */ async getTodos(filters: { status?: string; priority?: string }) { let query = "SELECT * FROM todos WHERE 1=1"; const params: any[] = []; if (filters.status) { query += " AND status = ?"; params.push(filters.status); } if (filters.priority) { query += " AND priority = ?"; params.push(filters.priority); } query += " ORDER BY created_at DESC"; const result = await executeQuery(query, params); // Handle different database result formats if (Array.isArray(result) && result.length > 0 && !Array.isArray(result[0])) { return result; } if (Array.isArray(result) && result[0] && Array.isArray(result[0])) { return result[0]; } return []; }, /** * Get todo by ID */ async getTodoById(id: string) { const query = "SELECT * FROM todos WHERE id = ? LIMIT 1"; const result = await executeQuery(query, [id]); if (Array.isArray(result) && result.length > 0) { return result[0]; } if (Array.isArray(result) && result[0] && Array.isArray(result[0]) && result[0].length > 0) { return result[0][0]; } return null; }, /** * Update todo */ async updateTodo(id: string, updates: Partial<{ title: string; description: string; status: string; priority: string }>) { const fields: string[] = []; const values: any[] = []; if (updates.title !== undefined) { fields.push("title = ?"); values.push(updates.title); } if (updates.description !== undefined) { fields.push("description = ?"); values.push(updates.description); } if (updates.status !== undefined) { fields.push("status = ?"); values.push(updates.status); } if (updates.priority !== undefined) { fields.push("priority = ?"); values.push(updates.priority); } if (fields.length === 0) { throw new Error("No fields to update"); } fields.push("updated_at = NOW()"); values.push(id); const query = `UPDATE todos SET ${fields.join(", ")} WHERE id = ? RETURNING *`; const result = await executeQuery(query, values); if (Array.isArray(result) && result.length > 0) { return result[0]; } if (Array.isArray(result) && result[0] && Array.isArray(result[0]) && result[0].length > 0) { return result[0][0]; } throw new Error("Failed to update todo"); }, /** * Delete todo */ async deleteTodo(id: string) { const query = "DELETE FROM todos WHERE id = ?"; await executeQuery(query, [id]); }, }; ``` ### Example: MongoDB Service (Using executeQuery) **`src/services/userService.ts`** (MongoDB example) ```typescript import { executeQuery } from "multibridge"; export const userService = { /** * Create user in MongoDB */ async createUser(data: { username: string; email: string; profile: any }) { const query = { collection: "users", method: "insertOne", args: [{ username: data.username, email: data.email, profile: data.profile, createdAt: new Date(), }], }; const result = await executeQuery(query); return result; }, /** * Find user by email */ async findUserByEmail(email: string) { const query = { collection: "users", method: "findOne", args: [{ email }], }; const result = await executeQuery(query); return result; }, /** * Update user profile */ async updateUserProfile(userId: string, profile: any) { const query = { collection: "users", method: "updateOne", args: [ { _id: userId }, { $set: { profile, updatedAt: new Date() } }, ], }; const result = await executeQuery(query); return result; }, /** * Get all users with pagination */ async getUsers(page: number = 1, limit: number = 10) { const skip = (page - 1) * limit; const query = { collection: "users", method: "find", args: [ {}, { limit, skip, sort: { createdAt: -1 } }, ], }; const result = await executeQuery(query); return Array.isArray(result) ? result : []; }, }; ``` --- ## Custom Queries with executeQuery The `executeQuery` function supports multiple query formats for different database types. ### PostgreSQL/MySQL Queries ```typescript import { executeQuery } from "multibridge"; // Simple string query const result = await executeQuery("SELECT * FROM users WHERE id = ?", [userId]); // SQLQuery object format const result = await executeQuery({ type: "sql", query: "SELECT * FROM users WHERE email = ?", params: [email], }); ``` ### MongoDB Queries ```typescript import { executeQuery } from "multibridge"; // Insert document await executeQuery({ collection: "users", method: "insertOne", args: [{ name: "John", email: "john@example.com" }], }); // Find documents const users = await executeQuery({ collection: "users", method: "find", args: [{ status: "active" }, { limit: 10 }], }); // Update document await executeQuery({ collection: "users", method: "updateOne", args: [ { _id: userId }, { $set: { status: "inactive" } }, ], }); // Aggregation pipeline const stats = await executeQuery({ collection: "orders", method: "aggregate", args: [[ { $match: { status: "completed" } }, { $group: { _id: "$userId", total: { $sum: "$amount" } } }, ]], }); ``` ### Cassandra Queries ```typescript import { executeQuery } from "multibridge"; // Simple CQL query const result = await executeQuery( "SELECT * FROM users WHERE user_id = ?", [userId] ); // CassandraQuery object format const result = await executeQuery({ type: "cassandra", query: "SELECT * FROM users WHERE email = ? ALLOW FILTERING", params: [email], }); ``` --- ## ORM Integration ### Sequelize #### Model Definition **`src/models/sequelize/User.ts`** ```typescript import { DataTypes, Model, Optional } from "sequelize"; export interface UserAttributes { id: number; username: string; email: string; passwordHash: string; createdAt?: Date; updatedAt?: Date; } export interface UserCreationAttributes extends Optional<UserAttributes, "id" | "createdAt" | "updatedAt"> {} export class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes { public id!: number; public username!: string; public email!: string; public passwordHash!: string; public readonly createdAt!: Date; public readonly updatedAt!: Date; static initialize(sequelize: any) { User.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, username: { type: DataTypes.STRING(100), allowNull: false, unique: true, }, email: { type: DataTypes.STRING(255), allowNull: false, unique: true, }, passwordHash: { type: DataTypes.STRING(255), allowNull: false, }, }, { sequelize, tableName: "users", timestamps: true, } ); } } ``` **`src/models/sequelize/Todo.ts`** ```typescript import { DataTypes, Model, Optional } from "sequelize"; export interface TodoAttributes { id: number; title: string; description?: string; status: "pending" | "in_progress" | "completed"; priority: "low" | "medium" | "high"; userId?: number; createdAt?: Date; updatedAt?: Date; } export interface TodoCreationAttributes extends Optional<TodoAttributes, "id" | "description" | "userId" | "createdAt" | "updatedAt"> {} export class Todo extends Model<TodoAttributes, TodoCreationAttributes> implements TodoAttributes { public id!: number; public title!: string; public description?: string; public status!: "pending" | "in_progress" | "completed"; public priority!: "low" | "medium" | "high"; public userId?: number; public readonly createdAt!: Date; public readonly updatedAt!: Date; static initialize(sequelize: any) { Todo.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, title: { type: DataTypes.STRING(255), allowNull: false, }, description: { type: DataTypes.TEXT, allowNull: true, }, status: { type: DataTypes.ENUM("pending", "in_progress", "completed"), defaultValue: "pending", }, priority: { type: DataTypes.ENUM("low", "medium", "high"), defaultValue: "medium", }, userId: { type: DataTypes.INTEGER, allowNull: true, }, }, { sequelize, tableName: "todos", timestamps: true, } ); } } ``` #### Service Using Sequelize **`src/services/sequelizeAuthService.ts`** ```typescript import { runWithTenant } from "multibridge"; import { getSequelizeInstance } from "multibridge"; import { User } from "../models/sequelize/User"; import { Todo } from "../models/sequelize/Todo"; export const sequelizeAuthService = { /** * Initialize models (call this once per tenant context) */ async initializeModels() { const sequelize = await getSequelizeInstance(); User.initialize(sequelize); Todo.initialize(sequelize); // Define associations User.hasMany(Todo, { foreignKey: "userId", as: "todos" }); Todo.belongsTo(User, { foreignKey: "userId", as: "user" }); }, /** * Create user */ async createUser(data: { username: string; email: string; passwordHash: string }) { await this.initializeModels(); const user = await User.create({ username: data.username, email: data.email, passwordHash: data.passwordHash, }); return user.toJSON(); }, /** * Find user by email */ async findUserByEmail(email: string) { await this.initializeModels(); const user = await User.findOne({ where: { email } }); return user ? user.toJSON() : null; }, /** * Get user with todos */ async getUserWithTodos(userId: number) { await this.initializeModels(); const user = await User.findByPk(userId, { include: [{ model: Todo, as: "todos" }], }); return user ? user.toJSON() : null; }, }; export const sequelizeTodoService = { async initializeModels() { const sequelize = await getSequelizeInstance(); Todo.initialize(sequelize); }, /** * Create todo */ async createTodo(data: { title: string; description?: string; priority?: string; userId?: number }) { await this.initializeModels(); const todo = await Todo.create({ title: data.title, description: data.description, priority: (data.priority || "medium") as any, userId: data.userId, }); return todo.toJSON(); }, /** * Get all todos */ async getTodos(filters: { status?: string; priority?: string; userId?: number }) { await this.initializeModels(); const where: any = {}; if (filters.status) where.status = filters.status; if (filters.priority) where.priority = filters.priority; if (filters.userId) where.userId = filters.userId; const todos = await Todo.findAll({ where, order: [["createdAt", "DESC"]] }); return todos.map((todo) => todo.toJSON()); }, /** * Update todo */ async updateTodo(id: number, updates: Partial<{ title: string; description: string; status: string; priority: string }>) { await this.initializeModels(); const todo = await Todo.findByPk(id); if (!todo) { throw new Error("Todo not found"); } await todo.update(updates); return todo.toJSON(); }, /** * Delete todo */ async deleteTodo(id: number) { await this.initializeModels(); const todo = await Todo.findByPk(id); if (!todo) { throw new Error("Todo not found"); } await todo.destroy(); }, }; ``` ### TypeORM #### Entity Definitions **`src/models/typeorm/User.entity.ts`** ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from "typeorm"; import { Todo } from "./Todo.entity"; @Entity("users") export class User { @PrimaryGeneratedColumn() id!: number; @Column({ length: 100, unique: true }) username!: string; @Column({ length: 255, unique: true }) email!: string; @Column({ name: "password_hash", length: 255 }) passwordHash!: string; @OneToMany(() => Todo, (todo) => todo.user) todos!: Todo[]; @CreateDateColumn({ name: "created_at" }) createdAt!: Date; @UpdateDateColumn({ name: "updated_at" }) updatedAt!: Date; } ``` **`src/models/typeorm/Todo.entity.ts`** ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from "typeorm"; import { User } from "./User.entity"; @Entity("todos") export class Todo { @PrimaryGeneratedColumn() id!: number; @Column({ length: 255 }) title!: string; @Column({ type: "text", nullable: true }) description?: string; @Column({ type: "enum", enum: ["pending", "in_progress", "completed"], default: "pending", }) status!: "pending" | "in_progress" | "completed"; @Column({ type: "enum", enum: ["low", "medium", "high"], default: "medium", }) priority!: "low" | "medium" | "high"; @Column({ name: "user_id", nullable: true }) userId?: number; @ManyToOne(() => User, (user) => user.todos) @JoinColumn({ name: "user_id" }) user?: User; @CreateDateColumn({ name: "created_at" }) createdAt!: Date; @UpdateDateColumn({ name: "updated_at" }) updatedAt!: Date; } ``` #### Service Using TypeORM **`src/services/typeormAuthService.ts`** ```typescript import { getTypeORMDataSource } from "multibridge"; import { User } from "../models/typeorm/User.entity"; import { Todo } from "../models/typeorm/Todo.entity"; export const typeormAuthService = { /** * Create user */ async createUser(data: { username: string; email: string; passwordHash: string }) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const userRepo = dataSource.getRepository(User); const user = userRepo.create({ username: data.username, email: data.email, passwordHash: data.passwordHash, }); const savedUser = await userRepo.save(user); return savedUser; }, /** * Find user by email */ async findUserByEmail(email: string) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const userRepo = dataSource.getRepository(User); const user = await userRepo.findOne({ where: { email } }); return user; }, /** * Get user with todos */ async getUserWithTodos(userId: number) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const userRepo = dataSource.getRepository(User); const user = await userRepo.findOne({ where: { id: userId }, relations: ["todos"], }); return user; }, }; export const typeormTodoService = { /** * Create todo */ async createTodo(data: { title: string; description?: string; priority?: string; userId?: number }) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const todoRepo = dataSource.getRepository(Todo); const todo = todoRepo.create({ title: data.title, description: data.description, priority: (data.priority || "medium") as any, userId: data.userId, }); const savedTodo = await todoRepo.save(todo); return savedTodo; }, /** * Get all todos */ async getTodos(filters: { status?: string; priority?: string; userId?: number }) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const todoRepo = dataSource.getRepository(Todo); const queryBuilder = todoRepo.createQueryBuilder("todo"); if (filters.status) { queryBuilder.where("todo.status = :status", { status: filters.status }); } if (filters.priority) { queryBuilder.andWhere("todo.priority = :priority", { priority: filters.priority }); } if (filters.userId) { queryBuilder.andWhere("todo.userId = :userId", { userId: filters.userId }); } queryBuilder.orderBy("todo.createdAt", "DESC"); const todos = await queryBuilder.getMany(); return todos; }, /** * Update todo */ async updateTodo(id: number, updates: Partial<{ title: string; description: string; status: string; priority: string }>) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const todoRepo = dataSource.getRepository(Todo); const todo = await todoRepo.findOne({ where: { id } }); if (!todo) { throw new Error("Todo not found"); } Object.assign(todo, updates); const updatedTodo = await todoRepo.save(todo); return updatedTodo; }, /** * Delete todo */ async deleteTodo(id: number) { const dataSource = await getTypeORMDataSource({ entities: [User, Todo], }); const todoRepo = dataSource.getRepository(Todo); const todo = await todoRepo.findOne({ where: { id } }); if (!todo) { throw new Error("Todo not found"); } await todoRepo.remove(todo); }, }; ``` ### Mongoose #### Schema Definitions **`src/models/mongoose/userSchema.ts`** ```typescript import { Schema, Document } from "mongoose"; export interface IUser extends Document { username: string; email: string; passwordHash: string; profile?: { firstName?: string; lastName?: string; avatar?: string; }; createdAt: Date; updatedAt: Date; } export const userSchema = new Schema<IUser>( { username: { type: String, required: true, unique: true, trim: true, maxlength: 100, }, email: { type: String, required: true, unique: true, lowercase: true, trim: true, }, passwordHash: { type: String, required: true, }, profile: { firstName: String, lastName: String, avatar: String, }, }, { timestamps: true, } ); // Indexes userSchema.index({ email: 1 }); userSchema.index({ username: 1 }); ``` **`src/models/mongoose/todoSchema.ts`** ```typescript import { Schema, Document } from "mongoose"; export interface ITodo extends Document { title: string; description?: string; status: "pending" | "in_progress" | "completed"; priority: "low" | "medium" | "high"; userId?: string; createdAt: Date; updatedAt: Date; } export const todoSchema = new Schema<ITodo>( { title: { type: String, required: true, trim: true, maxlength: 255, }, description: { type: String, trim: true, }, status: { type: String, enum: ["pending", "in_progress", "completed"], default: "pending", }, priority: { type: String, enum: ["low", "medium", "high"], default: "medium", }, userId: { type: String, ref: "User", }, }, { timestamps: true, } ); // Indexes todoSchema.index({ userId: 1, status: 1 }); todoSchema.index({ createdAt: -1 }); ``` #### Service Using Mongoose **`src/services/mongooseAuthService.ts`** ```typescript import { getMongooseConnection } from "multibridge"; import { userSchema, IUser } from "../models/mongoose/userSchema"; import { todoSchema, ITodo } from "../models/mongoose/todoSchema"; export const mongooseAuthService = { /** * Get User model */ async getUserModel() { const connection = await getMongooseConnection(); // Check if model already exists if (connection.models.User) { return connection.models.User; } return connection.model<IUser>("User", userSchema); }, /** * Create user */ async createUser(data: { username: string; email: string; passwordHash: string; profile?: any }) { const User = await this.getUserModel(); const user = new User({ username: data.username, email: data.email, passwordHash: data.passwordHash, profile: data.profile, }); const savedUser = await user.save(); return savedUser.toObject(); }, /** * Find user by email */ async findUserByEmail(email: string) { const User = await this.getUserModel(); const user = await User.findOne({ email }); return user ? user.toObject() : null; }, /** * Get user with todos */ async getUserWithTodos(userId: string) { const User = await this.getUserModel(); const user = await User.findById(userId); if (!user) return null; const Todo = await mongooseTodoService.getTodoModel(); const todos = await Todo.find({ userId: userId.toString() }); return { ...user.toObject(), todos: todos.map((todo) => todo.toObject()), }; }, }; export const mongooseTodoService = { /** * Get Todo model */ async getTodoModel() { const connection = await getMongooseConnection(); if (connection.models.Todo) { return connection.models.Todo; } return connection.model<ITodo>("Todo", todoSchema); }, /** * Create todo */ async createTodo(data: { title: string; description?: string; priority?: string; userId?: string }) { const Todo = await this.getTodoModel(); const todo = new Todo({ title: data.title, description: data.description, priority: data.priority || "medium", userId: data.userId, }); const savedTodo = await todo.save(); return savedTodo.toObject(); }, /** * Get all todos */ async getTodos(filters: { status?: string; priority?: string; userId?: string }) { const Todo = await this.getTodoModel(); const query: any = {}; if (filters.status) query.status = filters.status; if (filters.priority) query.priority = filters.priority; if (filters.userId) query.userId = filters.userId; const todos = await Todo.find(query).sort({ createdAt: -1 }); return todos.map((todo) => todo.toObject()); }, /** * Update todo */ async updateTodo(id: string, updates: Partial<{ title: string; description: string; status: string; priority: string }>) { const Todo = await this.getTodoModel(); const todo = await Todo.findByIdAndUpdate( id, { $set: updates }, { new: true, runValidators: true } ); if (!todo) { throw new Error("Todo not found"); } return todo.toObject(); }, /** * Delete todo */ async deleteTodo(id: string) { const Todo = await this.getTodoModel(); const todo = await Todo.findByIdAndDelete(id); if (!todo) { throw new Error("Todo not found"); } }, }; ``` ### Cassandra #### Model Definition **`src/models/cassandra/userModel.ts`** ```typescript import { CassandraModelDefinition } from "multibridge"; export const userModel: CassandraModelDefinition = { tableName: "users", partitionKeys: ["user_id"], columns: { user_id: "uuid", username: "text", email: "text", password_hash: "text", created_at: "timestamp", }, indexes: ["email"], }; ``` **`src/models/cassandra/todoModel.ts`** ```typescript import { CassandraModelDefinition } from "multibridge"; export const todoModel: CassandraModelDefinition = { tableName: "todos", partitionKeys: ["user_id"], clusteringKeys: ["todo_id"], columns: { todo_id: "uuid", user_id: "uuid", title: "text", description: "text", status: "text", priority: "text", created_at: "timestamp", }, }; ``` #### Service Using Cassandra **`src/services/cassandraAuthService.ts`** ```typescript import { getCassandraClient, createTable, insert, select, update, remove } from "multibridge"; import { userModel } from "../models/cassandra/userModel"; import { v4 as uuidv4 } from "uuid"; export const cassandraAuthService = { /** * Initialize user table */ async initializeUserTable() { const client = await getCassandraClient(); await createTable(client, userModel); }, /** * Create user */ async createUser(data: { username: string; email: string; passwordHash: string }) { const client = await getCassandraClient(); const userId = uuidv4(); await insert(client, userModel.tableName, { user_id: userId, username: data.username, email: data.email, password_hash: data.passwordHash, created_at: new Date(), }); return { userId, username: data.username, email: data.email }; }, /** * Find user by email */ async findUserByEmail(email: string) { const client = await getCassandraClient(); const result = await select( client, userModel.tableName, ["user_id", "username", "email", "password_hash"], { email } ); if (result && result.length > 0) { return result[0]; } return null; }, }; export const cassandraTodoService = { /** * Initialize todo table */ async initializeTodoTable() { const client = await getCassandraClient(); const { todoModel } = await import("../models/cassandra/todoModel"); await createTable(client, todoModel); }, /** * Create todo */ async createTodo(data: { userId: string; title: string; description?: string; priority?: string }) { const client = await getCassandraClient(); const todoId = uuidv4(); await insert(client, "todos", { todo_id: todoId, user_id: data.userId, title: data.title, description: data.description || "", status: "pending", priority: data.priority || "medium", created_at: new Date(), }); return { todoId, ...data }; }, /** * Get todos for user */ async getTodosByUserId(userId: string) { const client = await getCassandraClient(); const result = await select( client, "todos", ["todo_id", "user_id", "title", "description", "status", "priority", "created_at"], { user_id: userId } ); return result || []; }, /** * Update todo */ async updateTodo(todoId: string, userId: string, updates: Partial<{ title: string; description: string; status: string; priority: string }>) { const client = await getCassandraClient(); await update( client, "todos", updates, { todo_id: todoId, user_id: userId } ); }, /** * Delete todo */ async deleteTodo(todoId: string, userId: string) { const client = await getCassandraClient(); await remove( client, "todos", { todo_id: todoId, user_id: userId } ); }, }; ``` --- ## Model Organization ### Pattern: Separate Model Files Each model should be in its own file for better organization and maintainability. **Example Structure:** ``` src/models/ ├── sequelize/ ├── index.ts # Export all Sequelize models ├── User.ts └── Todo.ts ├── typeorm/ ├── index.ts # Export all TypeORM entities ├── User.entity.ts └── Todo.entity.ts ├── mongoose/ ├── index.ts # Export all Mongoose schemas ├── userSchema.ts └── todoSchema.ts └── cassandra/ ├── index.ts # Export all Cassandra models ├── userModel.ts └── todoModel.ts ``` **`src/models/sequelize/index.ts`** ```typescript export { User, UserAttributes, UserCreationAttributes } from "./User"; export { Todo, TodoAttributes, TodoCreationAttributes } from "./Todo"; ``` **`src/models/typeorm/index.ts`** ```typescript export { User } from "./User.entity"; export { Todo } from "./Todo.entity"; ``` **`src/models/mongoose/index.ts`** ```typescript export { userSchema, IUser } from "./userSchema"; export { todoSchema, ITodo } from "./todoSchema"; ``` **`src/models/cassandra/index.ts`** ```typescript export { userModel } from "./userModel"; export { todoModel } from "./todoModel"; ``` ### Using Models in Services ```typescript // Import from centralized index import { User, Todo } from "../models/sequelize"; // or import { User } from "../models/typeorm"; // or import { userSchema, IUser } from "../models/mongoose"; // or import { userModel } from "../models/cassandra"; ``` --- ## Error Handling MultiBridge provides custom error classes for better error handling: ```typescript import { MultiBridgeError, TenantContextError, ConnectionError, ConfigurationError, ValidationError, QueryError, TimeoutError, } from "multibridge"; try { await runWithTenant(tenant, async () => { await executeQuery("SELECT * FROM users"); }); } catch (error) { if (error instanceof TenantContextError) { // Handle tenant context issues console.error("Tenant error:", error.message, error.context); } else if (error instanceof ConnectionError) { // Handle connection issues console.error("Connection error:", error.message); } else if (error instanceof QueryError) { // Handle query execution errors console.error("Query error:", error.message, error.context); } else if (error instanceof TimeoutError) { // Handle timeout errors console.error("Timeout error:", error.message); } else { // Handle other errors console.error("Unexpected error:", error); } } ``` --- ## Best Practices ### 1. Always Use `runWithTenant` in Controllers ```typescript // Good await runWithTenant(tenant, async () => { await service.doSomething(); }); // Bad - Missing tenant context await service.doSomething(); ``` ### 2. Keep Services Pure Services should not handle HTTP concerns. They should only contain business logic and database operations. ```typescript // Good export const todoService = { async createTodo(data: CreateTodoDto) { // Business logic only return await executeQuery(...); }, }; // Bad export const todoService = { async createTodo(req: Request, res: Response) { // HTTP concerns in service }, }; ``` ### 3. Use Type Safety Always define types for your data structures: ```typescript interface CreateUserDto { username: string; email: string; password: string; } export const authService = { async createUser(data: CreateUserDto) { // Type-safe implementation }, }; ``` ### 4. Handle Database Result Formats Different databases return results in different formats: ```typescript const result = await executeQuery("SELECT * FROM users"); // PostgreSQL: result is QueryResult with rows array // MySQL: result is [rows, fields] // MongoDB: result is the document(s) // Cassandra: result is ResultSet with rows // Handle accordingly let users; if (Array.isArray(result) && result.length > 0) { if (Array.isArray(result[0])) { // MySQL format users = result[0]; } else { // PostgreSQL format users = result; } } else { users = result; } ``` ### 5. Initialize ORM Models Once Per Tenant Context ```typescript // Good - Initialize models once await runWithTenant(tenant, async () => { await initializeModels(); // Call once await service.createUser(...); await service.createTodo(...); }); // Bad - Initialize models multiple times await runWithTenant(tenant, async () => { await initializeModels(); await service.createUser(...); await initializeModels(); // Redundant await service.createTodo(...); }); ``` ### 6. Clean Up Connections on Shutdown ```typescript import { closeAllConnections, closeAllSequelizeInstances, closeAllTypeORMDataSources, closeAllMongooseConnections, closeAllCassandraClients, closeCentralDB, } from "multibridge"; // Graceful shutdown process.on("SIGTERM", async () => { await closeAllSequelizeInstances(); await closeAllTypeORMDataSources(); await closeAllMongooseConnections(); await closeAllCassandraClients(); await closeAllConnections(); await closeCentralDB(); process.exit(0); }); ``` ### 7. Use Connection Statistics for Monitoring ```typescript import { getConnectionStats } from "multibridge"; // Get connection statistics const stats = getConnectionStats(); console.log("Active connections:", stats.activeConnections); console.log("Cached connections:", stats.cachedConnections); console.log("Pending connections:", stats.pendingConnections); ``` ### 8. Validate Tenant Input MultiBridge automatically validates tenant input, but you can add additional validation: ```typescript const tenant: TenantInfo = { appid: req.headers["x-app-id"], orgid: req.headers["x-org-id"], appdbname: req.headers["x-app-db-name"], }; // Additional validation if (!tenant.appid || !tenant.orgid || !tenant.appdbname) { return res.status(400).json({ error: "Missing tenant information" }); } ``` --- ## Complete Server Example **`src/server.ts`** ```typescript import express from "express"; import authRoutes from "./routes/authRoutes"; import todoRoutes from "./routes/todoRoutes"; import { closeAllConnections, closeAllSequelizeInstances, closeAllTypeORMDataSources, closeAllMongooseConnections, closeAllCassandraClients, closeCentralDB, } from "multibridge"; const app = express(); // Middleware app.use(express.json()); // Routes app.use("/auth", authRoutes); app.use("/api", todoRoutes); // Health check app.get("/health", (req, res) => { res.json({ status: "ok" }); }); const PORT = process.env.PORT || 3000; const server = app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); // Graceful shutdown const gracefulShutdown = async () => { console.log("Shutting down gracefully..."); server.close(async () => { try { await closeAllSequelizeInstances(); await closeAllTypeORMDataSources(); await closeAllMongooseConnections(); await closeAllCassandraClients(); await closeAllConnections(); await closeCentralDB(); console.log("All connections closed"); process.exit(0); } catch (error) { console.error("Error during shutdown:", error); process.exit(1); } }); }; process.on("SIGTERM", gracefulShutdown); process.on("SIGINT", gracefulShutdown); ``` --- ## Summary This guide demonstrates: 1. **Controller Layer**: HTTP request handling with tenant context 2. **Service Layer**: Business logic with database operations 3. **Custom Queries**: Using `executeQuery` for direct database queries 4. **ORM Integration**: Using Sequelize, TypeORM, Mongoose, and Cassandra adapters 5. **Model Organization**: Separating models into individual files 6. **Error Handling**: Using MultiBridge's custom error classes 7. **Best Practices**: Following recommended patterns and conventions MultiBridge provides a unified interface for multi-tenant database operations while supporting both raw queries and popular ORMs, making it flexible for various use cases and team preferences.