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,128 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "🚀 Cloudflare D1 + Roda + Containers Setup"
|
|
5
|
+
echo "=========================================="
|
|
6
|
+
echo ""
|
|
7
|
+
|
|
8
|
+
# Check prerequisites
|
|
9
|
+
echo "Checking prerequisites..."
|
|
10
|
+
|
|
11
|
+
if ! command -v wrangler &> /dev/null; then
|
|
12
|
+
echo "❌ Wrangler CLI not found. Install with: npm install -g wrangler"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
echo "✅ Wrangler CLI found"
|
|
16
|
+
|
|
17
|
+
if ! command -v docker &> /dev/null; then
|
|
18
|
+
echo "❌ Docker not found. Please install Docker Desktop"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
echo "✅ Docker found"
|
|
22
|
+
|
|
23
|
+
if ! docker info &> /dev/null; then
|
|
24
|
+
echo "❌ Docker daemon not running. Please start Docker Desktop"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
echo "✅ Docker daemon running"
|
|
28
|
+
|
|
29
|
+
if ! command -v bundle &> /dev/null; then
|
|
30
|
+
echo "❌ Bundler not found. Install with: gem install bundler"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
echo "✅ Bundler found"
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
echo "Prerequisites check complete!"
|
|
37
|
+
echo ""
|
|
38
|
+
|
|
39
|
+
# Install Ruby dependencies
|
|
40
|
+
echo "📦 Installing Ruby dependencies..."
|
|
41
|
+
bundle install
|
|
42
|
+
echo "✅ Dependencies installed"
|
|
43
|
+
echo ""
|
|
44
|
+
|
|
45
|
+
# Create D1 database
|
|
46
|
+
echo "🗄️ Creating D1 database..."
|
|
47
|
+
echo "Running: wrangler d1 create roda-api-db"
|
|
48
|
+
echo ""
|
|
49
|
+
|
|
50
|
+
DB_OUTPUT=$(wrangler d1 create roda-api-db 2>&1 || true)
|
|
51
|
+
echo "$DB_OUTPUT"
|
|
52
|
+
|
|
53
|
+
if echo "$DB_OUTPUT" | grep -q "already exists"; then
|
|
54
|
+
echo "Database already exists, continuing..."
|
|
55
|
+
DATABASE_ID=$(echo "$DB_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)
|
|
56
|
+
elif echo "$DB_OUTPUT" | grep -q "database_id"; then
|
|
57
|
+
DATABASE_ID=$(echo "$DB_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)
|
|
58
|
+
else
|
|
59
|
+
echo "⚠️ Could not automatically extract database ID"
|
|
60
|
+
echo "Please create a database manually with: wrangler d1 create roda-api-db"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
echo ""
|
|
65
|
+
echo "✅ Database created/found: $DATABASE_ID"
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
# Get account ID
|
|
69
|
+
echo "📋 Getting your Cloudflare account ID..."
|
|
70
|
+
ACCOUNT_ID=$(wrangler whoami 2>&1 | grep -oE 'Account ID: [0-9a-f]+' | cut -d' ' -f3)
|
|
71
|
+
|
|
72
|
+
if [ -z "$ACCOUNT_ID" ]; then
|
|
73
|
+
echo "⚠️ Could not automatically get account ID"
|
|
74
|
+
echo "Please run: wrangler whoami"
|
|
75
|
+
read -p "Enter your Account ID: " ACCOUNT_ID
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "✅ Account ID: $ACCOUNT_ID"
|
|
79
|
+
echo ""
|
|
80
|
+
|
|
81
|
+
# Set secrets
|
|
82
|
+
echo "🔐 Setting secrets..."
|
|
83
|
+
echo ""
|
|
84
|
+
|
|
85
|
+
echo "Setting CLOUDFLARE_ACCOUNT_ID..."
|
|
86
|
+
echo "$ACCOUNT_ID" | wrangler secret put CLOUDFLARE_ACCOUNT_ID
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "Now you need to set your API token."
|
|
90
|
+
echo "Get it from: https://dash.cloudflare.com/profile/api-tokens"
|
|
91
|
+
echo "Create a token with 'Edit Cloudflare Workers' permissions"
|
|
92
|
+
echo ""
|
|
93
|
+
read -p "Enter your API Token: " API_TOKEN
|
|
94
|
+
echo "$API_TOKEN" | wrangler secret put CLOUDFLARE_API_TOKEN
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo "Setting DATABASE_ID..."
|
|
98
|
+
echo "$DATABASE_ID" | wrangler secret put DATABASE_ID
|
|
99
|
+
|
|
100
|
+
echo ""
|
|
101
|
+
echo "✅ Secrets configured"
|
|
102
|
+
echo ""
|
|
103
|
+
|
|
104
|
+
# Deploy
|
|
105
|
+
echo "🚀 Deploying to Cloudflare..."
|
|
106
|
+
echo ""
|
|
107
|
+
echo "This will take a few minutes on first deployment..."
|
|
108
|
+
echo ""
|
|
109
|
+
|
|
110
|
+
wrangler deploy
|
|
111
|
+
|
|
112
|
+
echo ""
|
|
113
|
+
echo "============================================"
|
|
114
|
+
echo "✅ Deployment complete!"
|
|
115
|
+
echo ""
|
|
116
|
+
echo "Your API is available at:"
|
|
117
|
+
echo "https://roda-d1-api.your-subdomain.workers.dev"
|
|
118
|
+
echo ""
|
|
119
|
+
echo "Get your URL with: wrangler deployments list"
|
|
120
|
+
echo ""
|
|
121
|
+
echo "Test your API:"
|
|
122
|
+
echo " curl https://your-url/health"
|
|
123
|
+
echo " curl https://your-url/users"
|
|
124
|
+
echo ""
|
|
125
|
+
echo "View logs:"
|
|
126
|
+
echo " wrangler tail"
|
|
127
|
+
echo ""
|
|
128
|
+
echo "============================================"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Worker that routes requests to Roda container instances
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Container class configuration
|
|
7
|
+
* Extends the Container base class to configure the Ruby/Roda application
|
|
8
|
+
*/
|
|
9
|
+
export class RodaContainer extends Container {
|
|
10
|
+
// Port the container application listens on
|
|
11
|
+
defaultPort = 8080;
|
|
12
|
+
|
|
13
|
+
// Time to keep container alive after inactivity
|
|
14
|
+
sleepAfter = "10m";
|
|
15
|
+
|
|
16
|
+
// Static environment variables (set at class level)
|
|
17
|
+
// For dynamic values, override in fetch() using startAndWaitForPorts()
|
|
18
|
+
envVars = {
|
|
19
|
+
PORT: "8080",
|
|
20
|
+
RACK_ENV: "production",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Lifecycle hooks
|
|
24
|
+
override onStart() {
|
|
25
|
+
console.log("[RodaContainer] Container starting...");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override onStop() {
|
|
29
|
+
console.log("[RodaContainer] Container stopping...");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override onError(error) {
|
|
33
|
+
console.error("[RodaContainer] Error:", error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Main Worker export
|
|
39
|
+
* Handles incoming requests and routes them to container instances
|
|
40
|
+
*/
|
|
41
|
+
export default {
|
|
42
|
+
async fetch(request, env, ctx) {
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
|
|
45
|
+
// Health check endpoint (runs on Worker, not container)
|
|
46
|
+
if (url.pathname === "/health") {
|
|
47
|
+
return new Response(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
status: "healthy",
|
|
50
|
+
service: "roda-d1-api",
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
}),
|
|
53
|
+
{
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get container instance
|
|
60
|
+
// Using getByName with a fixed ID creates a single stateful instance
|
|
61
|
+
// For load balancing across multiple instances, use getById with random IDs
|
|
62
|
+
const containerStub = env.RODA_CONTAINER.getByName("main");
|
|
63
|
+
|
|
64
|
+
// Start container with runtime environment variables (secrets)
|
|
65
|
+
await containerStub.startAndWaitForPorts({
|
|
66
|
+
startOptions: {
|
|
67
|
+
envVars: {
|
|
68
|
+
CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID,
|
|
69
|
+
CLOUDFLARE_API_TOKEN: env.CLOUDFLARE_API_TOKEN,
|
|
70
|
+
DATABASE_ID: env.DATABASE_ID,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Forward the request to the container
|
|
77
|
+
const response = await containerStub.fetch(request);
|
|
78
|
+
|
|
79
|
+
// Add custom headers
|
|
80
|
+
const headers = new Headers(response.headers);
|
|
81
|
+
headers.set("X-Powered-By", "Cloudflare-Containers");
|
|
82
|
+
|
|
83
|
+
return new Response(response.body, {
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
headers: headers,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Container request failed:", error);
|
|
90
|
+
|
|
91
|
+
return new Response(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
error: "Container unavailable",
|
|
94
|
+
message: error.message,
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
status: 503,
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Alternative routing strategies:
|
|
107
|
+
*
|
|
108
|
+
* 1. Load balanced (stateless):
|
|
109
|
+
* const containerId = Math.floor(Math.random() * 3);
|
|
110
|
+
* const containerStub = env.RODA_CONTAINER.getById(containerId.toString());
|
|
111
|
+
*
|
|
112
|
+
* 2. Session-based (sticky):
|
|
113
|
+
* const sessionId = request.headers.get("X-Session-ID") || "default";
|
|
114
|
+
* const containerStub = env.RODA_CONTAINER.getByName(sessionId);
|
|
115
|
+
*
|
|
116
|
+
* 3. Path-based routing:
|
|
117
|
+
* const pathHash = hashCode(url.pathname);
|
|
118
|
+
* const containerStub = env.RODA_CONTAINER.getById(pathHash.toString());
|
|
119
|
+
*/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Application name - must be unique across Cloudflare
|
|
3
|
+
"name": "roda-d1-api",
|
|
4
|
+
|
|
5
|
+
// Compatibility date for Workers runtime
|
|
6
|
+
"compatibility_date": "2024-01-01",
|
|
7
|
+
|
|
8
|
+
// Main Worker entrypoint
|
|
9
|
+
"main": "worker.js",
|
|
10
|
+
|
|
11
|
+
// Container configuration
|
|
12
|
+
"containers": [
|
|
13
|
+
{
|
|
14
|
+
// Unique identifier for this container class
|
|
15
|
+
"class_name": "RodaContainer",
|
|
16
|
+
|
|
17
|
+
// Path to Dockerfile (wrangler will build and push)
|
|
18
|
+
"image": "./Dockerfile",
|
|
19
|
+
|
|
20
|
+
// Maximum number of concurrent container instances
|
|
21
|
+
"max_instances": 10,
|
|
22
|
+
|
|
23
|
+
// Instance type: lite, basic, standard-1, standard-2, standard-3, standard-4
|
|
24
|
+
"instance_type": "lite",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
// Durable Objects bindings
|
|
29
|
+
"durable_objects": {
|
|
30
|
+
"bindings": [
|
|
31
|
+
{
|
|
32
|
+
// Binding name used in Worker code
|
|
33
|
+
"name": "RODA_CONTAINER",
|
|
34
|
+
|
|
35
|
+
// Must match container class_name
|
|
36
|
+
"class_name": "RodaContainer",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Migrations to register Durable Objects
|
|
42
|
+
"migrations": [
|
|
43
|
+
{
|
|
44
|
+
"tag": "v1",
|
|
45
|
+
// Must use new_sqlite_classes for container-backed Durable Objects
|
|
46
|
+
"new_sqlite_classes": ["RodaContainer"],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
|
|
50
|
+
// Environment variables (non-sensitive)
|
|
51
|
+
"vars": {
|
|
52
|
+
"PORT": "8080",
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Secrets (set with: wrangler secret put <NAME>)
|
|
56
|
+
// Run these commands:
|
|
57
|
+
// wrangler secret put CLOUDFLARE_ACCOUNT_ID
|
|
58
|
+
// wrangler secret put CLOUDFLARE_API_TOKEN
|
|
59
|
+
// wrangler secret put DATABASE_ID
|
|
60
|
+
|
|
61
|
+
// Optional: Custom routes
|
|
62
|
+
"routes": [
|
|
63
|
+
{
|
|
64
|
+
"pattern": "roda-d1-api.your-domain.com/*",
|
|
65
|
+
"custom_domain": true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}
|