aboutsummaryrefslogtreecommitdiffstats
path: root/web/lib
diff options
context:
space:
mode:
Diffstat (limited to 'web/lib')
-rw-r--r--web/lib/api.ts87
-rw-r--r--web/lib/config.ts1
-rw-r--r--web/lib/services/links.ts40
-rw-r--r--web/lib/types/api/links.ts11
4 files changed, 135 insertions, 4 deletions
diff --git a/web/lib/api.ts b/web/lib/api.ts
new file mode 100644
index 00000000..193d9bb7
--- /dev/null
+++ b/web/lib/api.ts
@@ -0,0 +1,87 @@
+"use client";
+
+import { ZodTypeAny, z } from "zod";
+import {
+ ZNewBookmarkedLinkRequest,
+ zGetLinksResponseSchema,
+} from "./types/api/links";
+
+import serverConfig from "./config";
+
+const BASE_URL = `${serverConfig.api_url}/api/v1`;
+
+export type FetchError = {
+ status?: number;
+ message?: string;
+};
+
+async function doRequest<Schema extends ZodTypeAny>(
+ _path: string,
+ respSchema: Schema,
+ _opts: RequestInit | undefined,
+): Promise<[z.infer<typeof respSchema>, undefined] | [undefined, FetchError]>;
+
+async function doRequest<_Schema>(
+ _path: string,
+ _respSchema: undefined,
+ _opts: RequestInit | undefined,
+): Promise<[undefined, undefined] | [undefined, FetchError]>;
+
+type InputSchema<T> = T extends ZodTypeAny ? T : undefined;
+
+async function doRequest<T>(
+ path: string,
+ respSchema?: InputSchema<T>,
+ opts?: RequestInit,
+): Promise<
+ | (InputSchema<T> extends ZodTypeAny
+ ? [z.infer<InputSchema<T>>, undefined]
+ : [undefined, undefined])
+ | [undefined, FetchError]
+> {
+ try {
+ const res = await fetch(`${BASE_URL}${path}`, opts);
+ if (!res.ok) {
+ return [
+ undefined,
+ { status: res.status, message: await res.text() },
+ ] as const;
+ }
+ if (!respSchema) {
+ return [undefined, undefined] as const;
+ }
+
+ let parsed = respSchema.safeParse(await res.json());
+ if (!parsed.success) {
+ return [
+ undefined,
+ { message: `Failed to parse response: ${parsed.error.toString()}` },
+ ] as const;
+ }
+
+ return [parsed.data, undefined] as const;
+ } catch (error: any) {
+ return [
+ undefined,
+ { message: `Failed to execute fetch request: ${error}` },
+ ] as const;
+ }
+}
+
+export default class APIClient {
+ static async getLinks() {
+ return await doRequest(`/links`, zGetLinksResponseSchema, {
+ next: { tags: ["links"] },
+ });
+ }
+
+ static async bookmarkLink(url: string) {
+ const body: ZNewBookmarkedLinkRequest = {
+ url,
+ };
+ return await doRequest(`/links`, undefined, {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+ }
+}
diff --git a/web/lib/config.ts b/web/lib/config.ts
index 6b55aa2c..ec042b54 100644
--- a/web/lib/config.ts
+++ b/web/lib/config.ts
@@ -13,6 +13,7 @@ function buildAuthentikConfig() {
}
const serverConfig = {
+ api_url: process.env.API_URL || "http://localhost:3000",
auth: {
authentik: buildAuthentikConfig(),
},
diff --git a/web/lib/services/links.ts b/web/lib/services/links.ts
new file mode 100644
index 00000000..f3ff1757
--- /dev/null
+++ b/web/lib/services/links.ts
@@ -0,0 +1,40 @@
+import { LinkCrawlerQueue } from "@remember/shared/queues";
+import prisma from "@remember/db";
+
+export async function bookmarkLink(url: string, userId: string) {
+ const link = await prisma.bookmarkedLink.create({
+ data: {
+ url,
+ userId,
+ },
+ });
+
+ // Enqueue crawling request
+ await LinkCrawlerQueue.add("crawl", {
+ linkId: link.id,
+ url: link.url,
+ });
+
+ return link;
+}
+
+export async function getLinks(userId: string) {
+ return await prisma.bookmarkedLink.findMany({
+ where: {
+ userId,
+ },
+ select: {
+ id: true,
+ url: true,
+ createdAt: true,
+ details: {
+ select: {
+ title: true,
+ description: true,
+ imageUrl: true,
+ favicon: true,
+ },
+ },
+ },
+ });
+}
diff --git a/web/lib/types/api/links.ts b/web/lib/types/api/links.ts
index 48214f9a..644589b4 100644
--- a/web/lib/types/api/links.ts
+++ b/web/lib/types/api/links.ts
@@ -7,10 +7,10 @@ export const zBookmarkedLinkSchema = z.object({
details: z
.object({
- title: z.string(),
- description: z.string().optional(),
- imageUrl: z.string().url().optional(),
- favicon: z.string().url().optional(),
+ title: z.string().nullish(),
+ description: z.string().nullish(),
+ imageUrl: z.string().url().nullish(),
+ favicon: z.string().url().nullish(),
})
.nullish(),
});
@@ -20,6 +20,9 @@ export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
export const zNewBookmarkedLinkRequestSchema = zBookmarkedLinkSchema.pick({
url: true,
});
+export type ZNewBookmarkedLinkRequest = z.infer<
+ typeof zNewBookmarkedLinkRequestSchema
+>;
// GET /v1/links
export const zGetLinksResponseSchema = z.object({