Skip to content

Commit 3eb50f2

Browse files
authored
Merge pull request #226 from takker99:unstable-api
feat(api): Implement the `/api/pages/:project` endpoint
2 parents c867f53 + abccd64 commit 3eb50f2

21 files changed

+1354
-9
lines changed

api.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export * as pages from "./api/pages.ts";
2+
export * as projects from "./api/projects.ts";
3+
export * as users from "./api/users.ts";
4+
export type { HTTPError, TypedError } from "./error.ts";
5+
export type { BaseOptions, ExtendedOptions, OAuthOptions } from "./util.ts";
6+
7+
export {
8+
get as listPages,
9+
list as listPagesStream,
10+
type ListPagesOption,
11+
type ListPagesStreamOption,
12+
makeGetRequest as makeListPagesRequest,
13+
} from "./api/pages/project.ts";
14+
export {
15+
makePostRequest as makeReplaceLinksRequest,
16+
post as replaceLinks,
17+
} from "./api/pages/project/replace/links.ts";
18+
export {
19+
get as searchForPages,
20+
makeGetRequest as makeSearchForPagesRequest,
21+
} from "./api/pages/project/search/query.ts";
22+
export {
23+
get as getLinks,
24+
type GetLinksOptions,
25+
list as readLinks,
26+
makeGetRequest as makeGetLinksRequest,
27+
} from "./api/pages/project/search/titles.ts";
28+
export {
29+
get as getPage,
30+
type GetPageOption,
31+
makeGetRequest as makeGetPageRequest,
32+
} from "./api/pages/project/title.ts";
33+
export {
34+
get as getText,
35+
type GetTextOption,
36+
makeGetRequest as makeGetTextRequest,
37+
} from "./api/pages/project/title/text.ts";
38+
export {
39+
get as getIcon,
40+
type GetIconOption,
41+
makeGetRequest as makeGetIconRequest,
42+
} from "./api/pages/project/title/icon.ts";
43+
export {
44+
get as getProject,
45+
makeGetRequest as makeGetProjectRequest,
46+
} from "./api/projects/project.ts";
47+
export {
48+
get as getUser,
49+
makeGetRequest as makeGetUserRequest,
50+
} from "./api/users/me.ts";

api/pages.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as project from "./pages/project.ts";

api/pages/project.ts

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type {
2+
BasePage,
3+
NotFoundError,
4+
NotLoggedInError,
5+
NotMemberError,
6+
PageList,
7+
} from "@cosense/types/rest";
8+
import { type BaseOptions, setDefaults } from "../../util.ts";
9+
import { cookie } from "../../rest/auth.ts";
10+
import type {
11+
ResponseOfEndpoint,
12+
TargetedResponse,
13+
} from "../../targeted_response.ts";
14+
import {
15+
type HTTPError,
16+
makeError,
17+
makeHTTPError,
18+
type TypedError,
19+
} from "../../error.ts";
20+
import { pooledMap } from "@std/async/pool";
21+
import { range } from "@core/iterutil/range";
22+
import { flatten } from "@core/iterutil/async/flatten";
23+
24+
/** Options for {@linkcode get} */
25+
export interface ListPagesOption<R extends Response | undefined>
26+
extends BaseOptions<R> {
27+
/** the sort of page list to return
28+
*
29+
* @default {"updated"}
30+
*/
31+
sort?:
32+
| "updatedWithMe"
33+
| "updated"
34+
| "created"
35+
| "accessed"
36+
| "pageRank"
37+
| "linked"
38+
| "views"
39+
| "title";
40+
/** the index getting page list from
41+
*
42+
* @default {0}
43+
*/
44+
skip?: number;
45+
/** threshold of the length of page list
46+
*
47+
* @default {100}
48+
*/
49+
limit?: number;
50+
}
51+
52+
/** Constructs a request for the `/api/pages/:project` endpoint
53+
*
54+
* @param project The project name to list pages from
55+
* @param options - Additional configuration options (sorting, pagination, etc.)
56+
* @returns A {@linkcode Request} object for fetching pages data
57+
*/
58+
export const makeGetRequest = <R extends Response | undefined>(
59+
project: string,
60+
options?: ListPagesOption<R>,
61+
): Request => {
62+
const { sid, baseURL, sort, limit, skip } = setDefaults(
63+
options ?? {},
64+
);
65+
const params = new URLSearchParams();
66+
if (sort !== undefined) params.append("sort", sort);
67+
if (limit !== undefined) params.append("limit", `${limit}`);
68+
if (skip !== undefined) params.append("skip", `${skip}`);
69+
70+
return new Request(
71+
`${baseURL}api/pages/${project}?${params}`,
72+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
73+
);
74+
};
75+
76+
/** Lists pages from a specified project
77+
*
78+
* @param project The project name to list pages from
79+
* @param options Configuration options for pagination and sorting
80+
* @returns A {@linkcode Result}<{@linkcode unknown}, {@linkcode Error}> containing:
81+
* - Success: The page data in JSON format
82+
* - Error: One of several possible errors:
83+
* - {@linkcode NotFoundError}: Page not found
84+
* - {@linkcode NotLoggedInError}: Authentication required
85+
* - {@linkcode NotMemberError}: User lacks access
86+
*/
87+
export const get = <R extends Response | undefined = Response>(
88+
project: string,
89+
options?: ListPagesOption<R>,
90+
): Promise<
91+
ResponseOfEndpoint<{
92+
200: PageList;
93+
404: NotFoundError;
94+
401: NotLoggedInError;
95+
403: NotMemberError;
96+
}, R>
97+
> =>
98+
setDefaults(options ?? {}).fetch(
99+
makeGetRequest(project, options),
100+
) as Promise<
101+
ResponseOfEndpoint<{
102+
200: PageList;
103+
404: NotFoundError;
104+
401: NotLoggedInError;
105+
403: NotMemberError;
106+
}, R>
107+
>;
108+
109+
/** Options for {@linkcode list} */
110+
export interface ListPagesStreamOption<R extends Response | undefined>
111+
extends ListPagesOption<R> {
112+
/** The number of requests to make concurrently
113+
*
114+
* @default {3}
115+
*/
116+
poolLimit?: number;
117+
}
118+
119+
/**
120+
* Lists pages from a given `project` with pagination
121+
*
122+
* @param project The project name to list pages from
123+
* @param options Configuration options for pagination and sorting
124+
* @throws {HTTPError | TypedError<"NotLoggedInError" | "NotMemberError" | "NotFoundError">} If any requests in the pagination sequence fail
125+
*/
126+
export async function* list(
127+
project: string,
128+
options?: ListPagesStreamOption<Response>,
129+
): AsyncGenerator<BasePage, void, unknown> {
130+
const props = {
131+
...(options ?? {}),
132+
skip: options?.skip ?? 0,
133+
limit: options?.limit ?? 100,
134+
};
135+
const response = await ensureResponse(await get(project, props));
136+
const list = await response.json();
137+
yield* list.pages;
138+
139+
const limit = list.limit;
140+
const skip = list.skip + limit;
141+
const times = Math.ceil((list.count - skip) / limit);
142+
143+
yield* flatten(
144+
pooledMap(
145+
options?.poolLimit ?? 3,
146+
range(0, times - 1),
147+
async (i) => {
148+
const response = await ensureResponse(
149+
await get(project, { ...props, skip: skip + i * limit, limit }),
150+
);
151+
const list = await response.json();
152+
return list.pages;
153+
},
154+
),
155+
);
156+
}
157+
158+
const ensureResponse = async (
159+
response: ResponseOfEndpoint<{
160+
200: PageList;
161+
404: NotFoundError;
162+
401: NotLoggedInError;
163+
403: NotMemberError;
164+
}, Response>,
165+
): Promise<TargetedResponse<200, PageList>> => {
166+
switch (response.status) {
167+
case 200:
168+
return response;
169+
case 401:
170+
case 403:
171+
case 404: {
172+
const error = await response.json();
173+
throw makeError(error.name, error.message) satisfies TypedError<
174+
"NotLoggedInError" | "NotMemberError" | "NotFoundError"
175+
>;
176+
}
177+
default:
178+
throw makeHTTPError(response) satisfies HTTPError;
179+
}
180+
};
181+
182+
export * as replace from "./project/replace.ts";
183+
export * as search from "./project/search.ts";
184+
export * as title from "./project/title.ts";

api/pages/project/replace.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as links from "./replace/links.ts";

api/pages/project/replace/links.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type {
2+
NotFoundError,
3+
NotLoggedInError,
4+
NotMemberError,
5+
} from "@cosense/types/rest";
6+
import type { ResponseOfEndpoint } from "../../../../targeted_response.ts";
7+
import { type ExtendedOptions, setDefaults } from "../../../../util.ts";
8+
import { cookie } from "../../../../rest/auth.ts";
9+
import { get } from "../../../users/me.ts";
10+
11+
/** Constructs a request for the `/api/pages/:project/replace/links` endpoint
12+
*
13+
* @param project - The project name where all links will be replaced
14+
* @param from - The original link text to be replaced
15+
* @param to - The new link text to replace with
16+
* @param init - Additional configuration options
17+
* @returns A {@linkcode Request} object for replacing links in `project`
18+
*/
19+
export const makePostRequest = <R extends Response | undefined>(
20+
project: string,
21+
from: string,
22+
to: string,
23+
init?: ExtendedOptions<R>,
24+
): Request => {
25+
const { sid, baseURL, csrf } = setDefaults(init ?? {});
26+
27+
return new Request(
28+
`${baseURL}api/pages/${project}/replace/links`,
29+
{
30+
method: "POST",
31+
headers: {
32+
"Content-Type": "application/json;charset=utf-8",
33+
"X-CSRF-TOKEN": csrf ?? "",
34+
...(sid ? { Cookie: cookie(sid) } : {}),
35+
},
36+
body: JSON.stringify({ from, to }),
37+
},
38+
);
39+
};
40+
41+
/** Retrieves JSON data for a specified page
42+
*
43+
* @param project - The project name where all links will be replaced
44+
* @param from - The original link text to be replaced
45+
* @param to - The new link text to replace with
46+
* @param init - Additional configuration options
47+
* @returns A {@linkcode Result}<{@linkcode unknown}, {@linkcode Error}> containing:
48+
* - Success: The page data in JSON format
49+
* - Error: One of several possible errors:
50+
* - {@linkcode NotFoundError}: Page not found
51+
* - {@linkcode NotLoggedInError}: Authentication required
52+
* - {@linkcode NotMemberError}: User lacks access
53+
*/
54+
export const post = async <R extends Response | undefined = Response>(
55+
project: string,
56+
from: string,
57+
to: string,
58+
init?: ExtendedOptions<R>,
59+
): Promise<
60+
ResponseOfEndpoint<{
61+
200: string;
62+
404: NotFoundError;
63+
401: NotLoggedInError;
64+
403: NotMemberError;
65+
}, R>
66+
> => {
67+
let { csrf, fetch, ...init2 } = setDefaults(init ?? {});
68+
69+
if (!csrf) {
70+
const res = await get(init2);
71+
if (!res.ok) return res;
72+
csrf = (await res.json()).csrfToken;
73+
}
74+
75+
return fetch(
76+
makePostRequest(project, from, to, { csrf, ...init2 }),
77+
) as Promise<
78+
ResponseOfEndpoint<{
79+
200: string;
80+
404: NotFoundError;
81+
401: NotLoggedInError;
82+
403: NotMemberError;
83+
}, R>
84+
>;
85+
};

api/pages/project/search.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as query from "./search/query.ts";
2+
export * as titles from "./search/titles.ts";

api/pages/project/search/query.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type {
2+
NotFoundError,
3+
NotLoggedInError,
4+
NotMemberError,
5+
SearchResult,
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+
11+
/** Constructs a request for the `/api/pages/:project/search/query` endpoint
12+
*
13+
* @param project The name of the project to search within
14+
* @param query The search query string to match against pages
15+
* @param options - Additional configuration options
16+
* @returns A {@linkcode Request} object for fetching page data
17+
*/
18+
export const makeGetRequest = <R extends Response | undefined>(
19+
project: string,
20+
query: string,
21+
options?: BaseOptions<R>,
22+
): Request => {
23+
const { sid, baseURL } = setDefaults(options ?? {});
24+
25+
return new Request(
26+
`${baseURL}api/pages/${project}/search/query?q=${
27+
encodeURIComponent(query)
28+
}`,
29+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
30+
);
31+
};
32+
33+
/** Search for pages within a specific project
34+
*
35+
* @param project The name of the project to search within
36+
* @param query The search query string to match against pages
37+
* @param options Additional configuration options for the request
38+
* @returns A {@linkcode Response} object containing the search results
39+
*/
40+
export const get = <R extends Response | undefined = Response>(
41+
project: string,
42+
query: string,
43+
options?: BaseOptions<R>,
44+
): Promise<
45+
ResponseOfEndpoint<{
46+
200: SearchResult;
47+
404: NotFoundError;
48+
401: NotLoggedInError;
49+
403: NotMemberError;
50+
422: { message: string };
51+
}, R>
52+
> =>
53+
setDefaults(options ?? {}).fetch(
54+
makeGetRequest(project, query, options),
55+
) as Promise<
56+
ResponseOfEndpoint<{
57+
200: SearchResult;
58+
404: NotFoundError;
59+
401: NotLoggedInError;
60+
403: NotMemberError;
61+
422: { message: string };
62+
}, R>
63+
>;

0 commit comments

Comments
 (0)