diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-10-20 17:36:02 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-10-20 17:36:02 +0000 |
| commit | 3c1ec3aa2f7d64932fd26c8cbcb1aee1e57861bd (patch) | |
| tree | 34eceb016bec57c7c3b59315a210747cdf0c8f33 | |
| parent | 4086c37b830c3c4141b37052e3c192a750470084 (diff) | |
| download | karakeep-3c1ec3aa2f7d64932fd26c8cbcb1aee1e57861bd.tar.zst | |
chore: Define hoarder's rest API in zod format
Diffstat (limited to '')
| -rw-r--r-- | packages/open-api/index.ts | 46 | ||||
| -rw-r--r-- | packages/open-api/lib/bookmarks.ts | 212 | ||||
| -rw-r--r-- | packages/open-api/lib/common.ts | 12 | ||||
| -rw-r--r-- | packages/open-api/lib/lists.ts | 199 | ||||
| -rw-r--r-- | packages/open-api/lib/pagination.ts | 24 | ||||
| -rw-r--r-- | packages/open-api/lib/tags.ts | 139 | ||||
| -rw-r--r-- | packages/open-api/package.json | 33 | ||||
| -rw-r--r-- | packages/open-api/tsconfig.json | 9 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 57 |
9 files changed, 726 insertions, 5 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" + }, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1d993ea..eaa245bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -876,6 +876,34 @@ importers: specifier: ^0.24.02 version: 0.24.2 + packages/open-api: + dependencies: + '@asteasolutions/zod-to-openapi': + specifier: ^7.2.0 + version: 7.2.0(zod@3.22.4) + '@hoarder/shared': + specifier: workspace:^0.1.0 + version: link:../shared + yaml: + specifier: ^2.6.0 + version: 2.6.0 + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: + '@hoarder/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@hoarder/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@hoarder/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + tsx: + specifier: ^4.7.1 + version: 4.7.1 + packages/queue: dependencies: async-mutex: @@ -1216,6 +1244,11 @@ packages: peerDependencies: ajv: '>=8' + '@asteasolutions/zod-to-openapi@7.2.0': + resolution: {integrity: sha512-Va+Fq1QzKkSgmiYINSp3cASFhMsbdRH/kmCk2feijhC+yNjGoC056CRqihrVFhR8MY8HOZHdlYm2Ns2lmszCiw==} + peerDependencies: + zod: ^3.20.2 + '@auth/core@0.27.0': resolution: {integrity: sha512-3bydnRJIM/Al6mkYmb53MsC+6G8ojw3lLPzwgVnX4dCo6N2lrib6Wq6r0vxZIhuHGjLObqqtUfpeaEj5aeTHFg==} peerDependencies: @@ -9508,6 +9541,9 @@ packages: zod: optional: true + openapi3-ts@4.4.0: + resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -12667,8 +12703,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.4.0: - resolution: {integrity: sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==} + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} engines: {node: '>= 14'} hasBin: true @@ -12854,6 +12890,12 @@ snapshots: leven: 3.1.0 dev: false + '@asteasolutions/zod-to-openapi@7.2.0(zod@3.22.4)': + dependencies: + openapi3-ts: 4.4.0 + zod: 3.22.4 + dev: false + '@auth/core@0.27.0': dependencies: '@panva/hkdf': 1.1.1 @@ -17291,7 +17333,7 @@ snapshots: semver: 7.6.0 strip-ansi: 5.2.0 wcwidth: 1.0.1 - yaml: 2.4.0 + yaml: 2.6.0 transitivePeerDependencies: - encoding dev: false @@ -25814,6 +25856,11 @@ snapshots: - encoding dev: false + openapi3-ts@4.4.0: + dependencies: + yaml: 2.6.0 + dev: false + opener@1.5.2: dev: false @@ -26258,7 +26305,7 @@ snapshots: dependencies: lilconfig: 3.1.1 postcss: 8.4.35 - yaml: 2.4.0 + yaml: 2.6.0 postcss-loader@7.3.4(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3): dependencies: @@ -29957,7 +30004,7 @@ snapshots: yaml@1.10.2: dev: false - yaml@2.4.0: {} + yaml@2.6.0: {} yargs-parser@18.1.3: dependencies: |
