Skip to content

Commit 26e90d8

Browse files
committed
feat(api): Implement the /api/pages/:project/search/titles endpoint
1 parent a59b747 commit 26e90d8

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

api/pages/project/search.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * as query from "./search/query.ts";
2+
export * as titles from "./search/titles.ts";

api/pages/project/search/titles.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type {
2+
NotFoundError,
3+
NotLoggedInError,
4+
NotMemberError,
5+
SearchedTitle,
6+
} from "@cosense/types/rest";
7+
import type { ResponseOfEndpoint } from "../../../../targeted_response.ts";
8+
import { type BaseOptions, setDefaults } from "../../../../util.ts";
9+
import { cookie } from "../../../../rest/auth.ts";
10+
import {
11+
type HTTPError,
12+
makeError,
13+
makeHTTPError,
14+
type TypedError,
15+
} from "../../../../error.ts";
16+
17+
/** Options for {@linkcode get} */
18+
export interface GetLinksOptions<R extends Response | undefined>
19+
extends BaseOptions<R> {
20+
/** ID indicating the next list of links */
21+
followingId?: string;
22+
}
23+
24+
/** Create a request to `GET /api/pages/:project/search/titles`
25+
*
26+
* @param project The project to get the links from
27+
* @param options - Additional configuration options
28+
* @returns A {@linkcode Request} object for fetching link data
29+
*/
30+
export const makeGetRequest = <R extends Response | undefined>(
31+
project: string,
32+
options?: GetLinksOptions<R>,
33+
): Request => {
34+
const { sid, hostName, followingId } = setDefaults(options ?? {});
35+
36+
return new Request(
37+
`https://${hostName}/api/pages/${project}/search/titles${
38+
followingId ? `?followingId=${followingId}` : ""
39+
}`,
40+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
41+
);
42+
};
43+
44+
/** Retrieve link data from a specified Scrapbox project
45+
*
46+
* This function fetches link data from a project, supporting pagination through
47+
* the {@linkcode GetLinksOptions.followingId} parameter. It returns both the link data and the next
48+
* followingId for subsequent requests.
49+
*
50+
* @param project The project to retrieve link data from
51+
* @param options Additional configuration options for the request
52+
* @returns A {@linkcode Response} object containing the link data
53+
*/
54+
export const get = <R extends Response | undefined = Response>(
55+
project: string,
56+
options?: GetLinksOptions<R>,
57+
): Promise<
58+
ResponseOfEndpoint<{
59+
200: SearchedTitle[];
60+
404: NotFoundError;
61+
401: NotLoggedInError;
62+
403: NotMemberError;
63+
422: { message: string };
64+
}, R>
65+
> =>
66+
setDefaults(options ?? {}).fetch(
67+
makeGetRequest(project, options),
68+
) as Promise<
69+
ResponseOfEndpoint<{
70+
200: SearchedTitle[];
71+
404: NotFoundError;
72+
401: NotLoggedInError;
73+
403: NotMemberError;
74+
422: { message: string };
75+
}, R>
76+
>;
77+
78+
/** Retrieve all link data from a specified project one by one
79+
*
80+
* @param project The project name to list pages from
81+
* @param options Additional configuration options for the request
82+
* @returns An async generator that yields each link data
83+
* @throws {TypedError<"NotLoggedInError" | "NotMemberError" | "NotFoundError" | "InvalidFollowingIdError"> | HTTPError}
84+
*/
85+
export async function* list(
86+
project: string,
87+
options?: GetLinksOptions<Response>,
88+
): AsyncGenerator<SearchedTitle, void, unknown> {
89+
let followingId = options?.followingId ?? "";
90+
do {
91+
const response = await get(project, { ...options, followingId });
92+
switch (response.status) {
93+
case 200:
94+
break;
95+
case 401:
96+
case 403:
97+
case 404: {
98+
const error = await response.json();
99+
throw makeError(error.name, error.message) satisfies TypedError<
100+
"NotLoggedInError" | "NotMemberError" | "NotFoundError"
101+
>;
102+
}
103+
case 422:
104+
throw makeError(
105+
"InvalidFollowingIdError",
106+
(await response.json()).message,
107+
) satisfies TypedError<
108+
"InvalidFollowingIdError"
109+
>;
110+
default:
111+
throw makeHTTPError(response) satisfies HTTPError;
112+
}
113+
const titles = await response.json();
114+
yield* titles;
115+
followingId = response.headers.get("X-following-id") ?? "";
116+
} while (followingId);
117+
}

deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"./unstable-api/pages/project/replace/links": "./api/pages/project/replace/links.ts",
2929
"./unstable-api/pages/project/search": "./api/pages/project/search.ts",
3030
"./unstable-api/pages/project/search/query": "./api/pages/project/search/query.ts",
31+
"./unstable-api/pages/project/search/titles": "./api/pages/project/search/titles.ts",
3132
"./unstable-api/pages/project/title": "./api/pages/project/title.ts",
3233
"./unstable-api/pages/project/title/text": "./api/pages/project/title/text.ts",
3334
"./unstable-api/pages/project/title/icon": "./api/pages/project/title/icon.ts",

0 commit comments

Comments
 (0)