Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3eb50f2

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

File tree

21 files changed

+1354
-9
lines changed

21 files changed

+1354
-9
lines changed

‎api.ts

Lines changed: 50 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as project from "./pages/project.ts";

‎api/pages/project.ts

Lines changed: 184 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 85 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 63 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /