Skip to content

Commit

Permalink
Add error handling for Todoist API requests (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiebrynes7 authored Oct 18, 2020
1 parent 6787575 commit 7e65a3e
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 79 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
}
```

### 🔃 Changed

- Errors are displayed more prominently in the injected Todoist query.

### ⚙ Internal

- Added the ability to turn on debug logging in the plugin.
Expand Down
187 changes: 118 additions & 69 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "./raw_models";
import { Task, Project, ID, ProjectID, SectionID, LabelID } from "./models";
import { ExtendedMap } from "../utils";
import { Result } from "../result";

export interface ITodoistMetadata {
projects: ExtendedMap<ProjectID, IProjectRaw>;
Expand All @@ -31,7 +32,7 @@ export class TodoistApi {
this.metadata.subscribe((value) => (this.metadataInstance = value));
}

async getTasks(filter?: string): Promise<Task[]> {
async getTasks(filter?: string): Promise<Result<Task[], Error>> {
let url = "https://api.todoist.com/rest/v1/tasks";

if (filter) {
Expand All @@ -40,48 +41,70 @@ export class TodoistApi {

debug(url);

const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
});

const tasks = (await result.json()) as ITaskRaw[];
const tree = Task.buildTree(tasks);

debug({
msg: "Built task tree",
context: tree,
});

return tree;
try {
const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
});

if (result.ok) {
const tasks = (await result.json()) as ITaskRaw[];
const tree = Task.buildTree(tasks);

debug({
msg: "Built task tree",
context: tree,
});

return Result.Ok(tree);
} else {
return Result.Err(new Error(await result.text()));
}
} catch (e) {
return Result.Err(e);
}
}

async getTasksGroupedByProject(filter?: string): Promise<Project[]> {
async getTasksGroupedByProject(
filter?: string
): Promise<Result<Project[], Error>> {
let url = "https://api.todoist.com/rest/v1/tasks";

if (filter) {
url += `?filter=${encodeURIComponent(filter)}`;
}

const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
});

// Force the metadata to update.
await this.fetchMetadata();

const tasks = (await result.json()) as ITaskRaw[];
const tree = Project.buildProjectTree(tasks, this.metadataInstance);

debug({
msg: "Built project tree",
context: tree,
});

return tree;
try {
const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
});

if (result.ok) {
// Force the metadata to update.
const metadataResult = await this.fetchMetadata();

if (metadataResult.isErr()) {
return Result.Err(metadataResult.unwrapErr());
}

const tasks = (await result.json()) as ITaskRaw[];
const tree = Project.buildProjectTree(tasks, this.metadataInstance);

debug({
msg: "Built project tree",
context: tree,
});

return Result.Ok(tree);
} else {
return Result.Err(new Error(await result.text()));
}
} catch (e) {
return Result.Err(e);
}
}

async closeTask(id: ID): Promise<boolean> {
Expand All @@ -99,13 +122,21 @@ export class TodoistApi {
return result.ok;
}

async fetchMetadata(): Promise<void> {
const [projects, sections, labels] = await Promise.all<
IProjectRaw[],
ISectionRaw[],
ILabelRaw[]
async fetchMetadata(): Promise<Result<object, Error>> {
const [projectResult, sectionResult, labelResult] = await Promise.all<
Result<IProjectRaw[], Error>,
Result<ISectionRaw[], Error>,
Result<ILabelRaw[], Error>
>([this.getProjects(), this.getSections(), this.getLabels()]);

const merged = Result.All(projectResult, sectionResult, labelResult);

if (merged.isErr()) {
return merged.intoErr();
}

const [projects, sections, labels] = merged.unwrap();

this.metadata.update((metadata) => {
metadata.projects.clear();
metadata.sections.clear();
Expand All @@ -115,44 +146,62 @@ export class TodoistApi {
labels.forEach((label) => metadata.labels.set(label.id, label.name));
return metadata;
});

return Result.Ok({});
}

private async getProjects(): Promise<IProjectRaw[]> {
private async getProjects(): Promise<Result<IProjectRaw[], Error>> {
const url = `https://api.todoist.com/rest/v1/projects`;

const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return (await result.json()) as IProjectRaw[];
try {
const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return result.ok
? Result.Ok((await result.json()) as IProjectRaw[])
: Result.Err(new Error(await result.text()));
} catch (e) {
return Result.Err(e);
}
}

private async getSections(): Promise<ISectionRaw[]> {
private async getSections(): Promise<Result<ISectionRaw[], Error>> {
const url = `https://api.todoist.com/rest/v1/sections`;

const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return (await result.json()) as ISectionRaw[];
try {
const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return result.ok
? Result.Ok((await result.json()) as ISectionRaw[])
: Result.Err(new Error(await result.text()));
} catch (e) {
return Result.Err(e);
}
}

private async getLabels(): Promise<ILabelRaw[]> {
private async getLabels(): Promise<Result<ILabelRaw[], Error>> {
const url = `https://api.todoist.com/rest/v1/labels`;

const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return (await result.json()) as ILabelRaw[];
try {
const result = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${this.token}`,
}),
method: "GET",
});

return result.ok
? Result.Ok((await result.json()) as ILabelRaw[])
: Result.Err(new Error(await result.text()));
} catch (e) {
return Result.Err(e);
}
}
}
12 changes: 10 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export default class TodoistPlugin<TBase extends Settings> {
callback: async () => {
if (this.api != null) {
debug("Refreshing metadata");
await this.api.fetchMetadata();
const result = await this.api.fetchMetadata();

if (result.isErr()) {
console.error(result.unwrapErr());
}
}
},
});
Expand All @@ -79,7 +83,11 @@ export default class TodoistPlugin<TBase extends Settings> {
if (fs.existsSync(tokenPath)) {
const token = fs.readFileSync(tokenPath).toString("utf-8");
this.api = new TodoistApi(token);
await this.api.fetchMetadata();
const result = await this.api.fetchMetadata();

if (result.isErr()) {
console.error(result.unwrapErr());
}
} else {
alert(`Could not load Todoist token at: ${tokenPath}`);
}
Expand Down
72 changes: 72 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export class Result<T, E> {
private ok?: T;
private error?: E;

static Capture<T>(closure: () => T): Result<T, Error> {
try {
return Result.Ok<T, Error>(closure());
} catch (e) {
return Result.Err<T, Error>(e);
}
}

static Ok<T, E>(value: T): Result<T, E> {
let result = new Result<T, E>();
result.ok = value;
return result;
}

static Err<T, E>(err: E): Result<T, E> {
let result = new Result<T, E>();
result.error = err;
return result;
}

static All<T1, T2, T3, E>(
first: Result<T1, E>,
second: Result<T2, E>,
third: Result<T3, E>
): Result<[T1, T2, T3], E> {
if (first.isErr()) {
return first.intoErr();
}

if (second.isErr()) {
return second.intoErr();
}

if (third.isErr()) {
return third.intoErr();
}

return Result.Ok([first.unwrap(), second.unwrap(), third.unwrap()]);
}

public isOk(): boolean {
return this.ok != null;
}

public isErr(): boolean {
return this.error != null;
}

public unwrap(): T {
if (!this.isOk()) {
throw new Error("Called 'unwrap' on a Result with an error.");
}

return this.ok;
}

public unwrapErr(): E {
if (!this.isErr()) {
throw new Error("Called 'unwrapErr' on a Result with a value.");
}

return this.error;
}

public intoErr<T2>(): Result<T2, E> {
return Result.Err(this.error);
}
}
8 changes: 8 additions & 0 deletions src/ui/ErrorDisplay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
export let error: Error = null;
</script>

<div class="todoist-error">
<p>Oh no, something went wrong!</p>
<code>{error}</code>
</div>
Loading

0 comments on commit 7e65a3e

Please sign in to comment.