import { drizzle } from 'drizzle-orm/node-postgres';import dotenv from "dotenv/config";import { sql } from "drizzle-orm";import { AsyncLocalStorage } from "async_hooks";export constdb = drizzle(process.env.NILEDB_URL);export const tenantContext = new AsyncLocalStorage<string | undefined>();export function tenantDB<T>(cb: (tx: any) => T | Promise<T>): Promise<T> { returndb.transaction(async (tx) => { const tenantId = tenantContext.getStore(); console.log("executing query with tenant: " + tenantId); // if there's a tenant ID, set it in the transaction context if (tenantId) { await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`); } return cb(tx); }) as Promise<T>;}
We recommend transferring the generated code from drizzle/schema.ts and drizzle/relations.ts to the actual schema file. In this guide we transferred code to src/db/schema.ts. Generated files for schema and relations can be deleted. This way you can manage your schema in a more structured way.
You can directly apply changes to your database using the drizzle-kit push command. This is a convenient method for quickly testing new schema designs or modifications in a local development environment, allowing for rapid iterations without the need to manage migration files:
现在我们已经设置了 Drizzle 连接到 Nile,并且我们的架构已经到位,我们可以在多租户 Web 应用中使用它们。本例中使用 Express 作为 Web 框架,但 Nile 和 Drizzle 可以在任何 Web 框架中使用。
为了简化示例,我们将在单个文件中实现 Web 应用。 - src/app.ts。我们将首先初始化 Web 应用:
src/app.ts
import express from "express";import { tenantDB, tenantContext, db } from "./db/db";import { tenants as tenantSchema, todos as todoSchema,} from "./db/schema";import { eq } from "drizzle-orm";const PORT = process.env.PORT || 3001;const app = express();app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));app.use(express.json());
初始化租户感知中间件
接下来,我们将在示例中添加中间件。此中间件从路径参数中获取租户 ID 并将其存储在 AsyncLocalStorage 中。我们在 src/db/index.ts 中创建的 tenantDB 封装器在执行查询时使用此租户 ID 设置 nile.tenant_id,从而保证查询将针对此租户的虚拟数据库执行。
src/app.ts
// set the tenant ID in the context based on the URL parameterapp.use('/api/tenants/:tenantId/*', (req, res, next) => { const tenantId = req.params.tenantId; console.log("setting context to tenant: " + tenantId); tenantContext.run(tenantId, next);});
最后,我们需要添加一些用于创建和列出租户和待办事项的路由。请注意,我们使用 tenantDB 封装器连接到租户的虚拟数据库。另请注意,在 app.get("/api/tenants/:tenantId/todos" 中,我们无需在查询中指定 where tenant_id=...。这是因为我们被路由到该租户的数据库,并且查询无法返回任何其他租户的数据。
src/app.ts
// create new tenantapp.post("/api/tenants", async (req, res) => { try { const name = req.body.name; var tenants: any = null; tenants = await tenantDB(async (tx) => { return await tx.insert(tenantSchema).values({ name }).returning(); }); res.json(tenants); } catch (error: any) { console.log("error creating tenant: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// return list of tenantsapp.get("/api/tenants", async (req, res) => { let tenants: any = []; try { tenants = await tenantDB(async (tx) => { return await tx.select().from(tenantSchema); }); res.json(tenants); } catch (error: any) { console.log("error listing tenants: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// add new task for tenantapp.post("/api/tenants/:tenantId/todos", async (req, res) => { try { const { title, complete } = req.body; if (!title) { res.status(400).json({message: "No task title provided",}); } const tenantId = req.params.tenantId; const newTodo = await tenantDB(async (tx) => { return await tx .insert(todoSchema) .values({ tenantId, title, complete }) .returning(); }); // return without the embedding vector, since it is huge and useless res.json(newTodo); } catch (error: any) { console.log("error adding task: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// update tasks for tenant// No need for where clause because we have the tenant in the contextapp.put("/api/tenants/:tenantId/todos", async (req, res) => { try { const { id, complete } = req.body; await tenantDB(async (tx) => { return await tx .update(todoSchema) .set({ complete }) .where(eq(todoSchema.id, id)); }); res.sendStatus(200); } catch (error: any) { console.log("error updating tasks: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// get all tasks for tenantapp.get("/api/tenants/:tenantId/todos", async (req, res) => { try { // No need for a "where" clause here because we are setting the tenant ID in the context const todos = await tenantDB(async (tx) => { return await tx .select({ id: todoSchema.id, tenant_id: todoSchema.tenantId, title: todoSchema.title, estimate: todoSchema.estimate, }) .from(todoSchema); }); res.json(todos); } catch (error: any) { console.log("error listing tasks: " + error.message); res.status(500).json({message: error.message,}); }});
试试看!
你现在可以运行新的 Web 应用了:
npx tsx src/app.ts
并使用 curl 尝试你刚刚创建的路由:
# create a tenantcurl --location --request POST 'localhost:3001/api/tenants' \--header 'Content-Type: application/json' \--data-raw '{"name":"my first customer"}'# get tenantscurl -X GET 'http://localhost:3001/api/tenants'# create a todo (don't forget to use a real tenant-id in the URL)curl -X POST \ 'http://localhost:3001/api/tenants/108124a5-2e34-418a-9735-b93082e9fbf2/todos' \ --header 'Content-Type: application/json' \ --data-raw '{"title": "feed the cat", "complete": false}'# list todos for tenant (don't forget to use a real tenant-id in the URL)curl -X GET \ 'http://localhost:3001/api/tenants/108124a5-2e34-418a-9735-b93082e9fbf2/todos'