Skip to content

Commit 2d6b4df

Browse files
authored
Update D1 template to support production seeding via drizzle (#43)
* Fix ENVIROMENT typo and add production seeding logic for drizzle * Remove dead code * Clarify biome comment * Print friendly error message when local wrangler d1 db not found
1 parent 1be4a10 commit 2d6b4df

File tree

5 files changed

+186
-14
lines changed

5 files changed

+186
-14
lines changed

examples/uptime-monitor/drizzle.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { config } from "dotenv";
44
import { defineConfig } from "drizzle-kit";
55

66
let dbConfig: ReturnType<typeof defineConfig>;
7-
if (process.env.ENVIROMENT === "production") {
7+
if (process.env.ENVIRONMENT === "production") {
88
config({ path: "./.prod.vars" });
99
dbConfig = defineConfig({
1010
schema: "./src/db/schema.ts",

examples/uptime-monitor/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"db:touch": "wrangler d1 execute uptime-d1-database --local --command='SELECT 1'",
77
"db:generate": "drizzle-kit generate",
88
"db:migrate": "wrangler d1 migrations apply uptime-d1-database --local",
9-
"db:migrate:prod": "ENVIROMENT=production drizzle-kit migrate",
9+
"db:migrate:prod": "ENVIRONMENT=production drizzle-kit migrate",
1010
"db:seed": "tsx seed.ts",
1111
"db:setup": "npm run db:touch && npm run db:generate && npm run db:migrate && npm run db:seed",
1212
"db:studio": "drizzle-kit studio",

templates/d1/drizzle.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { config } from "dotenv";
44
import { defineConfig } from "drizzle-kit";
55

66
let dbConfig: ReturnType<typeof defineConfig>;
7-
if (process.env.ENVIROMENT === "production") {
7+
if (process.env.ENVIRONMENT === "production") {
88
config({ path: "./.prod.vars" });
99
dbConfig = defineConfig({
1010
schema: "./src/db/schema.ts",

templates/d1/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"db:touch": "wrangler d1 execute honc-d1-database --local --command='SELECT 1'",
77
"db:generate": "drizzle-kit generate",
88
"db:migrate": "wrangler d1 migrations apply honc-d1-database --local",
9-
"db:migrate:prod": "ENVIROMENT=production drizzle-kit migrate",
9+
"db:migrate:prod": "ENVIRONMENT=production drizzle-kit migrate",
1010
"db:seed": "tsx seed.ts",
11+
"db:seed:prod": "ENVIRONMENT=production tsx seed.ts",
1112
"db:setup": "npm run db:touch && npm run db:generate && npm run db:migrate && npm run db:seed",
1213
"db:studio": "drizzle-kit studio",
1314
"fiberplane": "npx @fiberplane/studio@latest"

templates/d1/seed.ts

+181-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
import { createClient } from "@libsql/client";
22
import { drizzle } from "drizzle-orm/libsql";
3+
import {
4+
type AsyncBatchRemoteCallback,
5+
type AsyncRemoteCallback,
6+
drizzle as drizzleSQLiteProxy,
7+
type SqliteRemoteDatabase,
8+
} from "drizzle-orm/sqlite-proxy";
39
import { seed } from "drizzle-seed";
10+
411
import * as schema from "./src/db/schema";
512
import path from "node:path";
613
import fs from "node:fs";
14+
import { config } from "dotenv";
15+
16+
// biome-ignore lint/suspicious/noExplicitAny: Centralize usage of `any` type, since we use it in db results that are not worth the pain of typing
17+
type Any = any;
18+
19+
seedDatabase();
20+
21+
async function seedDatabase() {
22+
const isProd = process.env.ENVIRONMENT === "production";
23+
const db = isProd ? await getProductionDatabase() : getLocalD1Db();
24+
25+
if (isProd) {
26+
console.warn("🚨 Seeding production database");
27+
}
728

8-
const seedDatabase = async () => {
9-
const pathToDb = getLocalD1DB();
10-
const client = createClient({
11-
url: `file:${pathToDb}`,
12-
});
13-
const db = drizzle(client);
14-
1529
try {
1630
// Read more about seeding here: https://orm.drizzle.team/docs/seed-overview#drizzle-seed
1731
await seed(db, schema);
1832
console.log("✅ Database seeded successfully!");
19-
console.log("🪿 Run `npm run fiberplane` to explore data with your api.");
33+
if (!isProd) {
34+
console.log("🪿 Run `npm run fiberplane` to explore data with your api.");
35+
}
2036
} catch (error) {
2137
console.error("❌ Error seeding database:", error);
2238
process.exit(1);
@@ -25,7 +41,31 @@ const seedDatabase = async () => {
2541
}
2642
};
2743

28-
function getLocalD1DB() {
44+
/**
45+
* Creates a connection to the local D1 database and returns a Drizzle ORM instance.
46+
*
47+
* Relies on `getLocalD1DBPath` to find the path to the local D1 database inside the `.wrangler` directory.
48+
*/
49+
function getLocalD1Db() {
50+
const pathToDb = getLocalD1DBPath();
51+
if (!pathToDb) {
52+
console.error(
53+
"⚠️ Local D1 database not found. Try running `npm run db:touch` to create one.",
54+
);
55+
process.exit(1);
56+
}
57+
58+
const client = createClient({
59+
url: `file:${pathToDb}`,
60+
});
61+
const db = drizzle(client);
62+
return db;
63+
}
64+
65+
/**
66+
* Finds the path to the local D1 database inside the `.wrangler` directory.
67+
*/
68+
function getLocalD1DBPath() {
2969
try {
3070
const basePath = path.resolve(".wrangler");
3171
const files = fs
@@ -56,4 +96,135 @@ function getLocalD1DB() {
5696
}
5797
}
5898

59-
seedDatabase();
99+
/**
100+
* Creates a connection to the production Cloudflare D1 database and returns a Drizzle ORM instance.
101+
* Loads production environment variables from .prod.vars file.
102+
*
103+
* @returns {Promise<SqliteRemoteDatabase>} Drizzle ORM instance connected to production database
104+
* @throws {Error} If required environment variables are not set
105+
*/
106+
async function getProductionDatabase(): Promise<
107+
SqliteRemoteDatabase<Record<string, never>>
108+
> {
109+
config({ path: ".prod.vars" });
110+
111+
const apiToken = process.env.CLOUDFLARE_D1_TOKEN;
112+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
113+
const databaseId = process.env.CLOUDFLARE_DATABASE_ID;
114+
115+
if (!apiToken || !accountId || !databaseId) {
116+
console.error(
117+
"Database seed failed: production environment variables not set (make sure you have a .prod.vars file)",
118+
);
119+
process.exit(1);
120+
}
121+
122+
return createProductionD1Connection(accountId, databaseId, apiToken);
123+
}
124+
125+
/**
126+
* Creates a connection to a remote Cloudflare D1 database using the sqlite-proxy driver.
127+
* @param accountId - Cloudflare account ID
128+
* @param databaseId - D1 database ID
129+
* @param apiToken - Cloudflare API token with write access to D1
130+
* @returns Drizzle ORM instance connected to remote database
131+
*/
132+
export function createProductionD1Connection(
133+
accountId: string,
134+
databaseId: string,
135+
apiToken: string,
136+
) {
137+
/**
138+
* Executes a single query against the Cloudflare D1 HTTP API.
139+
*
140+
* @param accountId - Cloudflare account ID
141+
* @param databaseId - D1 database ID
142+
* @param apiToken - Cloudflare API token with write access to D1
143+
* @param sql - The SQL statement to execute
144+
* @param params - Parameters for the SQL statement
145+
* @param method - The method type for the SQL operation
146+
* @returns The result rows from the query
147+
* @throws If the HTTP request fails or returns an error
148+
*/
149+
async function executeCloudflareD1Query(
150+
accountId: string,
151+
databaseId: string,
152+
apiToken: string,
153+
sql: string,
154+
params: Any[],
155+
method: string,
156+
): Promise<{ rows: Any[][] }> {
157+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`;
158+
159+
const res = await fetch(url, {
160+
method: "POST",
161+
headers: {
162+
Authorization: `Bearer ${apiToken}`,
163+
"Content-Type": "application/json",
164+
},
165+
body: JSON.stringify({ sql, params, method }),
166+
});
167+
168+
const data: Any = await res.json();
169+
170+
if (res.status !== 200) {
171+
throw new Error(
172+
`Error from sqlite proxy server: ${res.status} ${res.statusText}\n${JSON.stringify(data)}`,
173+
);
174+
}
175+
176+
if (data.errors.length > 0 || !data.success) {
177+
throw new Error(
178+
`Error from sqlite proxy server: \n${JSON.stringify(data)}}`,
179+
);
180+
}
181+
182+
const qResult = data?.result?.[0];
183+
184+
if (!qResult?.success) {
185+
throw new Error(
186+
`Error from sqlite proxy server: \n${JSON.stringify(data)}`,
187+
);
188+
}
189+
190+
return { rows: qResult.results.map((r: Any) => Object.values(r)) };
191+
}
192+
193+
/**
194+
* Asynchronously executes a single query.
195+
*/
196+
const queryClient: AsyncRemoteCallback = async (sql, params, method) => {
197+
return executeCloudflareD1Query(
198+
accountId,
199+
databaseId,
200+
apiToken,
201+
sql,
202+
params,
203+
method,
204+
);
205+
};
206+
207+
/**
208+
* Asynchronously executes a batch of queries.
209+
*/
210+
const batchQueryClient: AsyncBatchRemoteCallback = async (queries) => {
211+
const results: { rows: Any[][] }[] = [];
212+
213+
for (const query of queries) {
214+
const { sql, params, method } = query;
215+
const result = await executeCloudflareD1Query(
216+
accountId,
217+
databaseId,
218+
apiToken,
219+
sql,
220+
params,
221+
method,
222+
);
223+
results.push(result);
224+
}
225+
226+
return results;
227+
};
228+
229+
return drizzleSQLiteProxy(queryClient, batchQueryClient);
230+
}

0 commit comments

Comments
 (0)