Skip to content

Refactor rest #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions is.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ErrorLike } from "./deps/scrapbox.ts";
// These code are based on https://deno.land/x/unknownutil@v1.1.0/is.ts

export const isNone = (value: unknown): value is undefined | null =>
Expand All @@ -8,3 +9,28 @@ export const isNumber = (value: unknown): value is number =>
typeof value === "number";
export const isArray = <T>(value: unknown): value is T[] =>
Array.isArray(value);
export const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;

export const isErrorLike = (e: unknown): e is ErrorLike => {
if (!isObject(e)) return false;
return (e.name === undefined || typeof e.name === "string") &&
typeof e.message === "string";
};

/** 与えられたobjectもしくはJSONテキストをErrorLikeに変換できるかどうか試す
*
* @param e 試したいobjectもしくはテキスト
* @return 変換できなかったら`false`を返す。変換できたらそのobjectを返す
*/
export const tryToErrorLike = (e: unknown): false | ErrorLike => {
try {
const json = typeof e === "string" ? JSON.parse(e) : e;
if (!isErrorLike(json)) return false;
return json;
} catch (e2: unknown) {
if (e2 instanceof SyntaxError) return false;
// JSONのparse error以外はそのまま投げる
throw e2;
}
};
28 changes: 28 additions & 0 deletions rest/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getProfile } from "./profile.ts";
import { BaseOptions } from "./util.ts";

// scrapbox.io内なら`window._csrf`にCSRF tokenが入っている
declare global {
interface Window {
_csrf?: string;
}
}

/** HTTP headerのCookieに入れる文字列を作る
*
* @param sid connect.sidに入っている文字列
*/
export const cookie = (sid: string): string => `connect.sid=${sid}`;

/** CSRF tokenを取得する
*
* @param init 認証情報など
*/
export const getCSRFToken = async (
init?: BaseOptions,
): Promise<string> => {
if (window._csrf) return window._csrf;

const user = await getProfile(init);
return user.csrfToken;
};
26 changes: 26 additions & 0 deletions rest/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export class UnexpectedResponseError extends Error {
name = "UnexpectedResponseError";
status: number;
statusText: string;
body: string;
path: URL;

constructor(
init: { status: number; statusText: string; body: string; path: URL },
) {
super(
`${init.status} ${init.statusText} when fetching ${init.path.toString()}`,
);

this.status = init.status;
this.statusText = init.statusText;
this.body = init.body;
this.path = init.path;

// @ts-ignore only available on V8
if (Error.captureStackTrace) {
// @ts-ignore only available on V8
Error.captureStackTrace(this, UnexpectedResponseError);
}
}
}
3 changes: 3 additions & 0 deletions rest/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export * from "./project.ts";
export * from "./profile.ts";
export * from "./replaceLinks.ts";
export * from "./page-data.ts";
export * from "./auth.ts";
export * from "./util.ts";
export * from "./error.ts";
98 changes: 38 additions & 60 deletions rest/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,27 @@ import type {
NotLoggedInError,
NotPrivilegeError,
} from "../deps/scrapbox.ts";
import {
cookie,
getCSRFToken,
makeCustomError,
tryToErrorLike,
} from "./utils.ts";
import type { Result } from "./utils.ts";

/** `importPages`の認証情報 */
export interface ImportInit {
/** connect.sid */ sid: string;
/** CSRF token
*
* If it isn't set, automatically get CSRF token from scrapbox.io server.
*/
csrf?: string;
}
import { cookie, getCSRFToken } from "./auth.ts";
import { UnexpectedResponseError } from "./error.ts";
import { tryToErrorLike } from "../is.ts";
import { BaseOptions, ExtendedOptions, Result, setDefaults } from "./util.ts";
/** projectにページをインポートする
*
* @param project - インポート先のprojectの名前
* @param data - インポートするページデータ
*/
export async function importPages(
export const importPages = async (
project: string,
data: ImportedData<boolean>,
{ sid, csrf }: ImportInit,
init: ExtendedOptions,
): Promise<
Result<string, ErrorLike>
> {
> => {
if (data.pages.length === 0) {
return { ok: true, value: "No pages to import." };
}

const { sid, hostName, fetch, csrf } = setDefaults(init ?? {});
const formData = new FormData();
formData.append(
"import-file",
Expand All @@ -47,90 +35,80 @@ export async function importPages(
}),
);
formData.append("name", "undefined");
const path = `https://${hostName}/api/page-data/import/${project}.json`;

csrf ??= await getCSRFToken(sid);

const path = `https://scrapbox.io/api/page-data/import/${project}.json`;
const res = await fetch(
path,
{
method: "POST",
headers: {
Cookie: cookie(sid),
...(sid ? { Cookie: cookie(sid) } : {}),
Accept: "application/json, text/plain, */*",
"X-CSRF-TOKEN": csrf,
"X-CSRF-TOKEN": csrf ?? await getCSRFToken(init),
},
body: formData,
},
);

if (!res.ok) {
if (res.status === 503) {
throw makeCustomError("ServerError", "503 Service Unavailable");
}
const value = tryToErrorLike(await res.text());
const text = await res.json();
const value = tryToErrorLike(text);
if (!value) {
throw makeCustomError(
"UnexpectedError",
`Unexpected error has occuerd when fetching "${path}"`,
);
throw new UnexpectedResponseError({
path: new URL(path),
...res,
body: await res.text(),
});
}
return { ok: false, value };
}

const { message } = (await res.json()) as { message: string };
return { ok: true, value: message };
}
};

/** `exportPages`の認証情報 */
export interface ExportInit<withMetadata extends true | false> {
/** connect.sid */ sid: string;
export interface ExportInit<withMetadata extends true | false>
extends BaseOptions {
/** whether to includes metadata */ metadata: withMetadata;
}
/** projectの全ページをエクスポートする
*
* @param project exportしたいproject
*/
export async function exportPages<withMetadata extends true | false>(
export const exportPages = async <withMetadata extends true | false>(
project: string,
{ sid, metadata }: ExportInit<withMetadata>,
init: ExportInit<withMetadata>,
): Promise<
Result<
ExportedData<withMetadata>,
NotFoundError | NotPrivilegeError | NotLoggedInError
>
> {
> => {
const { sid, hostName, fetch, metadata } = setDefaults(init ?? {});
const path =
`https://scrapbox.io/api/page-data/export/${project}.json?metadata=${metadata}`;
`https://${hostName}/api/page-data/export/${project}.json?metadata=${metadata}`;
const res = await fetch(
path,
{
headers: {
Cookie: cookie(sid),
},
},
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
);

if (!res.ok) {
const error = (await res.json());
return { ok: false, ...error };
}
if (!res.ok) {
const value = tryToErrorLike(await res.text()) as
| false
| NotFoundError
| NotPrivilegeError
| NotLoggedInError;
const text = await res.json();
const value = tryToErrorLike(text);
if (!value) {
throw makeCustomError(
"UnexpectedError",
`Unexpected error has occuerd when fetching "${path}"`,
);
throw new UnexpectedResponseError({
path: new URL(path),
...res,
body: await res.text(),
});
}
return {
ok: false,
value,
value: value as NotFoundError | NotPrivilegeError | NotLoggedInError,
};
}

const value = (await res.json()) as ExportedData<withMetadata>;
return { ok: true, value };
}
};
Loading