Skip to content
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
1 change: 1 addition & 0 deletions deps/scrapbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
104 changes: 104 additions & 0 deletions rest/link.ts
Original file line number Diff line number Diff line change
@@ -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<ErrorLike | AsyncGenerator<SearchedTitle[], void, unknown>> => {
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<ErrorLike | AsyncGenerator<SearchedTitle, void, unknown>> => {
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;
}
}
}();
};