├── .vscode └── settings.json ├── .gitignore ├── @types ├── index.d.ts └── github.d.ts ├── .editorconfig ├── cfw.js ├── tsconfig.json ├── src ├── utils.ts ├── index.ts └── routes │ ├── docs.ts │ └── todos.ts ├── package.json ├── .github └── workflows │ ├── branch.yml │ └── tag.yml ├── wrangler.example.toml └── readme.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /build 8 | /wrangler.toml 9 | -------------------------------------------------------------------------------- /@types/index.d.ts: -------------------------------------------------------------------------------- 1 | type TODO = any; 2 | 3 | type Nullable = T | null; 4 | type Arrayable = T | T[]; 5 | type Promisable = T | Promise; 6 | 7 | type TIMESTAMP = number; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /@types/github.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace GitHub { 2 | type AccessToken = string; 3 | 4 | // @see https://docs.github.com/en/rest/reference/users#get-a-user 5 | interface User { 6 | type: 'User'; 7 | /** First Last (?) */ 8 | name: string; 9 | /** fixed id */ 10 | id: number; 11 | /** username */ 12 | login: string; 13 | /** https://... */ 14 | avatar_url: string; 15 | /** profile link */ 16 | html_url: string; 17 | // ... truncated 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cfw.js: -------------------------------------------------------------------------------- 1 | const VARS = process.env; 2 | 3 | /** 4 | * @type {import('cfw').Config} 5 | */ 6 | module.exports = { 7 | profile: 'svelte', 8 | name: 'svelte-api', 9 | entry: 'index.ts', 10 | routes: [ 11 | 'api.svelte.dev/*' 12 | ], 13 | globals: { 14 | DATAB: `KV:${VARS.CLOUDFLARE_NAMESPACEID}`, 15 | DOCS: `KV:${VARS.CLOUDFLARE_NAMESPACEID_DOCS}`, 16 | SUPABASE_URL: `ENV:${VARS.SUPABASE_URL}`, 17 | SUPABASE_KEY: `SECRET:${VARS.SUPABASE_KEY}` 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "moduleResolution": "node", 8 | "lib": ["es2020", "webworker"], 9 | "forceConsistentCasingInFileNames": true, 10 | "strictFunctionTypes": true, 11 | "strictNullChecks": true, 12 | "removeComments": true, 13 | "skipLibCheck": true, 14 | "sourceMap": false, 15 | "strict": true, 16 | }, 17 | "include": [ 18 | "@types", 19 | "src" 20 | ], 21 | "exclude": [ 22 | "node_modules/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Handler } from 'worktop'; 2 | 3 | export function handler(fn: Handler): Handler { 4 | return async (req, res) => { 5 | try { 6 | await fn(req, res); 7 | } catch (err) { 8 | const status = (err as HttpError).statusCode || 500; 9 | const message = (err as HttpError).message; 10 | 11 | if (status >= 500) { 12 | console.error((err as HttpError).stack); 13 | } 14 | 15 | res.send(status, { status, message }); 16 | } 17 | }; 18 | } 19 | 20 | export class HttpError extends Error { 21 | statusCode: number; 22 | 23 | constructor(message: string, statusCode: number) { 24 | super(message); 25 | this.statusCode = statusCode; 26 | } 27 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'worktop'; 2 | import * as CORS from 'worktop/cors'; 3 | import * as Cache from 'worktop/cache'; 4 | import * as Todos from './routes/todos'; 5 | import * as Docs from './routes/docs'; 6 | 7 | const API = new Router(); 8 | 9 | API.prepare = CORS.preflight({ 10 | maxage: 3600 11 | }); 12 | 13 | API.add('GET', '/todos/:guestid', Todos.list); 14 | API.add('POST', '/todos/:guestid', Todos.create); 15 | API.add('PATCH', '/todos/:guestid/:uid', Todos.update); 16 | API.add('DELETE', '/todos/:guestid/:uid', Todos.destroy); 17 | 18 | API.add('GET', '/docs/:project/:type', Docs.list); 19 | API.add('GET', '/docs/:project/:type/:slug', Docs.entry); 20 | 21 | Cache.listen(API.run); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.4.2", 4 | "name": "api.svelte.dev", 5 | "main": "build/index.js", 6 | "author": { 7 | "name": "Luke Edwards", 8 | "email": "luke.edwards05@gmail.com", 9 | "url": "https://lukeed.com" 10 | }, 11 | "scripts": { 12 | "dev": "wrangler dev", 13 | "build": "cfw build src --single", 14 | "deploy": "cfw deploy --single", 15 | "test": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@supabase/supabase-js": "^1.29.0", 19 | "devalue": "2.0.1", 20 | "worktop": "0.4.2" 21 | }, 22 | "devDependencies": { 23 | "@cloudflare/wrangler": "1.19.5", 24 | "cfw": "0.1.3", 25 | "typescript": "4.2.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/branch.yml: -------------------------------------------------------------------------------- 1 | name: Branch (CI) 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '.vscode/**' 11 | - 'scripts/**' 12 | 13 | pull_request: 14 | branches: 15 | - '**' 16 | tags-ignore: 17 | - '**' 18 | paths-ignore: 19 | - '.vscode/**' 20 | - 'scripts/**' 21 | 22 | jobs: 23 | test: 24 | name: Node.js v14 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v1 29 | with: 30 | node-version: 14 31 | 32 | - name: Install 33 | run: yarn install 34 | 35 | - name: TypeCheck 36 | run: yarn test 37 | 38 | - name: Compiles 39 | run: yarn build 40 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag (CD) 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - '**' 7 | tags: 8 | - 'v**' 9 | 10 | env: 11 | # cfw authentication values 12 | CLOUDFLARE_ZONEID: ${{ secrets.CLOUDFLARE_ZONEID }} 13 | CLOUDFLARE_ACCOUNTID: ${{ secrets.CLOUDFLARE_ACCOUNTID }} 14 | CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN_DEPLOY }} 15 | # worker globals/secrets 16 | CLOUDFLARE_NAMESPACEID: ${{ secrets.CLOUDFLARE_NAMESPACEID }} 17 | CLOUDFLARE_NAMESPACEID_DOCS: ${{ secrets.CLOUDFLARE_NAMESPACEID_DOCS }} 18 | 19 | jobs: 20 | deploy: 21 | name: Node.js v14 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: 14 28 | 29 | - name: Install 30 | run: yarn install 31 | 32 | - name: Type Check 33 | run: yarn test 34 | 35 | - name: Compiles 36 | run: yarn build 37 | 38 | - name: Deploy 39 | run: yarn deploy 40 | -------------------------------------------------------------------------------- /wrangler.example.toml: -------------------------------------------------------------------------------- 1 | ### 2 | # NOTE: 3 | # We only use Wrangler for the `wrangler dev` command. 4 | # 5 | # Contributors: 6 | # Attach your own Cloudflare Account credentials. This 7 | # is safe/okay since these values will (and should) 8 | # never leave your machine. 9 | # Maintainers: 10 | # Deployments are done ONLY thru the GitHub Action workflow. 11 | # For local development, follow the Contributors section. 12 | # If you have account access, you may use `wrangler tail` for logs. 13 | ### 14 | 15 | name = "svelte-local" 16 | type = "javascript" 17 | workers_dev = true 18 | zone_id = "" 19 | route = "" 20 | 21 | account_id = "" 22 | 23 | [build] 24 | command = "npm run build" 25 | upload.format = "service-worker" 26 | 27 | [vars] 28 | SUPABASE_URL = "https://kpaaohfbmxvespqoqdzp.supabase.co" 29 | SUPABASE_KEY = "" 30 | 31 | [[kv_namespaces]] 32 | binding = "DATAB" 33 | preview_id = "" 34 | id = "" 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Svelte API Worker 2 | 3 | > Live: [https://api.svelte.dev](https://api.svelte.dev) 4 | 5 | ## Install 6 | 7 | ```sh 8 | $ pnpm install 9 | ``` 10 | 11 | ## Development 12 | 13 | We use [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler) for its local development server. This is effectively a proxy-service that (nearly) replicates the Cloudflare Worker runtime. 14 | 15 | Anyone can develop this repository locally. Simply copy the `wrangler.example.toml` file as your own `wrangler.toml` file and insert the appropriate values. These values may (and should) be your own personal account values. This way any changes you make will not affect the live, production server and/or data. 16 | 17 | ```sh 18 | $ cp wrangler.example.toml wrangler.toml 19 | ``` 20 | 21 | ## Build 22 | 23 | ```sh 24 | $ pnpm run build 25 | ``` 26 | 27 | ## Deploy 28 | 29 | > **Important:** For the relevent Svelte maintainers only~! 30 | 31 | Deployment is handled by GitHub Actions, and is triggered automatically via `tag` events. 32 | Tag & release this as if it were any other npm module – but don't publish it to the registry :wink: 33 | 34 | ```sh 35 | # Format: 36 | # npm version && git push origin master --tags 37 | # Example: 38 | $ npm version patch && git push origin master --tags 39 | ``` 40 | -------------------------------------------------------------------------------- /src/routes/docs.ts: -------------------------------------------------------------------------------- 1 | import { handler, HttpError } from "../utils"; 2 | 3 | import type { Handler } from "worktop"; 4 | import type { Params } from "worktop/request"; 5 | import type { KV } from "worktop/kv"; 6 | 7 | declare const DOCS: KV.Namespace; 8 | 9 | type ParamsDocsList = Params & { project: string; type: string }; 10 | type ParamsDocsEntry = Params & { project: string; type: string; slug: string }; 11 | 12 | const headers = { 13 | 'content-type': 'application/json' 14 | }; 15 | 16 | // GET /docs/:project/:type(?version=beta&content) 17 | export const list: Handler = handler(async (req, res) => { 18 | const { project, type } = req.params; 19 | const version = req.query.get("version") || "latest"; 20 | const full = req.query.get("content") !== null; 21 | 22 | const docs = await DOCS.get(`${project}@${version}:${type}:${full ? "content" : "list"}`); 23 | if (!docs) throw new HttpError('Missing document', 404); 24 | return res.send(200, docs, headers); 25 | }); 26 | 27 | // GET /docs/:project/:type/:slug(?version=beta) 28 | export const entry: Handler = handler(async (req, res) => { 29 | const { project, type, slug } = req.params; 30 | const version = req.query.get("version") || "latest"; 31 | 32 | const entry = await DOCS.get(`${project}@${version}:${type}:${slug}`); 33 | if (!entry) throw new HttpError('Missing document', 404); 34 | return res.send(200, entry, headers); 35 | }); 36 | -------------------------------------------------------------------------------- /src/routes/todos.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import { HttpError, handler } from '../utils'; 3 | 4 | import type { Handler } from 'worktop'; 5 | import type { Params } from 'worktop/request'; 6 | 7 | type TodoListParams = Params & { guestid: string }; 8 | type TodoParams = Params & { guestid: string; uid: string }; 9 | 10 | declare const SUPABASE_URL: string; 11 | declare const SUPABASE_KEY: string; 12 | 13 | const client = createClient(SUPABASE_URL, SUPABASE_KEY, { 14 | // global fetch is sensitive to context, so we need to do this silliness 15 | fetch: (init, info) => fetch(init, info) 16 | }); 17 | 18 | const todos = () => client.from('todo'); 19 | 20 | // GET /todos/:guestid 21 | export const list: Handler = handler(async (req, res) => { 22 | const { data, error } = await todos() 23 | .select('uid,text,done') 24 | .eq('guestid', req.params.guestid) 25 | .order('created_at'); 26 | 27 | if (error) throw new HttpError(error.message, 500); 28 | res.send(200, data); 29 | }); 30 | 31 | // POST /todos/:guestid 32 | export const create: Handler = handler(async (req, res) => { 33 | const body = await req.body<{ text: string }>(); 34 | if (!body) throw new HttpError('Missing request body', 400); 35 | 36 | const { data, error } = await todos().insert([ 37 | { 38 | guestid: req.params.guestid, 39 | text: body.text 40 | } 41 | ]); 42 | 43 | if (error) throw new HttpError(error.message, 500); 44 | res.send(201, (data as any[])[0]); 45 | }); 46 | 47 | // PATCH /todos/:guestid/:uid 48 | export const update: Handler = handler(async (req, res) => { 49 | const body = await req.body<{ text?: string, done?: boolean }>(); 50 | if (!body) throw new HttpError('Missing request body', 400); 51 | 52 | const updated: { text?: string, done?: boolean } = {}; 53 | if ('text' in body) updated.text = body.text; 54 | if ('done' in body) updated.done = body.done; 55 | 56 | const { data, error } = await todos() 57 | .update(updated) 58 | .eq('uid', req.params.uid) 59 | .eq('guestid', req.params.guestid); 60 | 61 | if (error) throw new HttpError(error.message, 500); 62 | res.send(200, data); 63 | }); 64 | 65 | // DELETE /todos/:guestid/:uid 66 | export const destroy: Handler = handler(async (req, res) => { 67 | const { error } = await todos() 68 | .delete() 69 | .eq('uid', req.params.uid) 70 | .eq('guestid', req.params.guestid); 71 | 72 | if (error) throw new HttpError(error.message, 500); 73 | res.send(200, {}); // TODO should really be a 204, but need to update the template first 74 | }); 75 | --------------------------------------------------------------------------------