1
1
import { createClient } from "@libsql/client" ;
2
2
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" ;
3
9
import { seed } from "drizzle-seed" ;
10
+
4
11
import * as schema from "./src/db/schema" ;
5
12
import path from "node:path" ;
6
13
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
+ }
7
28
8
- const seedDatabase = async ( ) => {
9
- const pathToDb = getLocalD1DB ( ) ;
10
- const client = createClient ( {
11
- url : `file:${ pathToDb } ` ,
12
- } ) ;
13
- const db = drizzle ( client ) ;
14
-
15
29
try {
16
30
// Read more about seeding here: https://orm.drizzle.team/docs/seed-overview#drizzle-seed
17
31
await seed ( db , schema ) ;
18
32
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
+ }
20
36
} catch ( error ) {
21
37
console . error ( "❌ Error seeding database:" , error ) ;
22
38
process . exit ( 1 ) ;
@@ -25,7 +41,31 @@ const seedDatabase = async () => {
25
41
}
26
42
} ;
27
43
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 ( ) {
29
69
try {
30
70
const basePath = path . resolve ( ".wrangler" ) ;
31
71
const files = fs
@@ -56,4 +96,135 @@ function getLocalD1DB() {
56
96
}
57
97
}
58
98
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