├── .gitignore ├── LICENSE ├── README.md ├── db.ts ├── demos ├── fresh_kv_api │ ├── README.md │ ├── components │ │ └── Button.tsx │ ├── deno.json │ ├── dev.ts │ ├── fresh.gen.ts │ ├── islands │ │ └── Counter.tsx │ ├── main.ts │ ├── routes │ │ ├── [name].tsx │ │ ├── api │ │ │ └── joke.ts │ │ ├── index.tsx │ │ └── kv │ │ │ └── [...path].ts │ └── static │ │ ├── favicon.ico │ │ └── logo.svg └── oak_kv_api │ └── server.ts ├── fresh.ts └── mod.ts /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | *.pyc 3 | *.swp 4 | .env 5 | 6 | /.cargo_home/ 7 | /.idea/ 8 | /.vs/ 9 | /.vscode/ 10 | gclient_config.py_entries 11 | /target/ 12 | /std/hash/_wasm/target 13 | /tools/wpt/manifest.json 14 | /test_napi/node_modules 15 | /test_napi/build 16 | /test_napi/third_party_tests/node_modules 17 | 18 | # MacOS generated files 19 | .DS_Store 20 | .DS_Store? 21 | 22 | # Flamegraphs 23 | /flamebench*.svg 24 | /flamegraph*.svg 25 | 26 | # WPT generated cert files 27 | /tools/wpt/certs/index.txt* 28 | /tools/wpt/certs/serial* 29 | 30 | /ext/websocket/autobahn/reports -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2023 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > **NOTE:** This module is still under active development, and not suitable for production use. 4 | 5 | # Deno KV REST API 6 | 7 | This module provides utilities to attach a flexible RESTful API to a [Deno KV](https://deno.com/kv) data store. This API makes it possible to connect other applications to your KV store in any environment that can make HTTP requests. 8 | 9 | ## Usage with Fresh 10 | 11 | This module provides a helper function to generate a Fresh [Handlers object](https://fresh.deno.dev/docs/getting-started/custom-handlers). In the `routes` folder of your Fresh project, create a new `kv` folder. In that folder, create a file called `[...path].ts`, and include the following code: 12 | 13 | ```typescript 14 | import { Handlers } from "$fresh/server.ts"; 15 | import { generateFreshHandlers } from "https://deno.land/x/kv_api@0.0.3/mod.ts"; 16 | 17 | export const handler: Handlers = generateFreshHandlers({ 18 | prefix: "/kv", 19 | }); 20 | ``` 21 | 22 | This will add the REST API documented below to your Fresh project, under `/kv`. 23 | 24 | ## Usage with Oak 25 | 26 | Support for [Oak](https://deno.land/x/oak) is coming soon! 27 | 28 | ## Authentication and authorization 29 | 30 | How you authenticate and authorize access to your Deno KV data store is currently the responsibility of the user. Our assumption is that these routes will be used alongside whatever authentication and authorization system is in place for your existing web application. 31 | 32 | We may provide more help for this in future iterations of this module. 33 | 34 | ## REST API Usage 35 | 36 | Once the API is attached to your server of choice, the following routes and HTTP methods are supported - this assumes you have attached the API to the default `/kv` path in your routing scheme. Each KV operation is mapped to a route and HTTP verb. For example, to [get a specific key](https://deno.com/manual/runtime/kv/operations#get) with a key value of `["users", "kevin"]`, you would make an HTTP `GET` request to: 37 | 38 | ``` 39 | /kv?key=users,kevin 40 | ``` 41 | 42 | Which would return a JSON representation of the same method call in Deno KV like the following: 43 | 44 | ``` 45 | { 46 | "key": ["users", "kevin"], 47 | "value": { 48 | "username": "kevin", 49 | "admin": true 50 | }, 51 | "versionstamp": "000001" 52 | } 53 | ``` 54 | 55 | ### Route overview 56 | 57 | | HTTP Method | Path | Deno KV operation | 58 | | ----------- | ----------------------- | ----------------- | 59 | | GET | `/kv` | [get](#get) | 60 | | GET | `/kv/list` | [list](#list) | 61 | | DELETE | `/kv` | [delete](#delete) | 62 | | POST | `/kv` | [set](#set) | 63 | | POST | `/kv/sum` | [sum](#sum) | 64 | | POST | `/kv/min` | [min](#min) | 65 | | POST | `/kv/max` | [max](#max) | 66 | 67 | ### get 68 | 69 | Execute a [get operation](https://deno.com/manual/runtime/kv/operations#get). 70 | 71 | Example request: 72 | 73 | ``` 74 | GET /kv?key=users,kevin 75 | ``` 76 | 77 | Example response (`application/json`): 78 | 79 | ``` 80 | { 81 | "key": ["users", "kevin"], 82 | "value": { 83 | "username": "kevin", 84 | "admin": true 85 | }, 86 | "versionstamp": "000001" 87 | } 88 | ``` 89 | 90 | ### list 91 | 92 | Execute a [list operation](https://deno.com/manual/runtime/kv/operations#list). 93 | 94 | Example request: 95 | 96 | ``` 97 | GET /kv/list?prefix=users&start=users,kevin 98 | ``` 99 | 100 | Example response (`application/json`): 101 | 102 | ``` 103 | [ 104 | { 105 | "key": ["users", "kevin"], 106 | "value": { 107 | "username": "kevin", 108 | "admin": true 109 | }, 110 | "versionstamp": "000001" 111 | } 112 | ] 113 | ``` 114 | 115 | ### delete 116 | 117 | Execute a [delete operation](https://deno.com/manual/runtime/kv/operations#delete). 118 | 119 | Example request: 120 | 121 | ``` 122 | DELETE /kv?key=users,kevin 123 | ``` 124 | 125 | Successful response returns 200 OK with no body. 126 | 127 | ### set 128 | 129 | Execute a [set operation](https://deno.com/manual/runtime/kv/operations#set). 130 | 131 | Example request: 132 | 133 | ``` 134 | POST /kv?key=users,kevin 135 | Body: 136 | { 137 | "username": "kevin", 138 | "admin": true 139 | } 140 | ``` 141 | 142 | Example response (`application/json`): 143 | 144 | ``` 145 | { 146 | "ok": true, 147 | "versionstamp": "00000000000000010000" 148 | } 149 | ``` 150 | 151 | ### sum 152 | 153 | Execute a [sum operation](https://deno.com/manual/runtime/kv/operations#sum). 154 | 155 | TODO 156 | 157 | ### min 158 | 159 | Execute a [min operation](https://deno.com/manual/runtime/kv/operations#min). 160 | 161 | TODO 162 | 163 | ### max 164 | 165 | Execute a [max operation](https://deno.com/manual/runtime/kv/operations#max). 166 | 167 | TODO 168 | 169 | ## License 170 | 171 | MIT 172 | -------------------------------------------------------------------------------- /db.ts: -------------------------------------------------------------------------------- 1 | const kv = await Deno.openKv(); 2 | 3 | export async function list(prefix: Array) { 4 | const iter = kv.list({ prefix }); 5 | const results = []; 6 | for await (const res of iter) results.push(res); 7 | return results; 8 | } 9 | 10 | export async function get(key: Array) { 11 | return await kv.get(key); 12 | } 13 | 14 | // deno-lint-ignore no-explicit-any 15 | export async function set(key: Array, value: any) { 16 | return await kv.set(key, value); 17 | } 18 | 19 | export async function deleteKey(key: Array) { 20 | return await kv.delete(key); 21 | } 22 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/README.md: -------------------------------------------------------------------------------- 1 | # Fresh project 2 | 3 | Your new Fresh project is ready to go. You can follow the Fresh "Getting 4 | Started" guide here: https://fresh.deno.dev/docs/getting-started 5 | 6 | ### Usage 7 | 8 | Make sure to install Deno: https://deno.land/manual/getting_started/installation 9 | 10 | Then start the project: 11 | 12 | ``` 13 | deno task start 14 | ``` 15 | 16 | This will watch the project directory and restart as necessary. 17 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "preact"; 2 | import { IS_BROWSER } from "$fresh/runtime.ts"; 3 | 4 | export function Button(props: JSX.HTMLAttributes) { 5 | return ( 6 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | 7 | import "$std/dotenv/load.ts"; 8 | 9 | import { start } from "$fresh/server.ts"; 10 | import manifest from "./fresh.gen.ts"; 11 | 12 | await start(manifest); 13 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/routes/[name].tsx: -------------------------------------------------------------------------------- 1 | import { PageProps } from "$fresh/server.ts"; 2 | 3 | export default function Greet(props: PageProps) { 4 | return
Hello {props.params.name}
; 5 | } 6 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/routes/api/joke.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "$fresh/server.ts"; 2 | 3 | // Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/ 4 | const JOKES = [ 5 | "Why do Java developers often wear glasses? They can't C#.", 6 | "A SQL query walks into a bar, goes up to two tables and says “can I join you?”", 7 | "Wasn't hard to crack Forrest Gump's password. 1forrest1.", 8 | "I love pressing the F5 key. It's refreshing.", 9 | "Called IT support and a chap from Australia came to fix my network connection. I asked “Do you come from a LAN down under?”", 10 | "There are 10 types of people in the world. Those who understand binary and those who don't.", 11 | "Why are assembly programmers often wet? They work below C level.", 12 | "My favourite computer based band is the Black IPs.", 13 | "What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.", 14 | "An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.", 15 | ]; 16 | 17 | export const handler = (_req: Request, _ctx: HandlerContext): Response => { 18 | const randomIndex = Math.floor(Math.random() * JOKES.length); 19 | const body = JOKES[randomIndex]; 20 | return new Response(body); 21 | }; 22 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Head } from "$fresh/runtime.ts"; 2 | import { useSignal } from "@preact/signals"; 3 | import Counter from "../islands/Counter.tsx"; 4 | 5 | export default function Home() { 6 | const count = useSignal(3); 7 | return ( 8 | <> 9 | 10 | Fresh App 11 | 12 |
13 | the fresh logo: a sliced lemon dripping with juice 19 |

20 | Welcome to `fresh`. Try updating this message in the 21 | ./routes/index.tsx file, and refresh. 22 |

23 | 24 |
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/routes/kv/[...path].ts: -------------------------------------------------------------------------------- 1 | import { Handlers } from "$fresh/server.ts"; 2 | import { generateFreshHandlers } from "../../../../mod.ts"; 3 | 4 | export const handler: Handlers = generateFreshHandlers({ 5 | prefix: "/kv", 6 | }); 7 | -------------------------------------------------------------------------------- /demos/fresh_kv_api/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/kv_api/c07e153918961256e32777dafb10327a135e8482/demos/fresh_kv_api/static/favicon.ico -------------------------------------------------------------------------------- /demos/fresh_kv_api/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demos/oak_kv_api/server.ts: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /fresh.ts: -------------------------------------------------------------------------------- 1 | import { deleteKey, get, list, set } from "./db.ts"; 2 | 3 | function getParts(prefix: string, requestUrl: URL) { 4 | const prefixParts = prefix.split("/"); 5 | const pathParts = requestUrl.pathname.split("/").slice( 6 | prefixParts.length, 7 | ); 8 | return pathParts; 9 | } 10 | 11 | export interface HandlerProps { 12 | prefix: string; 13 | } 14 | 15 | export default function generateHandlers(props: HandlerProps) { 16 | return { 17 | async GET(req: Request) { 18 | const requestUrl = new URL(req.url); 19 | const pathParts = getParts(props.prefix, requestUrl); 20 | let response; 21 | 22 | if (pathParts.length > 0 && pathParts[0] === "list") { 23 | const prefixParam = requestUrl 24 | .searchParams 25 | .get("prefix")?.split(",") || []; 26 | response = await list(prefixParam); 27 | } else { 28 | const keyParam = requestUrl.searchParams.get("key")?.split(",") || []; 29 | response = await get(keyParam); 30 | } 31 | 32 | return new Response(JSON.stringify(response), { 33 | headers: { "Content-Type": "application/json" }, 34 | }); 35 | }, 36 | 37 | async POST(req: Request) { 38 | const requestUrl = new URL(req.url); 39 | const pathParts = getParts(props.prefix, requestUrl); 40 | let response; 41 | 42 | if (pathParts.length > 0 && pathParts[0] === "sum") { 43 | response = []; // TODO 44 | } else if (pathParts.length > 0 && pathParts[0] === "min") { 45 | response = []; // TODO 46 | } else if (pathParts.length > 0 && pathParts[0] === "max") { 47 | response = []; // TODO 48 | } else { 49 | const keyParam = requestUrl.searchParams.get("key")?.split(",") || []; 50 | response = await set(keyParam, JSON.parse(await req.text())); 51 | } 52 | 53 | return new Response(JSON.stringify(response), { 54 | headers: { "Content-Type": "application/json" }, 55 | }); 56 | }, 57 | 58 | async DELETE(req: Request) { 59 | const requestUrl = new URL(req.url); 60 | const keyParam = requestUrl.searchParams.get("key")?.split(",") || []; 61 | await deleteKey(keyParam); 62 | 63 | return new Response("", { 64 | status: 200, 65 | }); 66 | }, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import generateHandlers from "./fresh.ts"; 2 | 3 | export { generateHandlers as generateFreshHandlers }; 4 | --------------------------------------------------------------------------------