Skip to content

Commit eb64747

Browse files
committed
getPage()の実装と型判定周りの改善
まだ甘い部分がある
1 parent 4a2f26d commit eb64747

File tree

4 files changed

+141
-13
lines changed

4 files changed

+141
-13
lines changed

deps/scrapbox.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ export type {
44
ImportedData,
55
NotFoundError,
66
NotLoggedInError,
7+
NotMemberError,
78
NotPrivilegeError,
9+
Page,
810
} from "https://pax.deno.dev/scrapbox-jp/[email protected]";

rest/page-data.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import type {
66
NotLoggedInError,
77
NotPrivilegeError,
88
} from "../deps/scrapbox.ts";
9-
import { cookie, getCSRFToken } from "./utils.ts";
9+
import {
10+
cookie,
11+
getCSRFToken,
12+
makeCustomError,
13+
tryToErrorLike,
14+
} from "./utils.ts";
1015
import type { Result } from "./utils.ts";
1116

1217
/** `importPages`の認証情報 */
@@ -49,8 +54,9 @@ export async function importPages(
4954
csrf = result.csrfToken;
5055
}
5156

57+
const path = `https://scrapbox.io/api/page-data/import/${project}.json`;
5258
const res = await fetch(
53-
`https://scrapbox.io/api/page-data/import/${project}.json`,
59+
path,
5460
{
5561
method: "POST",
5662
headers: {
@@ -64,12 +70,15 @@ export async function importPages(
6470

6571
if (!res.ok) {
6672
if (res.status === 503) {
67-
const error = new Error();
68-
error.name = "ServerError";
69-
error.message = "503 Service Unavailable";
70-
throw error;
73+
throw makeCustomError("ServerError", "503 Service Unavailable");
74+
}
75+
const error = tryToErrorLike(await res.text());
76+
if (!error) {
77+
throw makeCustomError(
78+
"UnexpectedError",
79+
`Unexpected error has occuerd when fetching "${path}"`,
80+
);
7181
}
72-
const error = (await res.json()) as ErrorLike;
7382
return { ok: false, ...error };
7483
}
7584
const result = (await res.json()) as { message: string };
@@ -94,8 +103,10 @@ export async function exportPages<withMetadata extends true | false>(
94103
NotFoundError | NotPrivilegeError | NotLoggedInError
95104
>
96105
> {
106+
const path =
107+
`https://scrapbox.io/api/page-data/export/${project}.json?metadata=${metadata}`;
97108
const res = await fetch(
98-
`https://scrapbox.io/api/page-data/export/${project}.json?metadata=${metadata}`,
109+
path,
99110
{
100111
headers: {
101112
Cookie: cookie(sid),
@@ -104,12 +115,22 @@ export async function exportPages<withMetadata extends true | false>(
104115
);
105116

106117
if (!res.ok) {
107-
const error = (await res.json()) as
108-
| NotFoundError
109-
| NotPrivilegeError
110-
| NotLoggedInError;
118+
const error = (await res.json());
111119
return { ok: false, ...error };
112120
}
121+
if (!res.ok) {
122+
const error = tryToErrorLike(await res.text());
123+
if (!error) {
124+
throw makeCustomError(
125+
"UnexpectedError",
126+
`Unexpected error has occuerd when fetching "${path}"`,
127+
);
128+
}
129+
return {
130+
ok: false,
131+
...(error as NotFoundError | NotPrivilegeError | NotLoggedInError),
132+
};
133+
}
113134
const result = (await res.json()) as ExportedData<withMetadata>;
114135
return { ok: true, ...result };
115136
}

rest/pages.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type {
2+
NotFoundError,
3+
NotLoggedInError,
4+
NotMemberError,
5+
Page,
6+
} from "../deps/scrapbox.ts";
7+
import {
8+
cookie,
9+
encodeTitle,
10+
makeCustomError,
11+
tryToErrorLike,
12+
} from "./utils.ts";
13+
import type { Result } from "./utils.ts";
14+
15+
/** Options for `getPage()` */
16+
export interface GetPageOption {
17+
/** use `followRename` */ followRename?: boolean;
18+
/** connect.sid */ sid?: string;
19+
}
20+
/** 指定したページのJSONデータを取得する
21+
*
22+
* @param project 取得したいページのproject名
23+
* @param title 取得したいページのtitle 大文字小文字は問わない
24+
* @param options オプション
25+
*/
26+
export async function getPage(
27+
project: string,
28+
title: string,
29+
options?: GetPageOption,
30+
): Promise<
31+
Result<
32+
Page,
33+
NotFoundError | NotLoggedInError | NotMemberError
34+
>
35+
> {
36+
const path = `https://scrapbox.io/api/pages/${project}/${
37+
encodeTitle(title)
38+
}?followRename=${options?.followRename ?? true}`;
39+
40+
const res = await fetch(
41+
path,
42+
options?.sid
43+
? {
44+
headers: {
45+
Cookie: cookie(options.sid),
46+
},
47+
}
48+
: undefined,
49+
);
50+
51+
if (!res.ok) {
52+
const error = tryToErrorLike(await res.text());
53+
if (!error) {
54+
throw makeCustomError(
55+
"UnexpectedError",
56+
`Unexpected error has occuerd when fetching "${path}"`,
57+
);
58+
}
59+
return {
60+
ok: false,
61+
...(error as (NotFoundError | NotLoggedInError | NotMemberError)),
62+
};
63+
}
64+
const result = (await res.json()) as Page;
65+
return { ok: true, ...result };
66+
}

rest/utils.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ErrorLike } from "../deps/scrapbox.ts";
66
*/
77
export const cookie = (sid: string) => `connect.sid=${sid}`;
88

9+
export type Result<T, E> = ({ ok: true } & T) | ({ ok: false } & E);
910
/** CSRF tokenを取得する
1011
*
1112
* @param sid - connect.sidに入っている文字列。不正な文字列を入れてもCSRF tokenを取得できるみたい
@@ -24,4 +25,42 @@ export async function getCSRFToken(
2425
return { ok: true, csrfToken };
2526
}
2627

27-
export type Result<T, E> = ({ ok: true } & T) | ({ ok: false } & E);
28+
// cf. https://blog.uhy.ooo/entry/2021-04-09/typescript-is-any-as/#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%AE%9A%E7%BE%A9%E5%9E%8B%E3%82%AC%E3%83%BC%E3%83%89%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%AE%E5%9E%8B%E3%82%92%E3%81%A9%E3%81%86%E3%81%99%E3%82%8B%E3%81%8B
29+
function isNotNullish(data: unknown): data is Record<string, unknown> {
30+
return data != null;
31+
}
32+
function isErrorLike(e: unknown): e is ErrorLike {
33+
if (!isNotNullish(e)) return false;
34+
return (e.name === undefined || typeof e.name === "string") &&
35+
typeof e.message === "string";
36+
}
37+
/** 与えられたobjectもしくはJSONテキストをErrorLikeに変換できるかどうか試す
38+
*
39+
* @param e 試したいobjectもしくはテキスト
40+
*/
41+
export function tryToErrorLike(e: unknown): false | ErrorLike {
42+
try {
43+
const json = typeof e === "string" ? JSON.parse(e) : e;
44+
if (!isErrorLike(json)) return false;
45+
return json;
46+
} catch (e2: unknown) {
47+
if (e2 instanceof SyntaxError) return false;
48+
throw e2;
49+
}
50+
}
51+
52+
/** classを使わずにカスタムエラーを作る */
53+
export function makeCustomError(name: string, message: string) {
54+
const error = new Error();
55+
error.name = name;
56+
error.message = message;
57+
return error;
58+
}
59+
60+
export const toTitleLc = (title: string) =>
61+
title.toLowerCase().replaceAll(" ", "_");
62+
export const encodeTitle = (title: string) =>
63+
title.replaceAll(" ", "_").replace(
64+
/[/?#\{}^|<>]/g,
65+
(char) => encodeURIComponent(char),
66+
);

0 commit comments

Comments
 (0)