diff options
Diffstat (limited to 'packages')
| -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 |
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" + }, +} |
