Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add /todos routes #4

Merged
merged 6 commits into from
Apr 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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