Skip to content

Commit 5f8ce35

Browse files
committed
feat(database): add SQLite schema initialization for stats_requests, visitor_logs, and badges tables
1 parent bbc5883 commit 5f8ce35

4 files changed

Lines changed: 169 additions & 12 deletions

File tree

.env.example

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,24 @@ CLOUDFLARE_ACCOUNT_ID=
2020
CLOUDFLARE_D1_DATABASE_ID=
2121
CLOUDFLARE_D1_TOKEN=
2222

23-
# Example: Redis Cloud
24-
REDIS_HOST=redis-10434.crce262.us-east-1-1.ec2.cloud.redislabs.com
25-
REDIS_PORT=10434
26-
REDIS_USERNAME=default
27-
REDIS_PASSWORD=your_password_here
28-
REDIS_TLS=true
23+
# Tunnel
24+
CLOUDFLARED_TUNNEL_NAME=github-stats
25+
CLOUDFLARED_TUNNEL_TOKEN=
26+
27+
# Local Docker Compose Redis (default)
28+
REDIS_HOST=redis
29+
REDIS_PORT=6379
30+
REDIS_USERNAME=
31+
REDIS_PASSWORD=
32+
REDIS_TLS=false
33+
REDIS_DB=0
34+
35+
# Example: Redis Cloud (optional)
36+
# REDIS_HOST=redis-10434.crce262.us-east-1-1.ec2.cloud.redislabs.com
37+
# REDIS_PORT=10434
38+
# REDIS_USERNAME=default
39+
# REDIS_PASSWORD=your_password_here
40+
# REDIS_TLS=true
2941

3042
# ============================================================================
3143
# Debugging

compose.yaml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
services:
2+
app:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
container_name: ${CLOUDFLARED_TUNNEL_NAME}-app
7+
restart: unless-stopped
8+
ports:
9+
- "3000:${PORT:-3000}"
10+
environment:
11+
NODE_ENV: production
12+
APP_ENV: production
13+
HOST: 0.0.0.0
14+
PORT: ${PORT:-3000}
15+
DATABASE_PROVIDER: ${DATABASE_PROVIDER:-sqlite}
16+
DATABASE_URL: ${DATABASE_URL:-/app/data/stats.db}
17+
CLOUDFLARE_ACCOUNT_ID: ${CLOUDFLARE_ACCOUNT_ID:-}
18+
CLOUDFLARE_D1_DATABASE_ID: ${CLOUDFLARE_D1_DATABASE_ID:-}
19+
CLOUDFLARE_D1_TOKEN: ${CLOUDFLARE_D1_TOKEN:-}
20+
REDIS_ENABLED: "true"
21+
REDIS_HOST: redis
22+
REDIS_PORT: 6379
23+
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
24+
volumes:
25+
- app-data:/app/data
26+
depends_on:
27+
redis:
28+
condition: service_healthy
29+
healthcheck:
30+
test:
31+
- CMD
32+
- node
33+
- -e
34+
- fetch(`http://127.0.0.1:${PORT:-3000}/health`).then((response)=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))
35+
interval: 30s
36+
timeout: 10s
37+
retries: 5
38+
start_period: 20s
39+
40+
redis:
41+
image: redis:7-alpine
42+
container_name: ${CLOUDFLARED_TUNNEL_NAME}-redis
43+
restart: unless-stopped
44+
command: ["redis-server", "--appendonly", "yes"]
45+
volumes:
46+
- redis-data:/data
47+
healthcheck:
48+
test: ["CMD", "redis-cli", "ping"]
49+
interval: 10s
50+
timeout: 5s
51+
retries: 5
52+
53+
cloudflared:
54+
image: cloudflare/cloudflared:latest
55+
container_name: ${CLOUDFLARED_TUNNEL_NAME}-cloudflared
56+
restart: unless-stopped
57+
network_mode: "service:app"
58+
depends_on:
59+
app:
60+
condition: service_healthy
61+
environment:
62+
TUNNEL_TOKEN: ${CLOUDFLARED_TUNNEL_TOKEN:?Set CLOUDFLARED_TUNNEL_TOKEN in .env}
63+
command:
64+
- tunnel
65+
- --no-autoupdate
66+
- run
67+
- ${CLOUDFLARED_TUNNEL_NAME}
68+
69+
volumes:
70+
app-data:
71+
redis-data:

docs/docker-build.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ Build:
1515
docker build -f Dockerfile -t github-stats:node .
1616
```
1717

18+
Compose:
19+
20+
```bash
21+
docker compose up --build
22+
```
23+
1824
Run:
1925

2026
```bash
@@ -45,12 +51,24 @@ View logs:
4551
docker logs -f github-stats
4652
```
4753

54+
Compose logs:
55+
56+
```bash
57+
docker compose logs -f
58+
```
59+
4860
Stop and remove:
4961

5062
```bash
5163
docker rm -f github-stats
5264
```
5365

66+
Compose stop:
67+
68+
```bash
69+
docker compose down
70+
```
71+
5472
## Important Environment Variables
5573

5674
Defaults are defined in `src/shared/config/env.ts`, so the app can run with minimal configuration. Recommended variables for production:
@@ -129,6 +147,19 @@ docker compose pull
129147
docker compose up -d --force-recreate
130148
```
131149

150+
The repository includes a `compose.yaml` for local development and self-hosting. It starts:
151+
152+
- `app`: the Node.js API built from the local `Dockerfile`
153+
- `redis`: a local Redis 7 instance for cache storage
154+
155+
The compose setup also forces local-safe defaults that differ from the Cloudflare deployment path:
156+
157+
- `DATABASE_PROVIDER=sqlite`
158+
- `DATABASE_URL=/app/data/stats.db`
159+
- `REDIS_HOST=redis`
160+
161+
If you have a `.env` file with a `GITHUB_TOKEN`, Docker Compose will pass it through to the app container.
162+
132163
## Notes
133164

134165
- `.dockerignore` excludes common local artifacts (`node_modules`, `dist`, `.git`, `.env`, and more) to keep build context small.

src/shared/config/db.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,48 @@ let dbInstance: unknown | null = null;
1212
let cloudflareDbInstance: ReturnType<typeof drizzleProxy> | null = null;
1313
let sqliteInstance: import('better-sqlite3').Database | null = null;
1414

15+
function ensureSqliteSchema(db: import('better-sqlite3').Database): void {
16+
db.exec(`
17+
CREATE TABLE IF NOT EXISTS stats_requests (
18+
id INTEGER PRIMARY KEY AUTOINCREMENT,
19+
username TEXT NOT NULL,
20+
url TEXT NOT NULL,
21+
created_at INTEGER
22+
);
23+
24+
CREATE UNIQUE INDEX IF NOT EXISTS uq_stats_request_url
25+
ON stats_requests (url);
26+
27+
CREATE TABLE IF NOT EXISTS visitor_logs (
28+
id INTEGER PRIMARY KEY AUTOINCREMENT,
29+
username TEXT NOT NULL,
30+
ip_hash TEXT NOT NULL,
31+
visit_date TEXT NOT NULL,
32+
created_at INTEGER
33+
);
34+
35+
CREATE UNIQUE INDEX IF NOT EXISTS uq_visitor_log
36+
ON visitor_logs (username, ip_hash, visit_date);
37+
38+
CREATE TABLE IF NOT EXISTS badges (
39+
username TEXT PRIMARY KEY,
40+
visitors INTEGER NOT NULL DEFAULT 0,
41+
repositories INTEGER,
42+
organization INTEGER,
43+
languages INTEGER,
44+
followers INTEGER,
45+
total_stars INTEGER,
46+
total_contributors INTEGER,
47+
total_commits INTEGER,
48+
total_code_reviews INTEGER,
49+
total_issues INTEGER,
50+
total_pull_requests INTEGER,
51+
total_joined_years INTEGER,
52+
updated_at INTEGER
53+
);
54+
`);
55+
}
56+
1557
function getRequiredCloudflareConfig() {
1658
const env = getEnv();
1759
const accountId = env.CLOUDFLARE_ACCOUNT_ID;
@@ -142,11 +184,11 @@ export function initializeDatabase() {
142184

143185
return cloudflareDbInstance;
144186
}
145-
187+
146188
if (!sqliteInstance) {
147189
throw new Error('SQLite initialization must use initializeDatabaseAsync()');
148190
}
149-
191+
150192
return dbInstance;
151193
}
152194

@@ -171,19 +213,20 @@ export async function initializeDatabaseAsync() {
171213

172214
const Database = sqliteModule.default;
173215
sqliteInstance = new Database(env.DATABASE_URL);
174-
216+
175217
// Enable WAL mode for better concurrent access
176218
sqliteInstance.pragma('journal_mode = WAL');
177-
219+
ensureSqliteSchema(sqliteInstance);
220+
178221
// Initialize Drizzle ORM
179222
dbInstance = drizzle(sqliteInstance, { schema });
180223

181224
// Populate the shared db proxy so service imports work on the Node.js path
182225
setDb(dbInstance as any);
183-
226+
184227
console.log(`✅ Database connected: ${env.DATABASE_URL}`);
185228
}
186-
229+
187230
return dbInstance;
188231
}
189232

0 commit comments

Comments
 (0)