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

feat(unstable-api): Implement the /unstable-api submodule partially #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
takker99 merged 16 commits into main from unstable-api
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e2ec5dd
feat(api): Implement the `/api/pages/:project/:title` endpoint
takker99 Mar 3, 2025
88bad85
feat(api): Implement the `/api/pages/:project` endpoint
takker99 Mar 3, 2025
a37eb3a
refactor(api): Include `R` into `ResponseOfEndpoint`
takker99 Mar 18, 2025
ed57c55
feat(api): Enhance error handling in `listPages` function and introdu...
takker99 Mar 18, 2025
872dd54
feat(api): Implement the `/api/users/me` endpoint
takker99 Mar 18, 2025
9efcc4a
feat(api): Implement the `/api/pages/:project/:title/text` endpoint
takker99 Mar 18, 2025
ecc74fd
feat(api): Implement the `/api/pages/:project/:title/icon` endpoint
takker99 Mar 19, 2025
8140a96
feat(api): Implement the `/api/pages/:project/replace/links` endpoint
takker99 Mar 19, 2025
05c5c9a
feat(api): Implement the `/api/pages/:project/search/query` endpoint
takker99 Mar 19, 2025
a59b747
fix(api): Remove an unused interface
takker99 Mar 19, 2025
26e90d8
feat(api): Implement the `/api/pages/:project/search/titles` endpoint
takker99 Mar 19, 2025
98dae98
feat(api): Implement the `/api/projects/:project` endpoint
takker99 Mar 24, 2025
b53e29a
feat(api): Use `baseURL` instead of `hostName`
takker99 Mar 24, 2025
ec34c84
feat(api): Make pagination in `list` function concurrent and add `poo...
takker99 Mar 24, 2025
1151b95
refactor(api): Re-export REST APIs from `/unstable-api` directly, dis...
takker99 Mar 24, 2025
abccd64
refactor(api): Export error types and options
takker99 Mar 24, 2025
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
50 changes: 50 additions & 0 deletions api.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export * as pages from "./api/pages.ts";
export * as projects from "./api/projects.ts";
export * as users from "./api/users.ts";
export type { HTTPError, TypedError } from "./error.ts";
export type { BaseOptions, ExtendedOptions, OAuthOptions } from "./util.ts";

export {
get as listPages,
list as listPagesStream,
type ListPagesOption,
type ListPagesStreamOption,
makeGetRequest as makeListPagesRequest,
} from "./api/pages/project.ts";
export {
makePostRequest as makeReplaceLinksRequest,
post as replaceLinks,
} from "./api/pages/project/replace/links.ts";
export {
get as searchForPages,
makeGetRequest as makeSearchForPagesRequest,
} from "./api/pages/project/search/query.ts";
export {
get as getLinks,
type GetLinksOptions,
list as readLinks,
makeGetRequest as makeGetLinksRequest,
} from "./api/pages/project/search/titles.ts";
export {
get as getPage,
type GetPageOption,
makeGetRequest as makeGetPageRequest,
} from "./api/pages/project/title.ts";
export {
get as getText,
type GetTextOption,
makeGetRequest as makeGetTextRequest,
} from "./api/pages/project/title/text.ts";
export {
get as getIcon,
type GetIconOption,
makeGetRequest as makeGetIconRequest,
} from "./api/pages/project/title/icon.ts";
export {
get as getProject,
makeGetRequest as makeGetProjectRequest,
} from "./api/projects/project.ts";
export {
get as getUser,
makeGetRequest as makeGetUserRequest,
} from "./api/users/me.ts";
1 change: 1 addition & 0 deletions api/pages.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as project from "./pages/project.ts";
184 changes: 184 additions & 0 deletions api/pages/project.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import type {
BasePage,
NotFoundError,
NotLoggedInError,
NotMemberError,
PageList,
} from "@cosense/types/rest";
import { type BaseOptions, setDefaults } from "../../util.ts";
import { cookie } from "../../rest/auth.ts";
import type {
ResponseOfEndpoint,
TargetedResponse,
} from "../../targeted_response.ts";
import {
type HTTPError,
makeError,
makeHTTPError,
type TypedError,
} from "../../error.ts";
import { pooledMap } from "@std/async/pool";
import { range } from "@core/iterutil/range";
import { flatten } from "@core/iterutil/async/flatten";

/** Options for {@linkcode get} */
export interface ListPagesOption<R extends Response | undefined>
extends BaseOptions<R> {
/** the sort of page list to return
*
* @default {"updated"}
*/
sort?:
| "updatedWithMe"
| "updated"
| "created"
| "accessed"
| "pageRank"
| "linked"
| "views"
| "title";
/** the index getting page list from
*
* @default {0}
*/
skip?: number;
/** threshold of the length of page list
*
* @default {100}
*/
limit?: number;
}

/** Constructs a request for the `/api/pages/:project` endpoint
*
* @param project The project name to list pages from
* @param options - Additional configuration options (sorting, pagination, etc.)
* @returns A {@linkcode Request} object for fetching pages data
*/
export const makeGetRequest = <R extends Response | undefined>(
project: string,
options?: ListPagesOption<R>,
): Request => {
const { sid, baseURL, sort, limit, skip } = setDefaults(
options ?? {},
);
const params = new URLSearchParams();
if (sort !== undefined) params.append("sort", sort);
if (limit !== undefined) params.append("limit", `${limit}`);
if (skip !== undefined) params.append("skip", `${skip}`);

return new Request(
`${baseURL}api/pages/${project}?${params}`,
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
);
};

/** Lists pages from a specified project
*
* @param project The project name to list pages from
* @param options Configuration options for pagination and sorting
* @returns A {@linkcode Result}<{@linkcode unknown}, {@linkcode Error}> containing:
* - Success: The page data in JSON format
* - Error: One of several possible errors:
* - {@linkcode NotFoundError}: Page not found
* - {@linkcode NotLoggedInError}: Authentication required
* - {@linkcode NotMemberError}: User lacks access
*/
export const get = <R extends Response | undefined = Response>(
project: string,
options?: ListPagesOption<R>,
): Promise<
ResponseOfEndpoint<{
200: PageList;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
}, R>
> =>
setDefaults(options ?? {}).fetch(
makeGetRequest(project, options),
) as Promise<
ResponseOfEndpoint<{
200: PageList;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
}, R>
>;

/** Options for {@linkcode list} */
export interface ListPagesStreamOption<R extends Response | undefined>
extends ListPagesOption<R> {
/** The number of requests to make concurrently
*
* @default {3}
*/
poolLimit?: number;
}

/**
* Lists pages from a given `project` with pagination
*
* @param project The project name to list pages from
* @param options Configuration options for pagination and sorting
* @throws {HTTPError | TypedError<"NotLoggedInError" | "NotMemberError" | "NotFoundError">} If any requests in the pagination sequence fail
*/
export async function* list(
project: string,
options?: ListPagesStreamOption<Response>,
): AsyncGenerator<BasePage, void, unknown> {
const props = {
...(options ?? {}),
skip: options?.skip ?? 0,
limit: options?.limit ?? 100,
};
const response = await ensureResponse(await get(project, props));
const list = await response.json();
yield* list.pages;

const limit = list.limit;
const skip = list.skip + limit;
const times = Math.ceil((list.count - skip) / limit);

yield* flatten(
pooledMap(
options?.poolLimit ?? 3,
range(0, times - 1),
async (i) => {
const response = await ensureResponse(
await get(project, { ...props, skip: skip + i * limit, limit }),
);
const list = await response.json();
return list.pages;
},
),
);
}

const ensureResponse = async (
response: ResponseOfEndpoint<{
200: PageList;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
}, Response>,
): Promise<TargetedResponse<200, PageList>> => {
switch (response.status) {
case 200:
return response;
case 401:
case 403:
case 404: {
const error = await response.json();
throw makeError(error.name, error.message) satisfies TypedError<
"NotLoggedInError" | "NotMemberError" | "NotFoundError"
>;
}
default:
throw makeHTTPError(response) satisfies HTTPError;
}
};

export * as replace from "./project/replace.ts";
export * as search from "./project/search.ts";
export * as title from "./project/title.ts";
1 change: 1 addition & 0 deletions api/pages/project/replace.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as links from "./replace/links.ts";
85 changes: 85 additions & 0 deletions api/pages/project/replace/links.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type {
NotFoundError,
NotLoggedInError,
NotMemberError,
} from "@cosense/types/rest";
import type { ResponseOfEndpoint } from "../../../../targeted_response.ts";
import { type ExtendedOptions, setDefaults } from "../../../../util.ts";
import { cookie } from "../../../../rest/auth.ts";
import { get } from "../../../users/me.ts";

/** Constructs a request for the `/api/pages/:project/replace/links` endpoint
*
* @param project - The project name where all links will be replaced
* @param from - The original link text to be replaced
* @param to - The new link text to replace with
* @param init - Additional configuration options
* @returns A {@linkcode Request} object for replacing links in `project`
*/
export const makePostRequest = <R extends Response | undefined>(
project: string,
from: string,
to: string,
init?: ExtendedOptions<R>,
): Request => {
const { sid, baseURL, csrf } = setDefaults(init ?? {});

return new Request(
`${baseURL}api/pages/${project}/replace/links`,
{
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
"X-CSRF-TOKEN": csrf ?? "",
...(sid ? { Cookie: cookie(sid) } : {}),
},
body: JSON.stringify({ from, to }),
},
);
};

/** Retrieves JSON data for a specified page
*
* @param project - The project name where all links will be replaced
* @param from - The original link text to be replaced
* @param to - The new link text to replace with
* @param init - Additional configuration options
* @returns A {@linkcode Result}<{@linkcode unknown}, {@linkcode Error}> containing:
* - Success: The page data in JSON format
* - Error: One of several possible errors:
* - {@linkcode NotFoundError}: Page not found
* - {@linkcode NotLoggedInError}: Authentication required
* - {@linkcode NotMemberError}: User lacks access
*/
export const post = async <R extends Response | undefined = Response>(
project: string,
from: string,
to: string,
init?: ExtendedOptions<R>,
): Promise<
ResponseOfEndpoint<{
200: string;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
}, R>
> => {
let { csrf, fetch, ...init2 } = setDefaults(init ?? {});

if (!csrf) {
const res = await get(init2);
if (!res.ok) return res;
csrf = (await res.json()).csrfToken;
}

return fetch(
makePostRequest(project, from, to, { csrf, ...init2 }),
) as Promise<
ResponseOfEndpoint<{
200: string;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
}, R>
>;
};
2 changes: 2 additions & 0 deletions api/pages/project/search.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as query from "./search/query.ts";
export * as titles from "./search/titles.ts";
63 changes: 63 additions & 0 deletions api/pages/project/search/query.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type {
NotFoundError,
NotLoggedInError,
NotMemberError,
SearchResult,
} from "@cosense/types/rest";
import type { ResponseOfEndpoint } from "../../../../targeted_response.ts";
import { type BaseOptions, setDefaults } from "../../../../util.ts";
import { cookie } from "../../../../rest/auth.ts";

/** Constructs a request for the `/api/pages/:project/search/query` endpoint
*
* @param project The name of the project to search within
* @param query The search query string to match against pages
* @param options - Additional configuration options
* @returns A {@linkcode Request} object for fetching page data
*/
export const makeGetRequest = <R extends Response | undefined>(
project: string,
query: string,
options?: BaseOptions<R>,
): Request => {
const { sid, baseURL } = setDefaults(options ?? {});

return new Request(
`${baseURL}api/pages/${project}/search/query?q=${
encodeURIComponent(query)
}`,
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
);
};

/** Search for pages within a specific project
*
* @param project The name of the project to search within
* @param query The search query string to match against pages
* @param options Additional configuration options for the request
* @returns A {@linkcode Response} object containing the search results
*/
export const get = <R extends Response | undefined = Response>(
project: string,
query: string,
options?: BaseOptions<R>,
): Promise<
ResponseOfEndpoint<{
200: SearchResult;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
422: { message: string };
}, R>
> =>
setDefaults(options ?? {}).fetch(
makeGetRequest(project, query, options),
) as Promise<
ResponseOfEndpoint<{
200: SearchResult;
404: NotFoundError;
401: NotLoggedInError;
403: NotMemberError;
422: { message: string };
}, R>
>;
Loading

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