Skip to content

Commit da57fd7

Browse files
authored
Adding Nile documentation for Drizzle (#476)
* Nile documentation for Drizzle * detailed explanation on using virtual tenant databases
1 parent 9fff264 commit da57fd7

File tree

9 files changed

+525
-0
lines changed

9 files changed

+525
-0
lines changed

public/svg/nile.svg

Lines changed: 14 additions & 0 deletions
Loading

src/content/docs/_meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
["connect-supabase", "Supabase"],
2525
["connect-xata", "Xata"],
2626
["connect-pglite", "PGLite"],
27+
["connect-nile", "Nile"],
2728
"---",
2829
["connect-planetscale", "PlanetScale"],
2930
["connect-tidb" , "TiDB"],

src/content/docs/connect-nile.mdx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import Tab from '@mdx/Tab.astro';
2+
import Tabs from '@mdx/Tabs.astro';
3+
import Npm from "@mdx/Npm.astro";
4+
import Callout from '@mdx/Callout.astro';
5+
import Steps from '@mdx/Steps.astro';
6+
import AnchorCards from '@mdx/AnchorCards.astro';
7+
import Prerequisites from "@mdx/Prerequisites.astro";
8+
import WhatsNextPostgres from "@mdx/WhatsNextPostgres.astro";
9+
10+
# Drizzle \<\> Nile
11+
12+
<Prerequisites>
13+
- Database [connection basics](/docs/connect-overview) with Drizzle
14+
- Nile Database - [website](https://thenile.dev)
15+
- Drizzle PostgreSQL drivers - [docs](/docs/get-started-postgresql)
16+
</Prerequisites>
17+
18+
According to the **[official website](https://thenile.dev)**, Nile is PostgreSQL re-engineered for multi-tenant apps.
19+
20+
Checkout official **[Nile + Drizzle Quickstart](https://www.thenile.dev/docs/getting-started/languages/drizzle)** and **[Migration](https://www.thenile.dev/docs/getting-started/schema_migrations/drizzle) docs.
21+
22+
You can use Nile with any of Drizzle's Postgres drivers, we'll be showing the use of `node-postgres` below.
23+
24+
#### Step 1 - Install packages
25+
26+
<Npm>
27+
drizzle-orm postgres
28+
-D drizzle-kit
29+
</Npm>
30+
31+
#### Step 2 - Initialize the driver and make a query
32+
33+
```typescript copy filename="index.ts"
34+
// Make sure to install the 'pg' package
35+
import { drizzle } from 'drizzle-orm/node-postgres'
36+
37+
const db = drizzle(process.env.NILEDB_URL);
38+
39+
const response = await db.select().from(...);
40+
```
41+
42+
If you need to provide your existing driver:
43+
44+
```typescript copy filename="index.ts"
45+
// Make sure to install the 'pg' package
46+
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
47+
import { drizzle } from "drizzle-orm/node-postgres";
48+
import { Pool } from "pg";
49+
const pool = new Pool({
50+
connectionString: process.env.DATABASE_URL,
51+
});
52+
const db = drizzle({ client: pool });
53+
54+
const response = await db.select().from(...);
55+
```
56+
57+
#### Connecting to a virtual tenant database
58+
59+
Nile provides virtual tenant databases, when you set the tenant context, Nile will direct your queries to the virtual database for this particular tenant and all queries will apply to that tenant (i.e. `select * from table` will result records only for this tenant).
60+
61+
In order to set the tenant context, we wrap each query in a transaction that sets the appropriate tenant context before running the transaction.
62+
63+
The tenant ID can simply be passed into the wrapper as an argument:
64+
65+
```typescript copy filename="index.ts"
66+
import { drizzle } from 'drizzle-orm/node-postgres';
67+
import { todosTable, tenants } from "./db/schema";
68+
import { sql } from 'drizzle-orm';
69+
import 'dotenv/config';
70+
71+
const db = drizzle(process.env.NILEDB_URL);
72+
73+
function tenantDB<T>(tenantId: string, cb: (tx: any) => T | Promise<T>): Promise<T> {
74+
return db.transaction(async (tx) => {
75+
if (tenantId) {
76+
await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
77+
}
78+
79+
return cb(tx);
80+
}) as Promise<T>;
81+
}
82+
83+
// In a webapp, you'll likely get it from the request path parameters or headers
84+
const tenantId = '01943e56-16df-754f-a7b6-6234c368b400'
85+
86+
const response = await tenantDB(tenantId, async (tx) => {
87+
// No need for a "where" clause here
88+
return await tx.select().from(todosTable);
89+
});
90+
91+
console.log(response);
92+
```
93+
94+
If you are using a web framwork that supports it, you can set up [AsyncLocalStorage](https://nodejs.org/api/async_context.html) and use middleware to populate it with the tenant ID. In this case, your Drizzle client setup will be:
95+
96+
```typescript copy filename="db/index.ts
97+
import { drizzle } from 'drizzle-orm/node-postgres';
98+
import dotenv from "dotenv/config";
99+
import { sql } from "drizzle-orm";
100+
import { AsyncLocalStorage } from "async_hooks";
101+
102+
export const db = drizzle(process.env.NILEDB_URL);
103+
export const tenantContext = new AsyncLocalStorage<string | undefined>();
104+
105+
export function tenantDB<T>(cb: (tx: any) => T | Promise<T>): Promise<T> {
106+
return db.transaction(async (tx) => {
107+
const tenantId = tenantContext.getStore();
108+
console.log("executing query with tenant: " + tenantId);
109+
// if there's a tenant ID, set it in the transaction context
110+
if (tenantId) {
111+
await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
112+
}
113+
114+
return cb(tx);
115+
}) as Promise<T>;
116+
}
117+
```
118+
119+
And then, configure a middleware to populate the the AsyncLocalStorage and use `tenantDB` method when handling requests:
120+
121+
```typescript copy filename="app.ts"
122+
// Middleware to set tenant context
123+
app.use("/api/tenants/:tenantId/*", async (c, next) => {
124+
const tenantId = c.req.param("tenantId");
125+
console.log("setting context to tenant: " + tenantId);
126+
return tenantContext.run(tenantId, () => next());
127+
});
128+
129+
// Route handler
130+
app.get("/api/tenants/:tenantId/todos", async (c) => {
131+
const todos = await tenantDB(c, async (tx) => {
132+
return await tx
133+
.select({
134+
id: todoSchema.id,
135+
tenant_id: todoSchema.tenantId,
136+
title: todoSchema.title,
137+
estimate: todoSchema.estimate,
138+
})
139+
.from(todoSchema);
140+
});
141+
return c.json(todos);
142+
});
143+
```
144+
145+
146+
#### What's next?
147+
148+
<WhatsNextPostgres/>
149+
150+
151+

src/content/docs/get-started/_meta.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
["xata-existing", "PostgreSQL"],
1212
["pglite-new", "PostgreSQL"],
1313
["pglite-existing", "PostgreSQL"],
14+
["nile-new", "PostgreSQL"],
15+
["nile-existing", "PostgreSQL"],
1416
["vercel-new", "PostgreSQL"],
1517
["vercel-existing", "PostgreSQL"],
1618
["mysql-new", "MySQL"],
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import Tab from '@mdx/Tab.astro';
2+
import Tabs from '@mdx/Tabs.astro';
3+
import Npm from "@mdx/Npm.astro";
4+
import Callout from '@mdx/Callout.astro';
5+
import Steps from '@mdx/Steps.astro';
6+
import AnchorCards from '@mdx/AnchorCards.astro';
7+
import Breadcrumbs from '@mdx/Breadcrumbs.astro';
8+
import CodeTabs from "@mdx/CodeTabs.astro";
9+
import Prerequisites from "@mdx/Prerequisites.astro";
10+
import IntrospectPostgreSQL from '@mdx/get-started/postgresql/IntrospectPostgreSQL.mdx';
11+
import FileStructure from '@mdx/get-started/FileStructure.mdx';
12+
import InstallPackages from '@mdx/get-started/InstallPackages.mdx';
13+
import SetupConfig from '@mdx/get-started/SetupConfig.mdx';
14+
import SetupEnv from '@mdx/get-started/SetupEnv.mdx';
15+
import TransferCode from '@mdx/get-started/TransferCode.mdx';
16+
import ApplyChanges from '@mdx/get-started/ApplyChanges.mdx';
17+
import RunFile from '@mdx/get-started/RunFile.mdx';
18+
import ConnectNile from '@mdx/get-started/postgresql/ConnectNile.mdx'
19+
import QueryNile from '@mdx/get-started/postgresql/QueryNile.mdx';
20+
import QueryDatabaseUpdated from '@mdx/get-started/QueryDatabaseUpdated.mdx';
21+
import UpdateSchema from '@mdx/get-started/postgresql/UpdateSchema.mdx';
22+
23+
<Breadcrumbs/>
24+
25+
# Get Started with Drizzle and Supabase in existing project
26+
27+
<Prerequisites>
28+
- **dotenv** - package for managing environment variables - [read here](https://www.npmjs.com/package/dotenv)
29+
- **tsx** - package for running TypeScript files - [read here](https://tsx.is/)
30+
- **Nile** - PostgreSQL re-engineered for multi-tenant apps - [read here](https://thenile.dev/)
31+
</Prerequisites>
32+
33+
<FileStructure/>
34+
35+
#### Step 1 - Install **postgres** package
36+
<InstallPackages lib='pg' devlib=' @types/pg'/>
37+
38+
#### Step 2 - Setup connection variables
39+
40+
<SetupEnv env_variable='NILEDB_URL' />
41+
42+
#### Step 3 - Setup Drizzle config file
43+
44+
<SetupConfig dialect='postgresql' env_variable='NILEDB_URL'/>
45+
46+
#### Step 4 - Introspect your database
47+
48+
Drizzle Kit provides a CLI command to introspect your database and generate a schema file with migrations. The schema file contains all the information about your database tables, columns, relations, and indices.
49+
50+
For example, you have such table in your database:
51+
52+
```sql copy
53+
CREATE TABLE IF NOT EXISTS "todos" (
54+
"id" uuid DEFAULT gen_random_uuid(),
55+
"tenant_id" uuid,
56+
"title" varchar(256),
57+
"estimate" varchar(256),
58+
"embedding" vector(3),
59+
"complete" boolean
60+
);
61+
```
62+
63+
Pull your database schema:
64+
65+
```bash copy
66+
npx drizzle-kit pull
67+
```
68+
69+
The result of introspection will be a `schema.ts` file, `meta` folder with snapshots of your database schema, sql file with the migration and `relations.ts` file for [relational queries](/docs/rqb).
70+
71+
<Callout title='built-in tables'>
72+
Nile has several built-in tables that are part of every database. When you introspect a Nile database, the built-in tables will be included.
73+
For example, the `tenants` table that you see in the example below. This will allow you to easily create new tenants, list tenants and other operations.
74+
</Callout>
75+
76+
Here is an example of the generated `schema.ts` file:
77+
78+
```typescript copy filename="src/db/schema.ts"
79+
// table schema generated by introspection
80+
import { pgTable, uuid, text, timestamp, varchar, vector, boolean } from "drizzle-orm/pg-core"
81+
import { sql } from "drizzle-orm"
82+
83+
export const tenants = pgTable("tenants", {
84+
id: uuid().default(sql`public.uuid_generate_v7()`).primaryKey().notNull(),
85+
name: text(),
86+
created: timestamp({ mode: 'string' }).default(sql`LOCALTIMESTAMP`).notNull(),
87+
updated: timestamp({ mode: 'string' }).default(sql`LOCALTIMESTAMP`).notNull(),
88+
deleted: timestamp({ mode: 'string' }),
89+
});
90+
91+
export const todos = pgTable("todos", {
92+
id: uuid().defaultRandom(),
93+
tenantId: uuid("tenant_id"),
94+
title: varchar({ length: 256 }),
95+
estimate: varchar({ length: 256 }),
96+
embedding: vector({ dimensions: 3 }),
97+
complete: boolean(),
98+
});
99+
```
100+
101+
Learn more about introspection in the [documentation](/docs/drizzle-kit-pull).
102+
103+
#### Step 5 - Transfer code to your actual schema file
104+
105+
<TransferCode/>
106+
107+
#### Step 6 - Connect Drizzle ORM to the database
108+
109+
<ConnectNile/>
110+
111+
#### Step 7 - Query the database
112+
113+
<QueryNile />
114+
115+
#### Step 8 - Run index.ts file
116+
117+
<RunFile/>
118+
119+
#### Step 9 - Update your table schema (optional)
120+
121+
If you want to update your table schema, you can do it in the `schema.ts` file. For example, let's add a new column `deadline` to the `todos` table`:
122+
123+
```typescript copy filename="src/db/schema.ts" {19}
124+
import { pgTable, uuid, text, timestamp, varchar, vector, boolean } from "drizzle-orm/pg-core"
125+
import { sql } from "drizzle-orm"
126+
127+
export const tenants = pgTable("tenants", {
128+
id: uuid().default(sql`public.uuid_generate_v7()`).primaryKey().notNull(),
129+
name: text(),
130+
created: timestamp({ mode: 'string' }).default(sql`LOCALTIMESTAMP`).notNull(),
131+
updated: timestamp({ mode: 'string' }).default(sql`LOCALTIMESTAMP`).notNull(),
132+
deleted: timestamp({ mode: 'string' }),
133+
});
134+
135+
export const todos = pgTable("todos", {
136+
id: uuid().defaultRandom(),
137+
tenantId: uuid("tenant_id"),
138+
title: varchar({ length: 256 }),
139+
estimate: varchar({ length: 256 }),
140+
embedding: vector({ dimensions: 3 }),
141+
complete: boolean(),
142+
deadline: timestamp({ mode: 'string' })
143+
});
144+
```
145+
146+
#### Step 10 - Applying changes to the database (optional)
147+
148+
<ApplyChanges />
149+
150+
#### Step 11 - Query the database with a new field (optional)
151+
152+
If you run the `index.ts` file again, you'll be able to see the new field that you've just added.
153+
The field will be `null` since we did not populate deadlines when inserting todos previously.
154+
155+
<RunFile/>

0 commit comments

Comments
 (0)