Skip to content

Commit

Permalink
feat: replace kv with cheaper alternative d1 (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
o-az authored Nov 10, 2024
2 parents 2f8483d + a399b83 commit aec994d
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 64 deletions.
1 change: 0 additions & 1 deletion .dev.vars.example

This file was deleted.

16 changes: 0 additions & 16 deletions .editorconfig

This file was deleted.

45 changes: 33 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
name: Checks

#
# NOTE:
# everything under the 'checkout' step is temporary until I figure out how to use devenv.sh in gh actions
# devenv has a guide for actions: https://devenv.sh/integrations/github-actions
# but it doesn't work if you are using devenv this way: https://devenv.sh/guides/using-with-flakes

on:
pull_request:
workflow_dispatch:
Expand All @@ -13,7 +19,6 @@ defaults:

env:
NAME: 'url-shortener'
CARGO_TERM_COLOR: 'always'
ACTIONS_RUNNER_DEBUG: true

jobs:
Expand All @@ -23,21 +28,37 @@ jobs:
- name: 🔑 Checkout
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v30
- name: 🦀 Set up Rust
uses: dtolnay/rust-toolchain@nightly
with:
targets: wasm32-unknown-unknown
components: rustc, cargo, rustfmt, clippy

- name: Setup Rust Cache
uses: Swatinem/[email protected]
with:
prefix-key: v0 # increment this to bust the cache if needed

- name: Install sccache
uses: mozilla-actions/[email protected]
env:
RUSTC_WRAPPER: 'sccache'
SCCACHE_GHA_ENABLED: true

- name: Setup Cachix
uses: cachix/cachix-action@v15
- name: 🐰 Set up Bun
uses: oven-sh/setup-bun@main
with:
name: 'devenv'
bun-version: 'latest'

- name: Install devenv.sh
run: nix profile install nixpkgs#devenv
- name: Format
run: |
bunx @taplo/cli@latest fmt *.toml
cargo fmt --all --check
- name: Lint
run: |
fmt
lint
bunx @taplo/cli@latest lint *.toml
cargo clippy --all-targets --all-features -- -D warnings
- name: Build
run: build
- name: 🛠️ Build worker
run: cargo install --quiet worker-build && worker-build --release
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Deploy
on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -13,7 +14,6 @@ defaults:

env:
NAME: 'url-shortener'
CARGO_TERM_COLOR: 'always'
ACTIONS_RUNNER_DEBUG: true

jobs:
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ crate-type = ["cdylib"]
[dependencies]
url = "2.5.3"
serde = "1.0.214"
serde_json = "1.0.104"
# needed to enable the "js" feature for compatibility with wasm,
# see https://docs.rs/getrandom/#webassembly-support
getrandom = { version = "0.2", features = ["js"] }
worker = { version = "0.4.2", features = ['http', 'axum'] }
worker = { version = "0.4.2", features = ['http', 'axum', 'd1'] }
uuid = { version = "1.11.0", features = [
"v4",
"fast-rng",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# URL Shortener - Cloudflare Worker

## Usage

> [!NOTE]
Expand Down
17 changes: 15 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
description = "URL Shortener Worker";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";
Expand Down Expand Up @@ -37,8 +38,14 @@
modules = [
{
# https://devenv.sh/reference/options/
languages.nix.enable = true;
scripts = import ./tasks.nix;

dotenv = {
enable = true;
filename = [ ".env" ];
};

languages.nix.enable = true;
languages.rust = {
enable = true;
channel = "nightly";
Expand All @@ -49,19 +56,25 @@
"clippy"
"rustfmt"
"rust-analyzer"

];
};

# for development only
# this is the default location when you run d1 with `--local`
env.D1_DATABASE_FILEPATH = ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.db";

packages = with pkgs; [
jq
git
bun
taplo
direnv
sqlfluff
binaryen
nixfmt-rfc-style
nodePackages_latest.nodejs
];
scripts = import ./tasks.nix;
}
];
};
Expand Down
4 changes: 4 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
max_width = 100
reorder_imports = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
8 changes: 8 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PRAGMA foreign_keys = OFF;
DROP TABLE IF EXISTS urls;

CREATE TABLE urls (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
14 changes: 14 additions & 0 deletions scripts/seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

# seed local d1 database with data

urls=(
"https://docs.union.build/reference/graphql/?query=%7B__typename%7D"
"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"
"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"
)

for url in "${urls[@]}"; do
d1-query --local --command="INSERT INTO urls (url) VALUES ('$url');"
done
73 changes: 50 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use uuid::Uuid;
use worker::*;

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

#[event(fetch)]
async fn main(request: Request, env: Env, _context: Context) -> Result<Response> {
Router::new()
let environment = env.var("ENVIRONMENT").unwrap().to_string();
if environment.trim().is_empty() {
return Response::error("not allowed", 403);
}

let mut router = Router::new()
// public routes
.get("/", index_route)
.post("/", index_route)
.post_async("/create", handle_create)
.get_async("/:key", handle_url_expand)
.run(request, env)
.await
.get_async("/:key", handle_url_expand);

if environment == "development" {
// dev-only routes
// quick way to check records are inserted
router = router.get_async("/list", dev_handle_list_urls);
}

return router.run(request, env).await;
}

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

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

if insert_new.is_err() {
return Response::error("failed to insert new key", 500);
if result {
return Response::ok("ok");
}

Response::ok(&key)
Response::error("failed to insert new key", 500)
}

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

let expanded_url = context.kv("KV")?.get(key).text().await?;
if expanded_url.is_some() {
return Response::redirect(Url::parse(&expanded_url.unwrap()).unwrap());
let d1 = context.env.d1("DB");
let statement = d1?.prepare("SELECT url FROM urls WHERE id = ?");
let query = statement.bind(&[key.into()]);
let result: Option<Value> = query?.first::<Value>(None).await?;

match result {
Some(Value::Object(object)) => {
if let Some(Value::String(url)) = object.get("url") {
return Response::redirect(Url::parse(url)?);
}
Response::error("Invalid URL format", 400)
}
_ => Response::error("Invalid key: ".to_string() + key, 400),
}
}

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

Response::redirect(Url::parse(base_url).unwrap())
let urls: Vec<Value> = result.results()?;
Response::from_json(&urls)
}
30 changes: 26 additions & 4 deletions tasks.nix
Original file line number Diff line number Diff line change
@@ -1,25 +1,47 @@
{
wrangler.exec = ''
bunx wrangler@latest --config='wrangler.toml' "$@"
'';
fmt.exec = ''
taplo fmt *.toml
cargo fmt --all --check
nixfmt *.nix --width=100
cargo fmt --all -- --config-path=rustfmt.toml
sqlfluff format --dialect sqlite ./schema.sql
'';
lint.exec = ''
taplo lint *.toml
cargo clippy --all-targets --all-features
sqlfluff lint --dialect sqlite ./schema.sql
'';
build.exec = ''
cargo build --release --target wasm32-unknown-unknown
'';
# optional: `--remote`
dev.exec = ''
bunx wrangler@latest --config='wrangler.toml' dev
bunx wrangler@latest --config='wrangler.toml' dev "$@"
'';
# optional: `--local`, `--remote`
d1-bootstrap.exec = ''
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 --file='schema.sql' "$@"
'';
# optional: `--local`, `--remote`
# required: `--command="SELECT * FROM urls"`
d1-query.exec = ''
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 "$@"
'';
dev-remote.exec = ''
bunx wrangler@latest --config='wrangler.toml' dev --remote
d1-seed.exec = ''
bash ./scripts/seed.sh
'';
# only works locally in development
d1-viewer.exec = ''
bunx @outerbase/studio@latest $D1_DATABASE_FILEPATH --port=4000
'';
deploy.exec = ''
bunx wrangler@latest deploy --env='production' --config='wrangler.toml'
'';
rustdoc.exec = ''
cargo rustdoc -- --default-theme='ayu'
'';
clean.exec = ''
rm -rf build
rm -rf target
Expand Down
10 changes: 6 additions & 4 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ compatibility_date = "2024-11-01"
workers_dev = true
vars = { ENVIRONMENT = "development" }

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

[build]
Expand All @@ -19,9 +19,11 @@ command = "cargo install --quiet worker-build && worker-build --release"
name = "url-shortener"
workers_dev = true
vars = { ENVIRONMENT = "production" }
kv_namespaces = [
{ binding = "KV", id = "96961fcf1ee84e3891cba0c326efe769", preview_id = "a4134835b7c74de38137d6545155aea0" },

d1_databases = [
{ binding = "DB", database_name = "url-short-d1", database_id = "3da5b327-e066-4915-a8dd-22cddbcbcf0b" },
]

# https://developers.cloudflare.com/workers/observability/logs/workers-logs/
[env.production.observability]
enabled = true

0 comments on commit aec994d

Please sign in to comment.