diff --git a/rest/__snapshots__/pages.test.ts.snap b/rest/__snapshots__/pages.test.ts.snap new file mode 100644 index 0000000..0d360de --- /dev/null +++ b/rest/__snapshots__/pages.test.ts.snap @@ -0,0 +1,21 @@ +export const snapshot = {}; + +snapshot[`getPage 1`] = ` +Request { + bodyUsed: false, + headers: Headers {}, + method: "GET", + redirect: "follow", + url: "https://scrapbox.io/api/pages/takker/%E3%83%86%E3%82%B9%E3%83%88%E3%83%9A%E3%83%BC%E3%82%B8?followRe..." +} +`; + +snapshot[`listPages 1`] = ` +Request { + bodyUsed: false, + headers: Headers {}, + method: "GET", + redirect: "follow", + url: "https://scrapbox.io/api/pages/takker?sort=updated" +} +`; diff --git a/rest/__snapshots__/project.test.ts.snap b/rest/__snapshots__/project.test.ts.snap new file mode 100644 index 0000000..f9e5ebd --- /dev/null +++ b/rest/__snapshots__/project.test.ts.snap @@ -0,0 +1,21 @@ +export const snapshot = {}; + +snapshot[`getProject 1`] = ` +Request { + bodyUsed: false, + headers: Headers {}, + method: "GET", + redirect: "follow", + url: "https://scrapbox.io/api/projects/takker" +} +`; + +snapshot[`listProjects 1`] = ` +Request { + bodyUsed: false, + headers: Headers {}, + method: "GET", + redirect: "follow", + url: "https://scrapbox.io/api/projects?ids=dummy-id1&ids=dummy-id2" +} +`; diff --git a/rest/error.ts b/rest/error.ts index 639f529..3b19eaa 100644 --- a/rest/error.ts +++ b/rest/error.ts @@ -4,19 +4,14 @@ import { tryToErrorLike } from "../is.ts"; /** 想定されない応答が帰ってきたときに投げる例外 */ export class UnexpectedResponseError extends Error { name = "UnexpectedResponseError"; - request: Request; - response: Response; constructor( - init: { request: Request; response: Response }, + public response: Response, ) { super( - `${init.response.status} ${init.response.statusText} when fetching ${init.request.url}`, + `${response.status} ${response.statusText} when fetching ${response.url}`, ); - this.request = init.request.clone(); - this.response = init.response.clone(); - // @ts-ignore only available on V8 if (Error.captureStackTrace) { // @ts-ignore only available on V8 @@ -27,14 +22,13 @@ export class UnexpectedResponseError extends Error { /** 失敗した要求からエラー情報を取り出す */ export const makeError = async ( - req: Request, res: Response, ): Promise<{ ok: false; value: T }> => { const response = res.clone(); const text = await response.text(); const value = tryToErrorLike(text); if (!value) { - throw new UnexpectedResponseError({ request: req, response }); + throw new UnexpectedResponseError(response); } return { ok: false, diff --git a/rest/getGyazoToken.ts b/rest/getGyazoToken.ts index 6dcf0e9..daefb6c 100644 --- a/rest/getGyazoToken.ts +++ b/rest/getGyazoToken.ts @@ -34,10 +34,7 @@ export const getGyazoToken = async ( const res = await fetch(req); if (!res.ok) { - return makeError( - req, - res, - ); + return makeError(res); } const { token } = (await res.json()) as { token?: string }; diff --git a/rest/getSnapshots.ts b/rest/getSnapshots.ts index 4d97200..e369207 100644 --- a/rest/getSnapshots.ts +++ b/rest/getSnapshots.ts @@ -58,10 +58,7 @@ export const getSnapshots = async ( }, }; } - return makeError( - req, - res, - ); + return makeError(res); } const data = (await res.json()) as PageSnapshot; diff --git a/rest/getTweetInfo.ts b/rest/getTweetInfo.ts index ea30d28..4871b3e 100644 --- a/rest/getTweetInfo.ts +++ b/rest/getTweetInfo.ts @@ -53,7 +53,7 @@ export const getTweetInfo = async ( }, }; } - return makeError(req, res); + return makeError(res); } const tweet = (await res.json()) as TweetInfo; diff --git a/rest/getWebPageTitle.ts b/rest/getWebPageTitle.ts index 9bf9bc6..0e23309 100644 --- a/rest/getWebPageTitle.ts +++ b/rest/getWebPageTitle.ts @@ -53,7 +53,7 @@ export const getWebPageTitle = async ( }, }; } - return makeError(req, res); + return makeError(res); } const { title } = (await res.json()) as { title: string }; diff --git a/rest/link.ts b/rest/link.ts index ff665e7..576925f 100644 --- a/rest/link.ts +++ b/rest/link.ts @@ -53,7 +53,7 @@ export const getLinks = async ( }; } - return makeError(req, res); + return makeError(res); } const pages = (await res.json()) as SearchedTitle[]; diff --git a/rest/page-data.ts b/rest/page-data.ts index a795f34..6407979 100644 --- a/rest/page-data.ts +++ b/rest/page-data.ts @@ -49,7 +49,7 @@ export const importPages = async ( const res = await fetch(req); if (!res.ok) { - return makeError(req, res); + return makeError(res); } const { message } = (await res.json()) as { message: string }; @@ -83,9 +83,7 @@ export const exportPages = async ( const res = await fetch(req); if (!res.ok) { - return makeError< - NotFoundError | NotPrivilegeError | NotLoggedInError - >(req, res); + return makeError(res); } const value = (await res.json()) as ExportedData; diff --git a/rest/pages.test.ts b/rest/pages.test.ts new file mode 100644 index 0000000..3c7f1b9 --- /dev/null +++ b/rest/pages.test.ts @@ -0,0 +1,15 @@ +import { getPage, listPages } from "./pages.ts"; +import { assertSnapshot } from "../deps/testing.ts"; + +Deno.test("getPage", async (t) => { + await assertSnapshot( + t, + getPage.toRequest("takker", "テストページ", { followRename: true }), + ); +}); +Deno.test("listPages", async (t) => { + await assertSnapshot( + t, + listPages.toRequest("takker", { sort: "updated" }), + ); +}); diff --git a/rest/pages.ts b/rest/pages.ts index 54ac8aa..d24668a 100644 --- a/rest/pages.ts +++ b/rest/pages.ts @@ -14,40 +14,84 @@ import { BaseOptions, Result, setDefaults } from "./util.ts"; export interface GetPageOption extends BaseOptions { /** use `followRename` */ followRename?: boolean; } + +const getPage_toRequest: GetPage["toRequest"] = ( + project, + title, + options, +) => { + const { sid, hostName, followRename } = setDefaults(options ?? {}); + const path = `https://${hostName}/api/pages/${project}/${ + encodeTitleURI(title) + }?followRename=${followRename ?? true}`; + return new Request( + path, + sid ? { headers: { Cookie: cookie(sid) } } : undefined, + ); +}; + +const getPage_fromResponse: GetPage["fromResponse"] = async (res) => { + if (!res.ok) { + return makeError(res); + } + const value = (await res.json()) as Page; + return { ok: true, value }; +}; + +export interface GetPage { + /** /api/pages/:project/:title の要求を組み立てる + * + * @param project 取得したいページのproject名 + * @param title 取得したいページのtitle 大文字小文字は問わない + * @param options オプション + * @return request + */ + toRequest: ( + project: string, + title: string, + options?: GetPageOption, + ) => Request; + + /** 帰ってきた応答からページのJSONデータを取得する + * + * @param res 応答 + * @return ページのJSONデータ + */ + fromResponse: (res: Response) => Promise< + Result< + Page, + NotFoundError | NotLoggedInError | NotMemberError + > + >; + + (project: string, title: string, options?: GetPageOption): Promise< + Result< + Page, + NotFoundError | NotLoggedInError | NotMemberError + > + >; +} + /** 指定したページのJSONデータを取得する * * @param project 取得したいページのproject名 * @param title 取得したいページのtitle 大文字小文字は問わない * @param options オプション */ -export const getPage = async ( - project: string, - title: string, - options?: GetPageOption, -): Promise< - Result< - Page, - NotFoundError | NotLoggedInError | NotMemberError - > -> => { - const { sid, hostName, fetch, followRename } = setDefaults(options ?? {}); - const req = new Request( - `https://${hostName}/api/pages/${project}/${ - encodeTitleURI(title) - }?followRename=${followRename ?? true}`, - sid ? { headers: { Cookie: cookie(sid) } } : undefined, - ); +export const getPage: GetPage = async ( + project, + title, + options, +) => { + const { fetch } = setDefaults(options ?? {}); + const req = getPage_toRequest(project, title, options); const res = await fetch(req); - if (!res.ok) { - return makeError( - req, - res, - ); - } - const value = (await res.json()) as Page; - return { ok: true, value }; + return await getPage_fromResponse(res); }; +getPage.toRequest = getPage_toRequest; +getPage.fromResponse = getPage_fromResponse; + /** Options for `listPages()` */ export interface ListPagesOption extends BaseOptions { /** the sort of page list to return @@ -74,39 +118,76 @@ export interface ListPagesOption extends BaseOptions { */ limit?: number; } -/** 指定したprojectのページを一覧する - * - * @param project 一覧したいproject - * @param options オプション 取得範囲や並び順を決める - */ -export const listPages = async ( - project: string, - options?: ListPagesOption, -): Promise< - Result< - PageList, - NotFoundError | NotLoggedInError | NotMemberError - > -> => { - const { sid, hostName, fetch, sort, limit, skip } = setDefaults( + +export interface ListPages { + /** /api/pages/:project の要求を組み立てる + * + * @param project 取得したいページのproject名 + * @param options オプション + * @return request + */ + toRequest: ( + project: string, + options?: ListPagesOption, + ) => Request; + + /** 帰ってきた応答からページのJSONデータを取得する + * + * @param res 応答 + * @return ページのJSONデータ + */ + fromResponse: (res: Response) => Promise< + Result< + PageList, + NotFoundError | NotLoggedInError | NotMemberError + > + >; + + (project: string, options?: ListPagesOption): Promise< + Result< + PageList, + NotFoundError | NotLoggedInError | NotMemberError + > + >; +} + +const listPages_toRequest: ListPages["toRequest"] = (project, options) => { + const { sid, hostName, sort, limit, skip } = setDefaults( options ?? {}, ); const params = new URLSearchParams(); if (sort !== undefined) params.append("sort", sort); if (limit !== undefined) params.append("limit", `${limit}`); if (skip !== undefined) params.append("skip", `${skip}`); - const req = new Request( - `https://${hostName}/api/pages/${project}?${params.toString()}`, + const path = `https://${hostName}/api/pages/${project}?${params.toString()}`; + + return new Request( + path, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); +}; - const res = await fetch(req); +const listPages_fromResponse: ListPages["fromResponse"] = async (res) => { if (!res.ok) { - return makeError( - req, - res, - ); + return makeError(res); } const value = (await res.json()) as PageList; return { ok: true, value }; }; + +/** 指定したprojectのページを一覧する + * + * @param project 一覧したいproject + * @param options オプション 取得範囲や並び順を決める + */ +export const listPages: ListPages = async ( + project, + options?, +) => { + const { fetch } = setDefaults(options ?? {}); + const res = await fetch(listPages_toRequest(project, options)); + return await listPages_fromResponse(res); +}; + +listPages.toRequest = listPages_toRequest; +listPages.fromResponse = listPages_fromResponse; diff --git a/rest/profile.ts b/rest/profile.ts index 4a68f2a..f2386d5 100644 --- a/rest/profile.ts +++ b/rest/profile.ts @@ -17,7 +17,7 @@ export const getProfile = async ( ); const response = await fetch(request); if (!response.ok) { - throw new UnexpectedResponseError({ request, response }); + throw new UnexpectedResponseError(response); } return (await response.json()) as MemberUser | GuestUser; }; diff --git a/rest/project.test.ts b/rest/project.test.ts new file mode 100644 index 0000000..044fd81 --- /dev/null +++ b/rest/project.test.ts @@ -0,0 +1,15 @@ +import { getProject, listProjects } from "./project.ts"; +import { assertSnapshot } from "../deps/testing.ts"; + +Deno.test("getProject", async (t) => { + await assertSnapshot( + t, + getProject.toRequest("takker"), + ); +}); +Deno.test("listProjects", async (t) => { + await assertSnapshot( + t, + listProjects.toRequest(["dummy-id1", "dummy-id2"]), + ); +}); diff --git a/rest/project.ts b/rest/project.ts index 2ca7189..fe8b7c5 100644 --- a/rest/project.ts +++ b/rest/project.ts @@ -11,66 +11,140 @@ import { cookie } from "./auth.ts"; import { makeError } from "./error.ts"; import { BaseOptions, Result, setDefaults } from "./util.ts"; -/** get the project information - * - * @param project project name to get - * @param init connect.sid etc. - */ -export const getProject = async ( - project: string, - init?: BaseOptions, -): Promise< - Result< - MemberProject | NotMemberProject, - NotFoundError | NotMemberError | NotLoggedInError - > -> => { - const { sid, hostName, fetch } = setDefaults(init ?? {}); - - const req = new Request( +export interface GetProject { + /** /api/project/:project の要求を組み立てる + * + * @param project project name to get + * @param init connect.sid etc. + * @return request + */ + toRequest: ( + project: string, + options?: BaseOptions, + ) => Request; + + /** 帰ってきた応答からprojectのJSONデータを取得する + * + * @param res 応答 + * @return projectのJSONデータ + */ + fromResponse: (res: Response) => Promise< + Result< + MemberProject | NotMemberProject, + NotFoundError | NotMemberError | NotLoggedInError + > + >; + + (project: string, options?: BaseOptions): Promise< + Result< + MemberProject | NotMemberProject, + NotFoundError | NotMemberError | NotLoggedInError + > + >; +} + +const getProject_toRequest: GetProject["toRequest"] = (project, init) => { + const { sid, hostName } = setDefaults(init ?? {}); + + return new Request( `https://${hostName}/api/projects/${project}`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); +}; - const res = await fetch(req); - +const getProject_fromResponse: GetProject["fromResponse"] = async (res) => { if (!res.ok) { - return makeError( - req, - res, - ); + return makeError(res); } const value = (await res.json()) as MemberProject | NotMemberProject; return { ok: true, value }; }; -/** list the projects' information +/** get the project information * - * @param projectIds project ids. This must have more than 1 id + * @param project project name to get * @param init connect.sid etc. */ -export const listProjects = async ( - projectIds: ProjectId[], - init?: BaseOptions, -): Promise> => { - const { sid, hostName, fetch } = setDefaults(init ?? {}); +export const getProject: GetProject = async ( + project, + init, +) => { + const { fetch } = setDefaults(init ?? {}); + + const req = getProject_toRequest(project, init); + const res = await fetch(req); + + return getProject_fromResponse(res); +}; + +getProject.toRequest = getProject_toRequest; +getProject.fromResponse = getProject_fromResponse; + +export interface ListProjects { + /** /api/project の要求を組み立てる + * + * @param projectIds project ids. This must have more than 1 id + * @param init connect.sid etc. + * @return request + */ + toRequest: ( + projectIds: ProjectId[], + init?: BaseOptions, + ) => Request; + + /** 帰ってきた応答からprojectのJSONデータを取得する + * + * @param res 応答 + * @return projectのJSONデータ + */ + fromResponse: ( + res: Response, + ) => Promise>; + + ( + projectIds: ProjectId[], + init?: BaseOptions, + ): Promise>; +} + +const ListProject_toRequest: ListProjects["toRequest"] = (projectIds, init) => { + const { sid, hostName } = setDefaults(init ?? {}); const param = new URLSearchParams(); for (const id of projectIds) { param.append("ids", id); } - const req = new Request( + return new Request( `https://${hostName}/api/projects?${param.toString()}`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); +}; - const res = await fetch(req); - +const ListProject_fromResponse: ListProjects["fromResponse"] = async (res) => { if (!res.ok) { - return makeError(req, res); + return makeError(res); } const value = (await res.json()) as ProjectResponse; return { ok: true, value }; }; + +/** list the projects' information + * + * @param projectIds project ids. This must have more than 1 id + * @param init connect.sid etc. + */ +export const listProjects: ListProjects = async ( + projectIds, + init, +) => { + const { fetch } = setDefaults(init ?? {}); + + const res = await fetch(ListProject_toRequest(projectIds, init)); + + return ListProject_fromResponse(res); +}; + +listProjects.toRequest = ListProject_toRequest; +listProjects.fromResponse = ListProject_fromResponse; diff --git a/rest/replaceLinks.ts b/rest/replaceLinks.ts index 5d66942..630a9fc 100644 --- a/rest/replaceLinks.ts +++ b/rest/replaceLinks.ts @@ -47,10 +47,7 @@ export const replaceLinks = async ( const res = await fetch(req); if (!res.ok) { - return makeError( - req, - res, - ); + return makeError(res); } // messageには"2 pages have been successfully updated!"というような文字列が入っているはず diff --git a/rest/search.ts b/rest/search.ts index 43ec1cd..8884110 100644 --- a/rest/search.ts +++ b/rest/search.ts @@ -47,10 +47,7 @@ export const searchForPages = async ( }, }; } - return makeError( - req, - res, - ); + return makeError(res); } const value = (await res.json()) as SearchResult; @@ -92,7 +89,7 @@ export const searchForJoinedProjects = async ( }, }; } - return makeError(req, res); + return makeError(res); } const value = (await res.json()) as ProjectSearchResult; @@ -145,7 +142,7 @@ export const searchForWatchList = async ( }, }; } - return makeError(req, res); + return makeError(res); } const value = (await res.json()) as ProjectSearchResult;