Skip to content

Commit aec994d

Browse files
authored
feat: replace kv with cheaper alternative d1 (#5)
2 parents 2f8483d + a399b83 commit aec994d

File tree

14 files changed

+162
-64
lines changed

14 files changed

+162
-64
lines changed

.dev.vars.example

Lines changed: 0 additions & 1 deletion
This file was deleted.

.editorconfig

Lines changed: 0 additions & 16 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
name: Checks
22

3+
#
4+
# NOTE:
5+
# everything under the 'checkout' step is temporary until I figure out how to use devenv.sh in gh actions
6+
# devenv has a guide for actions: https://devenv.sh/integrations/github-actions
7+
# but it doesn't work if you are using devenv this way: https://devenv.sh/guides/using-with-flakes
8+
39
on:
410
pull_request:
511
workflow_dispatch:
@@ -13,7 +19,6 @@ defaults:
1319

1420
env:
1521
NAME: 'url-shortener'
16-
CARGO_TERM_COLOR: 'always'
1722
ACTIONS_RUNNER_DEBUG: true
1823

1924
jobs:
@@ -23,21 +28,37 @@ jobs:
2328
- name: 🔑 Checkout
2429
uses: actions/checkout@v4
2530

26-
- name: Install Nix
27-
uses: cachix/install-nix-action@v30
31+
- name: 🦀 Set up Rust
32+
uses: dtolnay/rust-toolchain@nightly
33+
with:
34+
targets: wasm32-unknown-unknown
35+
components: rustc, cargo, rustfmt, clippy
36+
37+
- name: Setup Rust Cache
38+
uses: Swatinem/[email protected]
39+
with:
40+
prefix-key: v0 # increment this to bust the cache if needed
41+
42+
- name: Install sccache
43+
uses: mozilla-actions/[email protected]
44+
env:
45+
RUSTC_WRAPPER: 'sccache'
46+
SCCACHE_GHA_ENABLED: true
2847

29-
- name: Setup Cachix
30-
uses: cachix/cachix-action@v15
48+
- name: 🐰 Set up Bun
49+
uses: oven-sh/setup-bun@main
3150
with:
32-
name: 'devenv'
51+
bun-version: 'latest'
3352

34-
- name: Install devenv.sh
35-
run: nix profile install nixpkgs#devenv
53+
- name: Format
54+
run: |
55+
bunx @taplo/cli@latest fmt *.toml
56+
cargo fmt --all --check
3657
3758
- name: Lint
3859
run: |
39-
fmt
40-
lint
60+
bunx @taplo/cli@latest lint *.toml
61+
cargo clippy --all-targets --all-features -- -D warnings
4162
42-
- name: Build
43-
run: build
63+
- name: 🛠️ Build worker
64+
run: cargo install --quiet worker-build && worker-build --release

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Deploy
33
on:
44
push:
55
branches: [main]
6+
workflow_dispatch:
67

78
concurrency:
89
group: ${{ github.workflow }}-${{ github.ref }}
@@ -13,7 +14,6 @@ defaults:
1314

1415
env:
1516
NAME: 'url-shortener'
16-
CARGO_TERM_COLOR: 'always'
1717
ACTIONS_RUNNER_DEBUG: true
1818

1919
jobs:

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ crate-type = ["cdylib"]
1919
[dependencies]
2020
url = "2.5.3"
2121
serde = "1.0.214"
22+
serde_json = "1.0.104"
2223
# needed to enable the "js" feature for compatibility with wasm,
2324
# see https://docs.rs/getrandom/#webassembly-support
2425
getrandom = { version = "0.2", features = ["js"] }
25-
worker = { version = "0.4.2", features = ['http', 'axum'] }
26+
worker = { version = "0.4.2", features = ['http', 'axum', 'd1'] }
2627
uuid = { version = "1.11.0", features = [
2728
"v4",
2829
"fast-rng",

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# URL Shortener - Cloudflare Worker
2+
13
## Usage
24

35
> [!NOTE]

flake.nix

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
description = "URL Shortener Worker";
23
inputs = {
34
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
45
systems.url = "github:nix-systems/default";
@@ -37,8 +38,14 @@
3738
modules = [
3839
{
3940
# https://devenv.sh/reference/options/
40-
languages.nix.enable = true;
41+
scripts = import ./tasks.nix;
42+
43+
dotenv = {
44+
enable = true;
45+
filename = [ ".env" ];
46+
};
4147

48+
languages.nix.enable = true;
4249
languages.rust = {
4350
enable = true;
4451
channel = "nightly";
@@ -49,19 +56,25 @@
4956
"clippy"
5057
"rustfmt"
5158
"rust-analyzer"
59+
5260
];
5361
};
62+
63+
# for development only
64+
# this is the default location when you run d1 with `--local`
65+
env.D1_DATABASE_FILEPATH = ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.db";
66+
5467
packages = with pkgs; [
5568
jq
5669
git
5770
bun
5871
taplo
5972
direnv
73+
sqlfluff
6074
binaryen
6175
nixfmt-rfc-style
6276
nodePackages_latest.nodejs
6377
];
64-
scripts = import ./tasks.nix;
6578
}
6679
];
6780
};

rustfmt.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
max_width = 100
2+
reorder_imports = true
3+
imports_granularity = "Crate"
4+
group_imports = "StdExternalCrate"

schema.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
PRAGMA foreign_keys = OFF;
2+
DROP TABLE IF EXISTS urls;
3+
4+
CREATE TABLE urls (
5+
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
6+
url TEXT NOT NULL,
7+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
8+
);

scripts/seed.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# seed local d1 database with data
5+
6+
urls=(
7+
"https://docs.union.build/reference/graphql/?query=%7B__typename%7D"
8+
"https://docs.union.build/reference/graphql/?query=%7B%0A%20%20v1_daily_transfers%20%7B%0A%20%20%20%20count%0A%20%20%20%20day%0A%20%20%7D%0A%7D"
9+
"https://docs.union.build/reference/graphql/?query=%7B%0A%20%20get_route(%0A%20%20%20%20args%3A%20%7Bdestination_chain_id%3A%20%22stride-internal-1%22%2C%20receiver%3A%20%22me%22%2C%20source_chain_id%3A%20%2211155111%22%2C%20forward_chain_id%3A%20%22union-testnet-8%22%7D%0A%20%20)%20%7B%0A%20%20%20%20memo%0A%20%20%7D%0A%7D"
10+
)
11+
12+
for url in "${urls[@]}"; do
13+
d1-query --local --command="INSERT INTO urls (url) VALUES ('$url');"
14+
done

src/lib.rs

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use serde::{Deserialize, Serialize};
2+
use serde_json::Value;
23
use url::Url;
3-
use uuid::Uuid;
44
use worker::*;
55

66
#[derive(Debug, Deserialize, Serialize)]
@@ -11,16 +11,27 @@ struct GenericResponse {
1111

1212
#[event(fetch)]
1313
async fn main(request: Request, env: Env, _context: Context) -> Result<Response> {
14-
Router::new()
14+
let environment = env.var("ENVIRONMENT").unwrap().to_string();
15+
if environment.trim().is_empty() {
16+
return Response::error("not allowed", 403);
17+
}
18+
19+
let mut router = Router::new()
20+
// public routes
1521
.get("/", index_route)
1622
.post("/", index_route)
1723
.post_async("/create", handle_create)
18-
.get_async("/:key", handle_url_expand)
19-
.run(request, env)
20-
.await
24+
.get_async("/:key", handle_url_expand);
25+
26+
if environment == "development" {
27+
// dev-only routes
28+
// quick way to check records are inserted
29+
router = router.get_async("/list", dev_handle_list_urls);
30+
}
31+
32+
return router.run(request, env).await;
2133
}
2234

23-
// handles `GET /` and `POST /`
2435
pub fn index_route(_request: Request, _context: RouteContext<()>) -> worker::Result<Response> {
2536
Response::ok("zkgm")
2637
}
@@ -35,37 +46,53 @@ pub async fn handle_create(
3546
return Response::error("provided url is not valid", 400);
3647
}
3748

38-
let random_uuid = Uuid::new_v4();
39-
let key = random_uuid.to_string()[0..6].to_string();
40-
let insert_new = context.kv("KV")?.put(&key, url).unwrap().execute().await;
49+
let d1 = context.env.d1("DB");
50+
let statement = d1?.prepare("INSERT INTO urls (url) VALUES (?)");
51+
let query = statement.bind(&[url.into()]);
52+
let result = query?.run().await?.success();
4153

42-
if insert_new.is_err() {
43-
return Response::error("failed to insert new key", 500);
54+
if result {
55+
return Response::ok("ok");
4456
}
4557

46-
Response::ok(&key)
58+
Response::error("failed to insert new key", 500)
4759
}
4860

49-
// checks `GET /:key{[0-9a-z]{6}}`
61+
// checks `GET /:key{[0-9]}`
5062
pub async fn handle_url_expand(
5163
request: Request,
5264
context: RouteContext<()>,
5365
) -> worker::Result<Response> {
5466
let key = &request.path().to_string()[1..];
55-
if key.len() != 6 || !key.chars().all(|char| char.is_alphanumeric()) {
67+
if key.parse::<u64>().is_err() {
5668
return Response::error("invalid key: ".to_string() + key, 400);
5769
}
5870

59-
let expanded_url = context.kv("KV")?.get(key).text().await?;
60-
if expanded_url.is_some() {
61-
return Response::redirect(Url::parse(&expanded_url.unwrap()).unwrap());
71+
let d1 = context.env.d1("DB");
72+
let statement = d1?.prepare("SELECT url FROM urls WHERE id = ?");
73+
let query = statement.bind(&[key.into()]);
74+
let result: Option<Value> = query?.first::<Value>(None).await?;
75+
76+
match result {
77+
Some(Value::Object(object)) => {
78+
if let Some(Value::String(url)) = object.get("url") {
79+
return Response::redirect(Url::parse(url)?);
80+
}
81+
Response::error("Invalid URL format", 400)
82+
}
83+
_ => Response::error("Invalid key: ".to_string() + key, 400),
6284
}
85+
}
6386

64-
let environment = context.env.var("ENVIRONMENT").unwrap().to_string();
65-
let base_url = match environment.as_str() {
66-
"development" => "http://localhost:8787",
67-
_ => &request.url().unwrap().origin().ascii_serialization(),
68-
};
87+
pub async fn dev_handle_list_urls(
88+
_request: Request,
89+
context: RouteContext<()>,
90+
) -> worker::Result<Response> {
91+
let d1 = context.env.d1("DB");
92+
let statement = d1?.prepare("SELECT * FROM urls");
93+
let query = statement.bind(&[]);
94+
let result = query?.all().await?;
6995

70-
Response::redirect(Url::parse(base_url).unwrap())
96+
let urls: Vec<Value> = result.results()?;
97+
Response::from_json(&urls)
7198
}

tasks.nix

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
11
{
2+
wrangler.exec = ''
3+
bunx wrangler@latest --config='wrangler.toml' "$@"
4+
'';
25
fmt.exec = ''
36
taplo fmt *.toml
4-
cargo fmt --all --check
57
nixfmt *.nix --width=100
8+
cargo fmt --all -- --config-path=rustfmt.toml
9+
sqlfluff format --dialect sqlite ./schema.sql
610
'';
711
lint.exec = ''
812
taplo lint *.toml
913
cargo clippy --all-targets --all-features
14+
sqlfluff lint --dialect sqlite ./schema.sql
1015
'';
1116
build.exec = ''
1217
cargo build --release --target wasm32-unknown-unknown
1318
'';
19+
# optional: `--remote`
1420
dev.exec = ''
15-
bunx wrangler@latest --config='wrangler.toml' dev
21+
bunx wrangler@latest --config='wrangler.toml' dev "$@"
22+
'';
23+
# optional: `--local`, `--remote`
24+
d1-bootstrap.exec = ''
25+
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 --file='schema.sql' "$@"
26+
'';
27+
# optional: `--local`, `--remote`
28+
# required: `--command="SELECT * FROM urls"`
29+
d1-query.exec = ''
30+
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 "$@"
1631
'';
17-
dev-remote.exec = ''
18-
bunx wrangler@latest --config='wrangler.toml' dev --remote
32+
d1-seed.exec = ''
33+
bash ./scripts/seed.sh
34+
'';
35+
# only works locally in development
36+
d1-viewer.exec = ''
37+
bunx @outerbase/studio@latest $D1_DATABASE_FILEPATH --port=4000
1938
'';
2039
deploy.exec = ''
2140
bunx wrangler@latest deploy --env='production' --config='wrangler.toml'
2241
'';
42+
rustdoc.exec = ''
43+
cargo rustdoc -- --default-theme='ayu'
44+
'';
2345
clean.exec = ''
2446
rm -rf build
2547
rm -rf target

wrangler.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ compatibility_date = "2024-11-01"
88
workers_dev = true
99
vars = { ENVIRONMENT = "development" }
1010

11-
kv_namespaces = [
12-
{ binding = "KV", id = "96961fcf1ee84e3891cba0c326efe769", preview_id = "a4134835b7c74de38137d6545155aea0" },
11+
d1_databases = [
12+
{ binding = "DB", database_name = "url-short-d1", database_id = "3da5b327-e066-4915-a8dd-22cddbcbcf0b" },
1313
]
1414

1515
[build]
@@ -19,9 +19,11 @@ command = "cargo install --quiet worker-build && worker-build --release"
1919
name = "url-shortener"
2020
workers_dev = true
2121
vars = { ENVIRONMENT = "production" }
22-
kv_namespaces = [
23-
{ binding = "KV", id = "96961fcf1ee84e3891cba0c326efe769", preview_id = "a4134835b7c74de38137d6545155aea0" },
22+
23+
d1_databases = [
24+
{ binding = "DB", database_name = "url-short-d1", database_id = "3da5b327-e066-4915-a8dd-22cddbcbcf0b" },
2425
]
26+
2527
# https://developers.cloudflare.com/workers/observability/logs/workers-logs/
2628
[env.production.observability]
2729
enabled = true

0 commit comments

Comments
 (0)