diff --git a/cfw.js b/cfw.js index 83c17f0..67a588f 100644 --- a/cfw.js +++ b/cfw.js @@ -10,7 +10,7 @@ module.exports = { routes: [ 'api.svelte.dev/*' ], - globals: { + globals: { DATAB: `KV:${VARS.CLOUDFLARE_NAMESPACEID}`, GITHUB_CLIENT_ID: `ENV:${VARS.GITHUB_CLIENT_ID}`, GITHUB_CLIENT_SECRET: `SECRET:${VARS.GITHUB_CLIENT_SECRET}`, diff --git a/src/index.ts b/src/index.ts index e0a5cb6..02d98d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Router } from 'worktop'; import * as Cache from 'worktop/cache'; import * as Gists from './routes/gists'; +import * as Todos from './routes/todos'; import * as Auth from './routes/auth'; const API = new Router(); @@ -15,4 +16,9 @@ API.add('GET', '/gists/:uid', Gists.show); API.add('PUT', '/gists/:uid', Gists.update); API.add('DELETE', '/gists/:uid', Gists.destroy); +API.add('GET', '/todos/:userid', Todos.list); +API.add('POST', '/todos/:userid', Todos.create); +API.add('PATCH', '/todos/:userid/:uid', Todos.update); +API.add('DELETE', '/todos/:userid/:uid', Todos.destroy); + Cache.listen(API.run); diff --git a/src/models/todolist.ts b/src/models/todolist.ts new file mode 100644 index 0000000..fb2dfdd --- /dev/null +++ b/src/models/todolist.ts @@ -0,0 +1,89 @@ +import * as database from '../utils/database'; +import * as keys from '../utils/keys'; + +import type { UID } from 'worktop/utils'; + +export type TodoID = UID<36>; + +// to differentiate from UserID, since user's don't log in +// to read and write TODOs; they use an anonymous cookie +export type GuestID = string; + +export interface Todo { + uid: TodoID; + created_at: TIMESTAMP; + text: string; + done: boolean; +} + +export type TodoList = Todo[]; + +const TTL = 60 * 60 * 24 * 30; // 30 days, in seconds +export function sync(userid: GuestID, list: TodoList): Promise { + return database.put('todolist', userid, list, { expirationTtl: TTL }); +} + +export async function lookup(userid: GuestID) { + return (await database.get('todolist', userid)) || []; +} + +export async function insert(userid: GuestID, text: string) { + try { + const list = await lookup(userid) || []; + + const todo: Todo = { + uid: keys.gen(36), + created_at: Date.now(), + text, + done: false + }; + + list.push(todo); + if (!await sync(userid, list)) return; + + return todo; + } catch (err) { + console.error('todolist.insert ::', err); + } +} + +export async function update(userid: GuestID, uid: TodoID, patch: { text?: string, done?: boolean }) { + try { + const list = await lookup(userid); + if (!list) return; + + for (const todo of list) { + if (todo.uid === uid) { + if ('text' in patch) { + todo.text = patch.text as string; + } + + if ('done' in patch) { + todo.done = patch.done as boolean; + } + + if (await sync(userid, list)) return true; + } + } + } catch (err) { + console.error('todolist.update ::', err); + } +} + +export async function destroy(userid: GuestID, uid: TodoID) { + try { + const list = await lookup(userid); + if (!list) return; + + let i = list.length; + while (i--) { + if (list[i].uid === uid) { + list.splice(i, 1); + + if (await sync(userid, list)) return true; + } + } + } catch (err) { + console.error('todolist.destroy ::', err); + } +} diff --git a/src/routes/todos.ts b/src/routes/todos.ts new file mode 100644 index 0000000..a755cee --- /dev/null +++ b/src/routes/todos.ts @@ -0,0 +1,44 @@ +import * as TodoList from '../models/todolist'; + +import type { Handler } from 'worktop'; +import type { Params } from 'worktop/request'; +import type { TodoID, GuestID } from '../models/todolist'; + +type ParamsUserID = Params & { userid: GuestID }; + +// GET /todos/:userid +export const list: Handler = async (req, res) => { + const todos = await TodoList.lookup(req.params.userid); + if (todos) res.send(200, todos); + else res.send(404, 'Todo list not found'); +}; + +// POST /gists/:userid +export const create: Handler = async (req, res) => { + const input = await req.body<{ text: string }>(); + if (!input) return res.send(400, 'Missing request body'); + + const todo = await TodoList.insert(req.params.userid, input.text); + + if (todo) res.send(201, todo); + else res.send(500, 'Error creating todo'); +}; + +// PATCH /gists/:userid/:uid +export const update: Handler = async (req, res) => { + const { userid, uid } = req.params; + + const input = await req.body<{ text?: string, done?: boolean }>(); + if (!input) return res.send(400, 'Missing request body'); + + if (await TodoList.update(userid, uid as TodoID, input)) res.send(204); + else res.send(500, 'Error updating todo'); +}; + +// DELETE /gists/:userid/:uid +export const destroy: Handler = async (req, res) => { + const { userid, uid } = req.params; + + if (await TodoList.destroy(userid, uid as TodoID)) res.send(204); + else res.send(500, 'Error deleting todo'); +}; diff --git a/src/utils/database.ts b/src/utils/database.ts index 30d531c..ee76cbd 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -3,6 +3,7 @@ import * as keys from './keys'; import type { KV } from 'worktop/kv'; import type { Gist, GistID } from '../models/gist'; import type { Session, SessionID } from '../models/session'; +import type { TodoList, GuestID } from '../models/todolist'; import type { User, UserGist, UserID } from '../models/user'; declare const DATAB: KV.Namespace; @@ -11,6 +12,7 @@ export interface Identifiers { gist: GistID; owner: UserID; session: SessionID; + todolist: GuestID; user: UserID; } @@ -18,6 +20,7 @@ export interface Models { gist: Gist; owner: UserGist[]; session: Session; + todolist: TodoList; user: User; } @@ -26,9 +29,9 @@ export function get(type: K, uid: Identifiers[K]): return DATAB.get(keyname, 'json').then(x => x || false); } -export function put(type: K, uid: Identifiers[K], value: Models[K]): Promise { +export function put(type: K, uid: Identifiers[K], value: Models[K], options?: KV.WriteOptions): Promise { const keyname = keys.format(type, uid); - return DATAB.put(keyname, JSON.stringify(value)).then(() => true, () => false); + return DATAB.put(keyname, JSON.stringify(value), options).then(() => true, () => false); } export async function remove(type: K, uid: Identifiers[K]): Promise {