Skip to content

Commit a75577d

Browse files
committed
💥 ✨ いくつかのoption追加と、Error周りをきれいにした
- 想定外のエラーをErrorを継承したclassで投げる - fetchをカスタマイズできる - host nameを変えられる
1 parent 231cf0d commit a75577d

File tree

10 files changed

+249
-248
lines changed

10 files changed

+249
-248
lines changed

is.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ErrorLike } from "./deps/scrapbox.ts";
12
// These code are based on https://deno.land/x/unknownutil@v1.1.0/is.ts
23

34
export const isNone = (value: unknown): value is undefined | null =>
@@ -8,3 +9,28 @@ export const isNumber = (value: unknown): value is number =>
89
typeof value === "number";
910
export const isArray = <T>(value: unknown): value is T[] =>
1011
Array.isArray(value);
12+
export const isObject = (value: unknown): value is Record<string, unknown> =>
13+
typeof value === "object" && value !== null;
14+
15+
export const isErrorLike = (e: unknown): e is ErrorLike => {
16+
if (!isObject(e)) return false;
17+
return (e.name === undefined || typeof e.name === "string") &&
18+
typeof e.message === "string";
19+
};
20+
21+
/** 与えられたobjectもしくはJSONテキストをErrorLikeに変換できるかどうか試す
22+
*
23+
* @param e 試したいobjectもしくはテキスト
24+
* @return 変換できなかったら`false`を返す。変換できたらそのobjectを返す
25+
*/
26+
export const tryToErrorLike = (e: unknown): false | ErrorLike => {
27+
try {
28+
const json = typeof e === "string" ? JSON.parse(e) : e;
29+
if (!isErrorLike(json)) return false;
30+
return json;
31+
} catch (e2: unknown) {
32+
if (e2 instanceof SyntaxError) return false;
33+
// JSONのparse error以外はそのまま投げる
34+
throw e2;
35+
}
36+
};

rest/auth.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getProfile } from "./profile.ts";
2+
import { BaseOptions } from "./util.ts";
3+
4+
// scrapbox.io内なら`window._csrf`にCSRF tokenが入っている
5+
declare global {
6+
interface Window {
7+
_csrf?: string;
8+
}
9+
}
10+
11+
/** HTTP headerのCookieに入れる文字列を作る
12+
*
13+
* @param sid connect.sidに入っている文字列
14+
*/
15+
export const cookie = (sid: string) => `connect.sid=${sid}`;
16+
17+
/** CSRF tokenを取得する
18+
*
19+
* @param init 認証情報など
20+
*/
21+
export const getCSRFToken = async (
22+
init?: BaseOptions,
23+
): Promise<string> => {
24+
if (window._csrf) return window._csrf;
25+
26+
const user = await getProfile(init);
27+
return user.csrfToken;
28+
};

rest/error.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export class UnexpectedResponseError extends Error {
2+
name = "UnexpectedResponseError";
3+
status: number;
4+
statusText: string;
5+
body: string;
6+
path: URL;
7+
8+
constructor(
9+
init: { status: number; statusText: string; body: string; path: URL },
10+
) {
11+
super(
12+
`${init.status} ${init.statusText} when fetching ${init.path.toString()}`,
13+
);
14+
15+
this.status = init.status;
16+
this.statusText = init.statusText;
17+
this.body = init.body;
18+
this.path = init.path;
19+
20+
// @ts-ignore only available on V8
21+
if (Error.captureStackTrace) {
22+
Error.captureStackTrace(this, UnexpectedResponseError);
23+
}
24+
}
25+
}

rest/page-data.ts

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,10 @@ import type {
66
NotLoggedInError,
77
NotPrivilegeError,
88
} from "../deps/scrapbox.ts";
9-
import {
10-
cookie,
11-
getCSRFToken,
12-
makeCustomError,
13-
tryToErrorLike,
14-
} from "./utils.ts";
15-
import type { Result } from "./utils.ts";
16-
17-
/** `importPages`の認証情報 */
18-
export interface ImportInit {
19-
/** connect.sid */ sid: string;
20-
/** CSRF token
21-
*
22-
* If it isn't set, automatically get CSRF token from scrapbox.io server.
23-
*/
24-
csrf?: string;
25-
}
9+
import { cookie, getCSRFToken } from "./auth.ts";
10+
import { UnexpectedResponseError } from "./error.ts";
11+
import { tryToErrorLike } from "../is.ts";
12+
import { BaseOptions, ExtendedOptions, Result, setDefaults } from "./util.ts";
2613
/** projectにページをインポートする
2714
*
2815
* @param project - インポート先のprojectの名前
@@ -31,14 +18,15 @@ export interface ImportInit {
3118
export async function importPages(
3219
project: string,
3320
data: ImportedData<boolean>,
34-
{ sid, csrf }: ImportInit,
21+
init: ExtendedOptions,
3522
): Promise<
3623
Result<string, ErrorLike>
3724
> {
3825
if (data.pages.length === 0) {
3926
return { ok: true, value: "No pages to import." };
4027
}
4128

29+
const { sid, hostName, fetch, csrf } = setDefaults(init ?? {});
4230
const formData = new FormData();
4331
formData.append(
4432
"import-file",
@@ -47,43 +35,41 @@ export async function importPages(
4735
}),
4836
);
4937
formData.append("name", "undefined");
38+
const path = `https://${hostName}/api/page-data/import/${project}.json`;
5039

51-
csrf ??= await getCSRFToken(sid);
52-
53-
const path = `https://scrapbox.io/api/page-data/import/${project}.json`;
5440
const res = await fetch(
5541
path,
5642
{
5743
method: "POST",
5844
headers: {
59-
Cookie: cookie(sid),
45+
...(sid ? { Cookie: cookie(sid) } : {}),
6046
Accept: "application/json, text/plain, */*",
61-
"X-CSRF-TOKEN": csrf,
47+
"X-CSRF-TOKEN": csrf ?? await getCSRFToken(init),
6248
},
6349
body: formData,
6450
},
6551
);
6652

6753
if (!res.ok) {
68-
if (res.status === 503) {
69-
throw makeCustomError("ServerError", "503 Service Unavailable");
70-
}
71-
const value = tryToErrorLike(await res.text());
54+
const text = await res.json();
55+
const value = tryToErrorLike(text);
7256
if (!value) {
73-
throw makeCustomError(
74-
"UnexpectedError",
75-
`Unexpected error has occuerd when fetching "${path}"`,
76-
);
57+
throw new UnexpectedResponseError({
58+
path: new URL(path),
59+
...res,
60+
body: await res.text(),
61+
});
7762
}
7863
return { ok: false, value };
7964
}
65+
8066
const { message } = (await res.json()) as { message: string };
8167
return { ok: true, value: message };
8268
}
8369

8470
/** `exportPages`の認証情報 */
85-
export interface ExportInit<withMetadata extends true | false> {
86-
/** connect.sid */ sid: string;
71+
export interface ExportInit<withMetadata extends true | false>
72+
extends BaseOptions {
8773
/** whether to includes metadata */ metadata: withMetadata;
8874
}
8975
/** projectの全ページをエクスポートする
@@ -92,45 +78,37 @@ export interface ExportInit<withMetadata extends true | false> {
9278
*/
9379
export async function exportPages<withMetadata extends true | false>(
9480
project: string,
95-
{ sid, metadata }: ExportInit<withMetadata>,
81+
init: ExportInit<withMetadata>,
9682
): Promise<
9783
Result<
9884
ExportedData<withMetadata>,
9985
NotFoundError | NotPrivilegeError | NotLoggedInError
10086
>
10187
> {
88+
const { sid, hostName, fetch, metadata } = setDefaults(init ?? {});
10289
const path =
103-
`https://scrapbox.io/api/page-data/export/${project}.json?metadata=${metadata}`;
90+
`https://${hostName}/api/page-data/export/${project}.json?metadata=${metadata}`;
10491
const res = await fetch(
10592
path,
106-
{
107-
headers: {
108-
Cookie: cookie(sid),
109-
},
110-
},
93+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
11194
);
11295

11396
if (!res.ok) {
114-
const error = (await res.json());
115-
return { ok: false, ...error };
116-
}
117-
if (!res.ok) {
118-
const value = tryToErrorLike(await res.text()) as
119-
| false
120-
| NotFoundError
121-
| NotPrivilegeError
122-
| NotLoggedInError;
97+
const text = await res.json();
98+
const value = tryToErrorLike(text);
12399
if (!value) {
124-
throw makeCustomError(
125-
"UnexpectedError",
126-
`Unexpected error has occuerd when fetching "${path}"`,
127-
);
100+
throw new UnexpectedResponseError({
101+
path: new URL(path),
102+
...res,
103+
body: await res.text(),
104+
});
128105
}
129106
return {
130107
ok: false,
131-
value,
108+
value: value as NotFoundError | NotPrivilegeError | NotLoggedInError,
132109
};
133110
}
111+
134112
const value = (await res.json()) as ExportedData<withMetadata>;
135113
return { ok: true, value };
136114
}

rest/pages.ts

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import type {
55
Page,
66
PageList,
77
} from "../deps/scrapbox.ts";
8-
import { cookie, makeCustomError, tryToErrorLike } from "./utils.ts";
8+
import { cookie } from "./auth.ts";
9+
import { UnexpectedResponseError } from "./error.ts";
10+
import { tryToErrorLike } from "../is.ts";
911
import { encodeTitleURI } from "../title.ts";
10-
import type { Result } from "./utils.ts";
12+
import { BaseOptions, Result, setDefaults } from "./util.ts";
1113

1214
/** Options for `getPage()` */
13-
export interface GetPageOption {
15+
export interface GetPageOption extends BaseOptions {
1416
/** use `followRename` */ followRename?: boolean;
15-
/** connect.sid */ sid?: string;
1617
}
1718
/** 指定したページのJSONデータを取得する
1819
*
@@ -30,44 +31,38 @@ export async function getPage(
3031
NotFoundError | NotLoggedInError | NotMemberError
3132
>
3233
> {
33-
const path = `https://scrapbox.io/api/pages/${project}/${
34+
const { sid, hostName, fetch, followRename } = setDefaults(options ?? {});
35+
const path = `https://${hostName}/api/pages/${project}/${
3436
encodeTitleURI(title)
35-
}?followRename=${options?.followRename ?? true}`;
36-
37+
}?followRename=${followRename ?? true}`;
3738
const res = await fetch(
3839
path,
39-
options?.sid
40-
? {
41-
headers: {
42-
Cookie: cookie(options.sid),
43-
},
44-
}
45-
: undefined,
40+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
4641
);
47-
4842
if (!res.ok) {
49-
const value = tryToErrorLike(await res.text()) as
50-
| false
51-
| NotFoundError
52-
| NotLoggedInError
53-
| NotMemberError;
43+
const text = await res.text();
44+
const value = tryToErrorLike(text);
5445
if (!value) {
55-
throw makeCustomError(
56-
"UnexpectedError",
57-
`Unexpected error has occuerd when fetching "${path}"`,
58-
);
46+
throw new UnexpectedResponseError({
47+
path: new URL(path),
48+
...res,
49+
body: text,
50+
});
5951
}
6052
return {
6153
ok: false,
62-
value,
54+
value: value as
55+
| NotFoundError
56+
| NotLoggedInError
57+
| NotMemberError,
6358
};
6459
}
6560
const value = (await res.json()) as Page;
6661
return { ok: true, value };
6762
}
6863

6964
/** Options for `listPages()` */
70-
export interface ListPagesOption {
65+
export interface ListPagesOption extends BaseOptions {
7166
/** the sort of page list to return
7267
*
7368
* @default "updated"
@@ -91,8 +86,6 @@ export interface ListPagesOption {
9186
* @default 100
9287
*/
9388
limit?: number;
94-
/** connect.sid */
95-
sid?: string;
9689
}
9790
/** 指定したprojectのページを一覧する
9891
*
@@ -108,39 +101,35 @@ export async function listPages(
108101
NotFoundError | NotLoggedInError | NotMemberError
109102
>
110103
> {
111-
const { sort, limit, skip } = options ?? {};
104+
const { sid, hostName, fetch, sort, limit, skip } = setDefaults(
105+
options ?? {},
106+
);
112107
const params = new URLSearchParams();
113108
if (sort !== undefined) params.append("sort", sort);
114109
if (limit !== undefined) params.append("limit", `${limit}`);
115110
if (skip !== undefined) params.append("skip", `${skip}`);
116-
const path = `https://scrapbox.io/api/pages/${project}?${params.toString()}`;
111+
const path = `https://${hostName}/api/pages/${project}?${params.toString()}`;
117112

118113
const res = await fetch(
119114
path,
120-
options?.sid
121-
? {
122-
headers: {
123-
Cookie: cookie(options.sid),
124-
},
125-
}
126-
: undefined,
115+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
127116
);
128-
129117
if (!res.ok) {
130-
const value = tryToErrorLike(await res.text()) as
131-
| false
132-
| NotFoundError
133-
| NotLoggedInError
134-
| NotMemberError;
118+
const text = await res.text();
119+
const value = tryToErrorLike(text);
135120
if (!value) {
136-
throw makeCustomError(
137-
"UnexpectedError",
138-
`Unexpected error has occuerd when fetching "${path}"`,
139-
);
121+
throw new UnexpectedResponseError({
122+
path: new URL(path),
123+
...res,
124+
body: text,
125+
});
140126
}
141127
return {
142128
ok: false,
143-
value,
129+
value: value as
130+
| NotFoundError
131+
| NotLoggedInError
132+
| NotMemberError,
144133
};
145134
}
146135
const value = (await res.json()) as PageList;

0 commit comments

Comments
 (0)