-
-
Notifications
You must be signed in to change notification settings - Fork 92
Expand file tree
/
Copy pathindex.ts
More file actions
129 lines (102 loc) · 3.63 KB
/
index.ts
File metadata and controls
129 lines (102 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import camelize from "camelize-ts";
import snakify from "snakify-ts";
import type { Label } from "@/api/domain/label";
import type { Project } from "@/api/domain/project";
import type { Section } from "@/api/domain/section";
import type { CreateTaskParams, Task, TaskId } from "@/api/domain/task";
import type { UserInfo } from "@/api/domain/user";
import { type RequestParams, StatusCode, type WebFetcher, type WebResponse } from "@/api/fetcher";
import { debug } from "@/log";
type PaginatedResponse<T> = {
results: T[];
nextCursor: string | null;
};
export class TodoistApiClient {
private readonly token: string;
private readonly fetcher: WebFetcher;
constructor(token: string, fetcher: WebFetcher) {
this.token = token;
this.fetcher = fetcher;
}
public async getTasks(filter?: string): Promise<Task[]> {
if (filter !== undefined) {
return await this.doPaginated<Task>("/tasks/filter", { query: filter });
}
return await this.doPaginated<Task>("/tasks");
}
public async createTask(content: string, options?: CreateTaskParams): Promise<Task> {
const body = snakify({
content,
...(options ?? {}),
});
const response = await this.do("/tasks", "POST", body);
return camelize(JSON.parse(response.body)) as Task;
}
public async closeTask(id: TaskId): Promise<void> {
await this.do(`/tasks/${id}/close`, "POST");
}
public async getProjects(): Promise<Project[]> {
return await this.doPaginated<Project>("/projects");
}
public async getSections(): Promise<Section[]> {
return await this.doPaginated<Section>("/sections");
}
public async getLabels(): Promise<Label[]> {
return await this.doPaginated<Label>("/labels");
}
public async getUser(): Promise<UserInfo> {
const response = await this.do("/user", "GET");
return camelize(JSON.parse(response.body)) as UserInfo;
}
private async doPaginated<T>(path: string, params?: Record<string, string>): Promise<T[]> {
const allResults: T[] = [];
let cursor: string | null = null;
do {
const queryParams = new URLSearchParams(params);
if (cursor) {
queryParams.set("cursor", cursor);
}
const queryString = queryParams.toString();
const fullPath = queryString ? `${path}?${queryString}` : path;
const response = await this.do(fullPath, "GET");
const paginatedResponse = camelize(JSON.parse(response.body)) as PaginatedResponse<T>;
allResults.push(...paginatedResponse.results);
cursor = paginatedResponse.nextCursor;
} while (cursor);
return allResults;
}
private async do(path: string, method: string, json?: object): Promise<WebResponse> {
const params: RequestParams = {
url: `https://api.todoist.com/api/v1${path}`,
method,
headers: {
Authorization: `Bearer ${this.token}`,
},
};
if (json !== undefined) {
params.body = JSON.stringify(json);
params.headers["Content-Type"] = "application/json";
}
debug({
msg: "Sending Todoist API request",
context: params,
});
const response = await this.fetcher.fetch(params);
debug({
msg: "Received Todoist API response",
context: response,
});
if (StatusCode.isError(response.statusCode)) {
throw new TodoistApiError(params, response);
}
return response;
}
}
export class TodoistApiError extends Error {
public statusCode: StatusCode;
constructor(request: RequestParams, response: WebResponse) {
const message = `[${request.method}] ${request.url} returned '${response.statusCode}: ${response.body}`;
super(message);
this.statusCode = response.statusCode;
}
}