diff --git a/deps/scrapbox.ts b/deps/scrapbox.ts index 62e1109..0997c25 100644 --- a/deps/scrapbox.ts +++ b/deps/scrapbox.ts @@ -13,6 +13,7 @@ export type { Page, PageList, Scrapbox, + SearchedTitle, } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts"; import type { Page } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts"; export * from "https://esm.sh/@progfay/scrapbox-parser@7.2.0"; diff --git a/rest/link.ts b/rest/link.ts new file mode 100644 index 0000000..b848a45 --- /dev/null +++ b/rest/link.ts @@ -0,0 +1,104 @@ +import type { ErrorLike, SearchedTitle } from "../deps/scrapbox.ts"; +import { cookie } from "./auth.ts"; +import { UnexpectedResponseError } from "./error.ts"; +import { tryToErrorLike } from "../is.ts"; +import { BaseOptions, Result, setDefaults } from "./util.ts"; + +export interface GetLinksOptions extends BaseOptions { + /** 次のリンクリストを示すID */ + followingId?: string; +} + +/** 指定したprojectのリンクデータを取得する + * + * @param project データを取得したいproject + */ +export const getLinks = async ( + project: string, + options?: GetLinksOptions, +): Promise< + Result<{ + pages: SearchedTitle[]; + followingId: string; + }, ErrorLike> +> => { + const { sid, hostName, fetch, followingId } = setDefaults(options ?? {}); + const path = `https://${hostName}/api/pages/${project}/search/titles${ + followingId ? `?followingId=${followingId}` : "" + }`; + + const res = await fetch( + path, + sid ? { headers: { Cookie: cookie(sid) } } : undefined, + ); + + if (!res.ok) { + const text = await res.text(); + const value = tryToErrorLike(text); + if (!value) { + throw new UnexpectedResponseError({ + path: new URL(path), + ...res, + body: text, + }); + } + return { ok: false, value }; + } + const pages = (await res.json()) as SearchedTitle[]; + return { + ok: true, + value: { pages, followingId: res.headers.get("X-following-id") ?? "" }, + }; +}; + +/** 指定したprojectの全てのリンクデータを取得する + * + * responseで返ってきたリンクデータの塊ごとに返す + * + * @param project データを取得したいproject + * @return 認証が通らなかったらエラーを、通ったらasync generatorを返す + */ +export const readLinksBulk = async ( + project: string, + options?: BaseOptions, +): Promise> => { + const first = await getLinks(project, options); + if (!first.ok) return first.value; + + return async function* () { + yield first.value.pages; + let followingId = first.value.followingId; + + while (!followingId) { + const result = await getLinks(project, { followingId, ...options }); + + // すでに認証は通っているので、ここでエラーになるはずがない + if (!result.ok) { + throw new Error("The authorization cannot be unavailable"); + } + yield result.value.pages; + followingId = result.value.followingId; + } + }(); +}; + +/** 指定したprojectの全てのリンクデータを取得し、一つづつ返す + * + * @param project データを取得したいproject + * @return 認証が通らなかったらエラーを、通ったらasync generatorを返す + */ +export const readLinks = async ( + project: string, + options?: BaseOptions, +): Promise> => { + const reader = await readLinksBulk(project, options); + if ("name" in reader) return reader; + + return async function* () { + for await (const titles of reader) { + for (const title of titles) { + yield title; + } + } + }(); +};