From 4e06ea7bdbaaa196da5c3e2a755aeefb25cf4228 Mon Sep 17 00:00:00 2001 From: xuatz Date: Mon, 19 May 2025 00:18:58 +0900 Subject: feat(api): enable ?sortOrder= for relevant resources (#1398) * feat(api): enable `?sortOrder=` for relevant resources * fix tests --- .prettierignore | 1 + .../web/app/api/v1/bookmarks/[bookmarkId]/route.ts | 4 +- apps/web/app/api/v1/bookmarks/route.ts | 9 +++- apps/web/app/api/v1/bookmarks/search/route.ts | 1 + .../app/api/v1/lists/[listId]/bookmarks/route.ts | 4 +- .../web/app/api/v1/tags/[tagId]/bookmarks/route.ts | 5 +- apps/web/app/api/v1/utils/types.ts | 12 +++++ packages/open-api/karakeep-openapi-spec.json | 61 ++++++++++++++++++++-- packages/open-api/lib/bookmarks.ts | 6 +++ packages/open-api/lib/lists.ts | 15 ++++-- packages/open-api/lib/tags.ts | 15 ++++-- packages/open-api/package.json | 1 + packages/open-api/tsconfig.json | 2 +- packages/sdk/src/karakeep-api.d.ts | 24 +++------ 14 files changed, 126 insertions(+), 34 deletions(-) diff --git a/.prettierignore b/.prettierignore index 0acd8bfc..b9bf8b46 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,6 +10,7 @@ dist .env .env.* **/migrations/** +packages/open-api/karakeep-openapi-spec.json # Ignore files for PNPM, NPM and YARN pnpm-lock.yaml diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts index fa551894..9ad18fd3 100644 --- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts +++ b/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts @@ -3,7 +3,7 @@ import { buildHandler } from "@/app/api/v1/utils/handler"; import { zUpdateBookmarksRequestSchema } from "@karakeep/shared/types/bookmarks"; -import { zGetBookmarkSearchParamsSchema } from "../../utils/types"; +import { zGetBookmarkQueryParamsSchema } from "../../utils/types"; export const dynamic = "force-dynamic"; @@ -13,7 +13,7 @@ export const GET = ( ) => buildHandler({ req, - searchParamsSchema: zGetBookmarkSearchParamsSchema, + searchParamsSchema: zGetBookmarkQueryParamsSchema, handler: async ({ api, searchParams }) => { const bookmark = await api.bookmarks.getBookmark({ bookmarkId: params.bookmarkId, diff --git a/apps/web/app/api/v1/bookmarks/route.ts b/apps/web/app/api/v1/bookmarks/route.ts index 1605d2b5..4df4f6ad 100644 --- a/apps/web/app/api/v1/bookmarks/route.ts +++ b/apps/web/app/api/v1/bookmarks/route.ts @@ -1,7 +1,10 @@ import { NextRequest } from "next/server"; import { z } from "zod"; -import { zNewBookmarkRequestSchema } from "@karakeep/shared/types/bookmarks"; +import { + zNewBookmarkRequestSchema, + zSortOrder, +} from "@karakeep/shared/types/bookmarks"; import { buildHandler } from "../utils/handler"; import { adaptPagination, zPagination } from "../utils/pagination"; @@ -16,6 +19,10 @@ export const GET = (req: NextRequest) => .object({ favourited: zStringBool.optional(), archived: zStringBool.optional(), + sortOrder: zSortOrder + .exclude([zSortOrder.Enum.relevance]) + .optional() + .default(zSortOrder.Enum.desc), // TODO: Change the default to false in a couple of releases. includeContent: zStringBool.optional().default("true"), }) diff --git a/apps/web/app/api/v1/bookmarks/search/route.ts b/apps/web/app/api/v1/bookmarks/search/route.ts index 52081c7f..e85c7954 100644 --- a/apps/web/app/api/v1/bookmarks/search/route.ts +++ b/apps/web/app/api/v1/bookmarks/search/route.ts @@ -27,6 +27,7 @@ export const GET = (req: NextRequest) => const bookmarks = await api.bookmarks.searchBookmarks({ text: searchParams.q, cursor: searchParams.cursor, + sortOrder: searchParams.sortOrder, limit: searchParams.limit, includeContent: searchParams.includeContent, }); diff --git a/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts b/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts index 3977413a..daf78449 100644 --- a/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts +++ b/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts @@ -1,14 +1,14 @@ import { NextRequest } from "next/server"; import { buildHandler } from "@/app/api/v1/utils/handler"; import { adaptPagination, zPagination } from "@/app/api/v1/utils/pagination"; -import { zGetBookmarkSearchParamsSchema } from "@/app/api/v1/utils/types"; +import { zGetBookmarkQueryParamsSchema } from "@/app/api/v1/utils/types"; export const dynamic = "force-dynamic"; export const GET = (req: NextRequest, params: { params: { listId: string } }) => buildHandler({ req, - searchParamsSchema: zPagination.and(zGetBookmarkSearchParamsSchema), + searchParamsSchema: zPagination.and(zGetBookmarkQueryParamsSchema), handler: async ({ api, searchParams }) => { const bookmarks = await api.bookmarks.getBookmarks({ listId: params.params.listId, diff --git a/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts b/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts index cfc0af51..aaa5087b 100644 --- a/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts +++ b/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { buildHandler } from "@/app/api/v1/utils/handler"; import { adaptPagination, zPagination } from "@/app/api/v1/utils/pagination"; -import { zGetBookmarkSearchParamsSchema } from "@/app/api/v1/utils/types"; +import { zGetBookmarkQueryParamsSchema } from "@/app/api/v1/utils/types"; export const dynamic = "force-dynamic"; @@ -11,10 +11,11 @@ export const GET = ( ) => buildHandler({ req, - searchParamsSchema: zPagination.and(zGetBookmarkSearchParamsSchema), + searchParamsSchema: zPagination.and(zGetBookmarkQueryParamsSchema), handler: async ({ api, searchParams }) => { const bookmarks = await api.bookmarks.getBookmarks({ tagId: params.tagId, + sortOrder: searchParams.sortOrder, limit: searchParams.limit, cursor: searchParams.cursor, }); diff --git a/apps/web/app/api/v1/utils/types.ts b/apps/web/app/api/v1/utils/types.ts index f0fe6231..bf181ce4 100644 --- a/apps/web/app/api/v1/utils/types.ts +++ b/apps/web/app/api/v1/utils/types.ts @@ -1,11 +1,23 @@ import { z } from "zod"; +import { zSortOrder } from "@karakeep/shared/types/bookmarks"; + export const zStringBool = z .string() .refine((val) => val === "true" || val === "false", "Must be true or false") .transform((val) => val === "true"); +export const zGetBookmarkQueryParamsSchema = z.object({ + sortOrder: zSortOrder + .exclude([zSortOrder.Enum.relevance]) + .optional() + .default(zSortOrder.Enum.desc), + // TODO: Change the default to false in a couple of releases. + includeContent: zStringBool.optional().default("true"), +}); + export const zGetBookmarkSearchParamsSchema = z.object({ + sortOrder: zSortOrder.optional().default(zSortOrder.Enum.relevance), // TODO: Change the default to false in a couple of releases. includeContent: zStringBool.optional().default("true"), }); diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json index 3bdaed54..dbc2e5d0 100644 --- a/packages/open-api/karakeep-openapi-spec.json +++ b/packages/open-api/karakeep-openapi-spec.json @@ -549,6 +549,19 @@ "name": "favourited", "in": "query" }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, { "schema": { "type": "number" @@ -774,6 +787,20 @@ "name": "q", "in": "query" }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc", + "relevance" + ], + "default": "relevance" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, { "schema": { "type": "number" @@ -1969,8 +1996,8 @@ }, "/lists/{listId}/bookmarks": { "get": { - "description": "Get the bookmarks in a list", - "summary": "Get a bookmarks in a list", + "description": "Get bookmarks in the list", + "summary": "Get bookmarks in the list", "tags": [ "Lists" ], @@ -1983,6 +2010,19 @@ { "$ref": "#/components/parameters/ListId" }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, { "schema": { "type": "number" @@ -2367,8 +2407,8 @@ }, "/tags/{tagId}/bookmarks": { "get": { - "description": "Get the bookmarks with the tag", - "summary": "Get a bookmarks with the tag", + "description": "Get bookmarks with the tag", + "summary": "Get bookmarks with the tag", "tags": [ "Tags" ], @@ -2381,6 +2421,19 @@ { "$ref": "#/components/parameters/TagId" }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, { "schema": { "type": "number" diff --git a/packages/open-api/lib/bookmarks.ts b/packages/open-api/lib/bookmarks.ts index e344d656..8fb0eb8c 100644 --- a/packages/open-api/lib/bookmarks.ts +++ b/packages/open-api/lib/bookmarks.ts @@ -9,6 +9,7 @@ import { zBareBookmarkSchema, zManipulatedTagSchema, zNewBookmarkRequestSchema, + zSortOrder, zUpdateBookmarksRequestSchema, } from "@karakeep/shared/types/bookmarks"; @@ -60,6 +61,10 @@ registry.registerPath({ .object({ archived: z.boolean().optional(), favourited: z.boolean().optional(), + sortOrder: zSortOrder + .exclude(["relevance"]) + .optional() + .default(zSortOrder.Enum.desc), }) .merge(PaginationSchema) .merge(IncludeContentSearchParamSchema), @@ -87,6 +92,7 @@ registry.registerPath({ query: z .object({ q: z.string(), + sortOrder: zSortOrder.optional().default(zSortOrder.Enum.relevance), }) .merge(PaginationSchema) .merge(IncludeContentSearchParamSchema), diff --git a/packages/open-api/lib/lists.ts b/packages/open-api/lib/lists.ts index ab07e425..992b96c4 100644 --- a/packages/open-api/lib/lists.ts +++ b/packages/open-api/lib/lists.ts @@ -4,6 +4,7 @@ import { } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; +import { zSortOrder } from "@karakeep/shared/types/bookmarks"; import { zBookmarkListSchema, zEditBookmarkListSchema, @@ -190,13 +191,21 @@ registry.registerPath({ registry.registerPath({ method: "get", path: "/lists/{listId}/bookmarks", - description: "Get the bookmarks in a list", - summary: "Get a bookmarks in a list", + description: "Get bookmarks in the list", + summary: "Get bookmarks in the list", tags: ["Lists"], security: [{ [BearerAuth.name]: [] }], request: { params: z.object({ listId: ListIdSchema }), - query: PaginationSchema.merge(IncludeContentSearchParamSchema), + query: z + .object({ + sortOrder: zSortOrder + .exclude(["relevance"]) + .optional() + .default(zSortOrder.Enum.desc), + }) + .merge(PaginationSchema) + .merge(IncludeContentSearchParamSchema), }, responses: { 200: { diff --git a/packages/open-api/lib/tags.ts b/packages/open-api/lib/tags.ts index c51e3b84..b8136741 100644 --- a/packages/open-api/lib/tags.ts +++ b/packages/open-api/lib/tags.ts @@ -4,6 +4,7 @@ import { } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; +import { zSortOrder } from "@karakeep/shared/types/bookmarks"; import { zGetTagResponseSchema, zUpdateTagRequestSchema, @@ -152,13 +153,21 @@ registry.registerPath({ registry.registerPath({ method: "get", path: "/tags/{tagId}/bookmarks", - description: "Get the bookmarks with the tag", - summary: "Get a bookmarks with the tag", + description: "Get bookmarks with the tag", + summary: "Get bookmarks with the tag", tags: ["Tags"], security: [{ [BearerAuth.name]: [] }], request: { params: z.object({ tagId: TagIdSchema }), - query: PaginationSchema.merge(IncludeContentSearchParamSchema), + query: z + .object({ + sortOrder: zSortOrder + .exclude(["relevance"]) + .optional() + .default(zSortOrder.Enum.desc), + }) + .merge(PaginationSchema) + .merge(IncludeContentSearchParamSchema), }, responses: { 200: { diff --git a/packages/open-api/package.json b/packages/open-api/package.json index dfbb0bb6..900af481 100644 --- a/packages/open-api/package.json +++ b/packages/open-api/package.json @@ -19,6 +19,7 @@ "typecheck": "tsc --noEmit", "generate": "tsx index.ts", "format": "prettier . --ignore-path ../../.prettierignore", + "format:fix": "prettier . --write --ignore-path ../../.prettierignore", "lint": "eslint ." }, "main": "index.ts", diff --git a/packages/open-api/tsconfig.json b/packages/open-api/tsconfig.json index d97c8ef4..a795b96a 100644 --- a/packages/open-api/tsconfig.json +++ b/packages/open-api/tsconfig.json @@ -5,5 +5,5 @@ "exclude": ["node_modules"], "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" - }, + } } diff --git a/packages/sdk/src/karakeep-api.d.ts b/packages/sdk/src/karakeep-api.d.ts index 0b434b3d..1fa63dac 100644 --- a/packages/sdk/src/karakeep-api.d.ts +++ b/packages/sdk/src/karakeep-api.d.ts @@ -20,6 +20,7 @@ export interface paths { query?: { archived?: boolean; favourited?: boolean; + sortOrder?: "asc" | "desc"; limit?: number; cursor?: components["schemas"]["Cursor"]; /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */ @@ -135,6 +136,7 @@ export interface paths { parameters: { query: { q: string; + sortOrder?: "asc" | "desc" | "relevance"; limit?: number; cursor?: components["schemas"]["Cursor"]; /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */ @@ -971,12 +973,13 @@ export interface paths { cookie?: never; }; /** - * Get a bookmarks in a list - * @description Get the bookmarks in a list + * Get bookmarks in the list + * @description Get bookmarks in the list */ get: { parameters: { query?: { + sortOrder?: "asc" | "desc"; limit?: number; cursor?: components["schemas"]["Cursor"]; /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */ @@ -1052,18 +1055,6 @@ export interface paths { }; content?: never; }; - /** @description Bookmark already in list */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - code: string; - message: string; - }; - }; - }; /** @description List or bookmark not found */ 404: { headers: { @@ -1314,12 +1305,13 @@ export interface paths { cookie?: never; }; /** - * Get a bookmarks with the tag - * @description Get the bookmarks with the tag + * Get bookmarks with the tag + * @description Get bookmarks with the tag */ get: { parameters: { query?: { + sortOrder?: "asc" | "desc"; limit?: number; cursor?: components["schemas"]["Cursor"]; /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */ -- cgit v1.2.3-70-g09d2