Skip to content

Commit

Permalink
feat: return shortened url on create + sqlite viewer (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
o-az authored Nov 10, 2024
2 parents aec994d + 17da4e6 commit f5271b0
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ crate-type = ["cdylib"]

[dependencies]
url = "2.5.3"
serde = "1.0.214"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.104"
# needed to enable the "js" feature for compatibility with wasm,
# see https://docs.rs/getrandom/#webassembly-support
Expand Down
66 changes: 59 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,82 @@
# URL Shortener - Cloudflare Worker

## Usage
## Development

### Prerequisites

You have two options:

Option one (recommended):

- [install](https://devenv.sh/getting-started/) `devenv` && run `devenv up`
- done.

Now you can run `dev`, `fmt`, etc. (tasks are defined in [`tasks.nix`](./tasks.nix))

Option two: follow Cloudflare's [guide](https://developers.cloudflare.com/workers/languages/rust/)

- [install `Node.js`](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs)
- [install `wrangler`](https://bun.sh/docs/installation)
- [install `rust`](https://www.rust-lang.org/tools/install)

> [!NOTE]
> When running locally use `http://localhost:8787`
> the rest of the guide assumes you're using `devenv`
>
> if you're installing stuff manully,
> take a look at [`tasks.nix`](./tasks.nix) for the commands
### Shorten a URL
Once you've installed the prerequisites, you can run:

dev server

```bash
dev
```

rowser-based sqlite viewer

```bash
d1-viewer
```

seed local d1 database with data

```bash
wrangler dev --config='wrangler.toml' dev --preview
d1-seed
```

shorten a URL

```bash
curl --url http://localhost:8787/create \
--request 'POST' \
--data-binary 'https://docs.union.build/reference/graphql/?query=%7B%20__typename%20%7D'
```

now refresh the d1 viewer page and you should see the new record

## Usage

> [!NOTE]
> When running locally use `http://localhost:8787`
### Shorten a URL

```bash
curl --url http://localhost:8787/create \
--request 'POST' \
--data-binary 'https://docs.union.build/reference/graphql/?query=%7B%20__typename%20%7D'
```

This will return a short id, for example:
This will return a short the shortened URL, for example:

```sh
7312a5
# example
https://localhost/26
```

### Expand a short URL

```bash
curl --url http://localhost:8787/7312a5
curl --url http://localhost:8787/26
```
16 changes: 9 additions & 7 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
description = "URL Shortener Worker";
inputs = {

nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";

Expand All @@ -26,7 +27,6 @@
packages = forEachSystem (system: {
devenv-up = self.devShells.${system}.default.config.procfileScript;
});

devShells = forEachSystem (
system:
let
Expand All @@ -40,11 +40,7 @@
# https://devenv.sh/reference/options/
scripts = import ./tasks.nix;

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

dotenv.enable = true;
languages.nix.enable = true;
languages.rust = {
enable = true;
Expand All @@ -62,14 +58,20 @@

# 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";
env.D1_DATABASE_FILEPATH =
let
dbDir = ".wrangler/state/v3/d1/miniflare-D1DatabaseObject";
in
"${dbDir}/$(${pkgs.findutils}/bin/find ${dbDir} -maxdepth 1 -name '*.sqlite' ! -name '*-shm' ! -name '*-wal' -printf '%f\n' | head -n1)";

packages = with pkgs; [
jq
git
bun
taplo
direnv
sqlite
deadnix
sqlfluff
binaryen
nixfmt-rfc-style
Expand Down
89 changes: 65 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,90 @@ struct GenericResponse {
message: String,
}

const DEV_ROUTES: [&str; 2] = ["/list", "/env"];

pub fn get_secret(name: &str, env: &Env) -> Option<String> {
match env.secret(name) {
Ok(value) => Some(value.to_string()),
Err(_) => None,
}
}

pub fn get_var(name: &str, env: &Env) -> Option<String> {
match env.var(name) {
Ok(value) => Some(value.to_string()),
Err(_) => None,
}
}

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

let mut router = Router::new()
// public routes
.get("/", index_route)
.post("/", index_route)
let router = Router::new()
.get("/", |_, _| Response::ok("zkgm"))
.post("/", |_, _| Response::ok("zkgm"))
.post_async("/create", handle_create)
.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);
let url = request.url()?;
if !DEV_ROUTES.contains(&url.path()) {
return router.run(request, env).await;
}

return router.run(request, env).await;
}
console_log!("{}", url.query().unwrap_or_default());

pub fn index_route(_request: Request, _context: RouteContext<()>) -> worker::Result<Response> {
Response::ok("zkgm")
let url_key = url.query().and_then(|q| q.split("key=").nth(1));
if url_key.is_none() {
return router.run(request, env).await;
}

let stored_key = get_secret("DEV_ROUTES_KEY", &env).unwrap_or_default();

if url_key != Some(&stored_key) {
return router.run(request, env).await;
}
return router
.get_async("/list", dev_handle_list_urls)
.run(request, env)
.await;
}

// handles `POST /create --data-binary 'https://example.com/foo/bar'`
pub async fn handle_create(
mut request: Request,
context: RouteContext<()>,
) -> worker::Result<Response> {
let url = request.text().await?;
if Url::parse(&url).is_err() {
let payload_url = request.text().await?;
if Url::parse(&payload_url).is_err() {
return Response::error("provided url is not valid", 400);
}

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();
let d1 = context.env.d1("DB")?;
let statement = d1.prepare("INSERT INTO urls (url) VALUES (?)");
let query = statement.bind(&[payload_url.into()]);
let result = query?.run().await?;

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

let query_statement = d1.prepare("SELECT id FROM urls ORDER BY id DESC LIMIT 1");
let query = query_statement.bind(&[]);
let result = query?.first::<Value>(None).await?.unwrap();

if let Value::Object(object) = result {
if let Some(Value::Number(id)) = object.get("id") {
return Response::ok(format!(
"https://{}/{}",
request.url().unwrap().host_str().unwrap(),
id
));
}
}
Response::error("failed to insert new key", 500)
}

Expand All @@ -63,8 +101,10 @@ pub async fn handle_url_expand(
request: Request,
context: RouteContext<()>,
) -> worker::Result<Response> {
let key = &request.path().to_string()[1..];
if key.parse::<u64>().is_err() {
let url = request.url()?;
let key = url.path().trim_start_matches('/');

if key.parse::<u64>().is_err() || key.is_empty() {
return Response::error("invalid key: ".to_string() + key, 400);
}

Expand All @@ -84,6 +124,7 @@ pub async fn handle_url_expand(
}
}

// dev-only route: quick way to check records are inserted
pub async fn dev_handle_list_urls(
_request: Request,
context: RouteContext<()>,
Expand Down
11 changes: 9 additions & 2 deletions tasks.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
echo-env.exec = ''
echo $D1_DATABASE_FILEPATH
'';
wrangler.exec = ''
bunx wrangler@latest --config='wrangler.toml' "$@"
'';
Expand All @@ -12,6 +15,7 @@
taplo lint *.toml
cargo clippy --all-targets --all-features
sqlfluff lint --dialect sqlite ./schema.sql
deadnix --no-lambda-pattern-names && statix check .
'';
build.exec = ''
cargo build --release --target wasm32-unknown-unknown
Expand All @@ -20,9 +24,12 @@
dev.exec = ''
bunx wrangler@latest --config='wrangler.toml' dev "$@"
'';
d1-create-database.exec = ''
bunx wrangler@latest --config='wrangler.toml' d1 create url-short-d1 "$@"
'';
# optional: `--local`, `--remote`
d1-bootstrap.exec = ''
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 --file='schema.sql' "$@"
bunx wrangler@latest --config='wrangler.toml' d1 execute url-short-d1 --file='schema.sql'
'';
# optional: `--local`, `--remote`
# required: `--command="SELECT * FROM urls"`
Expand All @@ -34,7 +41,7 @@
'';
# only works locally in development
d1-viewer.exec = ''
bunx @outerbase/studio@latest $D1_DATABASE_FILEPATH --port=4000
bunx @outerbase/studio@latest $(eval echo $D1_DATABASE_FILEPATH) --port=4000
'';
deploy.exec = ''
bunx wrangler@latest deploy --env='production' --config='wrangler.toml'
Expand Down

0 comments on commit f5271b0

Please sign in to comment.