Skip to content

Commit f6ad85a

Browse files
authored
Merge pull request #29 from takker99/refactor-rest
Refactor rest
2 parents 231cf0d + b2ec8a5 commit f6ad85a

File tree

11 files changed

+274
-269
lines changed

11 files changed

+274
-269
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): 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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
// @ts-ignore only available on V8
23+
Error.captureStackTrace(this, UnexpectedResponseError);
24+
}
25+
}
26+
}

rest/mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export * from "./project.ts";
33
export * from "./profile.ts";
44
export * from "./replaceLinks.ts";
55
export * from "./page-data.ts";
6+
export * from "./auth.ts";
7+
export * from "./util.ts";
8+
export * from "./error.ts";

rest/page-data.ts

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,27 @@ 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の名前
2916
* @param data - インポートするページデータ
3017
*/
31-
export async function importPages(
18+
export const importPages = async (
3219
project: string,
3320
data: ImportedData<boolean>,
34-
{ sid, csrf }: ImportInit,
21+
init: ExtendedOptions,
3522
): Promise<
3623
Result<string, ErrorLike>
37-
> {
24+
> => {
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,90 +35,80 @@ 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 };
82-
}
68+
};
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の全ページをエクスポートする
9076
*
9177
* @param project exportしたいproject
9278
*/
93-
export async function exportPages<withMetadata extends true | false>(
79+
export const exportPages = async <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
>
101-
> {
87+
> => {
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 };
136-
}
114+
};

0 commit comments

Comments
 (0)