aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-10-20 17:36:02 +0000
committerMohamed Bassem <me@mbassem.com>2024-10-20 17:36:02 +0000
commit3c1ec3aa2f7d64932fd26c8cbcb1aee1e57861bd (patch)
tree34eceb016bec57c7c3b59315a210747cdf0c8f33 /packages
parent4086c37b830c3c4141b37052e3c192a750470084 (diff)
downloadkarakeep-3c1ec3aa2f7d64932fd26c8cbcb1aee1e57861bd.tar.zst
chore: Define hoarder's rest API in zod format
Diffstat (limited to 'packages')
-rw-r--r--packages/open-api/index.ts46
-rw-r--r--packages/open-api/lib/bookmarks.ts212
-rw-r--r--packages/open-api/lib/common.ts12
-rw-r--r--packages/open-api/lib/lists.ts199
-rw-r--r--packages/open-api/lib/pagination.ts24
-rw-r--r--packages/open-api/lib/tags.ts139
-rw-r--r--packages/open-api/package.json33
-rw-r--r--packages/open-api/tsconfig.json9
8 files changed, 674 insertions, 0 deletions
diff --git a/packages/open-api/index.ts b/packages/open-api/index.ts
new file mode 100644
index 00000000..da5b3729
--- /dev/null
+++ b/packages/open-api/index.ts
@@ -0,0 +1,46 @@
+import * as fs from "fs";
+import {
+ OpenApiGeneratorV3,
+ OpenAPIRegistry,
+} from "@asteasolutions/zod-to-openapi";
+import * as yaml from "yaml";
+
+import { registry as bookmarksRegistry } from "./lib/bookmarks";
+import { registry as commonRegistry } from "./lib/common";
+import { registry as listsRegistry } from "./lib/lists";
+import { registry as tagsRegistry } from "./lib/tags";
+
+function getOpenApiDocumentation() {
+ const registry = new OpenAPIRegistry([
+ commonRegistry,
+ bookmarksRegistry,
+ listsRegistry,
+ tagsRegistry,
+ ]);
+
+ const generator = new OpenApiGeneratorV3(registry.definitions);
+
+ return generator.generateDocument({
+ openapi: "3.0.0",
+ info: {
+ version: "1.0.0",
+ title: "Hoarder API",
+ description: "The API for the Hoarder app",
+ },
+ servers: [{ url: "v1" }],
+ });
+}
+
+function writeDocumentation() {
+ // OpenAPI JSON
+ const docs = getOpenApiDocumentation();
+
+ // YAML equivalent
+ const fileContent = yaml.stringify(docs);
+
+ fs.writeFileSync(`./openapi-spec.yml`, fileContent, {
+ encoding: "utf-8",
+ });
+}
+
+writeDocumentation();
diff --git a/packages/open-api/lib/bookmarks.ts b/packages/open-api/lib/bookmarks.ts
new file mode 100644
index 00000000..28ef7e0d
--- /dev/null
+++ b/packages/open-api/lib/bookmarks.ts
@@ -0,0 +1,212 @@
+import {
+ extendZodWithOpenApi,
+ OpenAPIRegistry,
+} from "@asteasolutions/zod-to-openapi";
+import { z } from "zod";
+
+import {
+ zBareBookmarkSchema,
+ zManipulatedTagSchema,
+ zNewBookmarkRequestSchema,
+ zUpdateBookmarksRequestSchema,
+} from "@hoarder/shared/types/bookmarks";
+
+import { BearerAuth } from "./common";
+import {
+ BookmarkSchema,
+ PaginatedBookmarksSchema,
+ PaginationSchema,
+} from "./pagination";
+import { TagIdSchema } from "./tags";
+
+export const registry = new OpenAPIRegistry();
+extendZodWithOpenApi(z);
+
+export const BookmarkIdSchema = registry.registerParameter(
+ "BookmarkId",
+ z.string().openapi({
+ param: {
+ name: "bookmarkId",
+ in: "path",
+ },
+ example: "ieidlxygmwj87oxz5hxttoc8",
+ }),
+);
+
+registry.registerPath({
+ method: "get",
+ path: "/bookmarks",
+ description: "Get all bookmarks",
+ summary: "Get all bookmarks",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ query: z
+ .object({
+ archived: z.boolean().optional(),
+ favourited: z.boolean().optional(),
+ })
+ .merge(PaginationSchema),
+ },
+ responses: {
+ 200: {
+ description: "Object with all bookmarks data.",
+ content: {
+ "application/json": {
+ schema: PaginatedBookmarksSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "post",
+ path: "/bookmarks",
+ description: "Create a new bookmark",
+ summary: "Create a new bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ body: {
+ description: "The bookmark to create",
+ content: {
+ "application/json": {
+ schema: zNewBookmarkRequestSchema,
+ },
+ },
+ },
+ },
+ responses: {
+ 201: {
+ description: "The created bookmark",
+ content: {
+ "application/json": {
+ schema: BookmarkSchema,
+ },
+ },
+ },
+ },
+});
+registry.registerPath({
+ method: "get",
+ path: "/bookmarks/{bookmarkId}",
+ description: "Get bookmark by its id",
+ summary: "Get a single bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ bookmarkId: BookmarkIdSchema }),
+ },
+ responses: {
+ 200: {
+ description: "Object with bookmark data.",
+ content: {
+ "application/json": {
+ schema: BookmarkSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "delete",
+ path: "/bookmarks/{bookmarkId}",
+ description: "Delete bookmark by its id",
+ summary: "Delete a bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ bookmarkId: BookmarkIdSchema }),
+ },
+ responses: {
+ 204: {
+ description: "No content - the bookmark was deleted",
+ },
+ },
+});
+
+registry.registerPath({
+ method: "patch",
+ path: "/bookmarks/{bookmarkId}",
+ description: "Update bookmark by its id",
+ summary: "Update a bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ bookmarkId: BookmarkIdSchema }),
+ body: {
+ description:
+ "The data to update. Only the fields you want to update need to be provided.",
+ content: {
+ "application/json": {
+ schema: zUpdateBookmarksRequestSchema.omit({ bookmarkId: true }),
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "The updated bookmark",
+ content: {
+ "application/json": {
+ schema: zBareBookmarkSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "post",
+ path: "/bookmarks/{bookmarkId}/tags",
+ description: "Attach tags to a bookmark",
+ summary: "Attach tags to a bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ bookmarkId: BookmarkIdSchema }),
+ body: {
+ description: "The tags to attach.",
+ content: {
+ "application/json": {
+ schema: z.object({ tags: z.array(zManipulatedTagSchema) }),
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "The list of attached tag ids",
+ content: {
+ "application/json": {
+ schema: z.object({ attached: z.array(TagIdSchema) }),
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "delete",
+ path: "/bookmarks/{bookmarkId}/tags",
+ description: "Detach tags from a bookmark",
+ summary: "Detach tags from a bookmark",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ bookmarkId: BookmarkIdSchema }),
+ body: {
+ description: "The tags to detach.",
+ content: {
+ "application/json": {
+ schema: z.object({ tags: z.array(zManipulatedTagSchema) }),
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "The list of detached tag ids",
+ content: {
+ "application/json": {
+ schema: z.object({ detached: z.array(TagIdSchema) }),
+ },
+ },
+ },
+ },
+});
diff --git a/packages/open-api/lib/common.ts b/packages/open-api/lib/common.ts
new file mode 100644
index 00000000..d1ac43e1
--- /dev/null
+++ b/packages/open-api/lib/common.ts
@@ -0,0 +1,12 @@
+import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
+
+export const registry = new OpenAPIRegistry();
+export const BearerAuth = registry.registerComponent(
+ "securitySchemes",
+ "bearerAuth",
+ {
+ type: "http",
+ scheme: "bearer",
+ bearerFormat: "JWT",
+ },
+);
diff --git a/packages/open-api/lib/lists.ts b/packages/open-api/lib/lists.ts
new file mode 100644
index 00000000..27f458fc
--- /dev/null
+++ b/packages/open-api/lib/lists.ts
@@ -0,0 +1,199 @@
+import {
+ extendZodWithOpenApi,
+ OpenAPIRegistry,
+} from "@asteasolutions/zod-to-openapi";
+import { z } from "zod";
+
+import {
+ zBookmarkListSchema,
+ zNewBookmarkListSchema,
+} from "@hoarder/shared/types/lists";
+
+import { BookmarkIdSchema } from "./bookmarks";
+import { BearerAuth } from "./common";
+import { PaginatedBookmarksSchema, PaginationSchema } from "./pagination";
+
+export const registry = new OpenAPIRegistry();
+extendZodWithOpenApi(z);
+
+export const ListIdSchema = registry.registerParameter(
+ "ListId",
+ z.string().openapi({
+ param: {
+ name: "listId",
+ in: "path",
+ },
+ example: "ieidlxygmwj87oxz5hxttoc8",
+ }),
+);
+
+export const ListSchema = zBookmarkListSchema.openapi("List");
+
+registry.registerPath({
+ method: "get",
+ path: "/lists",
+ description: "Get all lists",
+ summary: "Get all lists",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {},
+ responses: {
+ 200: {
+ description: "Object with all lists data.",
+ content: {
+ "application/json": {
+ schema: z.object({
+ lists: z.array(ListSchema),
+ }),
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "post",
+ path: "/lists",
+ description: "Create a new list",
+ summary: "Create a new list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ body: {
+ description: "The list to create",
+ content: {
+ "application/json": {
+ schema: zNewBookmarkListSchema,
+ },
+ },
+ },
+ },
+ responses: {
+ 201: {
+ description: "The created list",
+ content: {
+ "application/json": {
+ schema: ListSchema,
+ },
+ },
+ },
+ },
+});
+registry.registerPath({
+ method: "get",
+ path: "/lists/{listId}",
+ description: "Get list by its id",
+ summary: "Get a single list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema }),
+ },
+ responses: {
+ 200: {
+ description: "Object with list data.",
+ content: {
+ "application/json": {
+ schema: ListSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "delete",
+ path: "/lists/{listId}",
+ description: "Delete list by its id",
+ summary: "Delete a list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema }),
+ },
+ responses: {
+ 204: {
+ description: "No content - the bookmark was deleted",
+ },
+ },
+});
+
+registry.registerPath({
+ method: "patch",
+ path: "/list/{listId}",
+ description: "Update list by its id",
+ summary: "Update a list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema }),
+ body: {
+ description:
+ "The data to update. Only the fields you want to update need to be provided.",
+ content: {
+ "application/json": {
+ schema: zNewBookmarkListSchema.partial(),
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "The updated list",
+ content: {
+ "application/json": {
+ schema: ListSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "get",
+ path: "/lists/{listId}/bookmarks",
+ description: "Get the bookmarks in a list",
+ summary: "Get a bookmarks in a list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema }),
+ query: PaginationSchema,
+ },
+ responses: {
+ 200: {
+ description: "Object with list data.",
+ content: {
+ "application/json": {
+ schema: PaginatedBookmarksSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "put",
+ path: "/lists/{listId}/bookmarks/{bookmarkId}",
+ description: "Add the bookmarks to a list",
+ summary: "Add a bookmark to a list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema, bookmarkId: BookmarkIdSchema }),
+ },
+ responses: {
+ 204: {
+ description: "No content - the bookmark was added",
+ },
+ },
+});
+
+registry.registerPath({
+ method: "delete",
+ path: "/lists/{listId}/bookmarks/{bookmarkId}",
+ description: "Remove the bookmarks from a list",
+ summary: "Remove a bookmark from a list",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ listId: ListIdSchema, bookmarkId: BookmarkIdSchema }),
+ },
+ responses: {
+ 204: {
+ description: "No content - the bookmark was added",
+ },
+ },
+});
diff --git a/packages/open-api/lib/pagination.ts b/packages/open-api/lib/pagination.ts
new file mode 100644
index 00000000..fe98cd62
--- /dev/null
+++ b/packages/open-api/lib/pagination.ts
@@ -0,0 +1,24 @@
+import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
+import { z } from "zod";
+
+import { zBookmarkSchema } from "@hoarder/shared/types/bookmarks";
+
+extendZodWithOpenApi(z);
+
+export const BookmarkSchema = zBookmarkSchema.openapi("Bookmark");
+
+export const PaginatedBookmarksSchema = z
+ .object({
+ bookmarks: z.array(BookmarkSchema),
+ nextCursor: z.string().nullable(),
+ })
+ .openapi("PaginatedBookmarks");
+
+export const CursorSchema = z.string().openapi("Cursor");
+
+export const PaginationSchema = z
+ .object({
+ limit: z.number().optional(),
+ cursor: CursorSchema.optional(),
+ })
+ .openapi("Pagination");
diff --git a/packages/open-api/lib/tags.ts b/packages/open-api/lib/tags.ts
new file mode 100644
index 00000000..e13b7c60
--- /dev/null
+++ b/packages/open-api/lib/tags.ts
@@ -0,0 +1,139 @@
+import {
+ extendZodWithOpenApi,
+ OpenAPIRegistry,
+} from "@asteasolutions/zod-to-openapi";
+import { z } from "zod";
+
+import {
+ zGetTagResponseSchema,
+ zUpdateTagRequestSchema,
+} from "@hoarder/shared/types/tags";
+
+import { BearerAuth } from "./common";
+import { PaginatedBookmarksSchema, PaginationSchema } from "./pagination";
+
+export const registry = new OpenAPIRegistry();
+extendZodWithOpenApi(z);
+
+export const TagSchema = zGetTagResponseSchema.openapi("Tag");
+
+export const TagIdSchema = registry.registerParameter(
+ "TagId",
+ z.string().openapi({
+ param: {
+ name: "tagId",
+ in: "path",
+ },
+ example: "ieidlxygmwj87oxz5hxttoc8",
+ }),
+);
+
+registry.registerPath({
+ method: "get",
+ path: "/tags",
+ description: "Get all tags",
+ summary: "Get all tags",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {},
+ responses: {
+ 200: {
+ description: "Object with all tags data.",
+ content: {
+ "application/json": {
+ schema: z.object({
+ tags: z.array(TagSchema),
+ }),
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "get",
+ path: "/tags/{tagId}",
+ description: "Get tag by its id",
+ summary: "Get a single tag",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ tagId: TagIdSchema }),
+ },
+ responses: {
+ 200: {
+ description: "Object with list data.",
+ content: {
+ "application/json": {
+ schema: TagSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "delete",
+ path: "/tags/{tagId}",
+ description: "Delete tag by its id",
+ summary: "Delete a tag",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ tagId: TagIdSchema }),
+ },
+ responses: {
+ 204: {
+ description: "No content - the bookmark was deleted",
+ },
+ },
+});
+
+registry.registerPath({
+ method: "patch",
+ path: "/tags/{tagId}",
+ description: "Update tag by its id",
+ summary: "Update a tag",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ tagId: TagIdSchema }),
+ body: {
+ description:
+ "The data to update. Only the fields you want to update need to be provided.",
+ content: {
+ "application/json": {
+ schema: zUpdateTagRequestSchema.omit({ tagId: true }),
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "The updated tag",
+ content: {
+ "application/json": {
+ schema: TagSchema,
+ },
+ },
+ },
+ },
+});
+
+registry.registerPath({
+ method: "get",
+ path: "/tags/{tagId}/bookmarks",
+ description: "Get the bookmarks with the tag",
+ summary: "Get a bookmarks with the tag",
+ security: [{ [BearerAuth.name]: [] }],
+ request: {
+ params: z.object({ tagId: TagIdSchema }),
+ query: PaginationSchema,
+ },
+ responses: {
+ 200: {
+ description: "Object with list data.",
+ content: {
+ "application/json": {
+ schema: PaginatedBookmarksSchema,
+ },
+ },
+ },
+ },
+});
diff --git a/packages/open-api/package.json b/packages/open-api/package.json
new file mode 100644
index 00000000..fd5c1aee
--- /dev/null
+++ b/packages/open-api/package.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@hoarder/open-api",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@asteasolutions/zod-to-openapi": "^7.2.0",
+ "@hoarder/shared": "workspace:^0.1.0",
+ "yaml": "^2.6.0",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@hoarder/eslint-config": "workspace:^0.2.0",
+ "@hoarder/prettier-config": "workspace:^0.1.0",
+ "@hoarder/tsconfig": "workspace:^0.1.0",
+ "tsx": "^4.7.1"
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "generate": "tsx index.ts",
+ "format": "prettier . --ignore-path ../../.prettierignore",
+ "lint": "eslint ."
+ },
+ "main": "index.ts",
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@hoarder/eslint-config/base"
+ ]
+ },
+ "prettier": "@hoarder/prettier-config"
+}
diff --git a/packages/open-api/tsconfig.json b/packages/open-api/tsconfig.json
new file mode 100644
index 00000000..71bf61e7
--- /dev/null
+++ b/packages/open-api/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@hoarder/tsconfig/node.json",
+ "include": ["**/*.ts"],
+ "exclude": ["node_modules"],
+ "compilerOptions": {
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+ },
+}