Skip to content

Commit

Permalink
WIP add /todos routes (#4)
Browse files Browse the repository at this point in the history
* WIP add /todos routes

* chore(model): extract `todolist.sync` method

* chore(routes): add `TodoParams` type hint

* fix(model): use seconds for `expirationTtl` value

* return empty array if no list created yet

* append rather than prepend

Co-authored-by: Luke Edwards <[email protected]>
  • Loading branch information
Rich Harris and lukeed authored Apr 17, 2021
1 parent c666267 commit 353667c
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 3 deletions.
2 changes: 1 addition & 1 deletion cfw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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);
89 changes: 89 additions & 0 deletions src/models/todolist.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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);
}
}
44 changes: 44 additions & 0 deletions src/routes/todos.ts
Original file line number Diff line number Diff line change
@@ -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<ParamsUserID> = 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<ParamsUserID> = 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');
};
7 changes: 5 additions & 2 deletions src/utils/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,13 +12,15 @@ export interface Identifiers {
gist: GistID;
owner: UserID;
session: SessionID;
todolist: GuestID;
user: UserID;
}

export interface Models {
gist: Gist;
owner: UserGist[];
session: Session;
todolist: TodoList;
user: User;
}

Expand All @@ -26,9 +29,9 @@ export function get<K extends keyof Identifiers>(type: K, uid: Identifiers[K]):
return DATAB.get<Models[K]>(keyname, 'json').then(x => x || false);
}

export function put<K extends keyof Identifiers>(type: K, uid: Identifiers[K], value: Models[K]): Promise<boolean> {
export function put<K extends keyof Identifiers>(type: K, uid: Identifiers[K], value: Models[K], options?: KV.WriteOptions): Promise<boolean> {
const keyname = keys.format<K>(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<K extends keyof Identifiers>(type: K, uid: Identifiers[K]): Promise<boolean> {
Expand Down

0 comments on commit 353667c

Please sign in to comment.