diff options
Diffstat (limited to 'packages')
84 files changed, 15398 insertions, 643 deletions
diff --git a/packages/api/index.ts b/packages/api/index.ts new file mode 100644 index 00000000..5147ea37 --- /dev/null +++ b/packages/api/index.ts @@ -0,0 +1,50 @@ +import { Hono } from "hono"; +import { logger } from "hono/logger"; +import { poweredBy } from "hono/powered-by"; + +import { Context } from "@karakeep/trpc"; + +import trpcAdapter from "./middlewares/trpcAdapter"; +import assets from "./routes/assets"; +import bookmarks from "./routes/bookmarks"; +import highlights from "./routes/highlights"; +import lists from "./routes/lists"; +import publicRoute from "./routes/public"; +import rss from "./routes/rss"; +import tags from "./routes/tags"; +import users from "./routes/users"; + +const v1 = new Hono<{ + Variables: { + ctx: Context; + }; +}>() + .route("/highlights", highlights) + .route("/bookmarks", bookmarks) + .route("/lists", lists) + .route("/tags", tags) + .route("/users", users) + .route("/assets", assets) + .route("/rss", rss); + +const app = new Hono<{ + Variables: { + // This is going to be coming from the web app + ctx: Context; + }; +}>() + .use(logger()) + .use(poweredBy()) + .use(async (c, next) => { + // Ensure that the ctx is set + if (!c.var.ctx) { + throw new Error("Context is not set"); + } + await next(); + }) + .use(trpcAdapter) + .route("/v1", v1) + .route("/assets", assets) + .route("/public", publicRoute); + +export default app; diff --git a/packages/api/middlewares/auth.ts b/packages/api/middlewares/auth.ts new file mode 100644 index 00000000..42bca6c8 --- /dev/null +++ b/packages/api/middlewares/auth.ts @@ -0,0 +1,37 @@ +import { createMiddleware } from "hono/factory"; +import { HTTPException } from "hono/http-exception"; + +import { AuthedContext, Context, createCallerFactory } from "@karakeep/trpc"; +import { appRouter } from "@karakeep/trpc/routers/_app"; + +const createCaller = createCallerFactory(appRouter); + +export const unauthedMiddleware = createMiddleware<{ + Variables: { + ctx: Context; + api: ReturnType<typeof createCaller>; + }; +}>(async (c, next) => { + if (!c.var.ctx) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + c.set("api", createCaller(c.get("ctx"))); + await next(); +}); + +export const authMiddleware = createMiddleware<{ + Variables: { + ctx: AuthedContext; + api: ReturnType<typeof createCaller>; + }; +}>(async (c, next) => { + if (!c.var.ctx || !c.var.ctx.user || c.var.ctx.user === null) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + c.set("api", createCaller(c.get("ctx"))); + await next(); +}); diff --git a/packages/api/middlewares/trpcAdapter.ts b/packages/api/middlewares/trpcAdapter.ts new file mode 100644 index 00000000..6bb4a790 --- /dev/null +++ b/packages/api/middlewares/trpcAdapter.ts @@ -0,0 +1,41 @@ +import { TRPCError } from "@trpc/server"; +import { createMiddleware } from "hono/factory"; +import { HTTPException } from "hono/http-exception"; + +function trpcCodeToHttpCode(code: TRPCError["code"]) { + switch (code) { + case "BAD_REQUEST": + case "PARSE_ERROR": + return 400; + case "UNAUTHORIZED": + return 401; + case "FORBIDDEN": + return 403; + case "NOT_FOUND": + return 404; + case "METHOD_NOT_SUPPORTED": + return 405; + case "TIMEOUT": + return 408; + case "PAYLOAD_TOO_LARGE": + return 413; + case "INTERNAL_SERVER_ERROR": + return 500; + default: + return 500; + } +} + +const trpcAdapter = createMiddleware(async (c, next) => { + await next(); + const e = c.error; + if (e instanceof TRPCError) { + const code = trpcCodeToHttpCode(e.code); + throw new HTTPException(code, { + message: e.message, + cause: e.cause, + }); + } +}); + +export default trpcAdapter; diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 00000000..82b2b9d0 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@karakeep/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "format": "prettier . --ignore-path ../../.prettierignore", + "format:fix": "prettier . --write --ignore-path ../../.prettierignore", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "vitest" + }, + "dependencies": { + "@hono/zod-validator": "^0.5.0", + "@karakeep/db": "workspace:*", + "@karakeep/shared": "workspace:*", + "@karakeep/trpc": "workspace:*", + "hono": "^4.7.10", + "rss": "^1.2.2", + "zod": "^3.24.2" + }, + "devDependencies": { + "@karakeep/eslint-config": "workspace:^0.2.0", + "@karakeep/prettier-config": "workspace:^0.1.0", + "@karakeep/tsconfig": "workspace:^0.1.0", + "@types/bcryptjs": "^2.4.6", + "@types/deep-equal": "^1.0.4", + "@types/rss": "^0.0.32", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.6.1" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@karakeep/eslint-config/base" + ] + }, + "prettier": "@karakeep/prettier-config" +} diff --git a/packages/api/routes/assets.ts b/packages/api/routes/assets.ts new file mode 100644 index 00000000..9d9a60b3 --- /dev/null +++ b/packages/api/routes/assets.ts @@ -0,0 +1,48 @@ +import { zValidator } from "@hono/zod-validator"; +import { and, eq } from "drizzle-orm"; +import { Hono } from "hono"; +import { z } from "zod"; + +import { assets } from "@karakeep/db/schema"; + +import { authMiddleware } from "../middlewares/auth"; +import { serveAsset } from "../utils/assets"; +import { uploadAsset } from "../utils/upload"; + +const app = new Hono() + .use(authMiddleware) + .post( + "/", + zValidator( + "form", + z + .object({ file: z.instanceof(File) }) + .or(z.object({ image: z.instanceof(File) })), + ), + async (c) => { + const body = c.req.valid("form"); + const up = await uploadAsset(c.var.ctx.user, c.var.ctx.db, body); + if ("error" in up) { + return c.json({ error: up.error }, up.status); + } + return c.json({ + assetId: up.assetId, + contentType: up.contentType, + size: up.size, + fileName: up.fileName, + }); + }, + ) + .get("/:assetId", async (c) => { + const assetId = c.req.param("assetId"); + const assetDb = await c.var.ctx.db.query.assets.findFirst({ + where: and(eq(assets.id, assetId), eq(assets.userId, c.var.ctx.user.id)), + }); + + if (!assetDb) { + return c.json({ error: "Asset not found" }, { status: 404 }); + } + return await serveAsset(c, assetId, c.var.ctx.user.id); + }); + +export default app; diff --git a/packages/api/routes/bookmarks.ts b/packages/api/routes/bookmarks.ts new file mode 100644 index 00000000..abf0daae --- /dev/null +++ b/packages/api/routes/bookmarks.ts @@ -0,0 +1,319 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +import { + BookmarkTypes, + zAssetSchema, + zManipulatedTagSchema, + zNewBookmarkRequestSchema, + zUpdateBookmarksRequestSchema, +} from "@karakeep/shared/types/bookmarks"; + +import { authMiddleware } from "../middlewares/auth"; +import { adaptPagination, zPagination } from "../utils/pagination"; +import { + zGetBookmarkQueryParamsSchema, + zGetBookmarkSearchParamsSchema, + zIncludeContentSearchParamsSchema, + zStringBool, +} from "../utils/types"; +import { uploadAsset } from "../utils/upload"; + +const app = new Hono() + .use(authMiddleware) + + // GET /bookmarks + .get( + "/", + zValidator( + "query", + z + .object({ + favourited: zStringBool.optional(), + archived: zStringBool.optional(), + }) + .and(zGetBookmarkQueryParamsSchema) + .and(zPagination), + ), + async (c) => { + const searchParams = c.req.valid("query"); + const bookmarks = await c.var.api.bookmarks.getBookmarks(searchParams); + return c.json(adaptPagination(bookmarks), 200); + }, + ) + + // POST /bookmarks + .post("/", zValidator("json", zNewBookmarkRequestSchema), async (c) => { + const body = c.req.valid("json"); + const bookmark = await c.var.api.bookmarks.createBookmark(body); + return c.json(bookmark, 201); + }) + + // GET /bookmarks/search + .get( + "/search", + zValidator( + "query", + z + .object({ + q: z.string(), + limit: z.coerce.number().optional(), + cursor: z + .string() + .optional() + .transform((val) => + val ? { ver: 1 as const, offset: parseInt(val) } : undefined, + ), + }) + .and(zGetBookmarkSearchParamsSchema), + ), + async (c) => { + const searchParams = c.req.valid("query"); + const bookmarks = await c.var.api.bookmarks.searchBookmarks({ + text: searchParams.q, + cursor: searchParams.cursor, + limit: searchParams.limit, + includeContent: searchParams.includeContent, + }); + return c.json( + { + bookmarks: bookmarks.bookmarks, + nextCursor: bookmarks.nextCursor + ? `${bookmarks.nextCursor.offset}` + : null, + }, + 200, + ); + }, + ) + .post( + "/singlefile", + zValidator( + "query", + z.object({ + ifexists: z + .enum([ + "skip", + "overwrite", + "overwrite-recrawl", + "append", + "append-recrawl", + ]) + .optional() + .default("skip"), + }), + ), + zValidator( + "form", + z.object({ + url: z.string(), + file: z.instanceof(File), + }), + ), + async (c) => { + const form = c.req.valid("form"); + const up = await uploadAsset(c.var.ctx.user, c.var.ctx.db, form); + if ("error" in up) { + return c.json({ error: up.error }, up.status); + } + const bookmark = await c.var.api.bookmarks.createBookmark({ + type: BookmarkTypes.LINK, + url: form.url, + precrawledArchiveId: up.assetId, + }); + if (bookmark.alreadyExists) { + const ifexists = c.req.valid("query").ifexists; + switch (ifexists) { + case "skip": + break; + case "overwrite-recrawl": + case "overwrite": { + const existingPrecrawledArchiveId = bookmark.assets + .filter((a) => a.assetType == "precrawledArchive") + .at(-1)?.id; + if (existingPrecrawledArchiveId) { + await c.var.api.assets.replaceAsset({ + bookmarkId: bookmark.id, + oldAssetId: existingPrecrawledArchiveId, + newAssetId: up.assetId, + }); + } else { + await c.var.api.assets.attachAsset({ + bookmarkId: bookmark.id, + asset: { + id: up.assetId, + assetType: "precrawledArchive", + }, + }); + } + if (ifexists == "overwrite-recrawl") { + await c.var.api.bookmarks.recrawlBookmark({ + bookmarkId: bookmark.id, + }); + } + break; + } + case "append-recrawl": + case "append": { + await c.var.api.assets.attachAsset({ + bookmarkId: bookmark.id, + asset: { + id: up.assetId, + assetType: "precrawledArchive", + }, + }); + if (ifexists == "append-recrawl") { + await c.var.api.bookmarks.recrawlBookmark({ + bookmarkId: bookmark.id, + }); + } + break; + } + } + return c.json(bookmark, 200); + } else { + return c.json(bookmark, 201); + } + }, + ) + + // GET /bookmarks/[bookmarkId] + .get( + "/:bookmarkId", + zValidator("query", zIncludeContentSearchParamsSchema), + async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const searchParams = c.req.valid("query"); + const bookmark = await c.var.api.bookmarks.getBookmark({ + bookmarkId, + includeContent: searchParams.includeContent, + }); + return c.json(bookmark, 200); + }, + ) + + // PATCH /bookmarks/[bookmarkId] + .patch( + "/:bookmarkId", + zValidator( + "json", + zUpdateBookmarksRequestSchema.omit({ bookmarkId: true }), + ), + async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const body = c.req.valid("json"); + const bookmark = await c.var.api.bookmarks.updateBookmark({ + bookmarkId, + ...body, + }); + return c.json(bookmark, 200); + }, + ) + + // DELETE /bookmarks/[bookmarkId] + .delete("/:bookmarkId", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + await c.var.api.bookmarks.deleteBookmark({ bookmarkId }); + return c.body(null, 204); + }) + + // GET /bookmarks/[bookmarkId]/lists + .get("/:bookmarkId/lists", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const resp = await c.var.api.lists.getListsOfBookmark({ bookmarkId }); + return c.json(resp, 200); + }) + + // GET /bookmarks/[bookmarkId]/assets + .get("/:bookmarkId/assets", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const resp = await c.var.api.bookmarks.getBookmark({ bookmarkId }); + return c.json({ assets: resp.assets }, 200); + }) + + // POST /bookmarks/[bookmarkId]/assets + .post("/:bookmarkId/assets", zValidator("json", zAssetSchema), async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const body = c.req.valid("json"); + const asset = await c.var.api.assets.attachAsset({ + bookmarkId, + asset: body, + }); + return c.json(asset, 201); + }) + + // PUT /bookmarks/[bookmarkId]/assets/[assetId] + .put( + "/:bookmarkId/assets/:assetId", + zValidator("json", z.object({ assetId: z.string() })), + async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const assetId = c.req.param("assetId"); + const body = c.req.valid("json"); + await c.var.api.assets.replaceAsset({ + bookmarkId, + oldAssetId: assetId, + newAssetId: body.assetId, + }); + return c.body(null, 204); + }, + ) + + // DELETE /bookmarks/[bookmarkId]/assets/[assetId] + .delete("/:bookmarkId/assets/:assetId", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const assetId = c.req.param("assetId"); + await c.var.api.assets.detachAsset({ bookmarkId, assetId }); + return c.body(null, 204); + }) + + // POST /bookmarks/[bookmarkId]/tags + .post( + "/:bookmarkId/tags", + zValidator("json", z.object({ tags: z.array(zManipulatedTagSchema) })), + async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const body = c.req.valid("json"); + const resp = await c.var.api.bookmarks.updateTags({ + bookmarkId, + attach: body.tags, + detach: [], + }); + return c.json({ attached: resp.attached }, 200); + }, + ) + + // DELETE /bookmarks/[bookmarkId]/tags + .delete( + "/:bookmarkId/tags", + zValidator("json", z.object({ tags: z.array(zManipulatedTagSchema) })), + async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const body = c.req.valid("json"); + const resp = await c.var.api.bookmarks.updateTags({ + bookmarkId, + detach: body.tags, + attach: [], + }); + return c.json({ detached: resp.detached }, 200); + }, + ) + + // POST /bookmarks/[bookmarkId]/summarize + .post("/:bookmarkId/summarize", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const bookmark = await c.var.api.bookmarks.summarizeBookmark({ + bookmarkId, + }); + return c.json(bookmark, 200); + }) + + // GET /bookmarks/[bookmarkId]/highlights + .get("/:bookmarkId/highlights", async (c) => { + const bookmarkId = c.req.param("bookmarkId"); + const resp = await c.var.api.highlights.getForBookmark({ bookmarkId }); + return c.json(resp, 200); + }); + +export default app; diff --git a/packages/api/routes/highlights.ts b/packages/api/routes/highlights.ts new file mode 100644 index 00000000..d381f7e2 --- /dev/null +++ b/packages/api/routes/highlights.ts @@ -0,0 +1,54 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; + +import { + zNewHighlightSchema, + zUpdateHighlightSchema, +} from "@karakeep/shared/types/highlights"; + +import { authMiddleware } from "../middlewares/auth"; +import { adaptPagination, zPagination } from "../utils/pagination"; + +const app = new Hono() + .use(authMiddleware) + .get("/", zValidator("query", zPagination), async (c) => { + const searchParams = c.req.valid("query"); + const resp = await c.var.api.highlights.getAll({ + ...searchParams, + }); + return c.json(adaptPagination(resp)); + }) + .post("/", zValidator("json", zNewHighlightSchema), async (c) => { + const body = c.req.valid("json"); + const resp = await c.var.api.highlights.create(body); + return c.json(resp, 201); + }) + .get("/:highlightId", async (c) => { + const highlightId = c.req.param("highlightId"); + const highlight = await c.var.api.highlights.get({ + highlightId, + }); + return c.json(highlight, 200); + }) + .patch( + "/:highlightId", + zValidator("json", zUpdateHighlightSchema.omit({ highlightId: true })), + async (c) => { + const highlightId = c.req.param("highlightId"); + const body = c.req.valid("json"); + const highlight = await c.var.api.highlights.update({ + highlightId, + ...body, + }); + return c.json(highlight, 200); + }, + ) + .delete("/:highlightId", async (c) => { + const highlightId = c.req.param("highlightId"); + const highlight = await c.var.api.highlights.delete({ + highlightId, + }); + return c.json(highlight, 200); + }); + +export default app; diff --git a/packages/api/routes/lists.ts b/packages/api/routes/lists.ts new file mode 100644 index 00000000..33908629 --- /dev/null +++ b/packages/api/routes/lists.ts @@ -0,0 +1,70 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; + +import { + zEditBookmarkListSchema, + zNewBookmarkListSchema, +} from "@karakeep/shared/types/lists"; + +import { authMiddleware } from "../middlewares/auth"; +import { adaptPagination, zPagination } from "../utils/pagination"; +import { zGetBookmarkQueryParamsSchema } from "../utils/types"; + +const app = new Hono() + .use(authMiddleware) + .get("/", async (c) => { + const lists = await c.var.api.lists.list(); + return c.json(lists, 200); + }) + .post("/", zValidator("json", zNewBookmarkListSchema), async (c) => { + const body = c.req.valid("json"); + const list = await c.var.api.lists.create(body); + return c.json(list, 201); + }) + .get("/:listId", async (c) => { + const listId = c.req.param("listId"); + const list = await c.var.api.lists.get({ listId }); + return c.json(list, 200); + }) + .patch( + "/:listId", + zValidator("json", zEditBookmarkListSchema.omit({ listId: true })), + async (c) => { + const listId = c.req.param("listId"); + const body = c.req.valid("json"); + const list = await c.var.api.lists.edit({ ...body, listId }); + return c.json(list, 200); + }, + ) + .delete("/:listId", async (c) => { + const listId = c.req.param("listId"); + await c.var.api.lists.delete({ listId }); + return c.body(null, 204); + }) + .get( + "/:listId/bookmarks", + zValidator("query", zPagination.and(zGetBookmarkQueryParamsSchema)), + async (c) => { + const listId = c.req.param("listId"); + const searchParams = c.req.valid("query"); + const bookmarks = await c.var.api.bookmarks.getBookmarks({ + listId, + ...searchParams, + }); + return c.json(adaptPagination(bookmarks), 200); + }, + ) + .put("/:listId/bookmarks/:bookmarkId", async (c) => { + const listId = c.req.param("listId"); + const bookmarkId = c.req.param("bookmarkId"); + await c.var.api.lists.addToList({ listId, bookmarkId }); + return c.body(null, 204); + }) + .delete("/:listId/bookmarks/:bookmarkId", async (c) => { + const listId = c.req.param("listId"); + const bookmarkId = c.req.param("bookmarkId"); + await c.var.api.lists.removeFromList({ listId, bookmarkId }); + return c.body(null, 204); + }); + +export default app; diff --git a/packages/api/routes/public.ts b/packages/api/routes/public.ts new file mode 100644 index 00000000..d17049c4 --- /dev/null +++ b/packages/api/routes/public.ts @@ -0,0 +1,47 @@ +import { zValidator } from "@hono/zod-validator"; +import { and, eq } from "drizzle-orm"; +import { Hono } from "hono"; +import { z } from "zod"; + +import { assets } from "@karakeep/db/schema"; +import { verifySignedToken } from "@karakeep/shared/signedTokens"; +import { zAssetSignedTokenSchema } from "@karakeep/shared/types/assets"; + +import { unauthedMiddleware } from "../middlewares/auth"; +import { serveAsset } from "../utils/assets"; + +const app = new Hono().get( + "/assets/:assetId", + unauthedMiddleware, + zValidator( + "query", + z.object({ + token: z.string(), + }), + ), + async (c) => { + const assetId = c.req.param("assetId"); + const tokenPayload = verifySignedToken( + c.req.valid("query").token, + zAssetSignedTokenSchema, + ); + if (!tokenPayload) { + return c.json({ error: "Invalid or expired token" }, { status: 403 }); + } + if (tokenPayload.assetId !== assetId) { + return c.json({ error: "Invalid or expired token" }, { status: 403 }); + } + const userId = tokenPayload.userId; + + const assetDb = await c.var.ctx.db.query.assets.findFirst({ + where: and(eq(assets.id, assetId), eq(assets.userId, userId)), + }); + + if (!assetDb) { + return c.json({ error: "Asset not found" }, { status: 404 }); + } + return await serveAsset(c, assetId, userId); + }, +); + +export default app; diff --git a/packages/api/routes/rss.ts b/packages/api/routes/rss.ts new file mode 100644 index 00000000..88b943ad --- /dev/null +++ b/packages/api/routes/rss.ts @@ -0,0 +1,53 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +import serverConfig from "@karakeep/shared/config"; +import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@karakeep/shared/types/bookmarks"; +import { List } from "@karakeep/trpc/models/lists"; + +import { unauthedMiddleware } from "../middlewares/auth"; +import { toRSS } from "../utils/rss"; + +const app = new Hono().get( + "/lists/:listId", + zValidator( + "query", + z.object({ + token: z.string().min(1), + limit: z.coerce + .number() + .min(1) + .max(MAX_NUM_BOOKMARKS_PER_PAGE) + .optional(), + }), + ), + unauthedMiddleware, + async (c) => { + const listId = c.req.param("listId"); + const searchParams = c.req.valid("query"); + const token = searchParams.token; + + const res = await List.getPublicListContents(c.var.ctx, listId, token, { + limit: searchParams.limit ?? 20, + order: "desc", + cursor: null, + }); + const list = res.list; + + const rssFeed = toRSS( + { + title: `Bookmarks from ${list.icon} ${list.name}`, + feedUrl: `${serverConfig.publicApiUrl}/v1/rss/lists/${listId}`, + siteUrl: `${serverConfig.publicUrl}/dashboard/lists/${listId}`, + description: list.description ?? undefined, + }, + res.bookmarks, + ); + + c.header("Content-Type", "application/rss+xml"); + return c.body(rssFeed); + }, +); + +export default app; diff --git a/packages/api/routes/tags.ts b/packages/api/routes/tags.ts new file mode 100644 index 00000000..816e58b4 --- /dev/null +++ b/packages/api/routes/tags.ts @@ -0,0 +1,70 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; + +import { + zCreateTagRequestSchema, + zUpdateTagRequestSchema, +} from "@karakeep/shared/types/tags"; + +import { authMiddleware } from "../middlewares/auth"; +import { adaptPagination, zPagination } from "../utils/pagination"; +import { zGetBookmarkQueryParamsSchema } from "../utils/types"; + +const app = new Hono() + .use(authMiddleware) + + // GET /tags + .get("/", async (c) => { + const tags = await c.var.api.tags.list(); + return c.json(tags, 200); + }) + + // POST /tags + .post("/", zValidator("json", zCreateTagRequestSchema), async (c) => { + const body = c.req.valid("json"); + const tags = await c.var.api.tags.create(body); + return c.json(tags, 201); + }) + + // GET /tags/[tagId] + .get("/:tagId", async (c) => { + const tagId = c.req.param("tagId"); + const tag = await c.var.api.tags.get({ tagId }); + return c.json(tag, 200); + }) + + // PATCH /tags/[tagId] + .patch( + "/:tagId", + zValidator("json", zUpdateTagRequestSchema.omit({ tagId: true })), + async (c) => { + const tagId = c.req.param("tagId"); + const body = c.req.valid("json"); + const tag = await c.var.api.tags.update({ tagId, ...body }); + return c.json(tag, 200); + }, + ) + + // DELETE /tags/[tagId] + .delete("/:tagId", async (c) => { + const tagId = c.req.param("tagId"); + await c.var.api.tags.delete({ tagId }); + return c.body(null, 204); + }) + + // GET /tags/[tagId]/bookmarks + .get( + "/:tagId/bookmarks", + zValidator("query", zPagination.and(zGetBookmarkQueryParamsSchema)), + async (c) => { + const tagId = c.req.param("tagId"); + const searchParams = c.req.valid("query"); + const bookmarks = await c.var.api.bookmarks.getBookmarks({ + tagId, + ...searchParams, + }); + return c.json(adaptPagination(bookmarks), 200); + }, + ); + +export default app; diff --git a/packages/api/routes/users.ts b/packages/api/routes/users.ts new file mode 100644 index 00000000..81177fe3 --- /dev/null +++ b/packages/api/routes/users.ts @@ -0,0 +1,20 @@ +import { Hono } from "hono"; + +import { authMiddleware } from "../middlewares/auth"; + +const app = new Hono() + .use(authMiddleware) + + // GET /users/me + .get("/me", async (c) => { + const user = await c.var.api.users.whoami(); + return c.json(user, 200); + }) + + // GET /users/me/stats + .get("/me/stats", async (c) => { + const stats = await c.var.api.users.stats(); + return c.json(stats, 200); + }); + +export default app; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 00000000..0036ccfa --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@karakeep/tsconfig/node.json", + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules" + ], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + } +} diff --git a/packages/api/utils/assets.ts b/packages/api/utils/assets.ts new file mode 100644 index 00000000..d8a726a6 --- /dev/null +++ b/packages/api/utils/assets.ts @@ -0,0 +1,57 @@ +import { Context } from "hono"; +import { stream } from "hono/streaming"; + +import { + createAssetReadStream, + getAssetSize, + readAssetMetadata, +} from "@karakeep/shared/assetdb"; + +import { toWebReadableStream } from "./upload"; + +export async function serveAsset(c: Context, assetId: string, userId: string) { + const [metadata, size] = await Promise.all([ + readAssetMetadata({ + userId, + assetId, + }), + + getAssetSize({ + userId, + assetId, + }), + ]); + + const range = c.req.header("Range"); + if (range) { + const parts = range.replace(/bytes=/, "").split("-"); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : size - 1; + + const fStream = createAssetReadStream({ + userId, + assetId, + start, + end, + }); + c.status(206); // Partial Content + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + c.header("Accept-Ranges", "bytes"); + c.header("Content-Length", (end - start + 1).toString()); + c.header("Content-type", metadata.contentType); + return stream(c, async (stream) => { + await stream.pipe(toWebReadableStream(fStream)); + }); + } else { + const fStream = createAssetReadStream({ + userId, + assetId, + }); + c.status(200); + c.header("Content-Length", size.toString()); + c.header("Content-type", metadata.contentType); + return stream(c, async (stream) => { + await stream.pipe(toWebReadableStream(fStream)); + }); + } +} diff --git a/packages/api/utils/pagination.ts b/packages/api/utils/pagination.ts new file mode 100644 index 00000000..12a0b950 --- /dev/null +++ b/packages/api/utils/pagination.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@karakeep/shared/types/bookmarks"; +import { zCursorV2 } from "@karakeep/shared/types/pagination"; + +export const zPagination = z.object({ + limit: z.coerce.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(), + cursor: z + .string() + .refine((val) => val.includes("_"), "Must be a valid cursor") + .transform((val) => { + const [id, createdAt] = val.split("_"); + return { id, createdAt }; + }) + .pipe(z.object({ id: z.string(), createdAt: z.coerce.date() })) + .optional(), +}); + +export function adaptPagination< + T extends { nextCursor: z.infer<typeof zCursorV2> | null }, +>(input: T) { + const { nextCursor, ...rest } = input; + if (!nextCursor) { + return input; + } + return { + ...rest, + nextCursor: `${nextCursor.id}_${nextCursor.createdAt.toISOString()}`, + }; +} diff --git a/packages/api/utils/rss.ts b/packages/api/utils/rss.ts new file mode 100644 index 00000000..079b3f5a --- /dev/null +++ b/packages/api/utils/rss.ts @@ -0,0 +1,54 @@ +import RSS from "rss"; + +import serverConfig from "@karakeep/shared/config"; +import { + BookmarkTypes, + ZPublicBookmark, +} from "@karakeep/shared/types/bookmarks"; +import { getAssetUrl } from "@karakeep/shared/utils/assetUtils"; + +export function toRSS( + params: { + title: string; + description?: string; + feedUrl: string; + siteUrl: string; + }, + bookmarks: ZPublicBookmark[], +) { + const feed = new RSS({ + title: params.title, + feed_url: params.feedUrl, + site_url: params.siteUrl, + description: params.description, + generator: "Karakeep", + }); + + bookmarks + .filter( + (b) => + b.content.type === BookmarkTypes.LINK || + b.content.type === BookmarkTypes.ASSET, + ) + .forEach((bookmark) => { + feed.item({ + date: bookmark.createdAt, + title: bookmark.title ?? "", + url: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.url + : bookmark.content.type === BookmarkTypes.ASSET + ? `${serverConfig.publicUrl}${getAssetUrl(bookmark.content.assetId)}` + : "", + guid: bookmark.id, + author: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.author ?? undefined) + : undefined, + categories: bookmark.tags, + description: bookmark.description ?? "", + }); + }); + + return feed.xml({ indent: true }); +} diff --git a/packages/api/utils/types.ts b/packages/api/utils/types.ts new file mode 100644 index 00000000..bdaf815f --- /dev/null +++ b/packages/api/utils/types.ts @@ -0,0 +1,28 @@ +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 zIncludeContentSearchParamsSchema = z.object({ + // TODO: Change the default to false in a couple of releases. + includeContent: zStringBool.optional().default("true"), +}); + +export const zGetBookmarkQueryParamsSchema = z + .object({ + sortOrder: zSortOrder + .exclude([zSortOrder.Enum.relevance]) + .optional() + .default(zSortOrder.Enum.desc), + }) + .merge(zIncludeContentSearchParamsSchema); + +export const zGetBookmarkSearchParamsSchema = z + .object({ + sortOrder: zSortOrder.optional().default(zSortOrder.Enum.relevance), + }) + .merge(zIncludeContentSearchParamsSchema); diff --git a/packages/api/utils/upload.ts b/packages/api/utils/upload.ts new file mode 100644 index 00000000..d96a0f60 --- /dev/null +++ b/packages/api/utils/upload.ts @@ -0,0 +1,110 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; + +import { assets, AssetTypes } from "@karakeep/db/schema"; +import { + newAssetId, + saveAssetFromFile, + SUPPORTED_UPLOAD_ASSET_TYPES, +} from "@karakeep/shared/assetdb"; +import serverConfig from "@karakeep/shared/config"; +import { AuthedContext } from "@karakeep/trpc"; + +const MAX_UPLOAD_SIZE_BYTES = serverConfig.maxAssetSizeMb * 1024 * 1024; + +// Helper to convert Web Stream to Node Stream (requires Node >= 16.5 / 14.18) +export function webStreamToNode( + webStream: ReadableStream<Uint8Array>, +): Readable { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return Readable.fromWeb(webStream as any); // Type assertion might be needed +} + +export function toWebReadableStream( + nodeStream: fs.ReadStream, +): ReadableStream<Uint8Array> { + const reader = nodeStream as unknown as Readable; + + return new ReadableStream({ + start(controller) { + reader.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk))); + reader.on("end", () => controller.close()); + reader.on("error", (err) => controller.error(err)); + }, + }); +} + +export async function uploadAsset( + user: AuthedContext["user"], + db: AuthedContext["db"], + formData: { file: File } | { image: File }, +): Promise< + | { error: string; status: 400 | 413 } + | { + assetId: string; + contentType: string; + fileName: string; + size: number; + } +> { + let data: File; + if ("file" in formData) { + data = formData.file; + } else { + data = formData.image; + } + + const contentType = data.type; + const fileName = data.name; + if (!SUPPORTED_UPLOAD_ASSET_TYPES.has(contentType)) { + return { error: "Unsupported asset type", status: 400 }; + } + if (data.size > MAX_UPLOAD_SIZE_BYTES) { + return { error: "Asset is too big", status: 413 }; + } + + let tempFilePath: string | undefined; + + try { + tempFilePath = path.join(os.tmpdir(), `karakeep-upload-${Date.now()}`); + await pipeline( + webStreamToNode(data.stream()), + fs.createWriteStream(tempFilePath), + ); + const [assetDb] = await db + .insert(assets) + .values({ + id: newAssetId(), + // Initially, uploads are uploaded for unknown purpose + // And without an attached bookmark. + assetType: AssetTypes.UNKNOWN, + bookmarkId: null, + userId: user.id, + contentType, + size: data.size, + fileName, + }) + .returning(); + + await saveAssetFromFile({ + userId: user.id, + assetId: assetDb.id, + assetPath: tempFilePath, + metadata: { contentType, fileName }, + }); + + return { + assetId: assetDb.id, + contentType, + size: data.size, + fileName, + }; + } finally { + if (tempFilePath) { + await fs.promises.unlink(tempFilePath).catch(() => ({})); + } + } +} diff --git a/packages/db/drizzle/0046_add_rss_feed_enabled_col.sql b/packages/db/drizzle/0046_add_rss_feed_enabled_col.sql new file mode 100644 index 00000000..3352b5b0 --- /dev/null +++ b/packages/db/drizzle/0046_add_rss_feed_enabled_col.sql @@ -0,0 +1 @@ +ALTER TABLE `rssFeeds` ADD `enabled` integer DEFAULT true NOT NULL;
\ No newline at end of file diff --git a/packages/db/drizzle/0047_add_summarization_status.sql b/packages/db/drizzle/0047_add_summarization_status.sql new file mode 100644 index 00000000..6e8b40d0 --- /dev/null +++ b/packages/db/drizzle/0047_add_summarization_status.sql @@ -0,0 +1 @@ +ALTER TABLE `bookmarks` ADD `summarizationStatus` text DEFAULT 'pending';
\ No newline at end of file diff --git a/packages/db/drizzle/0048_add_user_settings.sql b/packages/db/drizzle/0048_add_user_settings.sql new file mode 100644 index 00000000..8d55c1e1 --- /dev/null +++ b/packages/db/drizzle/0048_add_user_settings.sql @@ -0,0 +1,6 @@ +CREATE TABLE `userSettings` ( + `userId` text PRIMARY KEY NOT NULL, + `bookmarkClickAction` text DEFAULT 'open_original_link' NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +);--> statement-breakpoint +INSERT INTO `userSettings` (`userId`, `bookmarkClickAction`) SELECT `id`, 'open_original_link' FROM `user`; diff --git a/packages/db/drizzle/0049_add_rss_token.sql b/packages/db/drizzle/0049_add_rss_token.sql new file mode 100644 index 00000000..c256f859 --- /dev/null +++ b/packages/db/drizzle/0049_add_rss_token.sql @@ -0,0 +1 @@ +ALTER TABLE `bookmarkLists` ADD `rssToken` text;
\ No newline at end of file diff --git a/packages/db/drizzle/0050_add_user_settings_archive_display_behaviour.sql b/packages/db/drizzle/0050_add_user_settings_archive_display_behaviour.sql new file mode 100644 index 00000000..6d9936b6 --- /dev/null +++ b/packages/db/drizzle/0050_add_user_settings_archive_display_behaviour.sql @@ -0,0 +1 @@ +ALTER TABLE `userSettings` ADD `archiveDisplayBehaviour` text DEFAULT 'show' NOT NULL; diff --git a/packages/db/drizzle/0051_public_lists.sql b/packages/db/drizzle/0051_public_lists.sql new file mode 100644 index 00000000..6f9714e4 --- /dev/null +++ b/packages/db/drizzle/0051_public_lists.sql @@ -0,0 +1 @@ +ALTER TABLE `bookmarkLists` ADD `public` integer DEFAULT false NOT NULL;
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0046_snapshot.json b/packages/db/drizzle/meta/0046_snapshot.json new file mode 100644 index 00000000..7015f5db --- /dev/null +++ b/packages/db/drizzle/meta/0046_snapshot.json @@ -0,0 +1,1959 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7a950001-03e7-43f2-8ef5-272dfe82d223", + "prevId": "dce61b2b-d896-425a-a9cd-6a79f596d7b2", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0047_snapshot.json b/packages/db/drizzle/meta/0047_snapshot.json new file mode 100644 index 00000000..f6908043 --- /dev/null +++ b/packages/db/drizzle/meta/0047_snapshot.json @@ -0,0 +1,1967 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "cd8ff1a8-c7bb-4576-9bec-2644785c894e", + "prevId": "7a950001-03e7-43f2-8ef5-272dfe82d223", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summarizationStatus": { + "name": "summarizationStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0048_snapshot.json b/packages/db/drizzle/meta/0048_snapshot.json new file mode 100644 index 00000000..ec098d85 --- /dev/null +++ b/packages/db/drizzle/meta/0048_snapshot.json @@ -0,0 +1,2006 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d3ffc2ee-399e-4652-813d-652b56d649f6", + "prevId": "cd8ff1a8-c7bb-4576-9bec-2644785c894e", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summarizationStatus": { + "name": "summarizationStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkClickAction": { + "name": "bookmarkClickAction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open_original_link'" + } + }, + "indexes": {}, + "foreignKeys": { + "userSettings_userId_user_id_fk": { + "name": "userSettings_userId_user_id_fk", + "tableFrom": "userSettings", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0049_snapshot.json b/packages/db/drizzle/meta/0049_snapshot.json new file mode 100644 index 00000000..4dc9f149 --- /dev/null +++ b/packages/db/drizzle/meta/0049_snapshot.json @@ -0,0 +1,2013 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "98a6c743-e257-4126-a95f-40850e03ee4e", + "prevId": "d3ffc2ee-399e-4652-813d-652b56d649f6", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rssToken": { + "name": "rssToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summarizationStatus": { + "name": "summarizationStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkClickAction": { + "name": "bookmarkClickAction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open_original_link'" + } + }, + "indexes": {}, + "foreignKeys": { + "userSettings_userId_user_id_fk": { + "name": "userSettings_userId_user_id_fk", + "tableFrom": "userSettings", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0050_snapshot.json b/packages/db/drizzle/meta/0050_snapshot.json new file mode 100644 index 00000000..860b0f54 --- /dev/null +++ b/packages/db/drizzle/meta/0050_snapshot.json @@ -0,0 +1,2021 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a92cdc19-e420-4f05-bfac-9fbbb2f5b8a3", + "prevId": "98a6c743-e257-4126-a95f-40850e03ee4e", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rssToken": { + "name": "rssToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summarizationStatus": { + "name": "summarizationStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkClickAction": { + "name": "bookmarkClickAction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open_original_link'" + }, + "archiveDisplayBehaviour": { + "name": "archiveDisplayBehaviour", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'show'" + } + }, + "indexes": {}, + "foreignKeys": { + "userSettings_userId_user_id_fk": { + "name": "userSettings_userId_user_id_fk", + "tableFrom": "userSettings", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0051_snapshot.json b/packages/db/drizzle/meta/0051_snapshot.json new file mode 100644 index 00000000..6db03ecf --- /dev/null +++ b/packages/db/drizzle/meta/0051_snapshot.json @@ -0,0 +1,2029 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5a549719-8f7d-49ff-91cc-5ad0f3b5c4ef", + "prevId": "a92cdc19-e420-4f05-bfac-9fbbb2f5b8a3", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + }, + "assets_userId_idx": { + "name": "assets_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_userId_user_id_fk": { + "name": "assets_userId_user_id_fk", + "tableFrom": "assets", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublished": { + "name": "datePublished", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModified": { + "name": "dateModified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "crawlStatusCode": { + "name": "crawlStatusCode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 200 + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rssToken": { + "name": "rssToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_userId_id_idx": { + "name": "bookmarkLists_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + }, + "bookmarkTags_userId_id_idx": { + "name": "bookmarkTags_userId_id_idx", + "columns": [ + "userId", + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifiedAt": { + "name": "modifiedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summarizationStatus": { + "name": "summarizationStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customPrompts": { + "name": "customPrompts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "appliesTo": { + "name": "appliesTo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "customPrompts_userId_idx": { + "name": "customPrompts_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "customPrompts_userId_user_id_fk": { + "name": "customPrompts_userId_user_id_fk", + "tableFrom": "customPrompts", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "highlights": { + "name": "highlights", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'yellow'" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "highlights_bookmarkId_idx": { + "name": "highlights_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "highlights_userId_idx": { + "name": "highlights_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "highlights_bookmarkId_bookmarks_id_fk": { + "name": "highlights_bookmarkId_bookmarks_id_fk", + "tableFrom": "highlights", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "highlights_userId_user_id_fk": { + "name": "highlights_userId_user_id_fk", + "tableFrom": "highlights", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeedImports": { + "name": "rssFeedImports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rssFeedId": { + "name": "rssFeedId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rssFeedImports_feedIdIdx_idx": { + "name": "rssFeedImports_feedIdIdx_idx", + "columns": [ + "rssFeedId" + ], + "isUnique": false + }, + "rssFeedImports_entryIdIdx_idx": { + "name": "rssFeedImports_entryIdIdx_idx", + "columns": [ + "entryId" + ], + "isUnique": false + }, + "rssFeedImports_rssFeedId_entryId_unique": { + "name": "rssFeedImports_rssFeedId_entryId_unique", + "columns": [ + "rssFeedId", + "entryId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rssFeedImports_rssFeedId_rssFeeds_id_fk": { + "name": "rssFeedImports_rssFeedId_rssFeeds_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "rssFeeds", + "columnsFrom": [ + "rssFeedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rssFeedImports_bookmarkId_bookmarks_id_fk": { + "name": "rssFeedImports_bookmarkId_bookmarks_id_fk", + "tableFrom": "rssFeedImports", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rssFeeds": { + "name": "rssFeeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetchedAt": { + "name": "lastFetchedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastFetchedStatus": { + "name": "lastFetchedStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rssFeeds_userId_idx": { + "name": "rssFeeds_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "rssFeeds_userId_user_id_fk": { + "name": "rssFeeds_userId_user_id_fk", + "tableFrom": "rssFeeds", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineActions": { + "name": "ruleEngineActions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngineActions_userId_idx": { + "name": "ruleEngineActions_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "ruleEngineActions_ruleId_idx": { + "name": "ruleEngineActions_ruleId_idx", + "columns": [ + "ruleId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineActions_userId_user_id_fk": { + "name": "ruleEngineActions_userId_user_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_ruleId_ruleEngineRules_id_fk": { + "name": "ruleEngineActions_ruleId_ruleEngineRules_id_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "ruleEngineRules", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_tagId_fk": { + "name": "ruleEngineActions_userId_tagId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineActions_userId_listId_fk": { + "name": "ruleEngineActions_userId_listId_fk", + "tableFrom": "ruleEngineActions", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ruleEngineRules": { + "name": "ruleEngineRules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "ruleEngine_userId_idx": { + "name": "ruleEngine_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ruleEngineRules_userId_user_id_fk": { + "name": "ruleEngineRules_userId_user_id_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_tagId_fk": { + "name": "ruleEngineRules_userId_tagId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "userId", + "tagId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ruleEngineRules_userId_listId_fk": { + "name": "ruleEngineRules_userId_listId_fk", + "tableFrom": "ruleEngineRules", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "userId", + "listId" + ], + "columnsTo": [ + "userId", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "tagId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bookmarkClickAction": { + "name": "bookmarkClickAction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open_original_link'" + }, + "archiveDisplayBehaviour": { + "name": "archiveDisplayBehaviour", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'show'" + } + }, + "indexes": {}, + "foreignKeys": { + "userSettings_userId_user_id_fk": { + "name": "userSettings_userId_user_id_fk", + "tableFrom": "userSettings", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhooks": { + "name": "webhooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhooks_userId_idx": { + "name": "webhooks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webhooks_userId_user_id_fk": { + "name": "webhooks_userId_user_id_fk", + "tableFrom": "webhooks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 8281b126..765eba59 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -323,6 +323,48 @@ "when": 1745705657846, "tag": "0045_add_rule_engine", "breakpoints": true + }, + { + "idx": 46, + "version": "6", + "when": 1746902541511, + "tag": "0046_add_rss_feed_enabled_col", + "breakpoints": true + }, + { + "idx": 47, + "version": "6", + "when": 1747598543992, + "tag": "0047_add_summarization_status", + "breakpoints": true + }, + { + "idx": 48, + "version": "6", + "when": 1748086734370, + "tag": "0048_add_user_settings", + "breakpoints": true + }, + { + "idx": 49, + "version": "6", + "when": 1748699971545, + "tag": "0049_add_rss_token", + "breakpoints": true + }, + { + "idx": 50, + "version": "6", + "when": 1748795265779, + "tag": "0050_add_user_settings_archive_display_behaviour", + "breakpoints": true + }, + { + "idx": 51, + "version": "6", + "when": 1748804695561, + "tag": "0051_public_lists", + "breakpoints": true } ] }
\ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json index 74f4f84f..e7819457 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -7,6 +7,7 @@ "scripts": { "typecheck": "tsc --noEmit", "migrate": "tsx migrate.ts", + "generate": "drizzle-kit generate", "studio": "drizzle-kit studio" }, "dependencies": { diff --git a/packages/db/schema.ts b/packages/db/schema.ts index bedcf9ad..e79bd2c9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -123,6 +123,9 @@ export const bookmarks = sqliteTable( taggingStatus: text("taggingStatus", { enum: ["pending", "failure", "success"], }).default("pending"), + summarizationStatus: text("summarizationStatus", { + enum: ["pending", "failure", "success"], + }).default("pending"), summary: text("summary"), note: text("note"), type: text("type", { @@ -333,6 +336,9 @@ export const bookmarkLists = sqliteTable( (): AnySQLiteColumn => bookmarkLists.id, { onDelete: "set null" }, ), + // Whoever have access to this token can read the content of this list + rssToken: text("rssToken"), + public: integer("public", { mode: "boolean" }).notNull().default(false), }, (bl) => [ index("bookmarkLists_userId_idx").on(bl.userId), @@ -389,6 +395,7 @@ export const rssFeedsTable = sqliteTable( .$defaultFn(() => createId()), name: text("name").notNull(), url: text("url").notNull(), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), createdAt: createdAtField(), lastFetchedAt: integer("lastFetchedAt", { mode: "timestamp" }), lastFetchedStatus: text("lastFetchedStatus", { @@ -415,7 +422,7 @@ export const webhooksTable = sqliteTable( .references(() => users.id, { onDelete: "cascade" }), events: text("events", { mode: "json" }) .notNull() - .$type<("created" | "edited" | "crawled" | "ai tagged")[]>(), + .$type<("created" | "edited" | "crawled" | "ai tagged" | "deleted")[]>(), token: text("token"), }, (bl) => [index("webhooks_userId_idx").on(bl.userId)], @@ -523,13 +530,34 @@ export const ruleEngineActionsTable = sqliteTable( ], ); +export const userSettings = sqliteTable("userSettings", { + userId: text("userId") + .notNull() + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + bookmarkClickAction: text("bookmarkClickAction", { + enum: ["open_original_link", "expand_bookmark_preview"], + }) + .notNull() + .default("open_original_link"), + archiveDisplayBehaviour: text("archiveDisplayBehaviour", { + enum: ["show", "hide"], + }) + .notNull() + .default("show"), +}); + // Relations -export const userRelations = relations(users, ({ many }) => ({ +export const userRelations = relations(users, ({ many, one }) => ({ tags: many(bookmarkTags), bookmarks: many(bookmarks), webhooks: many(webhooksTable), rules: many(ruleEngineRulesTable), + settings: one(userSettings, { + fields: [users.id], + references: [userSettings.userId], + }), })); export const bookmarkRelations = relations(bookmarks, ({ many, one }) => ({ @@ -664,3 +692,10 @@ export const rssFeedImportsTableRelations = relations( }), }), ); + +export const userSettingsRelations = relations(userSettings, ({ one }) => ({ + user: one(users, { + fields: [userSettings.userId], + references: [users.id], + }), +})); diff --git a/packages/e2e_tests/docker-compose.yml b/packages/e2e_tests/docker-compose.yml index 201db154..e1fe46bb 100644 --- a/packages/e2e_tests/docker-compose.yml +++ b/packages/e2e_tests/docker-compose.yml @@ -10,6 +10,7 @@ services: environment: DATA_DIR: /tmp NEXTAUTH_SECRET: secret + NEXTAUTH_URL: http://localhost:${KARAKEEP_PORT:-3000} MEILI_MASTER_KEY: dummy MEILI_ADDR: http://meilisearch:7700 BROWSER_WEB_URL: http://chrome:9222 diff --git a/packages/e2e_tests/tests/api/assets.test.ts b/packages/e2e_tests/tests/api/assets.test.ts index 5c294929..78a5c7fe 100644 --- a/packages/e2e_tests/tests/api/assets.test.ts +++ b/packages/e2e_tests/tests/api/assets.test.ts @@ -39,7 +39,7 @@ describe("Assets API", () => { // Retrieve the asset const resp = await fetch( - `http://localhost:${port}/api/assets/${uploadResponse.assetId}`, + `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`, { headers: { authorization: `Bearer ${apiKey}`, @@ -123,7 +123,7 @@ describe("Assets API", () => { // Verify asset is deleted const assetResponse = await fetch( - `http://localhost:${port}/api/assets/${uploadResponse.assetId}`, + `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`, { headers: { authorization: `Bearer ${apiKey}`, diff --git a/packages/e2e_tests/tests/api/bookmarks.test.ts b/packages/e2e_tests/tests/api/bookmarks.test.ts index 6c56f689..d40c1add 100644 --- a/packages/e2e_tests/tests/api/bookmarks.test.ts +++ b/packages/e2e_tests/tests/api/bookmarks.test.ts @@ -397,54 +397,258 @@ describe("Bookmarks API", () => { expect(finalPage!.nextCursor).toBeNull(); }); - it("should support precrawling via singlefile", async () => { - const file = new File(["<html>HELLO WORLD</html>"], "test.html", { - type: "text/html", - }); + describe("singlefile", () => { + async function uploadSinglefileAsset(ifexists?: string) { + const file = new File(["<html>HELLO WORLD</html>"], "test.html", { + type: "text/html", + }); - const formData = new FormData(); - formData.append("url", "https://example.com"); - formData.append("file", file); + const formData = new FormData(); + formData.append("url", "https://example.com"); + formData.append("file", file); - // OpenAPI typescript doesn't support multipart/form-data - // Upload the singlefile archive - const response = await fetch( - `http://localhost:${port}/api/v1/bookmarks/singlefile`, - { + const url = new URL( + `http://localhost:${port}/api/v1/bookmarks/singlefile`, + ); + if (ifexists) { + url.searchParams.append("ifexists", ifexists); + } + + const response = await fetch(url.toString(), { method: "POST", headers: { authorization: `Bearer ${apiKey}`, }, body: formData, - }, - ); + }); - if (!response.ok) { - throw new Error(`Failed to upload asset: ${response.statusText}`); + if (!response.ok) { + return [null, response] as const; + } + + const data = (await response.json()) as { id: string }; + return [data, response] as const; } - expect(response.status).toBe(201); + it("should support precrawling via singlefile with ifexists=skip", async () => { + // First upload: create a bookmark + const [data, response] = await uploadSinglefileAsset(); + expect(response?.status).toBe(201); + const bookmarkId = data?.id; + if (!bookmarkId) throw new Error("Bookmark ID not found"); - const { id: bookmarkId } = (await response.json()) as { - id: string; - }; + // Get the bookmark and record the precrawled asset id + const { data: bookmark, response: getResponse1 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse1.status).toBe(200); + const assetIds = bookmark!.assets + .filter((a) => a.assetType === "precrawledArchive") + .map((a) => a.id); + expect(assetIds.length).toBe(1); + const firstAssetId = assetIds[0]; - // Get the created bookmark - const { data: retrievedBookmark, response: getResponse } = await client.GET( - "/bookmarks/{bookmarkId}", - { - params: { - path: { - bookmarkId: bookmarkId, - }, + // Second upload with skip + const [data2, response2] = await uploadSinglefileAsset("skip"); + expect(response2?.status).toBe(200); + expect(data2?.id).toBe(bookmarkId); + + // Get the bookmark again + const { data: bookmark2, response: getResponse2 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, }, - }, - ); + ); + expect(getResponse2.status).toBe(200); + const assetIds2 = bookmark2!.assets + .filter((a) => a.assetType === "precrawledArchive") + .map((a) => a.id); + expect(assetIds2).toEqual([firstAssetId]); // same asset + }); - expect(getResponse.status).toBe(200); - assert(retrievedBookmark!.content.type === "link"); - expect(retrievedBookmark!.assets.map((a) => a.assetType)).toContain( - "precrawledArchive", - ); + it("should support precrawling via singlefile with ifexists=overwrite", async () => { + // First upload + const [data, response] = await uploadSinglefileAsset("overwrite"); + expect(response?.status).toBe(201); + const bookmarkId = data?.id; + if (!bookmarkId) throw new Error("Bookmark ID not found"); + + // Record the asset + const { data: bookmark, response: getResponse1 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse1.status).toBe(200); + const firstAssetId = bookmark!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(firstAssetId).toBeDefined(); + + // Second upload with overwrite + const [data2, response2] = await uploadSinglefileAsset("overwrite"); + expect(response2?.status).toBe(200); + expect(data2?.id).toBe(bookmarkId); + + // Get the bookmark again + const { data: bookmark2, response: getResponse2 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse2.status).toBe(200); + const secondAssetId = bookmark2!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(secondAssetId).toBeDefined(); + expect(secondAssetId).not.toBe(firstAssetId); + // There should be only one precrawledArchive asset + const precrawledAssets = bookmark2!.assets.filter( + (a) => a.assetType === "precrawledArchive", + ); + expect(precrawledAssets.length).toBe(1); + }); + + it("should support precrawling via singlefile with ifexists=overwrite-recrawl", async () => { + // First upload + const [data, response] = await uploadSinglefileAsset("overwrite-recrawl"); + expect(response?.status).toBe(201); + const bookmarkId = data?.id; + if (!bookmarkId) throw new Error("Bookmark ID not found"); + + // Record the asset + const { data: bookmark, response: getResponse1 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse1.status).toBe(200); + const firstAssetId = bookmark!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(firstAssetId).toBeDefined(); + + // Second upload with overwrite-recrawl + const [data2, response2] = + await uploadSinglefileAsset("overwrite-recrawl"); + expect(response2?.status).toBe(200); + expect(data2?.id).toBe(bookmarkId); + + // Get the bookmark again + const { data: bookmark2, response: getResponse2 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse2.status).toBe(200); + const secondAssetId = bookmark2!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(secondAssetId).toBeDefined(); + expect(secondAssetId).not.toBe(firstAssetId); + // There should be only one precrawledArchive asset + const precrawledAssets = bookmark2!.assets.filter( + (a) => a.assetType === "precrawledArchive", + ); + expect(precrawledAssets.length).toBe(1); + }); + + it("should support precrawling via singlefile with ifexists=append", async () => { + // First upload + const [data, response] = await uploadSinglefileAsset("append"); + expect(response?.status).toBe(201); + const bookmarkId = data?.id; + if (!bookmarkId) throw new Error("Bookmark ID not found"); + + // Record the first asset + const { data: bookmark, response: getResponse1 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse1.status).toBe(200); + const firstAssetId = bookmark!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(firstAssetId).toBeDefined(); + + // Second upload with append + const [data2, response2] = await uploadSinglefileAsset("append"); + expect(response2?.status).toBe(200); + expect(data2?.id).toBe(bookmarkId); + + // Get the bookmark again + const { data: bookmark2, response: getResponse2 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse2.status).toBe(200); + const precrawledAssets = bookmark2!.assets.filter( + (a) => a.assetType === "precrawledArchive", + ); + expect(precrawledAssets.length).toBe(2); + expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId); + // The second asset id should be different + const secondAssetId = precrawledAssets.find( + (asset) => asset.id !== firstAssetId, + )?.id; + expect(secondAssetId).toBeDefined(); + }); + + it("should support precrawling via singlefile with ifexists=append-recrawl", async () => { + // First upload + const [data, response] = await uploadSinglefileAsset("append-recrawl"); + expect(response?.status).toBe(201); + const bookmarkId = data?.id; + if (!bookmarkId) throw new Error("Bookmark ID not found"); + + // Record the first asset + const { data: bookmark, response: getResponse1 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse1.status).toBe(200); + const firstAssetId = bookmark!.assets.find( + (a) => a.assetType === "precrawledArchive", + )?.id; + expect(firstAssetId).toBeDefined(); + + // Second upload with append-recrawl + const [data2, response2] = await uploadSinglefileAsset("append-recrawl"); + expect(response2?.status).toBe(200); + expect(data2?.id).toBe(bookmarkId); + + // Get the bookmark again + const { data: bookmark2, response: getResponse2 } = await client.GET( + "/bookmarks/{bookmarkId}", + { + params: { path: { bookmarkId } }, + }, + ); + expect(getResponse2.status).toBe(200); + const precrawledAssets = bookmark2!.assets.filter( + (a) => a.assetType === "precrawledArchive", + ); + expect(precrawledAssets.length).toBe(2); + expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId); + // The second asset id should be different + const secondAssetId = precrawledAssets.find( + (asset) => asset.id !== firstAssetId, + )?.id; + expect(secondAssetId).toBeDefined(); + }); }); }); diff --git a/packages/e2e_tests/tests/api/public.test.ts b/packages/e2e_tests/tests/api/public.test.ts new file mode 100644 index 00000000..54ef79ea --- /dev/null +++ b/packages/e2e_tests/tests/api/public.test.ts @@ -0,0 +1,322 @@ +import { assert, beforeEach, describe, expect, inject, it } from "vitest"; +import { z } from "zod"; + +import { createSignedToken } from "../../../shared/signedTokens"; +import { zAssetSignedTokenSchema } from "../../../shared/types/assets"; +import { BookmarkTypes } from "../../../shared/types/bookmarks"; +import { createTestUser, uploadTestAsset } from "../../utils/api"; +import { waitUntil } from "../../utils/general"; +import { getTrpcClient } from "../../utils/trpc"; + +describe("Public API", () => { + const port = inject("karakeepPort"); + + if (!port) { + throw new Error("Missing required environment variables"); + } + + let apiKey: string; // For the primary test user + + async function seedDatabase(currentApiKey: string) { + const trpcClient = getTrpcClient(currentApiKey); + + // Create two lists + const publicList = await trpcClient.lists.create.mutate({ + name: "Public List", + icon: "🚀", + type: "manual", + }); + + await trpcClient.lists.edit.mutate({ + listId: publicList.id, + public: true, + }); + + // Create two bookmarks + const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({ + title: "Test Bookmark #1", + url: "http://nginx:80/hello.html", + type: BookmarkTypes.LINK, + }); + + // Create a second bookmark with an asset + const file = new File(["test content"], "test.pdf", { + type: "application/pdf", + }); + + const uploadResponse = await uploadTestAsset(currentApiKey, port, file); + const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({ + title: "Test Bookmark #2", + type: BookmarkTypes.ASSET, + assetType: "pdf", + assetId: uploadResponse.assetId, + }); + + await trpcClient.lists.addToList.mutate({ + listId: publicList.id, + bookmarkId: createBookmark1.id, + }); + await trpcClient.lists.addToList.mutate({ + listId: publicList.id, + bookmarkId: createBookmark2.id, + }); + + return { publicList, createBookmark1, createBookmark2 }; + } + + beforeEach(async () => { + apiKey = await createTestUser(); + }); + + it("should get public bookmarks", async () => { + const { publicList } = await seedDatabase(apiKey); + const trpcClient = getTrpcClient(apiKey); + + const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query( + { + listId: publicList.id, + }, + ); + + expect(res.bookmarks.length).toBe(2); + }); + + it("should be able to access the assets of the public bookmarks", async () => { + const { publicList, createBookmark1, createBookmark2 } = + await seedDatabase(apiKey); + + const trpcClient = getTrpcClient(apiKey); + // Wait for link bookmark to be crawled and have a banner image (screenshot) + await waitUntil( + async () => { + const res = await trpcClient.bookmarks.getBookmark.query({ + bookmarkId: createBookmark1.id, + }); + assert(res.content.type === BookmarkTypes.LINK); + // Check for screenshotAssetId as bannerImageUrl might be derived from it or original imageUrl + return !!res.content.screenshotAssetId || !!res.content.imageUrl; + }, + "Bookmark is crawled and has banner info", + 20000, // Increased timeout as crawling can take time + ); + + const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query( + { + listId: publicList.id, + }, + ); + + const b1Resp = res.bookmarks.find((b) => b.id === createBookmark1.id); + expect(b1Resp).toBeDefined(); + const b2Resp = res.bookmarks.find((b) => b.id === createBookmark2.id); + expect(b2Resp).toBeDefined(); + + assert(b1Resp!.content.type === BookmarkTypes.LINK); + assert(b2Resp!.content.type === BookmarkTypes.ASSET); + + { + // Banner image fetch for link bookmark + assert( + b1Resp!.bannerImageUrl, + "Link bookmark should have a bannerImageUrl", + ); + const assetFetch = await fetch(b1Resp!.bannerImageUrl); + expect(assetFetch.status).toBe(200); + } + + { + // Actual asset fetch for asset bookmark + assert( + b2Resp!.content.assetUrl, + "Asset bookmark should have an assetUrl", + ); + const assetFetch = await fetch(b2Resp!.content.assetUrl); + expect(assetFetch.status).toBe(200); + } + }); + + it("Accessing non public list should fail", async () => { + const trpcClient = getTrpcClient(apiKey); + const nonPublicList = await trpcClient.lists.create.mutate({ + name: "Non Public List", + icon: "🚀", + type: "manual", + }); + + await expect( + trpcClient.publicBookmarks.getPublicBookmarksInList.query({ + listId: nonPublicList.id, + }), + ).rejects.toThrow(/List not found/); + }); + + describe("Public asset token validation", () => { + let userId: string; + let assetId: string; // Asset belonging to the primary user (userId) + + beforeEach(async () => { + const trpcClient = getTrpcClient(apiKey); + const whoami = await trpcClient.users.whoami.query(); + userId = whoami.id; + const assetUpload = await uploadTestAsset( + apiKey, + port, + new File(["test content for token validation"], "token_test.pdf", { + type: "application/pdf", + }), + ); + assetId = assetUpload.assetId; + }); + + it("should succeed with a valid token", async () => { + const token = createSignedToken( + { + assetId, + userId, + } as z.infer<typeof zAssetSignedTokenSchema>, + Date.now() + 60000, // Expires in 60 seconds + ); + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`, + ); + expect(res.status).toBe(200); + expect((await res.blob()).type).toBe("application/pdf"); + }); + + it("should fail without a token", async () => { + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}`, + ); + expect(res.status).toBe(400); // Bad Request due to missing token query param + }); + + it("should fail with a malformed token string (e.g., not base64)", async () => { + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=thisIsNotValidBase64!@#`, + ); + expect(res.status).toBe(403); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Invalid or expired token" }), + ); + }); + + it("should fail with a token having a structurally invalid inner payload", async () => { + // Payload that doesn't conform to zAssetSignedTokenSchema (e.g. misspelled key) + const malformedInnerPayload = { + asset_id_mispelled: assetId, + userId: userId, + }; + const token = createSignedToken( + malformedInnerPayload, + Date.now() + 60000, + ); + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`, + ); + expect(res.status).toBe(403); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Invalid or expired token" }), + ); + }); + + it("should fail after token expiry", async () => { + const token = createSignedToken( + { + assetId, + userId, + } as z.infer<typeof zAssetSignedTokenSchema>, + Date.now() + 1000, // Expires in 1 second + ); + + // Wait for more than 1 second to ensure expiry + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`, + ); + expect(res.status).toBe(403); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Invalid or expired token" }), + ); + }); + + it("should fail when using a valid token for a different asset", async () => { + const anotherAssetUpload = await uploadTestAsset( + apiKey, // Same user + port, + new File(["other content"], "other_asset.pdf", { + type: "application/pdf", + }), + ); + const anotherAssetId = anotherAssetUpload.assetId; + + // Token is valid for 'anotherAssetId' + const tokenForAnotherAsset = createSignedToken( + { + assetId: anotherAssetId, + userId, + } as z.infer<typeof zAssetSignedTokenSchema>, + Date.now() + 60000, + ); + + // Attempt to use this token to access the original 'assetId' + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForAnotherAsset}`, + ); + expect(res.status).toBe(403); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Invalid or expired token" }), + ); + }); + + it("should fail if token's userId does not own the requested assetId (expect 404)", async () => { + // User1 (primary, `apiKey`, `userId`) owns `assetId` (from beforeEach) + + // Create User2 - ensure unique email for user creation + const apiKeyUser2 = await createTestUser(); + const trpcClientUser2 = getTrpcClient(apiKeyUser2); + const whoamiUser2 = await trpcClientUser2.users.whoami.query(); + const userIdUser2 = whoamiUser2.id; + + // Generate a token where the payload claims assetId is being accessed by userIdUser2, + // but assetId actually belongs to the original userId. + const tokenForUser2AttemptingAsset1 = createSignedToken( + { + assetId: assetId, // assetId belongs to user1 (userId) + userId: userIdUser2, // token claims user2 is accessing it + } as z.infer<typeof zAssetSignedTokenSchema>, + Date.now() + 60000, + ); + + // User2 attempts to access assetId (owned by User1) using a token that has User2's ID in its payload. + // The API route will use userIdUser2 from the token to query the DB for assetId. + // Since assetId is not owned by userIdUser2, the DB query will find nothing. + const res = await fetch( + `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForUser2AttemptingAsset1}`, + ); + expect(res.status).toBe(404); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Asset not found" }), + ); + }); + + it("should fail for a token referencing a non-existent assetId (expect 404)", async () => { + const nonExistentAssetId = `nonexistent-asset-${Date.now()}`; + const token = createSignedToken( + { + assetId: nonExistentAssetId, + userId, // Valid userId from the primary user + } as z.infer<typeof zAssetSignedTokenSchema>, + Date.now() + 60000, + ); + + const res = await fetch( + `http://localhost:${port}/api/public/assets/${nonExistentAssetId}?token=${token}`, + ); + expect(res.status).toBe(404); + expect(await res.json()).toEqual( + expect.objectContaining({ error: "Asset not found" }), + ); + }); + }); +}); diff --git a/packages/e2e_tests/tests/api/rss.test.ts b/packages/e2e_tests/tests/api/rss.test.ts new file mode 100644 index 00000000..8a7447e6 --- /dev/null +++ b/packages/e2e_tests/tests/api/rss.test.ts @@ -0,0 +1,155 @@ +import { beforeEach, describe, expect, inject, it } from "vitest"; + +import { BookmarkTypes } from "../../../shared/types/bookmarks"; +import { createTestUser } from "../../utils/api"; +import { getTrpcClient } from "../../utils/trpc"; + +describe("RSS Feed API", () => { + const port = inject("karakeepPort"); + + if (!port) { + throw new Error("Missing required environment variables"); + } + + async function fetchRssFeed(listId: string, token: string) { + return await fetch( + `http://localhost:${port}/api/v1/rss/lists/${listId}?token=${token}`, + ); + } + + async function seedDatabase() { + const trpcClient = getTrpcClient(apiKey); + + // Create two lists + const manualList = await trpcClient.lists.create.mutate({ + name: "Test List #1", + icon: "🚀", + type: "manual", + }); + + const smartList = await trpcClient.lists.create.mutate({ + name: "Test List #2", + icon: "🚀", + type: "smart", + query: "is:fav", + }); + + // Create two bookmarks + const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({ + title: "Test Bookmark #1", + url: "https://example.com", + type: BookmarkTypes.LINK, + }); + + const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({ + title: "Test Bookmark #2", + url: "https://example.com/2", + type: BookmarkTypes.LINK, + favourited: true, + }); + + await trpcClient.lists.addToList.mutate({ + listId: manualList.id, + bookmarkId: createBookmark1.id, + }); + + return { manualList, smartList, createBookmark1, createBookmark2 }; + } + + let apiKey: string; + + beforeEach(async () => { + apiKey = await createTestUser(); + }); + + it("should generate rss feed for manual lists", async () => { + const { manualList } = await seedDatabase(); + const trpcClient = getTrpcClient(apiKey); + + // Enable rss feed + const token = await trpcClient.lists.regenRssToken.mutate({ + listId: manualList.id, + }); + + const res = await fetchRssFeed(manualList.id, token.token); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe("application/rss+xml"); + + const text = await res.text(); + expect(text).toContain("Test Bookmark #1"); + expect(text).not.toContain("Test Bookmark #2"); + }); + + it("should generate rss feed for smart lists", async () => { + const { smartList } = await seedDatabase(); + const trpcClient = getTrpcClient(apiKey); + + // Enable rss feed + const token = await trpcClient.lists.regenRssToken.mutate({ + listId: smartList.id, + }); + + const res = await fetchRssFeed(smartList.id, token.token); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe("application/rss+xml"); + + const text = await res.text(); + expect(text).not.toContain("Test Bookmark #1"); + expect(text).toContain("Test Bookmark #2"); + }); + + it("should fail when the token is invalid", async () => { + const { smartList } = await seedDatabase(); + const trpcClient = getTrpcClient(apiKey); + + // Enable rss feed + const token = await trpcClient.lists.regenRssToken.mutate({ + listId: smartList.id, + }); + + let res = await fetchRssFeed(smartList.id, token.token); + expect(res.status).toBe(200); + + // Invalidate the token + await trpcClient.lists.regenRssToken.mutate({ + listId: smartList.id, + }); + + res = await fetchRssFeed(smartList.id, token.token); + expect(res.status).toBe(404); + }); + + it("should fail when rss gets disabled", async () => { + const { smartList } = await seedDatabase(); + const trpcClient = getTrpcClient(apiKey); + + // Enable rss feed + const token = await trpcClient.lists.regenRssToken.mutate({ + listId: smartList.id, + }); + + const res = await fetchRssFeed(smartList.id, token.token); + expect(res.status).toBe(200); + + // Disable rss feed + await trpcClient.lists.clearRssToken.mutate({ + listId: smartList.id, + }); + + const res2 = await fetchRssFeed(smartList.id, token.token); + expect(res2.status).toBe(404); + }); + + it("should fail when no token is provided", async () => { + const { smartList } = await seedDatabase(); + const trpcClient = getTrpcClient(apiKey); + + // Enable rss feed + await trpcClient.lists.regenRssToken.mutate({ + listId: smartList.id, + }); + + const res2 = await fetchRssFeed(smartList.id, ""); + expect(res2.status).toBe(400); + }); +}); diff --git a/packages/e2e_tests/tests/api/tags.test.ts b/packages/e2e_tests/tests/api/tags.test.ts index 3e3cacc0..6c387628 100644 --- a/packages/e2e_tests/tests/api/tags.test.ts +++ b/packages/e2e_tests/tests/api/tags.test.ts @@ -26,31 +26,16 @@ describe("Tags API", () => { }); it("should get, update and delete a tag", async () => { - // Create a bookmark first - const { data: createdBookmark } = await client.POST("/bookmarks", { + // Create a tag by attaching it to the bookmark + const { data: tag } = await client.POST("/tags", { body: { - type: "text", - title: "Test Bookmark", - text: "This is a test bookmark", + name: "Test Tag", }, }); + expect(tag).toBeDefined(); + expect(tag!.name).toBe("Test Tag"); - // Create a tag by attaching it to the bookmark - const { data: addTagResponse } = await client.POST( - "/bookmarks/{bookmarkId}/tags", - { - params: { - path: { - bookmarkId: createdBookmark!.id, - }, - }, - body: { - tags: [{ tagName: "Test Tag" }], - }, - }, - ); - - const tagId = addTagResponse!.attached[0]; + const tagId = tag!.id; // Get the tag const { data: retrievedTag, response: getResponse } = await client.GET( diff --git a/packages/e2e_tests/utils/api.ts b/packages/e2e_tests/utils/api.ts index 84a6eb91..9f9052fc 100644 --- a/packages/e2e_tests/utils/api.ts +++ b/packages/e2e_tests/utils/api.ts @@ -15,7 +15,7 @@ export async function uploadTestAsset( const formData = new FormData(); formData.append("file", file); - const response = await fetch(`http://localhost:${port}/api/assets`, { + const response = await fetch(`http://localhost:${port}/api/v1/assets`, { method: "POST", headers: { authorization: `Bearer ${apiKey}`, diff --git a/packages/e2e_tests/vitest.config.ts b/packages/e2e_tests/vitest.config.ts index bb1c7ea4..2735f1e2 100644 --- a/packages/e2e_tests/vitest.config.ts +++ b/packages/e2e_tests/vitest.config.ts @@ -14,5 +14,8 @@ export default defineConfig({ teardownTimeout: 30000, include: ["tests/**/*.test.ts"], testTimeout: 60000, + env: { + NEXTAUTH_SECRET: "secret", + }, }, }); diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json index 0f8074d4..a8eb2ac2 100644 --- a/packages/open-api/karakeep-openapi-spec.json +++ b/packages/open-api/karakeep-openapi-spec.json @@ -77,6 +77,15 @@ "pending" ] }, + "summarizationStatus": { + "type": "string", + "nullable": true, + "enum": [ + "success", + "failure", + "pending" + ] + }, "note": { "type": "string", "nullable": true @@ -307,6 +316,7 @@ "archived", "favourited", "taggingStatus", + "summarizationStatus", "tags", "content", "assets" @@ -416,13 +426,17 @@ "query": { "type": "string", "nullable": true + }, + "public": { + "type": "boolean" } }, "required": [ "id", "name", "icon", - "parentId" + "parentId", + "public" ] }, "Tag": { @@ -551,6 +565,19 @@ }, { "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, + { + "schema": { "type": "number" }, "required": false, @@ -776,6 +803,20 @@ }, { "schema": { + "type": "string", + "enum": [ + "asc", + "desc", + "relevance" + ], + "default": "relevance" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, + { + "schema": { "type": "number" }, "required": false, @@ -1042,6 +1083,15 @@ "pending" ] }, + "summarizationStatus": { + "type": "string", + "nullable": true, + "enum": [ + "success", + "failure", + "pending" + ] + }, "note": { "type": "string", "nullable": true @@ -1057,7 +1107,8 @@ "modifiedAt", "archived", "favourited", - "taggingStatus" + "taggingStatus", + "summarizationStatus" ] } } @@ -1142,6 +1193,15 @@ "pending" ] }, + "summarizationStatus": { + "type": "string", + "nullable": true, + "enum": [ + "success", + "failure", + "pending" + ] + }, "note": { "type": "string", "nullable": true @@ -1157,7 +1217,8 @@ "modifiedAt", "archived", "favourited", - "taggingStatus" + "taggingStatus", + "summarizationStatus" ] } } @@ -1925,6 +1986,9 @@ "query": { "type": "string", "minLength": 1 + }, + "public": { + "type": "boolean" } } } @@ -1969,8 +2033,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" ], @@ -1985,6 +2049,19 @@ }, { "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, + { + "schema": { "type": "number" }, "required": false, @@ -2071,28 +2148,6 @@ "204": { "description": "No content - the bookmark was added" }, - "400": { - "description": "Bookmark already in list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "message" - ] - } - } - } - }, "404": { "description": "List or bookmark not found", "content": { @@ -2222,6 +2277,61 @@ } } } + }, + "post": { + "description": "Create a new tag", + "summary": "Create a new tag", + "tags": [ + "Tags" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "description": "The data to create the tag with.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name" + ] + } + } + } + }, + "responses": { + "201": { + "description": "The created tag", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + } + } + } } }, "/tags/{tagId}": { @@ -2357,7 +2467,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Tag" + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] } } } @@ -2389,8 +2511,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" ], @@ -2405,6 +2527,19 @@ }, { "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "required": false, + "name": "sortOrder", + "in": "query" + }, + { + "schema": { "type": "number" }, "required": false, 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 2273d33b..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: { @@ -232,14 +241,6 @@ registry.registerPath({ 204: { description: "No content - the bookmark was added", }, - 400: { - description: "Bookmark already in list", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 404: { description: "List or bookmark not found", content: { diff --git a/packages/open-api/lib/tags.ts b/packages/open-api/lib/tags.ts index c51e3b84..0a4f62cb 100644 --- a/packages/open-api/lib/tags.ts +++ b/packages/open-api/lib/tags.ts @@ -4,8 +4,11 @@ import { } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; +import { zSortOrder } from "@karakeep/shared/types/bookmarks"; import { + zCreateTagRequestSchema, zGetTagResponseSchema, + zTagBasicSchema, zUpdateTagRequestSchema, } from "@karakeep/shared/types/tags"; @@ -56,6 +59,35 @@ registry.registerPath({ }); registry.registerPath({ + method: "post", + path: "/tags", + description: "Create a new tag", + summary: "Create a new tag", + tags: ["Tags"], + security: [{ [BearerAuth.name]: [] }], + request: { + body: { + description: "The data to create the tag with.", + content: { + "application/json": { + schema: zCreateTagRequestSchema, + }, + }, + }, + }, + responses: { + 201: { + description: "The created tag", + content: { + "application/json": { + schema: zTagBasicSchema, + }, + }, + }, + }, +}); + +registry.registerPath({ method: "get", path: "/tags/{tagId}", description: "Get tag by its id", @@ -134,7 +166,7 @@ registry.registerPath({ description: "The updated tag", content: { "application/json": { - schema: TagSchema, + schema: zTagBasicSchema, }, }, }, @@ -152,13 +184,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..e8c048c4 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: { @@ -1167,7 +1158,40 @@ export interface paths { }; }; put?: never; - post?: never; + /** + * Create a new tag + * @description Create a new tag + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description The data to create the tag with. */ + requestBody?: { + content: { + "application/json": { + name: string; + }; + }; + }; + responses: { + /** @description The created tag */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + name: string; + }; + }; + }; + }; + }; delete?: never; options?: never; head?: never; @@ -1287,7 +1311,10 @@ export interface paths { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Tag"]; + "application/json": { + id: string; + name: string; + }; }; }; /** @description Tag not found */ @@ -1314,12 +1341,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. */ diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts index e06c7841..15b678cc 100644 --- a/packages/shared-react/hooks/bookmarks.ts +++ b/packages/shared-react/hooks/bookmarks.ts @@ -1,5 +1,6 @@ +import { isBookmarkStillLoading } from "@karakeep/shared/utils/bookmarkUtils"; + import { api } from "../trpc"; -import { isBookmarkStillLoading } from "../utils/bookmarkUtils"; import { useBookmarkGridContext } from "./bookmark-grid-context"; import { useAddBookmarkToList } from "./lists"; diff --git a/packages/shared-react/hooks/users.ts b/packages/shared-react/hooks/users.ts new file mode 100644 index 00000000..e896f8e4 --- /dev/null +++ b/packages/shared-react/hooks/users.ts @@ -0,0 +1,14 @@ +import { api } from "../trpc"; + +export function useUpdateUserSettings( + ...opts: Parameters<typeof api.users.updateSettings.useMutation> +) { + const apiUtils = api.useUtils(); + return api.users.updateSettings.useMutation({ + ...opts[0], + onSuccess: (res, req, meta) => { + apiUtils.users.settings.invalidate(); + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} diff --git a/packages/shared-react/utils/bookmarkUtils.ts b/packages/shared-react/utils/bookmarkUtils.ts index 1f840f78..e69de29b 100644 --- a/packages/shared-react/utils/bookmarkUtils.ts +++ b/packages/shared-react/utils/bookmarkUtils.ts @@ -1,70 +0,0 @@ -import { - BookmarkTypes, - ZBookmark, - ZBookmarkedLink, -} from "@karakeep/shared/types/bookmarks"; - -import { getAssetUrl } from "./assetUtils"; - -const MAX_LOADING_MSEC = 30 * 1000; - -export function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) { - if (bookmark.imageAssetId) { - return { url: getAssetUrl(bookmark.imageAssetId), localAsset: true }; - } - if (bookmark.screenshotAssetId) { - return { url: getAssetUrl(bookmark.screenshotAssetId), localAsset: true }; - } - return bookmark.imageUrl - ? { url: bookmark.imageUrl, localAsset: false } - : null; -} - -export function isBookmarkStillCrawling(bookmark: ZBookmark) { - return ( - bookmark.content.type == BookmarkTypes.LINK && - !bookmark.content.crawledAt && - Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC - ); -} - -export function isBookmarkStillTagging(bookmark: ZBookmark) { - return ( - bookmark.taggingStatus == "pending" && - Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC - ); -} - -export function isBookmarkStillLoading(bookmark: ZBookmark) { - return isBookmarkStillTagging(bookmark) || isBookmarkStillCrawling(bookmark); -} - -export function getSourceUrl(bookmark: ZBookmark) { - if (bookmark.content.type === BookmarkTypes.LINK) { - return bookmark.content.url; - } - if (bookmark.content.type === BookmarkTypes.ASSET) { - return bookmark.content.sourceUrl ?? null; - } - if (bookmark.content.type === BookmarkTypes.TEXT) { - return bookmark.content.sourceUrl ?? null; - } - return null; -} - -export function getBookmarkTitle(bookmark: ZBookmark) { - let title: string | null = null; - switch (bookmark.content.type) { - case BookmarkTypes.LINK: - title = bookmark.content.title ?? bookmark.content.url; - break; - case BookmarkTypes.TEXT: - title = null; - break; - case BookmarkTypes.ASSET: - title = bookmark.content.fileName ?? null; - break; - } - - return bookmark.title ? bookmark.title : title; -} diff --git a/packages/shared/config.ts b/packages/shared/config.ts index 046583c6..b899dbeb 100644 --- a/packages/shared/config.ts +++ b/packages/shared/config.ts @@ -17,6 +17,8 @@ const optionalStringBool = () => const allEnv = z.object({ API_URL: z.string().url().default("http://localhost:3000"), + NEXTAUTH_URL: z.string().url().default("http://localhost:3000"), + NEXTAUTH_SECRET: z.string().optional(), DISABLE_SIGNUPS: stringBool("false"), DISABLE_PASSWORD_AUTH: stringBool("false"), OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool("false"), @@ -40,6 +42,8 @@ const allEnv = z.object({ INFERENCE_OUTPUT_SCHEMA: z .enum(["structured", "json", "plain"]) .default("structured"), + INFERENCE_ENABLE_AUTO_TAGGING: stringBool("true"), + INFERENCE_ENABLE_AUTO_SUMMARIZATION: stringBool("false"), OCR_CACHE_DIR: z.string().optional(), OCR_LANGS: z .string() @@ -89,6 +93,14 @@ const allEnv = z.object({ const serverConfigSchema = allEnv.transform((val) => { return { apiUrl: val.API_URL, + publicUrl: val.NEXTAUTH_URL, + publicApiUrl: `${val.NEXTAUTH_URL}/api`, + signingSecret: () => { + if (!val.NEXTAUTH_SECRET) { + throw new Error("NEXTAUTH_SECRET is not set"); + } + return val.NEXTAUTH_SECRET; + }, auth: { disableSignups: val.DISABLE_SIGNUPS, disablePasswordAuth: val.DISABLE_PASSWORD_AUTH, @@ -120,6 +132,8 @@ const serverConfigSchema = allEnv.transform((val) => { ? ("structured" as const) : ("plain" as const) : val.INFERENCE_OUTPUT_SCHEMA, + enableAutoTagging: val.INFERENCE_ENABLE_AUTO_TAGGING, + enableAutoSummarization: val.INFERENCE_ENABLE_AUTO_SUMMARIZATION, }, embedding: { textModel: val.EMBEDDING_TEXT_MODEL, @@ -177,12 +191,17 @@ const serverConfigSchema = allEnv.transform((val) => { const serverConfig = serverConfigSchema.parse(process.env); // Always explicitly pick up stuff from server config to avoid accidentally leaking stuff export const clientConfig = { + publicUrl: serverConfig.publicUrl, + publicApiUrl: serverConfig.publicApiUrl, demoMode: serverConfig.demoMode, auth: { disableSignups: serverConfig.auth.disableSignups, disablePasswordAuth: serverConfig.auth.disablePasswordAuth, }, inference: { + isConfigured: + !!serverConfig.inference.openAIApiKey || + !!serverConfig.inference.ollamaBaseUrl, inferredTagLang: serverConfig.inference.inferredTagLang, }, serverVersion: serverConfig.serverVersion, diff --git a/packages/shared/inference.ts b/packages/shared/inference.ts index e1f21dae..04fa8cfc 100644 --- a/packages/shared/inference.ts +++ b/packages/shared/inference.ts @@ -71,6 +71,10 @@ class OpenAIInferenceClient implements InferenceClient { this.openAI = new OpenAI({ apiKey: serverConfig.inference.openAIApiKey, baseURL: serverConfig.inference.openAIBaseUrl, + defaultHeaders: { + "X-Title": "Karakeep", + "HTTP-Referer": "https://karakeep.app", + }, }); } diff --git a/packages/shared/langs.ts b/packages/shared/langs.ts index 5cbe923c..74e1a42a 100644 --- a/packages/shared/langs.ts +++ b/packages/shared/langs.ts @@ -6,6 +6,7 @@ export const langNameMappings: Record<string, string> = { da: "Danish", nl: "Dutch", en: "English", + en_US: "English (US)", fr: "French", gl: "Galician", de: "German", diff --git a/packages/shared/package.json b/packages/shared/package.json index a262267a..478fb77d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -6,6 +6,7 @@ "type": "module", "dependencies": { "glob": "^11.0.0", + "js-tiktoken": "^1.0.20", "liteque": "^0.3.2", "meilisearch": "^0.37.0", "ollama": "^0.5.14", diff --git a/packages/shared/prompts.ts b/packages/shared/prompts.ts index 40e706b2..b7957dd4 100644 --- a/packages/shared/prompts.ts +++ b/packages/shared/prompts.ts @@ -1,15 +1,15 @@ -// TODO: Use a proper tokenizer +import { getEncoding } from "js-tiktoken"; + +const encoding = getEncoding("o200k_base"); + function calculateNumTokens(text: string) { - return text.split(" ").length; + return encoding.encode(text).length; } function truncateContent(content: string, length: number) { - let words = content.split(" "); - if (words.length > length) { - words = words.slice(0, length); - content = words.join(" "); - } - return content; + const tokens = encoding.encode(content); + const truncatedTokens = tokens.slice(0, length); + return encoding.decode(truncatedTokens); } export function buildImagePrompt(lang: string, customPrompts: string[]) { diff --git a/packages/shared/queues.ts b/packages/shared/queues.ts index 571df568..a434414c 100644 --- a/packages/shared/queues.ts +++ b/packages/shared/queues.ts @@ -32,9 +32,10 @@ export const LinkCrawlerQueue = new SqliteQueue<ZCrawlLinkRequest>( }, ); -// OpenAI Worker +// Inference Worker export const zOpenAIRequestSchema = z.object({ bookmarkId: z.string(), + type: z.enum(["summarize", "tag"]).default("tag"), }); export type ZOpenAIRequest = z.infer<typeof zOpenAIRequestSchema>; @@ -171,7 +172,7 @@ export const AssetPreprocessingQueue = // Webhook worker export const zWebhookRequestSchema = z.object({ bookmarkId: z.string(), - operation: z.enum(["crawled", "created", "edited", "ai tagged"]), + operation: z.enum(["crawled", "created", "edited", "ai tagged", "deleted"]), }); export type ZWebhookRequest = z.infer<typeof zWebhookRequestSchema>; export const WebhookQueue = new SqliteQueue<ZWebhookRequest>( @@ -195,7 +196,7 @@ export async function triggerWebhook( }); } -// RuleEgine worker +// RuleEngine worker export const zRuleEngineRequestSchema = z.object({ bookmarkId: z.string(), events: z.array(zRuleEngineEventSchema), diff --git a/packages/shared/signedTokens.ts b/packages/shared/signedTokens.ts new file mode 100644 index 00000000..b5e27f3e --- /dev/null +++ b/packages/shared/signedTokens.ts @@ -0,0 +1,71 @@ +import crypto from "node:crypto"; +import { z } from "zod"; + +import serverConfig from "./config"; + +const zTokenPayload = z.object({ + payload: z.unknown(), + expiresAt: z.number(), +}); + +const zSignedTokenPayload = z.object({ + payload: zTokenPayload, + signature: z.string(), +}); + +export type SignedTokenPayload = z.infer<typeof zSignedTokenPayload>; + +export function createSignedToken( + payload: unknown, + expiryEpoch?: number, +): string { + const expiresAt = expiryEpoch ?? Date.now() + 5 * 60 * 1000; // 5 minutes from now + + const toBeSigned: z.infer<typeof zTokenPayload> = { + payload, + expiresAt, + }; + + const payloadString = JSON.stringify(toBeSigned); + const signature = crypto + .createHmac("sha256", serverConfig.signingSecret()) + .update(payloadString) + .digest("hex"); + + const tokenData: z.infer<typeof zSignedTokenPayload> = { + payload: toBeSigned, + signature, + }; + + return Buffer.from(JSON.stringify(tokenData)).toString("base64"); +} + +export function verifySignedToken<T>( + token: string, + schema: z.ZodSchema<T>, +): T | null { + try { + const tokenData = zSignedTokenPayload.parse( + JSON.parse(Buffer.from(token, "base64").toString()), + ); + const { payload, signature } = tokenData; + + // Verify signature + const expectedSignature = crypto + .createHmac("sha256", serverConfig.signingSecret()) + .update(JSON.stringify(payload)) + .digest("hex"); + + if (signature !== expectedSignature) { + return null; + } + // Check expiry + if (Date.now() > payload.expiresAt) { + return null; + } + + return schema.parse(payload.payload); + } catch { + return null; + } +} diff --git a/packages/shared/types/assets.ts b/packages/shared/types/assets.ts new file mode 100644 index 00000000..fe0adcfd --- /dev/null +++ b/packages/shared/types/assets.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +export const zAssetSignedTokenSchema = z.object({ + assetId: z.string(), + userId: z.string(), +}); diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts index 3cac2845..ea1ab717 100644 --- a/packages/shared/types/bookmarks.ts +++ b/packages/shared/types/bookmarks.ts @@ -12,7 +12,7 @@ export const enum BookmarkTypes { UNKNOWN = "unknown", } -export const zSortOrder = z.enum(["asc", "desc"]); +export const zSortOrder = z.enum(["asc", "desc", "relevance"]); export type ZSortOrder = z.infer<typeof zSortOrder>; export const zAssetTypesSchema = z.enum([ @@ -87,6 +87,7 @@ export const zBareBookmarkSchema = z.object({ archived: z.boolean(), favourited: z.boolean(), taggingStatus: z.enum(["success", "failure", "pending"]).nullable(), + summarizationStatus: z.enum(["success", "failure", "pending"]).nullable(), note: z.string().nullish(), summary: z.string().nullish(), }); @@ -178,7 +179,7 @@ export const zGetBookmarksRequestSchema = z.object({ // The value is currently not being used, but keeping it so that client can still set it to true for older // servers. useCursorV2: z.boolean().optional(), - sortOrder: zSortOrder.optional().default("desc"), + sortOrder: zSortOrder.exclude(["relevance"]).optional().default("desc"), includeContent: z.boolean().optional().default(false), }); export type ZGetBookmarksRequest = z.infer<typeof zGetBookmarksRequestSchema>; @@ -238,6 +239,37 @@ export const zSearchBookmarksRequestSchema = z.object({ text: z.string(), limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(), cursor: zSearchBookmarksCursor.nullish(), - sortOrder: zSortOrder.optional().default("desc"), + sortOrder: zSortOrder.optional().default("relevance"), includeContent: z.boolean().optional().default(false), }); + +export const zPublicBookmarkSchema = z.object({ + id: z.string(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), + title: z.string().nullish(), + tags: z.array(z.string()), + description: z.string().nullish(), + bannerImageUrl: z.string().nullable(), + content: z.discriminatedUnion("type", [ + z.object({ + type: z.literal(BookmarkTypes.LINK), + url: z.string(), + author: z.string().nullish(), + }), + z.object({ + type: z.literal(BookmarkTypes.TEXT), + text: z.string(), + }), + z.object({ + type: z.literal(BookmarkTypes.ASSET), + assetType: z.enum(["image", "pdf"]), + assetId: z.string(), + assetUrl: z.string(), + fileName: z.string().nullish(), + sourceUrl: z.string().nullish(), + }), + ]), +}); + +export type ZPublicBookmark = z.infer<typeof zPublicBookmarkSchema>; diff --git a/packages/shared/types/feeds.ts b/packages/shared/types/feeds.ts index 3771624b..060f6d10 100644 --- a/packages/shared/types/feeds.ts +++ b/packages/shared/types/feeds.ts @@ -9,6 +9,7 @@ export const zFeedSchema = z.object({ id: z.string(), name: z.string().min(1).max(MAX_FEED_NAME_LENGTH), url: z.string().url(), + enabled: z.boolean(), lastFetchedStatus: z.enum(["success", "failure", "pending"]).nullable(), lastFetchedAt: z.date().nullable(), }); @@ -18,10 +19,12 @@ export type ZFeed = z.infer<typeof zFeedSchema>; export const zNewFeedSchema = z.object({ name: z.string().min(1).max(MAX_FEED_NAME_LENGTH), url: z.string().max(MAX_FEED_URL_LENGTH).url(), + enabled: z.boolean(), }); export const zUpdateFeedSchema = z.object({ feedId: z.string(), - name: z.string().min(1).max(MAX_FEED_NAME_LENGTH), - url: z.string().max(MAX_FEED_URL_LENGTH).url(), + name: z.string().min(1).max(MAX_FEED_NAME_LENGTH).optional(), + url: z.string().max(MAX_FEED_URL_LENGTH).url().optional(), + enabled: z.boolean().optional(), }); diff --git a/packages/shared/types/lists.ts b/packages/shared/types/lists.ts index 7ef5687c..51fb458c 100644 --- a/packages/shared/types/lists.ts +++ b/packages/shared/types/lists.ts @@ -47,6 +47,7 @@ export const zBookmarkListSchema = z.object({ parentId: z.string().nullable(), type: z.enum(["manual", "smart"]).default("manual"), query: z.string().nullish(), + public: z.boolean(), }); export type ZBookmarkList = z.infer<typeof zBookmarkListSchema>; @@ -66,6 +67,7 @@ export const zEditBookmarkListSchema = z.object({ icon: z.string().optional(), parentId: z.string().nullish(), query: z.string().min(1).optional(), + public: z.boolean().optional(), }); export const zEditBookmarkListSchemaWithValidation = zEditBookmarkListSchema diff --git a/packages/shared/types/pagination.ts b/packages/shared/types/pagination.ts index d2312982..3b9dead4 100644 --- a/packages/shared/types/pagination.ts +++ b/packages/shared/types/pagination.ts @@ -4,3 +4,5 @@ export const zCursorV2 = z.object({ createdAt: z.date(), id: z.string(), }); + +export type ZCursor = z.infer<typeof zCursorV2>; diff --git a/packages/shared/types/tags.ts b/packages/shared/types/tags.ts index c2d8b28d..c7a0103e 100644 --- a/packages/shared/types/tags.ts +++ b/packages/shared/types/tags.ts @@ -1,5 +1,9 @@ import { z } from "zod"; +export const zCreateTagRequestSchema = z.object({ + name: z.string().min(1), +}); + export const zAttachedByEnumSchema = z.enum(["ai", "human"]); export type ZAttachedByEnum = z.infer<typeof zAttachedByEnumSchema>; export const zBookmarkTagSchema = z.object({ diff --git a/packages/shared/types/users.ts b/packages/shared/types/users.ts index 86c5a9ee..0abe13b1 100644 --- a/packages/shared/types/users.ts +++ b/packages/shared/types/users.ts @@ -39,3 +39,18 @@ export const zUserStatsResponseSchema = z.object({ numLists: z.number(), numHighlights: z.number(), }); + +export const zUserSettingsSchema = z.object({ + bookmarkClickAction: z.enum([ + "open_original_link", + "expand_bookmark_preview", + ]), + archiveDisplayBehaviour: z.enum(["show", "hide"]), +}); + +export type ZUserSettings = z.infer<typeof zUserSettingsSchema>; + +export const zUpdateUserSettingsSchema = zUserSettingsSchema.partial().pick({ + bookmarkClickAction: true, + archiveDisplayBehaviour: true, +}); diff --git a/packages/shared/types/webhooks.ts b/packages/shared/types/webhooks.ts index 819951ce..75980b70 100644 --- a/packages/shared/types/webhooks.ts +++ b/packages/shared/types/webhooks.ts @@ -8,6 +8,7 @@ export const zWebhookEventSchema = z.enum([ "edited", "crawled", "ai tagged", + "deleted", ]); export type ZWebhookEvent = z.infer<typeof zWebhookEventSchema>; diff --git a/packages/shared-react/utils/assetUtils.ts b/packages/shared/utils/assetUtils.ts index 119451d9..119451d9 100644 --- a/packages/shared-react/utils/assetUtils.ts +++ b/packages/shared/utils/assetUtils.ts diff --git a/packages/shared/utils/bookmarkUtils.ts b/packages/shared/utils/bookmarkUtils.ts new file mode 100644 index 00000000..97ef08fc --- /dev/null +++ b/packages/shared/utils/bookmarkUtils.ts @@ -0,0 +1,90 @@ +import { BookmarkTypes, ZBookmark, ZBookmarkedLink } from "../types/bookmarks"; +import { getAssetUrl } from "./assetUtils"; + +const MAX_LOADING_MSEC = 30 * 1000; + +export function getBookmarkLinkAssetIdOrUrl(bookmark: ZBookmarkedLink) { + if (bookmark.imageAssetId) { + return { assetId: bookmark.imageAssetId, localAsset: true as const }; + } + if (bookmark.screenshotAssetId) { + return { assetId: bookmark.screenshotAssetId, localAsset: true as const }; + } + return bookmark.imageUrl + ? { url: bookmark.imageUrl, localAsset: false as const } + : null; +} + +export function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) { + const assetOrUrl = getBookmarkLinkAssetIdOrUrl(bookmark); + if (!assetOrUrl) { + return null; + } + if (!assetOrUrl.localAsset) { + return assetOrUrl; + } + return { + url: getAssetUrl(assetOrUrl.assetId), + localAsset: true, + }; +} + +export function isBookmarkStillCrawling(bookmark: ZBookmark) { + return ( + bookmark.content.type == BookmarkTypes.LINK && + !bookmark.content.crawledAt && + Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC + ); +} + +export function isBookmarkStillTagging(bookmark: ZBookmark) { + return ( + bookmark.taggingStatus == "pending" && + Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC + ); +} + +export function isBookmarkStillSummarizing(bookmark: ZBookmark) { + return ( + bookmark.summarizationStatus == "pending" && + Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC + ); +} + +export function isBookmarkStillLoading(bookmark: ZBookmark) { + return ( + isBookmarkStillTagging(bookmark) || + isBookmarkStillCrawling(bookmark) || + isBookmarkStillSummarizing(bookmark) + ); +} + +export function getSourceUrl(bookmark: ZBookmark) { + if (bookmark.content.type === BookmarkTypes.LINK) { + return bookmark.content.url; + } + if (bookmark.content.type === BookmarkTypes.ASSET) { + return bookmark.content.sourceUrl ?? null; + } + if (bookmark.content.type === BookmarkTypes.TEXT) { + return bookmark.content.sourceUrl ?? null; + } + return null; +} + +export function getBookmarkTitle(bookmark: ZBookmark) { + let title: string | null = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + title = bookmark.content.title ?? bookmark.content.url; + break; + case BookmarkTypes.TEXT: + title = null; + break; + case BookmarkTypes.ASSET: + title = bookmark.content.fileName ?? null; + break; + } + + return bookmark.title ? bookmark.title : title; +} diff --git a/packages/trpc/lib/__tests__/search.test.ts b/packages/trpc/lib/__tests__/search.test.ts index 72b53368..9d9b39d7 100644 --- a/packages/trpc/lib/__tests__/search.test.ts +++ b/packages/trpc/lib/__tests__/search.test.ts @@ -34,12 +34,6 @@ beforeEach(async () => { email: "test@example.com", role: "user", }, - { - id: "another-user", - name: "Another User", - email: "another@example.com", - role: "user", - }, ]); // Setup test data @@ -92,14 +86,6 @@ beforeEach(async () => { favourited: false, createdAt: new Date("2024-01-06"), }, - { - id: "b7", - type: BookmarkTypes.ASSET, - userId: "another-user", - archived: true, - favourited: false, - createdAt: new Date("2024-01-06"), - }, ]); await db.insert(bookmarkLinks).values([ @@ -157,21 +143,6 @@ beforeEach(async () => { type: "manual", }, { id: "l4", userId: testUserId, name: "work", icon: "💼", type: "manual" }, - { - id: "l5", - userId: testUserId, - name: "smartlist", - icon: "🧠", - type: "smart", - query: "#tag1 or #tag2", - }, - { - id: "l6", - userId: testUserId, - name: "emptylist", - icon: "∅", - type: "manual", - }, ]); await db.insert(bookmarksInLists).values([ @@ -253,26 +224,6 @@ describe("getBookmarkIdsFromMatcher", () => { expect(result).toEqual(["b1", "b6"]); }); - it("should handle listName matcher with smartList", async () => { - const matcher: Matcher = { - type: "listName", - listName: "smartlist", - inverse: false, - }; - const result = await getBookmarkIdsFromMatcher(mockCtx, matcher); - expect(result).toEqual(["b1", "b2"]); - }); - - it("should handle listName matcher with empty list", async () => { - const matcher: Matcher = { - type: "listName", - listName: "emptylist", - inverse: false, - }; - const result = await getBookmarkIdsFromMatcher(mockCtx, matcher); - expect(result).toEqual([]); - }); - it("should handle listName matcher with inverse=true", async () => { const matcher: Matcher = { type: "listName", @@ -283,26 +234,6 @@ describe("getBookmarkIdsFromMatcher", () => { expect(result.sort()).toEqual(["b2", "b3", "b4", "b5"]); }); - it("should handle listName matcher with smartList with inverse=true", async () => { - const matcher: Matcher = { - type: "listName", - listName: "smartlist", - inverse: true, - }; - const result = await getBookmarkIdsFromMatcher(mockCtx, matcher); - expect(result).toEqual(["b3", "b4", "b5", "b6"]); - }); - - it("should handle listName matcher with empty list with inverse=true", async () => { - const matcher: Matcher = { - type: "listName", - listName: "emptylist", - inverse: true, - }; - const result = await getBookmarkIdsFromMatcher(mockCtx, matcher); - expect(result).toEqual(["b1", "b2", "b3", "b4", "b5", "b6"]); - }); - it("should handle archived matcher", async () => { const matcher: Matcher = { type: "archived", archived: true }; const result = await getBookmarkIdsFromMatcher(mockCtx, matcher); diff --git a/packages/trpc/lib/attachments.ts b/packages/trpc/lib/attachments.ts index 15cbba74..739aa8f5 100644 --- a/packages/trpc/lib/attachments.ts +++ b/packages/trpc/lib/attachments.ts @@ -55,7 +55,7 @@ export function isAllowedToAttachAsset(type: ZAssetType) { screenshot: true, assetScreenshot: true, fullPageArchive: false, - precrawledArchive: false, + precrawledArchive: true, bannerImage: true, video: false, bookmarkAsset: false, diff --git a/packages/trpc/lib/impersonate.ts b/packages/trpc/lib/impersonate.ts new file mode 100644 index 00000000..f44a2c70 --- /dev/null +++ b/packages/trpc/lib/impersonate.ts @@ -0,0 +1,30 @@ +import { eq } from "drizzle-orm"; + +import { db } from "@karakeep/db"; +import { users } from "@karakeep/db/schema"; + +import { AuthedContext } from ".."; + +export async function buildImpersonatingAuthedContext( + userId: string, +): Promise<AuthedContext> { + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + }); + if (!user) { + throw new Error("User not found"); + } + + return { + user: { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + }, + db, + req: { + ip: null, + }, + }; +} diff --git a/packages/trpc/lib/search.ts b/packages/trpc/lib/search.ts index 7bb78a01..d4130798 100644 --- a/packages/trpc/lib/search.ts +++ b/packages/trpc/lib/search.ts @@ -4,21 +4,21 @@ import { exists, gt, gte, - inArray, isNotNull, like, lt, lte, ne, notExists, - notInArray, notLike, } from "drizzle-orm"; import { bookmarkAssets, bookmarkLinks, + bookmarkLists, bookmarks, + bookmarksInLists, bookmarkTags, rssFeedImportsTable, rssFeedsTable, @@ -28,7 +28,6 @@ import { Matcher } from "@karakeep/shared/types/search"; import { toAbsoluteDate } from "@karakeep/shared/utils/relativeDateUtils"; import { AuthedContext } from ".."; -import { List } from "../models/lists"; interface BookmarkQueryReturnType { id: string; @@ -88,20 +87,21 @@ function union(vals: BookmarkQueryReturnType[][]): BookmarkQueryReturnType[] { } async function getIds( - ctx: AuthedContext, + db: AuthedContext["db"], + userId: string, matcher: Matcher, ): Promise<BookmarkQueryReturnType[]> { switch (matcher.type) { case "tagName": { const comp = matcher.inverse ? notExists : exists; - return ctx.db + return db .selectDistinct({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp( - ctx.db + db .select() .from(tagsOnBookmarks) .innerJoin( @@ -111,7 +111,7 @@ async function getIds( .where( and( eq(tagsOnBookmarks.bookmarkId, bookmarks.id), - eq(bookmarkTags.userId, ctx.user.id), + eq(bookmarkTags.userId, userId), eq(bookmarkTags.name, matcher.tagName), ), ), @@ -121,14 +121,14 @@ async function getIds( } case "tagged": { const comp = matcher.tagged ? exists : notExists; - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp( - ctx.db + db .select() .from(tagsOnBookmarks) .where(and(eq(tagsOnBookmarks.bookmarkId, bookmarks.id))), @@ -137,43 +137,59 @@ async function getIds( ); } case "listName": { - const lists = await List.fromName(ctx, matcher.listName); - const ids = await Promise.all(lists.map((l) => l.getBookmarkIds())); - const comp = matcher.inverse ? notInArray : inArray; - return ctx.db - .select({ id: bookmarks.id }) + const comp = matcher.inverse ? notExists : exists; + return db + .selectDistinct({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), - comp(bookmarks.id, ids.flat()), + eq(bookmarks.userId, userId), + comp( + db + .select() + .from(bookmarksInLists) + .innerJoin( + bookmarkLists, + eq(bookmarksInLists.listId, bookmarkLists.id), + ) + .where( + and( + eq(bookmarksInLists.bookmarkId, bookmarks.id), + eq(bookmarkLists.userId, userId), + eq(bookmarkLists.name, matcher.listName), + ), + ), + ), ), ); } case "inlist": { - const lists = await List.getAll(ctx); - const ids = await Promise.all(lists.map((l) => l.getBookmarkIds())); - const comp = matcher.inList ? inArray : notInArray; - return ctx.db + const comp = matcher.inList ? exists : notExists; + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), - comp(bookmarks.id, ids.flat()), + eq(bookmarks.userId, userId), + comp( + db + .select() + .from(bookmarksInLists) + .where(and(eq(bookmarksInLists.bookmarkId, bookmarks.id))), + ), ), ); } case "rssFeedName": { const comp = matcher.inverse ? notExists : exists; - return ctx.db + return db .selectDistinct({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp( - ctx.db + db .select() .from(rssFeedImportsTable) .innerJoin( @@ -183,7 +199,7 @@ async function getIds( .where( and( eq(rssFeedImportsTable.bookmarkId, bookmarks.id), - eq(rssFeedsTable.userId, ctx.user.id), + eq(rssFeedsTable.userId, userId), eq(rssFeedsTable.name, matcher.feedName), ), ), @@ -192,36 +208,36 @@ async function getIds( ); } case "archived": { - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), eq(bookmarks.archived, matcher.archived), ), ); } case "url": { const comp = matcher.inverse ? notLike : like; - return ctx.db + return db .select({ id: bookmarkLinks.id }) .from(bookmarkLinks) .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id)) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp(bookmarkLinks.url, `%${matcher.url}%`), ), ) .union( - ctx.db + db .select({ id: bookmarkAssets.id }) .from(bookmarkAssets) .leftJoin(bookmarks, eq(bookmarks.id, bookmarkAssets.id)) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), // When a user is asking for a link, the inverse matcher should match only assets with URLs. isNotNull(bookmarkAssets.sourceUrl), comp(bookmarkAssets.sourceUrl, `%${matcher.url}%`), @@ -230,73 +246,73 @@ async function getIds( ); } case "favourited": { - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), eq(bookmarks.favourited, matcher.favourited), ), ); } case "dateAfter": { const comp = matcher.inverse ? lt : gte; - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp(bookmarks.createdAt, matcher.dateAfter), ), ); } case "dateBefore": { const comp = matcher.inverse ? gt : lte; - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp(bookmarks.createdAt, matcher.dateBefore), ), ); } case "age": { const comp = matcher.relativeDate.direction === "newer" ? gte : lt; - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp(bookmarks.createdAt, toAbsoluteDate(matcher.relativeDate)), ), ); } case "type": { const comp = matcher.inverse ? ne : eq; - return ctx.db + return db .select({ id: bookmarks.id }) .from(bookmarks) .where( and( - eq(bookmarks.userId, ctx.user.id), + eq(bookmarks.userId, userId), comp(bookmarks.type, matcher.typeName), ), ); } case "and": { const vals = await Promise.all( - matcher.matchers.map((m) => getIds(ctx, m)), + matcher.matchers.map((m) => getIds(db, userId, m)), ); return intersect(vals); } case "or": { const vals = await Promise.all( - matcher.matchers.map((m) => getIds(ctx, m)), + matcher.matchers.map((m) => getIds(db, userId, m)), ); return union(vals); } @@ -311,6 +327,6 @@ export async function getBookmarkIdsFromMatcher( ctx: AuthedContext, matcher: Matcher, ): Promise<string[]> { - const results = await getIds(ctx, matcher); + const results = await getIds(ctx.db, ctx.user.id, matcher); return results.map((r) => r.id); } diff --git a/packages/trpc/models/bookmarks.ts b/packages/trpc/models/bookmarks.ts new file mode 100644 index 00000000..6e9e5651 --- /dev/null +++ b/packages/trpc/models/bookmarks.ts @@ -0,0 +1,422 @@ +import { TRPCError } from "@trpc/server"; +import { + and, + asc, + desc, + eq, + exists, + gt, + gte, + inArray, + lt, + lte, + or, +} from "drizzle-orm"; +import invariant from "tiny-invariant"; +import { z } from "zod"; + +import { + assets, + AssetTypes, + bookmarkAssets, + bookmarkLinks, + bookmarks, + bookmarksInLists, + bookmarkTags, + bookmarkTexts, + rssFeedImportsTable, + tagsOnBookmarks, +} from "@karakeep/db/schema"; +import serverConfig from "@karakeep/shared/config"; +import { createSignedToken } from "@karakeep/shared/signedTokens"; +import { zAssetSignedTokenSchema } from "@karakeep/shared/types/assets"; +import { + BookmarkTypes, + DEFAULT_NUM_BOOKMARKS_PER_PAGE, + ZBookmark, + ZBookmarkContent, + zGetBookmarksRequestSchema, + ZPublicBookmark, +} from "@karakeep/shared/types/bookmarks"; +import { ZCursor } from "@karakeep/shared/types/pagination"; +import { + getBookmarkLinkAssetIdOrUrl, + getBookmarkTitle, +} from "@karakeep/shared/utils/bookmarkUtils"; + +import { AuthedContext } from ".."; +import { mapDBAssetTypeToUserType } from "../lib/attachments"; +import { List } from "./lists"; +import { PrivacyAware } from "./privacy"; + +export class Bookmark implements PrivacyAware { + protected constructor( + protected ctx: AuthedContext, + public bookmark: ZBookmark & { userId: string }, + ) {} + + ensureCanAccess(ctx: AuthedContext): void { + if (this.bookmark.userId != ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access resource", + }); + } + } + + static fromData(ctx: AuthedContext, data: ZBookmark) { + return new Bookmark(ctx, { + ...data, + userId: ctx.user.id, + }); + } + + static async loadMulti( + ctx: AuthedContext, + input: z.infer<typeof zGetBookmarksRequestSchema>, + ): Promise<{ + bookmarks: Bookmark[]; + nextCursor: ZCursor | null; + }> { + if (input.ids && input.ids.length == 0) { + return { bookmarks: [], nextCursor: null }; + } + if (!input.limit) { + input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE; + } + if (input.listId) { + const list = await List.fromId(ctx, input.listId); + if (list.type === "smart") { + input.ids = await list.getBookmarkIds(); + delete input.listId; + } + } + + const sq = ctx.db.$with("bookmarksSq").as( + ctx.db + .select() + .from(bookmarks) + .where( + and( + eq(bookmarks.userId, ctx.user.id), + input.archived !== undefined + ? eq(bookmarks.archived, input.archived) + : undefined, + input.favourited !== undefined + ? eq(bookmarks.favourited, input.favourited) + : undefined, + input.ids ? inArray(bookmarks.id, input.ids) : undefined, + input.tagId !== undefined + ? exists( + ctx.db + .select() + .from(tagsOnBookmarks) + .where( + and( + eq(tagsOnBookmarks.bookmarkId, bookmarks.id), + eq(tagsOnBookmarks.tagId, input.tagId), + ), + ), + ) + : undefined, + input.rssFeedId !== undefined + ? exists( + ctx.db + .select() + .from(rssFeedImportsTable) + .where( + and( + eq(rssFeedImportsTable.bookmarkId, bookmarks.id), + eq(rssFeedImportsTable.rssFeedId, input.rssFeedId), + ), + ), + ) + : undefined, + input.listId !== undefined + ? exists( + ctx.db + .select() + .from(bookmarksInLists) + .where( + and( + eq(bookmarksInLists.bookmarkId, bookmarks.id), + eq(bookmarksInLists.listId, input.listId), + ), + ), + ) + : undefined, + input.cursor + ? input.sortOrder === "asc" + ? or( + gt(bookmarks.createdAt, input.cursor.createdAt), + and( + eq(bookmarks.createdAt, input.cursor.createdAt), + gte(bookmarks.id, input.cursor.id), + ), + ) + : or( + lt(bookmarks.createdAt, input.cursor.createdAt), + and( + eq(bookmarks.createdAt, input.cursor.createdAt), + lte(bookmarks.id, input.cursor.id), + ), + ) + : undefined, + ), + ) + .limit(input.limit + 1) + .orderBy( + input.sortOrder === "asc" + ? asc(bookmarks.createdAt) + : desc(bookmarks.createdAt), + desc(bookmarks.id), + ), + ); + // TODO: Consider not inlining the tags in the response of getBookmarks as this query is getting kinda expensive + const results = await ctx.db + .with(sq) + .select() + .from(sq) + .leftJoin(tagsOnBookmarks, eq(sq.id, tagsOnBookmarks.bookmarkId)) + .leftJoin(bookmarkTags, eq(tagsOnBookmarks.tagId, bookmarkTags.id)) + .leftJoin(bookmarkLinks, eq(bookmarkLinks.id, sq.id)) + .leftJoin(bookmarkTexts, eq(bookmarkTexts.id, sq.id)) + .leftJoin(bookmarkAssets, eq(bookmarkAssets.id, sq.id)) + .leftJoin(assets, eq(assets.bookmarkId, sq.id)) + .orderBy(desc(sq.createdAt), desc(sq.id)); + + const bookmarksRes = results.reduce<Record<string, ZBookmark>>( + (acc, row) => { + const bookmarkId = row.bookmarksSq.id; + if (!acc[bookmarkId]) { + let content: ZBookmarkContent; + if (row.bookmarkLinks) { + content = { + type: BookmarkTypes.LINK, + url: row.bookmarkLinks.url, + title: row.bookmarkLinks.title, + description: row.bookmarkLinks.description, + imageUrl: row.bookmarkLinks.imageUrl, + favicon: row.bookmarkLinks.favicon, + htmlContent: input.includeContent + ? row.bookmarkLinks.htmlContent + : null, + crawledAt: row.bookmarkLinks.crawledAt, + author: row.bookmarkLinks.author, + publisher: row.bookmarkLinks.publisher, + datePublished: row.bookmarkLinks.datePublished, + dateModified: row.bookmarkLinks.dateModified, + }; + } else if (row.bookmarkTexts) { + content = { + type: BookmarkTypes.TEXT, + text: row.bookmarkTexts.text ?? "", + sourceUrl: row.bookmarkTexts.sourceUrl ?? null, + }; + } else if (row.bookmarkAssets) { + content = { + type: BookmarkTypes.ASSET, + assetId: row.bookmarkAssets.assetId, + assetType: row.bookmarkAssets.assetType, + fileName: row.bookmarkAssets.fileName, + sourceUrl: row.bookmarkAssets.sourceUrl ?? null, + size: null, // This will get filled in the asset loop + content: input.includeContent + ? (row.bookmarkAssets.content ?? null) + : null, + }; + } else { + content = { + type: BookmarkTypes.UNKNOWN, + }; + } + acc[bookmarkId] = { + ...row.bookmarksSq, + content, + tags: [], + assets: [], + }; + } + + if ( + row.bookmarkTags && + // Duplicates may occur because of the join, so we need to make sure we're not adding the same tag twice + !acc[bookmarkId].tags.some((t) => t.id == row.bookmarkTags!.id) + ) { + invariant( + row.tagsOnBookmarks, + "if bookmark tag is set, its many-to-many relation must also be set", + ); + acc[bookmarkId].tags.push({ + ...row.bookmarkTags, + attachedBy: row.tagsOnBookmarks.attachedBy, + }); + } + + if ( + row.assets && + !acc[bookmarkId].assets.some((a) => a.id == row.assets!.id) + ) { + if (acc[bookmarkId].content.type == BookmarkTypes.LINK) { + const content = acc[bookmarkId].content; + invariant(content.type == BookmarkTypes.LINK); + if (row.assets.assetType == AssetTypes.LINK_SCREENSHOT) { + content.screenshotAssetId = row.assets.id; + } + if (row.assets.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE) { + content.fullPageArchiveAssetId = row.assets.id; + } + if (row.assets.assetType == AssetTypes.LINK_BANNER_IMAGE) { + content.imageAssetId = row.assets.id; + } + if (row.assets.assetType == AssetTypes.LINK_VIDEO) { + content.videoAssetId = row.assets.id; + } + if (row.assets.assetType == AssetTypes.LINK_PRECRAWLED_ARCHIVE) { + content.precrawledArchiveAssetId = row.assets.id; + } + acc[bookmarkId].content = content; + } + if (acc[bookmarkId].content.type == BookmarkTypes.ASSET) { + const content = acc[bookmarkId].content; + if (row.assets.id == content.assetId) { + // If this is the bookmark's main aset, caputure its size. + content.size = row.assets.size; + } + } + acc[bookmarkId].assets.push({ + id: row.assets.id, + assetType: mapDBAssetTypeToUserType(row.assets.assetType), + }); + } + + return acc; + }, + {}, + ); + + const bookmarksArr = Object.values(bookmarksRes); + + bookmarksArr.sort((a, b) => { + if (a.createdAt != b.createdAt) { + return input.sortOrder === "asc" + ? a.createdAt.getTime() - b.createdAt.getTime() + : b.createdAt.getTime() - a.createdAt.getTime(); + } else { + return b.id.localeCompare(a.id); + } + }); + + let nextCursor = null; + if (bookmarksArr.length > input.limit) { + const nextItem = bookmarksArr.pop()!; + nextCursor = { + id: nextItem.id, + createdAt: nextItem.createdAt, + }; + } + + return { + bookmarks: bookmarksArr.map((b) => Bookmark.fromData(ctx, b)), + nextCursor, + }; + } + + asZBookmark(): ZBookmark { + return this.bookmark; + } + + asPublicBookmark(): ZPublicBookmark { + const getPublicSignedAssetUrl = (assetId: string) => { + const payload: z.infer<typeof zAssetSignedTokenSchema> = { + assetId, + userId: this.ctx.user.id, + }; + const signedToken = createSignedToken(payload); + return `${serverConfig.publicApiUrl}/public/assets/${assetId}?token=${signedToken}`; + }; + const getContent = ( + content: ZBookmarkContent, + ): ZPublicBookmark["content"] => { + switch (content.type) { + case BookmarkTypes.LINK: { + return { + type: BookmarkTypes.LINK, + url: content.url, + }; + } + case BookmarkTypes.TEXT: { + return { + type: BookmarkTypes.TEXT, + text: content.text, + }; + } + case BookmarkTypes.ASSET: { + return { + type: BookmarkTypes.ASSET, + assetType: content.assetType, + assetId: content.assetId, + assetUrl: getPublicSignedAssetUrl(content.assetId), + fileName: content.fileName, + sourceUrl: content.sourceUrl, + }; + } + default: { + throw new Error("Unknown bookmark content type"); + } + } + }; + + const getBannerImageUrl = (content: ZBookmarkContent): string | null => { + switch (content.type) { + case BookmarkTypes.LINK: { + const assetIdOrUrl = getBookmarkLinkAssetIdOrUrl(content); + if (!assetIdOrUrl) { + return null; + } + if (assetIdOrUrl.localAsset) { + return getPublicSignedAssetUrl(assetIdOrUrl.assetId); + } else { + return assetIdOrUrl.url; + } + } + case BookmarkTypes.TEXT: { + return null; + } + case BookmarkTypes.ASSET: { + switch (content.assetType) { + case "image": + return `${getPublicSignedAssetUrl(content.assetId)}`; + case "pdf": { + const screenshotAssetId = this.bookmark.assets.find( + (r) => r.assetType === "assetScreenshot", + )?.id; + if (!screenshotAssetId) { + return null; + } + return getPublicSignedAssetUrl(screenshotAssetId); + } + default: { + const _exhaustiveCheck: never = content.assetType; + return null; + } + } + } + default: { + throw new Error("Unknown bookmark content type"); + } + } + }; + + // WARNING: Everything below is exposed in the public APIs, don't use spreads! + return { + id: this.bookmark.id, + createdAt: this.bookmark.createdAt, + modifiedAt: this.bookmark.modifiedAt, + title: getBookmarkTitle(this.bookmark), + tags: this.bookmark.tags.map((t) => t.name), + content: getContent(this.bookmark.content), + bannerImageUrl: getBannerImageUrl(this.bookmark.content), + }; + } +} diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts index d278f8d9..2631ca7e 100644 --- a/packages/trpc/models/lists.ts +++ b/packages/trpc/models/lists.ts @@ -1,5 +1,6 @@ +import crypto from "node:crypto"; import { TRPCError } from "@trpc/server"; -import { and, count, eq } from "drizzle-orm"; +import { and, count, eq, or } from "drizzle-orm"; import invariant from "tiny-invariant"; import { z } from "zod"; @@ -7,14 +8,18 @@ import { SqliteError } from "@karakeep/db"; import { bookmarkLists, bookmarksInLists } from "@karakeep/db/schema"; import { triggerRuleEngineOnEvent } from "@karakeep/shared/queues"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; +import { ZSortOrder } from "@karakeep/shared/types/bookmarks"; import { ZBookmarkList, zEditBookmarkListSchemaWithValidation, zNewBookmarkListSchema, } from "@karakeep/shared/types/lists"; +import { ZCursor } from "@karakeep/shared/types/pagination"; -import { AuthedContext } from ".."; +import { AuthedContext, Context } from ".."; +import { buildImpersonatingAuthedContext } from "../lib/impersonate"; import { getBookmarkIdsFromMatcher } from "../lib/search"; +import { Bookmark } from "./bookmarks"; import { PrivacyAware } from "./privacy"; export abstract class List implements PrivacyAware { @@ -26,7 +31,7 @@ export abstract class List implements PrivacyAware { private static fromData( ctx: AuthedContext, data: ZBookmarkList & { userId: string }, - ): ManualList | SmartList { + ) { if (data.type === "smart") { return new SmartList(ctx, data); } else { @@ -34,21 +39,6 @@ export abstract class List implements PrivacyAware { } } - static async fromName( - ctx: AuthedContext, - name: string, - ): Promise<(ManualList | SmartList)[]> { - // Names are not unique, so we need to find all lists with the same name - const lists = await ctx.db.query.bookmarkLists.findMany({ - where: and( - eq(bookmarkLists.name, name), - eq(bookmarkLists.userId, ctx.user.id), - ), - }); - - return lists.map((l) => this.fromData(ctx, l)); - } - static async fromId( ctx: AuthedContext, id: string, @@ -66,7 +56,64 @@ export abstract class List implements PrivacyAware { message: "List not found", }); } - return this.fromData(ctx, list); + if (list.type === "smart") { + return new SmartList(ctx, list); + } else { + return new ManualList(ctx, list); + } + } + + static async getPublicListContents( + ctx: Context, + listId: string, + token: string | null, + pagination: { + limit: number; + order: Exclude<ZSortOrder, "relevance">; + cursor: ZCursor | null | undefined; + }, + ) { + const listdb = await ctx.db.query.bookmarkLists.findFirst({ + where: and( + eq(bookmarkLists.id, listId), + or( + eq(bookmarkLists.public, true), + token !== null ? eq(bookmarkLists.rssToken, token) : undefined, + ), + ), + }); + if (!listdb) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "List not found", + }); + } + + // The token here acts as an authed context, so we can create + // an impersonating context for the list owner as long as + // we don't leak the context. + const authedCtx = await buildImpersonatingAuthedContext(listdb.userId); + const list = List.fromData(authedCtx, listdb); + const bookmarkIds = await list.getBookmarkIds(); + + const bookmarks = await Bookmark.loadMulti(authedCtx, { + ids: bookmarkIds, + includeContent: false, + limit: pagination.limit, + sortOrder: pagination.order, + cursor: pagination.cursor, + }); + + return { + list: { + icon: list.list.icon, + name: list.list.name, + description: list.list.description, + numItems: bookmarkIds.length, + }, + bookmarks: bookmarks.bookmarks.map((b) => b.asPublicBookmark()), + nextCursor: bookmarks.nextCursor, + }; } static async create( @@ -90,6 +137,9 @@ export abstract class List implements PrivacyAware { static async getAll(ctx: AuthedContext): Promise<(ManualList | SmartList)[]> { const lists = await ctx.db.query.bookmarkLists.findMany({ + columns: { + rssToken: false, + }, where: and(eq(bookmarkLists.userId, ctx.user.id)), }); return lists.map((l) => this.fromData(ctx, l)); @@ -99,7 +149,11 @@ export abstract class List implements PrivacyAware { const lists = await ctx.db.query.bookmarksInLists.findMany({ where: and(eq(bookmarksInLists.bookmarkId, bookmarkId)), with: { - list: true, + list: { + columns: { + rssToken: false, + }, + }, }, }); invariant(lists.map((l) => l.list.userId).every((id) => id == ctx.user.id)); @@ -140,6 +194,7 @@ export abstract class List implements PrivacyAware { icon: input.icon, parentId: input.parentId, query: input.query, + public: input.public, }) .where( and( @@ -154,6 +209,45 @@ export abstract class List implements PrivacyAware { this.list = result[0]; } + private async setRssToken(token: string | null) { + const result = await this.ctx.db + .update(bookmarkLists) + .set({ rssToken: token }) + .where( + and( + eq(bookmarkLists.id, this.list.id), + eq(bookmarkLists.userId, this.ctx.user.id), + ), + ) + .returning(); + if (result.length == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + return result[0].rssToken; + } + + async getRssToken(): Promise<string | null> { + const [result] = await this.ctx.db + .select({ rssToken: bookmarkLists.rssToken }) + .from(bookmarkLists) + .where( + and( + eq(bookmarkLists.id, this.list.id), + eq(bookmarkLists.userId, this.ctx.user.id), + ), + ) + .limit(1); + return result.rssToken ?? null; + } + + async regenRssToken() { + return await this.setRssToken(crypto.randomBytes(32).toString("hex")); + } + + async clearRssToken() { + await this.setRssToken(null); + } + abstract get type(): "manual" | "smart"; abstract getBookmarkIds(ctx: AuthedContext): Promise<string[]>; abstract getSize(ctx: AuthedContext): Promise<number>; @@ -271,10 +365,8 @@ export class ManualList extends List { } catch (e) { if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_PRIMARYKEY") { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Bookmark ${bookmarkId} is already in the list ${this.list.id}`, - }); + // this is fine, it just means the bookmark is already in the list + return; } } throw new TRPCError({ diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index 394e95e7..e09f959e 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -7,6 +7,7 @@ import { feedsAppRouter } from "./feeds"; import { highlightsAppRouter } from "./highlights"; import { listsAppRouter } from "./lists"; import { promptsAppRouter } from "./prompts"; +import { publicBookmarks } from "./publicBookmarks"; import { rulesAppRouter } from "./rules"; import { tagsAppRouter } from "./tags"; import { usersAppRouter } from "./users"; @@ -25,6 +26,7 @@ export const appRouter = router({ webhooks: webhooksAppRouter, assets: assetsAppRouter, rules: rulesAppRouter, + publicBookmarks: publicBookmarks, }); // export type definition of API export type AppRouter = typeof appRouter; diff --git a/packages/trpc/routers/admin.ts b/packages/trpc/routers/admin.ts index e4985b5c..91f4a34f 100644 --- a/packages/trpc/routers/admin.ts +++ b/packages/trpc/routers/admin.ts @@ -1,5 +1,5 @@ import { TRPCError } from "@trpc/server"; -import { count, eq, sum } from "drizzle-orm"; +import { count, eq, or, sum } from "drizzle-orm"; import { z } from "zod"; import { assets, bookmarkLinks, bookmarks, users } from "@karakeep/db/schema"; @@ -129,11 +129,21 @@ export const adminAppRouter = router({ ctx.db .select({ value: count() }) .from(bookmarks) - .where(eq(bookmarks.taggingStatus, "pending")), + .where( + or( + eq(bookmarks.taggingStatus, "pending"), + eq(bookmarks.summarizationStatus, "pending"), + ), + ), ctx.db .select({ value: count() }) .from(bookmarks) - .where(eq(bookmarks.taggingStatus, "failure")), + .where( + or( + eq(bookmarks.taggingStatus, "failure"), + eq(bookmarks.summarizationStatus, "failure"), + ), + ), // Tidy Assets TidyAssetsQueue.stats(), @@ -233,7 +243,8 @@ export const adminAppRouter = router({ reRunInferenceOnAllBookmarks: adminProcedure .input( z.object({ - taggingStatus: z.enum(["success", "failure", "all"]), + type: z.enum(["tag", "summarize"]), + status: z.enum(["success", "failure", "all"]), }), ) .mutation(async ({ input, ctx }) => { @@ -241,13 +252,22 @@ export const adminAppRouter = router({ columns: { id: true, }, - ...(input.taggingStatus === "all" - ? {} - : { where: eq(bookmarks.taggingStatus, input.taggingStatus) }), + ...{ + tag: + input.status === "all" + ? {} + : { where: eq(bookmarks.taggingStatus, input.status) }, + summarize: + input.status === "all" + ? {} + : { where: eq(bookmarks.summarizationStatus, input.status) }, + }[input.type], }); await Promise.all( - bookmarkIds.map((b) => OpenAIQueue.enqueue({ bookmarkId: b.id })), + bookmarkIds.map((b) => + OpenAIQueue.enqueue({ bookmarkId: b.id, type: input.type }), + ), ); }), tidyAssets: adminProcedure.mutation(async () => { diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts index e2179542..575b4d9a 100644 --- a/packages/trpc/routers/bookmarks.test.ts +++ b/packages/trpc/routers/bookmarks.test.ts @@ -25,7 +25,11 @@ describe("Bookmark Routes", () => { feedUrl: string, ) { // Create an RSS feed and return its ID - const feed = await api.feeds.create({ name: feedName, url: feedUrl }); + const feed = await api.feeds.create({ + name: feedName, + url: feedUrl, + enabled: true, + }); return feed.id; } diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index b9a21400..04d15d1f 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -1,17 +1,5 @@ import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; -import { - and, - asc, - desc, - eq, - exists, - gt, - gte, - inArray, - lt, - lte, - or, -} from "drizzle-orm"; +import { and, eq, gt, inArray, lt, or } from "drizzle-orm"; import invariant from "tiny-invariant"; import { z } from "zod"; @@ -27,11 +15,9 @@ import { bookmarkAssets, bookmarkLinks, bookmarks, - bookmarksInLists, bookmarkTags, bookmarkTexts, customPrompts, - rssFeedImportsTable, tagsOnBookmarks, } from "@karakeep/db/schema"; import { @@ -69,7 +55,7 @@ import type { AuthedContext, Context } from "../index"; import { authedProcedure, router } from "../index"; import { mapDBAssetTypeToUserType } from "../lib/attachments"; import { getBookmarkIdsFromMatcher } from "../lib/search"; -import { List } from "../models/lists"; +import { Bookmark } from "../models/bookmarks"; import { ensureAssetOwnership } from "./assets"; export const ensureBookmarkOwnership = experimental_trpcMiddleware<{ @@ -420,6 +406,7 @@ export const bookmarksAppRouter = router({ case BookmarkTypes.TEXT: { await OpenAIQueue.enqueue({ bookmarkId: bookmark.id, + type: "tag", }); break; } @@ -659,6 +646,7 @@ export const bookmarksAppRouter = router({ ), ); await triggerSearchDeletion(input.bookmarkId); + await triggerWebhook(input.bookmarkId, "deleted"); if (deleted.changes > 0 && bookmark) { await cleanupAssetForBookmark({ asset: bookmark.asset, @@ -712,7 +700,7 @@ export const bookmarksAppRouter = router({ if (!input.limit) { input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE; } - const sortOrder = input.sortOrder || "desc"; + const sortOrder = input.sortOrder || "relevance"; const client = await getSearchIdxClient(); if (!client) { throw new TRPCError({ @@ -735,11 +723,16 @@ export const bookmarksAppRouter = router({ filter = [`userId = '${ctx.user.id}'`]; } + /** + * preserve legacy behaviour + */ + const createdAtSortOrder = sortOrder === "relevance" ? "desc" : sortOrder; + const resp = await client.search(parsedQuery.text, { filter, showRankingScore: true, attributesToRetrieve: ["id"], - sort: [`createdAt:${sortOrder}`], + sort: [`createdAt:${createdAtSortOrder}`], limit: input.limit, ...(input.cursor ? { @@ -775,7 +768,18 @@ export const bookmarksAppRouter = router({ assets: true, }, }); - results.sort((a, b) => idToRank[b.id] - idToRank[a.id]); + + switch (true) { + case sortOrder === "relevance": + results.sort((a, b) => idToRank[b.id] - idToRank[a.id]); + break; + case sortOrder === "desc": + results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + break; + case sortOrder === "asc": + results.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + break; + } return { bookmarks: results.map((b) => toZodSchema(b, input.includeContent)), @@ -792,245 +796,11 @@ export const bookmarksAppRouter = router({ .input(zGetBookmarksRequestSchema) .output(zGetBookmarksResponseSchema) .query(async ({ input, ctx }) => { - if (input.ids && input.ids.length == 0) { - return { bookmarks: [], nextCursor: null }; - } - if (!input.limit) { - input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE; - } - if (input.listId) { - const list = await List.fromId(ctx, input.listId); - if (list.type === "smart") { - input.ids = await list.getBookmarkIds(); - delete input.listId; - } - } - - const sq = ctx.db.$with("bookmarksSq").as( - ctx.db - .select() - .from(bookmarks) - .where( - and( - eq(bookmarks.userId, ctx.user.id), - input.archived !== undefined - ? eq(bookmarks.archived, input.archived) - : undefined, - input.favourited !== undefined - ? eq(bookmarks.favourited, input.favourited) - : undefined, - input.ids ? inArray(bookmarks.id, input.ids) : undefined, - input.tagId !== undefined - ? exists( - ctx.db - .select() - .from(tagsOnBookmarks) - .where( - and( - eq(tagsOnBookmarks.bookmarkId, bookmarks.id), - eq(tagsOnBookmarks.tagId, input.tagId), - ), - ), - ) - : undefined, - input.rssFeedId !== undefined - ? exists( - ctx.db - .select() - .from(rssFeedImportsTable) - .where( - and( - eq(rssFeedImportsTable.bookmarkId, bookmarks.id), - eq(rssFeedImportsTable.rssFeedId, input.rssFeedId), - ), - ), - ) - : undefined, - input.listId !== undefined - ? exists( - ctx.db - .select() - .from(bookmarksInLists) - .where( - and( - eq(bookmarksInLists.bookmarkId, bookmarks.id), - eq(bookmarksInLists.listId, input.listId), - ), - ), - ) - : undefined, - input.cursor - ? input.sortOrder === "asc" - ? or( - gt(bookmarks.createdAt, input.cursor.createdAt), - and( - eq(bookmarks.createdAt, input.cursor.createdAt), - gte(bookmarks.id, input.cursor.id), - ), - ) - : or( - lt(bookmarks.createdAt, input.cursor.createdAt), - and( - eq(bookmarks.createdAt, input.cursor.createdAt), - lte(bookmarks.id, input.cursor.id), - ), - ) - : undefined, - ), - ) - .limit(input.limit + 1) - .orderBy( - input.sortOrder === "asc" - ? asc(bookmarks.createdAt) - : desc(bookmarks.createdAt), - desc(bookmarks.id), - ), - ); - // TODO: Consider not inlining the tags in the response of getBookmarks as this query is getting kinda expensive - const results = await ctx.db - .with(sq) - .select() - .from(sq) - .leftJoin(tagsOnBookmarks, eq(sq.id, tagsOnBookmarks.bookmarkId)) - .leftJoin(bookmarkTags, eq(tagsOnBookmarks.tagId, bookmarkTags.id)) - .leftJoin(bookmarkLinks, eq(bookmarkLinks.id, sq.id)) - .leftJoin(bookmarkTexts, eq(bookmarkTexts.id, sq.id)) - .leftJoin(bookmarkAssets, eq(bookmarkAssets.id, sq.id)) - .leftJoin(assets, eq(assets.bookmarkId, sq.id)) - .orderBy(desc(sq.createdAt), desc(sq.id)); - - const bookmarksRes = results.reduce<Record<string, ZBookmark>>( - (acc, row) => { - const bookmarkId = row.bookmarksSq.id; - if (!acc[bookmarkId]) { - let content: ZBookmarkContent; - if (row.bookmarkLinks) { - content = { - type: BookmarkTypes.LINK, - url: row.bookmarkLinks.url, - title: row.bookmarkLinks.title, - description: row.bookmarkLinks.description, - imageUrl: row.bookmarkLinks.imageUrl, - favicon: row.bookmarkLinks.favicon, - htmlContent: input.includeContent - ? row.bookmarkLinks.htmlContent - : null, - crawledAt: row.bookmarkLinks.crawledAt, - author: row.bookmarkLinks.author, - publisher: row.bookmarkLinks.publisher, - datePublished: row.bookmarkLinks.datePublished, - dateModified: row.bookmarkLinks.dateModified, - }; - } else if (row.bookmarkTexts) { - content = { - type: BookmarkTypes.TEXT, - text: row.bookmarkTexts.text ?? "", - sourceUrl: row.bookmarkTexts.sourceUrl ?? null, - }; - } else if (row.bookmarkAssets) { - content = { - type: BookmarkTypes.ASSET, - assetId: row.bookmarkAssets.assetId, - assetType: row.bookmarkAssets.assetType, - fileName: row.bookmarkAssets.fileName, - sourceUrl: row.bookmarkAssets.sourceUrl ?? null, - size: null, // This will get filled in the asset loop - content: input.includeContent - ? (row.bookmarkAssets.content ?? null) - : null, - }; - } else { - content = { - type: BookmarkTypes.UNKNOWN, - }; - } - acc[bookmarkId] = { - ...row.bookmarksSq, - content, - tags: [], - assets: [], - }; - } - - if ( - row.bookmarkTags && - // Duplicates may occur because of the join, so we need to make sure we're not adding the same tag twice - !acc[bookmarkId].tags.some((t) => t.id == row.bookmarkTags!.id) - ) { - invariant( - row.tagsOnBookmarks, - "if bookmark tag is set, its many-to-many relation must also be set", - ); - acc[bookmarkId].tags.push({ - ...row.bookmarkTags, - attachedBy: row.tagsOnBookmarks.attachedBy, - }); - } - - if ( - row.assets && - !acc[bookmarkId].assets.some((a) => a.id == row.assets!.id) - ) { - if (acc[bookmarkId].content.type == BookmarkTypes.LINK) { - const content = acc[bookmarkId].content; - invariant(content.type == BookmarkTypes.LINK); - if (row.assets.assetType == AssetTypes.LINK_SCREENSHOT) { - content.screenshotAssetId = row.assets.id; - } - if (row.assets.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE) { - content.fullPageArchiveAssetId = row.assets.id; - } - if (row.assets.assetType == AssetTypes.LINK_BANNER_IMAGE) { - content.imageAssetId = row.assets.id; - } - if (row.assets.assetType == AssetTypes.LINK_VIDEO) { - content.videoAssetId = row.assets.id; - } - if (row.assets.assetType == AssetTypes.LINK_PRECRAWLED_ARCHIVE) { - content.precrawledArchiveAssetId = row.assets.id; - } - acc[bookmarkId].content = content; - } - if (acc[bookmarkId].content.type == BookmarkTypes.ASSET) { - const content = acc[bookmarkId].content; - if (row.assets.id == content.assetId) { - // If this is the bookmark's main aset, caputure its size. - content.size = row.assets.size; - } - } - acc[bookmarkId].assets.push({ - id: row.assets.id, - assetType: mapDBAssetTypeToUserType(row.assets.assetType), - }); - } - - return acc; - }, - {}, - ); - - const bookmarksArr = Object.values(bookmarksRes); - - bookmarksArr.sort((a, b) => { - if (a.createdAt != b.createdAt) { - return input.sortOrder === "asc" - ? a.createdAt.getTime() - b.createdAt.getTime() - : b.createdAt.getTime() - a.createdAt.getTime(); - } else { - return b.id.localeCompare(a.id); - } - }); - - let nextCursor = null; - if (bookmarksArr.length > input.limit) { - const nextItem = bookmarksArr.pop()!; - nextCursor = { - id: nextItem.id, - createdAt: nextItem.createdAt, - }; - } - - return { bookmarks: bookmarksArr, nextCursor }; + const res = await Bookmark.loadMulti(ctx, input); + return { + bookmarks: res.bookmarks.map((b) => b.asZBookmark()), + nextCursor: res.nextCursor, + }; }), updateTags: authedProcedure diff --git a/packages/trpc/routers/feeds.ts b/packages/trpc/routers/feeds.ts index 819ba8ef..07c48f0a 100644 --- a/packages/trpc/routers/feeds.ts +++ b/packages/trpc/routers/feeds.ts @@ -55,6 +55,7 @@ export const feedsAppRouter = router({ name: input.name, url: input.url, userId: ctx.user.id, + enabled: input.enabled, }) .returning(); return feed; @@ -69,6 +70,7 @@ export const feedsAppRouter = router({ .set({ name: input.name, url: input.url, + enabled: input.enabled, }) .where( and( diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts index 65cffd2d..bb949962 100644 --- a/packages/trpc/routers/lists.ts +++ b/packages/trpc/routers/lists.ts @@ -131,4 +131,47 @@ export const listsAppRouter = router({ const sizes = await Promise.all(lists.map((l) => l.getSize())); return { stats: new Map(lists.map((l, i) => [l.list.id, sizes[i]])) }; }), + + // Rss endpoints + regenRssToken: authedProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .output( + z.object({ + token: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const list = await List.fromId(ctx, input.listId); + const token = await list.regenRssToken(); + return { token: token! }; + }), + clearRssToken: authedProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const list = await List.fromId(ctx, input.listId); + await list.clearRssToken(); + }), + getRssToken: authedProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .output( + z.object({ + token: z.string().nullable(), + }), + ) + .query(async ({ input, ctx }) => { + const list = await List.fromId(ctx, input.listId); + return { token: await list.getRssToken() }; + }), }); diff --git a/packages/trpc/routers/publicBookmarks.ts b/packages/trpc/routers/publicBookmarks.ts new file mode 100644 index 00000000..6b643354 --- /dev/null +++ b/packages/trpc/routers/publicBookmarks.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +import { + MAX_NUM_BOOKMARKS_PER_PAGE, + zPublicBookmarkSchema, + zSortOrder, +} from "@karakeep/shared/types/bookmarks"; +import { zBookmarkListSchema } from "@karakeep/shared/types/lists"; +import { zCursorV2 } from "@karakeep/shared/types/pagination"; + +import { publicProcedure, router } from "../index"; +import { List } from "../models/lists"; + +export const publicBookmarks = router({ + getPublicBookmarksInList: publicProcedure + .input( + z.object({ + listId: z.string(), + cursor: zCursorV2.nullish(), + limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).default(20), + sortOrder: zSortOrder.exclude(["relevance"]).optional().default("desc"), + }), + ) + .output( + z.object({ + list: zBookmarkListSchema + .pick({ + name: true, + description: true, + icon: true, + }) + .merge(z.object({ numItems: z.number() })), + bookmarks: z.array(zPublicBookmarkSchema), + nextCursor: zCursorV2.nullable(), + }), + ) + .query(async ({ input, ctx }) => { + return await List.getPublicListContents( + ctx, + input.listId, + /* token */ null, + { + limit: input.limit, + order: input.sortOrder, + cursor: input.cursor, + }, + ); + }), +}); diff --git a/packages/trpc/routers/tags.ts b/packages/trpc/routers/tags.ts index 7f75c16e..cade4b45 100644 --- a/packages/trpc/routers/tags.ts +++ b/packages/trpc/routers/tags.ts @@ -7,7 +7,9 @@ import { SqliteError } from "@karakeep/db"; import { bookmarkTags, tagsOnBookmarks } from "@karakeep/db/schema"; import { triggerSearchReindex } from "@karakeep/shared/queues"; import { + zCreateTagRequestSchema, zGetTagResponseSchema, + zTagBasicSchema, zUpdateTagRequestSchema, } from "@karakeep/shared/types/tags"; @@ -53,19 +55,8 @@ export const ensureTagOwnership = experimental_trpcMiddleware<{ export const tagsAppRouter = router({ create: authedProcedure - .input( - z.object({ - name: z.string().min(1), // Ensure the name is provided and not empty - }), - ) - .output( - z.object({ - id: z.string(), - name: z.string(), - userId: z.string(), - createdAt: z.date(), - }), - ) + .input(zCreateTagRequestSchema) + .output(zTagBasicSchema) .mutation(async ({ input, ctx }) => { try { const [newTag] = await ctx.db @@ -76,7 +67,10 @@ export const tagsAppRouter = router({ }) .returning(); - return newTag; + return { + id: newTag.id, + name: newTag.name, + }; } catch (e) { if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { throw new TRPCError({ @@ -195,14 +189,7 @@ export const tagsAppRouter = router({ }), update: authedProcedure .input(zUpdateTagRequestSchema) - .output( - z.object({ - id: z.string(), - name: z.string(), - userId: z.string(), - createdAt: z.date(), - }), - ) + .output(zTagBasicSchema) .use(ensureTagOwnership) .mutation(async ({ input, ctx }) => { try { @@ -242,7 +229,10 @@ export const tagsAppRouter = router({ console.error("Failed to reindex affected bookmarks", e); } - return res[0]; + return { + id: res[0].id, + name: res[0].name, + }; } catch (e) { if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { diff --git a/packages/trpc/routers/users.test.ts b/packages/trpc/routers/users.test.ts index ea342d33..3fd939b1 100644 --- a/packages/trpc/routers/users.test.ts +++ b/packages/trpc/routers/users.test.ts @@ -94,4 +94,41 @@ describe("User Routes", () => { // A normal user can't list all users await expect(() => user2Caller.users.list()).rejects.toThrow(/FORBIDDEN/); }); + + test<CustomTestContext>("get/update user settings", async ({ + db, + unauthedAPICaller, + }) => { + const user = await unauthedAPICaller.users.create({ + name: "Test User", + email: "testupdate@test.com", + password: "pass1234", + confirmPassword: "pass1234", + }); + const caller = getApiCaller(db, user.id); + + const settings = await caller.users.settings(); + // The default settings + expect(settings).toEqual({ + bookmarkClickAction: "open_original_link", + archiveDisplayBehaviour: "show", + }); + + // Update settings + await caller.users.updateSettings({ + bookmarkClickAction: "expand_bookmark_preview", + }); + + // Verify updated settings + const updatedSettings = await caller.users.settings(); + expect(updatedSettings).toEqual({ + bookmarkClickAction: "expand_bookmark_preview", + archiveDisplayBehaviour: "show", + }); + + // Test invalid update (e.g., empty input, if schema enforces it) + await expect(() => caller.users.updateSettings({})).rejects.toThrow( + /No settings provided/, + ); + }); }); diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index c56daaee..3813387f 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -10,11 +10,14 @@ import { bookmarkTags, highlights, users, + userSettings, } from "@karakeep/db/schema"; import { deleteUserAssets } from "@karakeep/shared/assetdb"; import serverConfig from "@karakeep/shared/config"; import { zSignUpSchema, + zUpdateUserSettingsSchema, + zUserSettingsSchema, zUserStatsResponseSchema, zWhoAmIResponseSchema, } from "@karakeep/shared/types/users"; @@ -59,6 +62,12 @@ export async function createUser( email: users.email, role: users.role, }); + + // Insert user settings for the new user + await trx.insert(userSettings).values({ + userId: result[0].id, + }); + return result[0]; } catch (e) { if (e instanceof SqliteError) { @@ -242,4 +251,38 @@ export const usersAppRouter = router({ numHighlights, }; }), + settings: authedProcedure + .output(zUserSettingsSchema) + .query(async ({ ctx }) => { + const settings = await ctx.db.query.userSettings.findFirst({ + where: eq(userSettings.userId, ctx.user.id), + }); + if (!settings) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User settings not found", + }); + } + return { + bookmarkClickAction: settings.bookmarkClickAction, + archiveDisplayBehaviour: settings.archiveDisplayBehaviour, + }; + }), + updateSettings: authedProcedure + .input(zUpdateUserSettingsSchema) + .mutation(async ({ input, ctx }) => { + if (Object.keys(input).length === 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "No settings provided", + }); + } + await ctx.db + .update(userSettings) + .set({ + bookmarkClickAction: input.bookmarkClickAction, + archiveDisplayBehaviour: input.archiveDisplayBehaviour, + }) + .where(eq(userSettings.userId, ctx.user.id)); + }), }); |
