Skip to content

Commit e2ec5dd

Browse files
committed
feat(api): Implement the /api/pages/:project/:title endpoint
1 parent c867f53 commit e2ec5dd

File tree

9 files changed

+537
-4
lines changed

9 files changed

+537
-4
lines changed

api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as pages from "./api/pages.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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as title from "./project/title.ts";

api/pages/project/title.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type {
2+
NotFoundError,
3+
NotLoggedInError,
4+
NotMemberError,
5+
Page,
6+
} from "@cosense/types/rest";
7+
import type { ResponseOfEndpoint } from "../../../targeted_response.ts";
8+
import { type BaseOptions, setDefaults } from "../../../util.ts";
9+
import { encodeTitleURI } from "../../../title.ts";
10+
import { cookie } from "../../../rest/auth.ts";
11+
12+
/** Options for {@linkcode getPage} */
13+
export interface GetPageOption<R extends Response | undefined>
14+
extends BaseOptions<R> {
15+
/** use `followRename` */
16+
followRename?: boolean;
17+
18+
/** project ids to get External links */
19+
projects?: string[];
20+
}
21+
22+
/** Constructs a request for the `/api/pages/:project/:title` endpoint
23+
*
24+
* @param project The project name containing the desired page
25+
* @param title The page title to retrieve (case insensitive)
26+
* @param options - Additional configuration options
27+
* @returns A {@linkcode Request} object for fetching page data
28+
*/
29+
export const makeGetRequest = <R extends Response | undefined>(
30+
project: string,
31+
title: string,
32+
options?: GetPageOption<R>,
33+
): Request => {
34+
const { sid, hostName, followRename, projects } = setDefaults(options ?? {});
35+
36+
const params = new URLSearchParams([
37+
["followRename", `${followRename ?? true}`],
38+
...(projects?.map?.((id) => ["projects", id]) ?? []),
39+
]);
40+
41+
return new Request(
42+
`https://${hostName}/api/pages/${project}/${
43+
encodeTitleURI(title)
44+
}?${params}`,
45+
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
46+
);
47+
};
48+
49+
/** Retrieves JSON data for a specified page
50+
*
51+
* @param project The project name containing the desired page
52+
* @param title The page title to retrieve (case insensitive)
53+
* @param options Additional configuration options for the request
54+
* @returns A {@linkcode Result}<{@linkcode unknown}, {@linkcode Error}> containing:
55+
* - Success: The page data in JSON format
56+
* - Error: One of several possible errors:
57+
* - {@linkcode NotFoundError}: Page not found
58+
* - {@linkcode NotLoggedInError}: Authentication required
59+
* - {@linkcode NotMemberError}: User lacks access
60+
*/
61+
export const get = <R extends Response | undefined = Response>(
62+
project: string,
63+
title: string,
64+
options?: GetPageOption<R>,
65+
): Promise<
66+
| ResponseOfEndpoint<{
67+
200: Page;
68+
404: NotFoundError;
69+
401: NotLoggedInError;
70+
403: NotMemberError;
71+
}>
72+
| (undefined extends R ? undefined : never)
73+
> =>
74+
setDefaults(options ?? {}).fetch(
75+
makeGetRequest(project, title, options),
76+
) as Promise<
77+
| ResponseOfEndpoint<{
78+
200: Page;
79+
404: NotFoundError;
80+
401: NotLoggedInError;
81+
403: NotMemberError;
82+
}>
83+
| (undefined extends R ? undefined : never)
84+
>;

deno.jsonc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
"./rest": "./rest/mod.ts",
2121
"./text": "./text.ts",
2222
"./title": "./title.ts",
23-
"./websocket": "./websocket/mod.ts"
23+
"./websocket": "./websocket/mod.ts",
24+
"./unstable-api": "./api.ts",
25+
"./unstable-api/pages": "./api/pages.ts",
26+
"./unstable-api/pages/project": "./api/pages/project.ts",
27+
"./unstable-api/pages/project/title": "./api/pages/project/title.ts"
2428
},
2529
"imports": {
2630
"@core/unknownutil": "jsr:@core/unknownutil@^4.0.0",
@@ -34,7 +38,9 @@
3438
"@std/assert": "jsr:@std/assert@1",
3539
"@std/async": "jsr:@std/async@1",
3640
"@std/encoding": "jsr:@std/encoding@1",
41+
"@std/http": "jsr:@std/http@^1.0.13",
3742
"@std/json": "jsr:@std/json@^1.0.0",
43+
"@std/testing": "jsr:@std/testing@^1.0.9",
3844
"@std/testing/snapshot": "jsr:@std/testing@1/snapshot",
3945
"@takker/md5": "jsr:@takker/md5@0.1",
4046
"@takker/onp": "./vendor/raw.githubusercontent.com/takker99/onp/0.0.1/mod.ts",

deno.lock

Lines changed: 51 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_compatible.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { JsonValue } from "@std/json/types";
2+
import type { IsAny } from "@std/testing/types";
3+
export type { IsAny, JsonValue };
4+
5+
/**
6+
* Check if a property {@linkcode K} is optional in {@linkcode T}.
7+
*
8+
* ```ts
9+
* import type { Assert } from "@std/testing/types";
10+
*
11+
* type _1 = Assert<IsOptional<{ a?: number }, "a">, true>;
12+
* type _2 = Assert<IsOptional<{ a?: undefined }, "a">, true>;
13+
* type _3 = Assert<IsOptional<{ a?: number | undefined }, "a">, true>;
14+
* type _4 = Assert<IsOptional<{ a: number }, "a">, false>;
15+
* type _5 = Assert<IsOptional<{ a: undefined }, "a">, false>;
16+
* type _6 = Assert<IsOptional<{ a: number | undefined }, "a">, false>;
17+
* ```
18+
* @internal
19+
*
20+
* @see https://dev.to/zirkelc/typescript-how-to-check-for-optional-properties-3192
21+
*/
22+
export type IsOptional<T, K extends keyof T> =
23+
Record<PropertyKey, never> extends Pick<T, K> ? true : false;
24+
25+
/**
26+
* A type that is compatible with JSON.
27+
*
28+
* ```ts
29+
* import type { JsonValue } from "@std/json/types";
30+
* import { assertType } from "@std/testing/types";
31+
*
32+
* type IsJsonCompatible<T> = [T] extends [JsonCompatible<T>] ? true : false;
33+
*
34+
* assertType<IsJsonCompatible<null>>(true);
35+
* assertType<IsJsonCompatible<false>>(true);
36+
* assertType<IsJsonCompatible<0>>(true);
37+
* assertType<IsJsonCompatible<"">>(true);
38+
* assertType<IsJsonCompatible<[]>>(true);
39+
* assertType<IsJsonCompatible<JsonValue>>(true);
40+
* assertType<IsJsonCompatible<symbol>>(false);
41+
* // deno-lint-ignore no-explicit-any
42+
* assertType<IsJsonCompatible<any>>(false);
43+
* assertType<IsJsonCompatible<unknown>>(false);
44+
* assertType<IsJsonCompatible<undefined>>(false);
45+
* // deno-lint-ignore ban-types
46+
* assertType<IsJsonCompatible<Function>>(false);
47+
* assertType<IsJsonCompatible<() => void>>(false);
48+
* assertType<IsJsonCompatible<number | undefined>>(false);
49+
* assertType<IsJsonCompatible<symbol | undefined>>(false);
50+
*
51+
* assertType<IsJsonCompatible<object>>(true);
52+
* // deno-lint-ignore ban-types
53+
* assertType<IsJsonCompatible<{}>>(true);
54+
* assertType<IsJsonCompatible<{ a: 0 }>>(true);
55+
* assertType<IsJsonCompatible<{ a: "" }>>(true);
56+
* assertType<IsJsonCompatible<{ a: [] }>>(true);
57+
* assertType<IsJsonCompatible<{ a: null }>>(true);
58+
* assertType<IsJsonCompatible<{ a: false }>>(true);
59+
* assertType<IsJsonCompatible<{ a: boolean }>>(true);
60+
* assertType<IsJsonCompatible<{ a: Date }>>(false);
61+
* assertType<IsJsonCompatible<{ a?: Date }>>(false);
62+
* assertType<IsJsonCompatible<{ a: number }>>(true);
63+
* assertType<IsJsonCompatible<{ a?: number }>>(true);
64+
* assertType<IsJsonCompatible<{ a: undefined }>>(false);
65+
* assertType<IsJsonCompatible<{ a?: undefined }>>(true);
66+
* assertType<IsJsonCompatible<{ a: number | undefined }>>(false);
67+
* assertType<IsJsonCompatible<{ a: null }>>(true);
68+
* assertType<IsJsonCompatible<{ a: null | undefined }>>(false);
69+
* assertType<IsJsonCompatible<{ a?: null }>>(true);
70+
* assertType<IsJsonCompatible<{ a: JsonValue }>>(true);
71+
* // deno-lint-ignore no-explicit-any
72+
* assertType<IsJsonCompatible<{ a: any }>>(false);
73+
* assertType<IsJsonCompatible<{ a: unknown }>>(false);
74+
* // deno-lint-ignore ban-types
75+
* assertType<IsJsonCompatible<{ a: Function }>>(false);
76+
* // deno-lint-ignore no-explicit-any
77+
* assertType<IsJsonCompatible<{ a: () => any }>>(false);
78+
* // deno-lint-ignore no-explicit-any
79+
* assertType<IsJsonCompatible<{ a: (() => any) | number }>>(false);
80+
* // deno-lint-ignore no-explicit-any
81+
* assertType<IsJsonCompatible<{ a?: () => any }>>(false);
82+
* class A {
83+
* a = 34;
84+
* }
85+
* assertType<IsJsonCompatible<A>>(true);
86+
* class B {
87+
* fn() {
88+
* return "hello";
89+
* };
90+
* }
91+
* assertType<IsJsonCompatible<B>>(false);
92+
*
93+
* assertType<IsJsonCompatible<{ a: number } | { a: string }>>(true);
94+
* assertType<IsJsonCompatible<{ a: number } | { a: () => void }>>(false);
95+
*
96+
* assertType<IsJsonCompatible<{ a: { aa: string } }>>(true);
97+
* interface D {
98+
* aa: string;
99+
* }
100+
* assertType<IsJsonCompatible<D>>(true);
101+
* interface E {
102+
* a: D;
103+
* }
104+
* assertType<IsJsonCompatible<E>>(true);
105+
* interface F {
106+
* _: E;
107+
* }
108+
* assertType<IsJsonCompatible<F>>(true);
109+
* ```
110+
*
111+
* @see This implementation is heavily inspired by https://github.com/microsoft/TypeScript/issues/1897#issuecomment-580962081 .
112+
*/
113+
export type JsonCompatible<T> =
114+
// deno-lint-ignore ban-types
115+
[Extract<T, Function | symbol | undefined>] extends [never] ? {
116+
[K in keyof T]: [IsAny<T[K]>] extends [true] ? never
117+
: T[K] extends JsonValue ? T[K]
118+
: [IsOptional<T, K>] extends [true]
119+
? JsonCompatible<Exclude<T[K], undefined>> | Extract<T[K], undefined>
120+
: undefined extends T[K] ? never
121+
: JsonCompatible<T[K]>;
122+
}
123+
: never;

0 commit comments

Comments
 (0)