cloudflare-d1 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/d1.yaml +560 -0
- data/.claude/test-driving-rails.md +5482 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/DATABASE_MANAGEMENT.md +365 -0
- data/LICENSE.txt +21 -0
- data/README.md +253 -0
- data/RELEASING.md +199 -0
- data/Rakefile +16 -0
- data/cloudflare-d1.gemspec +40 -0
- data/examples/roda/.dockerignore +14 -0
- data/examples/roda/.gitignore +11 -0
- data/examples/roda/Dockerfile +21 -0
- data/examples/roda/Gemfile +12 -0
- data/examples/roda/Gemfile.lock +35 -0
- data/examples/roda/README.md +459 -0
- data/examples/roda/app.rb +165 -0
- data/examples/roda/db/migrations/001_create_users.rb +17 -0
- data/examples/roda/setup.sh +128 -0
- data/examples/roda/worker.js +119 -0
- data/examples/roda/wrangler.jsonc +68 -0
- data/lib/active_record/connection_adapters/cloudflare_d1_adapter.rb +447 -0
- data/lib/cloudflare/d1/client.rb +149 -0
- data/lib/cloudflare/d1/model.rb +46 -0
- data/lib/cloudflare/d1/railtie.rb +13 -0
- data/lib/cloudflare/d1/version.rb +7 -0
- data/lib/cloudflare/d1.rb +39 -0
- data/lib/sequel/adapters/cloudflare_d1.rb +265 -0
- data/lib/tasks/database.rake +158 -0
- data/lib/tasks/sequel.rake +199 -0
- data/sig/cloudflare/d1.rbs +6 -0
- metadata +163 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Roda + Cloudflare D1 + Containers Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates deploying a Roda application using Cloudflare Containers with a D1 database backend.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Internet Request
|
|
9
|
+
↓
|
|
10
|
+
Cloudflare Worker (worker.js)
|
|
11
|
+
↓
|
|
12
|
+
Container Instance (Durable Object)
|
|
13
|
+
↓
|
|
14
|
+
Roda App (Ruby)
|
|
15
|
+
↓
|
|
16
|
+
Cloudflare D1 Database (SQLite via REST API)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Roda** - Fast, simple Ruby web framework
|
|
22
|
+
- **Sequel** - Database toolkit for Ruby
|
|
23
|
+
- **Cloudflare D1** - SQLite database at the edge
|
|
24
|
+
- **Cloudflare Containers** - Run containerized apps on Workers
|
|
25
|
+
- **RESTful API** - Full CRUD operations for users
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
1. **Cloudflare Account** with Workers Paid plan
|
|
30
|
+
2. **Wrangler CLI**:
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g wrangler
|
|
33
|
+
wrangler login
|
|
34
|
+
```
|
|
35
|
+
3. **Docker** running locally
|
|
36
|
+
4. **Ruby 3.2+** (for local development)
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### 1. Create D1 Database
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Create a new D1 database
|
|
44
|
+
wrangler d1 create roda-api-db
|
|
45
|
+
|
|
46
|
+
# Note the database ID from the output
|
|
47
|
+
# Add it to your secrets (see step 3)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Install Dependencies
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bundle install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Configure Secrets
|
|
57
|
+
|
|
58
|
+
Set your Cloudflare credentials and database ID as Worker secrets:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Your Cloudflare account ID
|
|
62
|
+
wrangler secret put CLOUDFLARE_ACCOUNT_ID
|
|
63
|
+
# Enter: your_account_id
|
|
64
|
+
|
|
65
|
+
# Your Cloudflare API token (needs D1 permissions)
|
|
66
|
+
wrangler secret put CLOUDFLARE_API_TOKEN
|
|
67
|
+
# Enter: your_api_token
|
|
68
|
+
|
|
69
|
+
# Your D1 database UUID (from step 1)
|
|
70
|
+
wrangler secret put DATABASE_ID
|
|
71
|
+
# Enter: your_database_uuid
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
These secrets are stored in the Worker and passed to the container at runtime via `worker.js`.
|
|
75
|
+
|
|
76
|
+
**To get your credentials:**
|
|
77
|
+
- Account ID: `wrangler whoami` or Cloudflare dashboard
|
|
78
|
+
- API Token: Dashboard → My Profile → API Tokens → Create Token
|
|
79
|
+
- Template: "Edit Cloudflare Workers"
|
|
80
|
+
- Permissions: Include D1 Database permissions
|
|
81
|
+
|
|
82
|
+
### 4. Update wrangler.jsonc
|
|
83
|
+
|
|
84
|
+
Edit `wrangler.jsonc` and update:
|
|
85
|
+
- `name`: Must be unique across Cloudflare (change `roda-d1-api` to something unique)
|
|
86
|
+
- `routes`: Optional, configure custom domain
|
|
87
|
+
|
|
88
|
+
## Local Development
|
|
89
|
+
|
|
90
|
+
The app uses **local SQLite** for development (since D1 is only accessible via Cloudflare's API).
|
|
91
|
+
|
|
92
|
+
### Run Locally
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Install dependencies
|
|
96
|
+
bundle install
|
|
97
|
+
|
|
98
|
+
# Run the app (uses local SQLite)
|
|
99
|
+
ruby app.rb
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**No environment variables needed** - it automatically uses `db/development.db`.
|
|
103
|
+
|
|
104
|
+
### Test Locally
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Create a user
|
|
108
|
+
curl -X POST http://localhost:8080/users \
|
|
109
|
+
-H "Content-Type: application/json" \
|
|
110
|
+
-d '{"name": "John Doe", "email": "john@example.com"}'
|
|
111
|
+
|
|
112
|
+
# List users
|
|
113
|
+
curl http://localhost:8080/users
|
|
114
|
+
|
|
115
|
+
# Get user by ID
|
|
116
|
+
curl http://localhost:8080/users/1
|
|
117
|
+
|
|
118
|
+
# Update user
|
|
119
|
+
curl -X PUT http://localhost:8080/users/1 \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-d '{"name": "Jane Doe"}'
|
|
122
|
+
|
|
123
|
+
# Delete user
|
|
124
|
+
curl -X DELETE http://localhost:8080/users/1
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### How It Works
|
|
128
|
+
|
|
129
|
+
The app detects the environment:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
if ENV["RACK_ENV"] == "production"
|
|
133
|
+
# Use Cloudflare D1 (in container)
|
|
134
|
+
DB = Sequel.connect(adapter: :cloudflare_d1, ...)
|
|
135
|
+
else
|
|
136
|
+
# Use local SQLite (in development)
|
|
137
|
+
DB = Sequel.connect("sqlite://db/development.db")
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**In production** (container): Uses D1 via HTTP API
|
|
142
|
+
**In development** (local): Uses SQLite file directly
|
|
143
|
+
|
|
144
|
+
Same migrations work for both!
|
|
145
|
+
|
|
146
|
+
### Alternative: Test Against Real D1
|
|
147
|
+
|
|
148
|
+
If you need to test against the actual D1 database:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Set environment variables
|
|
152
|
+
export RACK_ENV=production
|
|
153
|
+
export CLOUDFLARE_ACCOUNT_ID=your_account_id
|
|
154
|
+
export CLOUDFLARE_API_TOKEN=your_api_token
|
|
155
|
+
export DATABASE_ID=your_database_uuid
|
|
156
|
+
|
|
157
|
+
# Run locally (will use D1 API)
|
|
158
|
+
ruby app.rb
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Note:** This makes real HTTP requests to Cloudflare's API on every query (slow for development).
|
|
162
|
+
|
|
163
|
+
## Deployment
|
|
164
|
+
|
|
165
|
+
### 1. Deploy to Cloudflare
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Make sure Docker is running
|
|
169
|
+
docker info
|
|
170
|
+
|
|
171
|
+
# Deploy the application
|
|
172
|
+
wrangler deploy
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Note:** First deployment takes several minutes as Cloudflare provisions container infrastructure.
|
|
176
|
+
|
|
177
|
+
**Migrations run automatically** when the container starts. No manual migration step needed.
|
|
178
|
+
|
|
179
|
+
### 2. Check Status
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# List containers
|
|
183
|
+
wrangler containers list
|
|
184
|
+
|
|
185
|
+
# List container images
|
|
186
|
+
wrangler containers images list
|
|
187
|
+
|
|
188
|
+
# View logs
|
|
189
|
+
wrangler tail
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 3. Test Deployed App
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Replace with your deployed URL
|
|
196
|
+
export API_URL=https://roda-d1-api.your-subdomain.workers.dev
|
|
197
|
+
|
|
198
|
+
# Health check
|
|
199
|
+
curl $API_URL/health
|
|
200
|
+
|
|
201
|
+
# Create user
|
|
202
|
+
curl -X POST $API_URL/users \
|
|
203
|
+
-H "Content-Type: application/json" \
|
|
204
|
+
-d '{"name": "Alice", "email": "alice@example.com"}'
|
|
205
|
+
|
|
206
|
+
# List users
|
|
207
|
+
curl $API_URL/users
|
|
208
|
+
|
|
209
|
+
# Get user
|
|
210
|
+
curl $API_URL/users/1
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Endpoints
|
|
214
|
+
|
|
215
|
+
### Root
|
|
216
|
+
```
|
|
217
|
+
GET /
|
|
218
|
+
```
|
|
219
|
+
Returns API information and available endpoints.
|
|
220
|
+
|
|
221
|
+
### Users
|
|
222
|
+
|
|
223
|
+
#### List all users
|
|
224
|
+
```
|
|
225
|
+
GET /users
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Response:
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"users": [
|
|
232
|
+
{
|
|
233
|
+
"id": 1,
|
|
234
|
+
"name": "John Doe",
|
|
235
|
+
"email": "john@example.com",
|
|
236
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
237
|
+
"updated_at": "2024-01-01T00:00:00Z"
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### Get user by ID
|
|
244
|
+
```
|
|
245
|
+
GET /users/:id
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Response:
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"user": {
|
|
252
|
+
"id": 1,
|
|
253
|
+
"name": "John Doe",
|
|
254
|
+
"email": "john@example.com",
|
|
255
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
256
|
+
"updated_at": "2024-01-01T00:00:00Z"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Create user
|
|
262
|
+
```
|
|
263
|
+
POST /users
|
|
264
|
+
Content-Type: application/json
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
"name": "John Doe",
|
|
268
|
+
"email": "john@example.com"
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Response: `201 Created`
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"user": {
|
|
276
|
+
"id": 1,
|
|
277
|
+
"name": "John Doe",
|
|
278
|
+
"email": "john@example.com",
|
|
279
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
280
|
+
"updated_at": "2024-01-01T00:00:00Z"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Update user
|
|
286
|
+
```
|
|
287
|
+
PUT /users/:id
|
|
288
|
+
Content-Type: application/json
|
|
289
|
+
|
|
290
|
+
{
|
|
291
|
+
"name": "Jane Doe",
|
|
292
|
+
"email": "jane@example.com"
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Response:
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"user": {
|
|
300
|
+
"id": 1,
|
|
301
|
+
"name": "Jane Doe",
|
|
302
|
+
"email": "jane@example.com",
|
|
303
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
304
|
+
"updated_at": "2024-01-01T00:00:01Z"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### Delete user
|
|
310
|
+
```
|
|
311
|
+
DELETE /users/:id
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Response: `204 No Content`
|
|
315
|
+
|
|
316
|
+
## Migrations
|
|
317
|
+
|
|
318
|
+
Migrations run automatically on container boot. To add a new migration:
|
|
319
|
+
|
|
320
|
+
1. Create a file in `db/migrations/` with format `00X_description.rb`:
|
|
321
|
+
```ruby
|
|
322
|
+
# db/migrations/002_add_age_to_users.rb
|
|
323
|
+
Sequel.migration do
|
|
324
|
+
up do
|
|
325
|
+
alter_table(:users) do
|
|
326
|
+
add_column :age, Integer
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
down do
|
|
331
|
+
alter_table(:users) do
|
|
332
|
+
drop_column :age
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
2. Deploy:
|
|
339
|
+
```bash
|
|
340
|
+
wrangler deploy # Migration runs on boot
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Project Structure
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
examples/roda/
|
|
347
|
+
├── Dockerfile # Container image definition
|
|
348
|
+
├── Gemfile # Ruby dependencies
|
|
349
|
+
├── Gemfile.lock # Locked dependency versions
|
|
350
|
+
├── app.rb # Roda application
|
|
351
|
+
├── worker.js # Cloudflare Worker (routes to container)
|
|
352
|
+
├── wrangler.jsonc # Cloudflare deployment configuration
|
|
353
|
+
├── db/
|
|
354
|
+
│ └── migrations/ # Database migrations (run on boot)
|
|
355
|
+
└── README.md # This file
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Configuration
|
|
359
|
+
|
|
360
|
+
### Container Settings (wrangler.jsonc)
|
|
361
|
+
|
|
362
|
+
- **max_instances**: Maximum concurrent container instances (default: 10)
|
|
363
|
+
- **instance_type**: Container size - `lite`, `basic`, `standard-1` through `standard-4`
|
|
364
|
+
- **sleepAfter**: How long to keep container alive after inactivity (default: "10m")
|
|
365
|
+
|
|
366
|
+
### Worker Routing Strategies
|
|
367
|
+
|
|
368
|
+
The `worker.js` file uses a single stateful container instance:
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
const containerStub = env.RODA_CONTAINER.getByName("main");
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Alternative strategies:**
|
|
375
|
+
|
|
376
|
+
1. **Load balanced (stateless)**:
|
|
377
|
+
```javascript
|
|
378
|
+
const id = Math.floor(Math.random() * 3);
|
|
379
|
+
const containerStub = env.RODA_CONTAINER.getById(id.toString());
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
2. **Session-based (sticky sessions)**:
|
|
383
|
+
```javascript
|
|
384
|
+
const sessionId = request.headers.get("X-Session-ID") || "default";
|
|
385
|
+
const containerStub = env.RODA_CONTAINER.getByName(sessionId);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Troubleshooting
|
|
389
|
+
|
|
390
|
+
### Container not starting
|
|
391
|
+
|
|
392
|
+
Check logs:
|
|
393
|
+
```bash
|
|
394
|
+
wrangler tail
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Verify Docker is running:
|
|
398
|
+
```bash
|
|
399
|
+
docker info
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Database connection errors
|
|
403
|
+
|
|
404
|
+
Verify secrets are set:
|
|
405
|
+
```bash
|
|
406
|
+
wrangler secret list
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Test D1 connection:
|
|
410
|
+
```bash
|
|
411
|
+
wrangler d1 execute roda-api-db --command="SELECT 1"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Build errors
|
|
415
|
+
|
|
416
|
+
Clear Docker cache:
|
|
417
|
+
```bash
|
|
418
|
+
docker system prune -a
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Rebuild:
|
|
422
|
+
```bash
|
|
423
|
+
wrangler deploy
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### "Class not found" errors
|
|
427
|
+
|
|
428
|
+
Make sure:
|
|
429
|
+
- `class_name` matches between `containers` and `durable_objects.bindings`
|
|
430
|
+
- `new_sqlite_classes` includes the class name
|
|
431
|
+
- You've run `wrangler deploy` after changing configuration
|
|
432
|
+
|
|
433
|
+
## Development Tips
|
|
434
|
+
|
|
435
|
+
1. **Use local Ruby for faster iteration** - Test changes locally before deploying
|
|
436
|
+
2. **Monitor logs** - Keep `wrangler tail` running during development
|
|
437
|
+
3. **Version your migrations** - Use Sequel migrations for database changes
|
|
438
|
+
4. **Test error handling** - Containers can fail, handle errors gracefully
|
|
439
|
+
5. **Monitor costs** - Containers use compute time, monitor usage in dashboard
|
|
440
|
+
|
|
441
|
+
## Limitations
|
|
442
|
+
|
|
443
|
+
- Container cold starts take a few seconds
|
|
444
|
+
- Maximum 10 concurrent instances (configurable)
|
|
445
|
+
- No persistent filesystem (use D1 for data)
|
|
446
|
+
- Must use linux/amd64 architecture
|
|
447
|
+
- Requires Workers Paid plan
|
|
448
|
+
|
|
449
|
+
## Learn More
|
|
450
|
+
|
|
451
|
+
- [Cloudflare Containers](https://developers.cloudflare.com/containers/)
|
|
452
|
+
- [Cloudflare D1](https://developers.cloudflare.com/d1/)
|
|
453
|
+
- [Roda Framework](http://roda.jeremyevans.net/)
|
|
454
|
+
- [Sequel ORM](https://sequel.jeremyevans.net/)
|
|
455
|
+
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
require "roda"
|
|
2
|
+
require "sequel"
|
|
3
|
+
require "sequel/adapters/cloudflare_d1"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# Connect to database
|
|
7
|
+
# In development: Use local SQLite
|
|
8
|
+
# In production: Use Cloudflare D1 via API
|
|
9
|
+
if ENV["RACK_ENV"] == "production"
|
|
10
|
+
DB = Sequel.connect(
|
|
11
|
+
adapter: :cloudflare_d1,
|
|
12
|
+
account_id: ENV["CLOUDFLARE_ACCOUNT_ID"],
|
|
13
|
+
api_token: ENV["CLOUDFLARE_API_TOKEN"],
|
|
14
|
+
database: ENV["DATABASE_ID"]
|
|
15
|
+
)
|
|
16
|
+
else
|
|
17
|
+
# Local development with SQLite
|
|
18
|
+
DB = Sequel.connect("sqlite://db/development.db")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Run migrations on startup
|
|
22
|
+
Sequel.extension :migration
|
|
23
|
+
puts "Running migrations..."
|
|
24
|
+
Sequel::Migrator.run(DB, File.join(__dir__, "db/migrations"))
|
|
25
|
+
puts "Migrations complete!"
|
|
26
|
+
|
|
27
|
+
class App < Roda
|
|
28
|
+
plugin :json
|
|
29
|
+
plugin :all_verbs
|
|
30
|
+
plugin :status_handler
|
|
31
|
+
plugin :error_handler
|
|
32
|
+
|
|
33
|
+
status_handler(404) do
|
|
34
|
+
{ error: "Not found" }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
error_handler do |e|
|
|
38
|
+
{ error: e.message }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
route do |r|
|
|
42
|
+
r.root do
|
|
43
|
+
{
|
|
44
|
+
message: "Cloudflare D1 + Roda API",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
endpoints: {
|
|
47
|
+
users: {
|
|
48
|
+
list: "GET /users",
|
|
49
|
+
show: "GET /users/:id",
|
|
50
|
+
create: "POST /users",
|
|
51
|
+
update: "PUT /users/:id",
|
|
52
|
+
delete: "DELETE /users/:id"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
r.on "users" do
|
|
59
|
+
r.is do
|
|
60
|
+
# GET /users - List all users
|
|
61
|
+
r.get do
|
|
62
|
+
users = DB[:users].all
|
|
63
|
+
{ users: users }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# POST /users - Create a new user
|
|
67
|
+
r.post do
|
|
68
|
+
data = JSON.parse(r.body.read)
|
|
69
|
+
|
|
70
|
+
unless data["name"] && data["email"]
|
|
71
|
+
response.status = 400
|
|
72
|
+
next { error: "Name and email are required" }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
user_id = DB[:users].insert(
|
|
77
|
+
name: data["name"],
|
|
78
|
+
email: data["email"],
|
|
79
|
+
created_at: Time.now,
|
|
80
|
+
updated_at: Time.now
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
user = DB[:users].where(id: user_id).first
|
|
84
|
+
response.status = 201
|
|
85
|
+
{ user: user }
|
|
86
|
+
rescue Sequel::UniqueConstraintViolation
|
|
87
|
+
response.status = 422
|
|
88
|
+
{ error: "Email already exists" }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
r.on Integer do |user_id|
|
|
94
|
+
r.is do
|
|
95
|
+
# GET /users/:id - Get a specific user
|
|
96
|
+
r.get do
|
|
97
|
+
user = DB[:users].where(id: user_id).first
|
|
98
|
+
|
|
99
|
+
unless user
|
|
100
|
+
response.status = 404
|
|
101
|
+
next { error: "User not found" }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
{ user: user }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# PUT /users/:id - Update a user
|
|
108
|
+
r.put do
|
|
109
|
+
user = DB[:users].where(id: user_id).first
|
|
110
|
+
|
|
111
|
+
unless user
|
|
112
|
+
response.status = 404
|
|
113
|
+
next { error: "User not found" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
data = JSON.parse(r.body.read)
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
DB[:users].where(id: user_id).update(
|
|
120
|
+
name: data["name"] || user[:name],
|
|
121
|
+
email: data["email"] || user[:email],
|
|
122
|
+
updated_at: Time.now
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
updated_user = DB[:users].where(id: user_id).first
|
|
126
|
+
{ user: updated_user }
|
|
127
|
+
rescue Sequel::UniqueConstraintViolation
|
|
128
|
+
response.status = 422
|
|
129
|
+
{ error: "Email already exists" }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# DELETE /users/:id - Delete a user
|
|
134
|
+
r.delete do
|
|
135
|
+
user = DB[:users].where(id: user_id).first
|
|
136
|
+
|
|
137
|
+
unless user
|
|
138
|
+
response.status = 404
|
|
139
|
+
next { error: "User not found" }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
DB[:users].where(id: user_id).delete
|
|
143
|
+
response.status = 204
|
|
144
|
+
""
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Run the application with Puma
|
|
153
|
+
require "puma"
|
|
154
|
+
|
|
155
|
+
port = ENV.fetch("PORT", 8080).to_i
|
|
156
|
+
|
|
157
|
+
puts "Starting Roda + Cloudflare D1 API on port #{port}..."
|
|
158
|
+
puts "Database: #{ENV['DATABASE_ID']}"
|
|
159
|
+
|
|
160
|
+
Rack::Handler::Puma.run(
|
|
161
|
+
App.freeze.app,
|
|
162
|
+
Port: port,
|
|
163
|
+
Host: "0.0.0.0",
|
|
164
|
+
Threads: "1:5"
|
|
165
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
create_table(:users) do
|
|
4
|
+
primary_key :id
|
|
5
|
+
String :name, null: false
|
|
6
|
+
String :email, null: false
|
|
7
|
+
DateTime :created_at
|
|
8
|
+
DateTime :updated_at
|
|
9
|
+
|
|
10
|
+
index :email, unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
down do
|
|
15
|
+
drop_table(:users)
|
|
16
|
+
end
|
|
17
|
+
end
|