aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-10-04 13:40:24 +0100
committerGitHub <noreply@github.com>2025-10-04 13:40:24 +0100
commit4a580d713621f99abb8baabc9b847ce039d44842 (patch)
treea2aa6f3ae8045ad50a9316624e2a7028dd098c6b /packages
parent5e331a7d5b8d9666812170547574804d8b6da741 (diff)
downloadkarakeep-4a580d713621f99abb8baabc9b847ce039d44842.tar.zst
feat: Revamp import experience (#2001)
* WIP: import v2 * remove new session button * don't redirect after import * store and lint to root list * models + tests * redesign the progress * simplify the import session for ow * drop status from session schema * split the import session page * i18n * fix test * remove pagination * fix some colors in darkmode * one last fix * add privacy filter * privacy check * fix interactivity of import progress * fix test
Diffstat (limited to 'packages')
-rw-r--r--packages/db/drizzle/0062_add_import_session.sql25
-rw-r--r--packages/db/drizzle/meta/0062_snapshot.json2489
-rw-r--r--packages/db/drizzle/meta/_journal.json7
-rw-r--r--packages/db/schema.ts70
-rw-r--r--packages/open-api/karakeep-openapi-spec.json3
-rw-r--r--packages/shared/import-export/importer.test.ts12
-rw-r--r--packages/shared/import-export/importer.ts50
-rw-r--r--packages/shared/types/bookmarks.ts1
-rw-r--r--packages/shared/types/importSessions.ts76
-rw-r--r--packages/trpc/models/importSessions.ts180
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/bookmarks.ts15
-rw-r--r--packages/trpc/routers/importSessions.test.ts204
-rw-r--r--packages/trpc/routers/importSessions.ts48
14 files changed, 3163 insertions, 19 deletions
diff --git a/packages/db/drizzle/0062_add_import_session.sql b/packages/db/drizzle/0062_add_import_session.sql
new file mode 100644
index 00000000..ce2823c4
--- /dev/null
+++ b/packages/db/drizzle/0062_add_import_session.sql
@@ -0,0 +1,25 @@
+CREATE TABLE `importSessionBookmarks` (
+ `id` text PRIMARY KEY NOT NULL,
+ `importSessionId` text NOT NULL,
+ `bookmarkId` text NOT NULL,
+ `createdAt` integer NOT NULL,
+ FOREIGN KEY (`importSessionId`) REFERENCES `importSessions`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE INDEX `importSessionBookmarks_sessionId_idx` ON `importSessionBookmarks` (`importSessionId`);--> statement-breakpoint
+CREATE INDEX `importSessionBookmarks_bookmarkId_idx` ON `importSessionBookmarks` (`bookmarkId`);--> statement-breakpoint
+CREATE UNIQUE INDEX `importSessionBookmarks_importSessionId_bookmarkId_unique` ON `importSessionBookmarks` (`importSessionId`,`bookmarkId`);--> statement-breakpoint
+CREATE TABLE `importSessions` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `userId` text NOT NULL,
+ `message` text,
+ `rootListId` text,
+ `createdAt` integer NOT NULL,
+ `modifiedAt` integer,
+ FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`rootListId`) REFERENCES `bookmarkLists`(`id`) ON UPDATE no action ON DELETE set null
+);
+--> statement-breakpoint
+CREATE INDEX `importSessions_userId_idx` ON `importSessions` (`userId`); \ No newline at end of file
diff --git a/packages/db/drizzle/meta/0062_snapshot.json b/packages/db/drizzle/meta/0062_snapshot.json
new file mode 100644
index 00000000..8d8fa60c
--- /dev/null
+++ b/packages/db/drizzle/meta/0062_snapshot.json
@@ -0,0 +1,2489 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "85dc1c66-ccf6-4db8-b722-c5cbaf18f806",
+ "prevId": "e868f89a-f304-428c-8e96-705453facb18",
+ "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
+ },
+ "htmlContent": {
+ "name": "htmlContent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contentAssetId": {
+ "name": "contentAssetId",
+ "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": {}
+ },
+ "importSessionBookmarks": {
+ "name": "importSessionBookmarks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "importSessionId": {
+ "name": "importSessionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bookmarkId": {
+ "name": "bookmarkId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "importSessionBookmarks_sessionId_idx": {
+ "name": "importSessionBookmarks_sessionId_idx",
+ "columns": [
+ "importSessionId"
+ ],
+ "isUnique": false
+ },
+ "importSessionBookmarks_bookmarkId_idx": {
+ "name": "importSessionBookmarks_bookmarkId_idx",
+ "columns": [
+ "bookmarkId"
+ ],
+ "isUnique": false
+ },
+ "importSessionBookmarks_importSessionId_bookmarkId_unique": {
+ "name": "importSessionBookmarks_importSessionId_bookmarkId_unique",
+ "columns": [
+ "importSessionId",
+ "bookmarkId"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "importSessionBookmarks_importSessionId_importSessions_id_fk": {
+ "name": "importSessionBookmarks_importSessionId_importSessions_id_fk",
+ "tableFrom": "importSessionBookmarks",
+ "tableTo": "importSessions",
+ "columnsFrom": [
+ "importSessionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "importSessionBookmarks_bookmarkId_bookmarks_id_fk": {
+ "name": "importSessionBookmarks_bookmarkId_bookmarks_id_fk",
+ "tableFrom": "importSessionBookmarks",
+ "tableTo": "bookmarks",
+ "columnsFrom": [
+ "bookmarkId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "importSessions": {
+ "name": "importSessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rootListId": {
+ "name": "rootListId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "modifiedAt": {
+ "name": "modifiedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "importSessions_userId_idx": {
+ "name": "importSessions_userId_idx",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "importSessions_userId_user_id_fk": {
+ "name": "importSessions_userId_user_id_fk",
+ "tableFrom": "importSessions",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "importSessions_rootListId_bookmarkLists_id_fk": {
+ "name": "importSessions_rootListId_bookmarkLists_id_fk",
+ "tableFrom": "importSessions",
+ "tableTo": "bookmarkLists",
+ "columnsFrom": [
+ "rootListId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "invites": {
+ "name": "invites",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "usedAt": {
+ "name": "usedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "invitedBy": {
+ "name": "invitedBy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "invites_token_unique": {
+ "name": "invites_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "invites_invitedBy_user_id_fk": {
+ "name": "invites_invitedBy_user_id_fk",
+ "tableFrom": "invites",
+ "tableTo": "user",
+ "columnsFrom": [
+ "invitedBy"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "passwordResetToken": {
+ "name": "passwordResetToken",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "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
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "passwordResetToken_token_unique": {
+ "name": "passwordResetToken_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ },
+ "passwordResetTokens_userId_idx": {
+ "name": "passwordResetTokens_userId_idx",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "passwordResetToken_userId_user_id_fk": {
+ "name": "passwordResetToken_userId_user_id_fk",
+ "tableFrom": "passwordResetToken",
+ "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": {}
+ },
+ "subscriptions": {
+ "name": "subscriptions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stripeCustomerId": {
+ "name": "stripeCustomerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stripeSubscriptionId": {
+ "name": "stripeSubscriptionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tier": {
+ "name": "tier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'free'"
+ },
+ "priceId": {
+ "name": "priceId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cancelAtPeriodEnd": {
+ "name": "cancelAtPeriodEnd",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "startDate": {
+ "name": "startDate",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "endDate": {
+ "name": "endDate",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "modifiedAt": {
+ "name": "modifiedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "subscriptions_userId_unique": {
+ "name": "subscriptions_userId_unique",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": true
+ },
+ "subscriptions_userId_idx": {
+ "name": "subscriptions_userId_idx",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ },
+ "subscriptions_stripeCustomerId_idx": {
+ "name": "subscriptions_stripeCustomerId_idx",
+ "columns": [
+ "stripeCustomerId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "subscriptions_userId_user_id_fk": {
+ "name": "subscriptions_userId_user_id_fk",
+ "tableFrom": "subscriptions",
+ "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'"
+ },
+ "bookmarkQuota": {
+ "name": "bookmarkQuota",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "storageQuota": {
+ "name": "storageQuota",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "browserCrawlingEnabled": {
+ "name": "browserCrawlingEnabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "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'"
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'UTC'"
+ }
+ },
+ "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 4c045422..a1e3b96e 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -435,6 +435,13 @@
"when": 1754236017965,
"tag": "0061_merge_user_settings",
"breakpoints": true
+ },
+ {
+ "idx": 62,
+ "version": "6",
+ "when": 1759573697911,
+ "tag": "0062_add_import_session",
+ "breakpoints": true
}
]
} \ No newline at end of file
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index d94d3963..c112b075 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -628,6 +628,49 @@ export const subscriptions = sqliteTable(
],
);
+export const importSessions = sqliteTable(
+ "importSessions",
+ {
+ id: text("id")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => createId()),
+ name: text("name").notNull(),
+ userId: text("userId")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ message: text("message"),
+ rootListId: text("rootListId").references(() => bookmarkLists.id, {
+ onDelete: "set null",
+ }),
+ createdAt: createdAtField(),
+ modifiedAt: modifiedAtField(),
+ },
+ (is) => [index("importSessions_userId_idx").on(is.userId)],
+);
+
+export const importSessionBookmarks = sqliteTable(
+ "importSessionBookmarks",
+ {
+ id: text("id")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => createId()),
+ importSessionId: text("importSessionId")
+ .notNull()
+ .references(() => importSessions.id, { onDelete: "cascade" }),
+ bookmarkId: text("bookmarkId")
+ .notNull()
+ .references(() => bookmarks.id, { onDelete: "cascade" }),
+ createdAt: createdAtField(),
+ },
+ (isb) => [
+ index("importSessionBookmarks_sessionId_idx").on(isb.importSessionId),
+ index("importSessionBookmarks_bookmarkId_idx").on(isb.bookmarkId),
+ unique().on(isb.importSessionId, isb.bookmarkId),
+ ],
+);
+
// Relations
export const userRelations = relations(users, ({ many, one }) => ({
@@ -637,6 +680,7 @@ export const userRelations = relations(users, ({ many, one }) => ({
rules: many(ruleEngineRulesTable),
invites: many(invites),
subscription: one(subscriptions),
+ importSessions: many(importSessions),
}));
export const bookmarkRelations = relations(bookmarks, ({ many, one }) => ({
@@ -660,6 +704,7 @@ export const bookmarkRelations = relations(bookmarks, ({ many, one }) => ({
bookmarksInLists: many(bookmarksInLists),
assets: many(assets),
rssFeeds: many(rssFeedImportsTable),
+ importSessionBookmarks: many(importSessionBookmarks),
}));
export const assetRelations = relations(assets, ({ one }) => ({
@@ -795,3 +840,28 @@ export const passwordResetTokensRelations = relations(
}),
}),
);
+
+export const importSessionsRelations = relations(
+ importSessions,
+ ({ one, many }) => ({
+ user: one(users, {
+ fields: [importSessions.userId],
+ references: [users.id],
+ }),
+ bookmarks: many(importSessionBookmarks),
+ }),
+);
+
+export const importSessionBookmarksRelations = relations(
+ importSessionBookmarks,
+ ({ one }) => ({
+ importSession: one(importSessions, {
+ fields: [importSessionBookmarks.importSessionId],
+ references: [importSessions.id],
+ }),
+ bookmark: one(bookmarks, {
+ fields: [importSessionBookmarks.bookmarkId],
+ references: [bookmarks.id],
+ }),
+ }),
+);
diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json
index ffa9c357..b525d39f 100644
--- a/packages/open-api/karakeep-openapi-spec.json
+++ b/packages/open-api/karakeep-openapi-spec.json
@@ -693,6 +693,9 @@
"low",
"normal"
]
+ },
+ "importSessionId": {
+ "type": "string"
}
}
},
diff --git a/packages/shared/import-export/importer.test.ts b/packages/shared/import-export/importer.test.ts
index 2ea63846..00f892a9 100644
--- a/packages/shared/import-export/importer.test.ts
+++ b/packages/shared/import-export/importer.test.ts
@@ -85,6 +85,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -96,6 +98,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -167,6 +170,7 @@ describe("importBookmarksFromFile", () => {
createBookmark: vi.fn(),
addBookmarkToLists: vi.fn(),
updateBookmarkTags: vi.fn(),
+ createImportSession: vi.fn(async () => ({ id: "session-1" })),
},
},
{ parsers },
@@ -174,6 +178,7 @@ describe("importBookmarksFromFile", () => {
expect(res).toEqual({
counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },
rootListId: null,
+ importSessionId: null,
});
});
@@ -244,6 +249,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -255,6 +262,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -353,6 +361,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -364,6 +374,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -371,6 +382,7 @@ describe("importBookmarksFromFile", () => {
);
expect(res.rootListId).toBe("Imported");
+ expect(res.importSessionId).toBe("session-1");
// All bookmarks are created successfully, but 2 fail in post-processing
expect(res.counts).toEqual({
diff --git a/packages/shared/import-export/importer.ts b/packages/shared/import-export/importer.ts
index 88c0c3bc..b32c49c1 100644
--- a/packages/shared/import-export/importer.ts
+++ b/packages/shared/import-export/importer.ts
@@ -17,6 +17,7 @@ export interface ImportDeps {
}) => Promise<{ id: string }>;
createBookmark: (
bookmark: ParsedBookmark,
+ sessionId: string,
) => Promise<{ id: string; alreadyExists?: boolean }>;
addBookmarkToLists: (input: {
bookmarkId: string;
@@ -26,6 +27,10 @@ export interface ImportDeps {
bookmarkId: string;
tags: string[];
}) => Promise<void>;
+ createImportSession: (input: {
+ name: string;
+ rootListId: string;
+ }) => Promise<{ id: string }>;
}
export interface ImportOptions {
@@ -38,6 +43,7 @@ export interface ImportOptions {
export interface ImportResult {
counts: ImportCounts;
rootListId: string | null;
+ importSessionId: string | null;
}
export async function importBookmarksFromFile(
@@ -66,10 +72,15 @@ export async function importBookmarksFromFile(
return {
counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },
rootListId: null,
+ importSessionId: null,
};
}
const rootList = await deps.createList({ name: rootListName, icon: "⬆️" });
+ const session = await deps.createImportSession({
+ name: `${source.charAt(0).toUpperCase() + source.slice(1)} Import - ${new Date().toLocaleDateString()}`,
+ rootListId: rootList.id,
+ });
onProgress?.(0, parsedBookmarks.length);
@@ -109,22 +120,28 @@ export async function importBookmarksFromFile(
pathMap[pathKey] = folderList.id;
}
+ let done = 0;
const importPromises = parsedBookmarks.map((bookmark) => async () => {
- const listIds = bookmark.paths.map(
- (path) => pathMap[path.join(PATH_DELIMITER)] || rootList.id,
- );
- if (listIds.length === 0) listIds.push(rootList.id);
-
- const created = await deps.createBookmark(bookmark);
- await deps.addBookmarkToLists({ bookmarkId: created.id, listIds });
- if (bookmark.tags && bookmark.tags.length > 0) {
- await deps.updateBookmarkTags({
- bookmarkId: created.id,
- tags: bookmark.tags,
- });
- }
+ try {
+ const listIds = bookmark.paths.map(
+ (path) => pathMap[path.join(PATH_DELIMITER)] || rootList.id,
+ );
+ if (listIds.length === 0) listIds.push(rootList.id);
+
+ const created = await deps.createBookmark(bookmark, session.id);
+ await deps.addBookmarkToLists({ bookmarkId: created.id, listIds });
+ if (bookmark.tags && bookmark.tags.length > 0) {
+ await deps.updateBookmarkTags({
+ bookmarkId: created.id,
+ tags: bookmark.tags,
+ });
+ }
- return created;
+ return created;
+ } finally {
+ done += 1;
+ onProgress?.(done, parsedBookmarks.length);
+ }
});
const resultsPromises = limitConcurrency(importPromises, concurrencyLimit);
@@ -134,7 +151,6 @@ export async function importBookmarksFromFile(
let failures = 0;
let alreadyExisted = 0;
- let done = 0;
for (const r of results) {
if (r.status === "fulfilled") {
if (r.value.alreadyExists) alreadyExisted++;
@@ -142,10 +158,7 @@ export async function importBookmarksFromFile(
} else {
failures++;
}
- done += 1;
- onProgress?.(done, parsedBookmarks.length);
}
-
return {
counts: {
successes,
@@ -154,5 +167,6 @@ export async function importBookmarksFromFile(
total: parsedBookmarks.length,
},
rootListId: rootList.id,
+ importSessionId: session.id,
};
}
diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts
index a22e7632..71cf1012 100644
--- a/packages/shared/types/bookmarks.ts
+++ b/packages/shared/types/bookmarks.ts
@@ -142,6 +142,7 @@ export const zNewBookmarkRequestSchema = z
// A mechanism to prioritize crawling of bookmarks depending on whether
// they were created by a user interaction or by a bulk import.
crawlPriority: z.enum(["low", "normal"]).optional(),
+ importSessionId: z.string().optional(),
})
.and(
z.discriminatedUnion("type", [
diff --git a/packages/shared/types/importSessions.ts b/packages/shared/types/importSessions.ts
new file mode 100644
index 00000000..0c1edd03
--- /dev/null
+++ b/packages/shared/types/importSessions.ts
@@ -0,0 +1,76 @@
+import { z } from "zod";
+
+export const zImportSessionStatusSchema = z.enum([
+ "pending",
+ "in_progress",
+ "completed",
+ "failed",
+]);
+export type ZImportSessionStatus = z.infer<typeof zImportSessionStatusSchema>;
+
+export const zImportSessionBookmarkStatusSchema = z.enum([
+ "pending",
+ "processing",
+ "completed",
+ "failed",
+]);
+export type ZImportSessionBookmarkStatus = z.infer<
+ typeof zImportSessionBookmarkStatusSchema
+>;
+
+export const zImportSessionSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ userId: z.string(),
+ message: z.string().nullable(),
+ rootListId: z.string().nullable(),
+ createdAt: z.date(),
+ modifiedAt: z.date().nullable(),
+});
+export type ZImportSession = z.infer<typeof zImportSessionSchema>;
+
+export const zImportSessionWithStatsSchema = zImportSessionSchema.extend({
+ status: z.enum(["pending", "in_progress", "completed", "failed"]),
+ totalBookmarks: z.number(),
+ completedBookmarks: z.number(),
+ failedBookmarks: z.number(),
+ pendingBookmarks: z.number(),
+ processingBookmarks: z.number(),
+});
+export type ZImportSessionWithStats = z.infer<
+ typeof zImportSessionWithStatsSchema
+>;
+
+export const zCreateImportSessionRequestSchema = z.object({
+ name: z.string().min(1).max(255),
+ rootListId: z.string().optional(),
+});
+export type ZCreateImportSessionRequest = z.infer<
+ typeof zCreateImportSessionRequestSchema
+>;
+
+export const zGetImportSessionStatsRequestSchema = z.object({
+ importSessionId: z.string(),
+});
+export type ZGetImportSessionStatsRequest = z.infer<
+ typeof zGetImportSessionStatsRequestSchema
+>;
+
+export const zListImportSessionsRequestSchema = z.object({});
+export type ZListImportSessionsRequest = z.infer<
+ typeof zListImportSessionsRequestSchema
+>;
+
+export const zListImportSessionsResponseSchema = z.object({
+ sessions: z.array(zImportSessionWithStatsSchema),
+});
+export type ZListImportSessionsResponse = z.infer<
+ typeof zListImportSessionsResponseSchema
+>;
+
+export const zDeleteImportSessionRequestSchema = z.object({
+ importSessionId: z.string(),
+});
+export type ZDeleteImportSessionRequest = z.infer<
+ typeof zDeleteImportSessionRequestSchema
+>;
diff --git a/packages/trpc/models/importSessions.ts b/packages/trpc/models/importSessions.ts
new file mode 100644
index 00000000..270c2bce
--- /dev/null
+++ b/packages/trpc/models/importSessions.ts
@@ -0,0 +1,180 @@
+import { TRPCError } from "@trpc/server";
+import { and, count, eq } from "drizzle-orm";
+import { z } from "zod";
+
+import {
+ bookmarkLinks,
+ bookmarks,
+ importSessionBookmarks,
+ importSessions,
+} from "@karakeep/db/schema";
+import {
+ zCreateImportSessionRequestSchema,
+ ZImportSession,
+ ZImportSessionWithStats,
+} from "@karakeep/shared/types/importSessions";
+
+import type { AuthedContext } from "../index";
+import { PrivacyAware } from "./privacy";
+
+export class ImportSession implements PrivacyAware {
+ protected constructor(
+ protected ctx: AuthedContext,
+ public session: ZImportSession,
+ ) {}
+
+ static async fromId(
+ ctx: AuthedContext,
+ importSessionId: string,
+ ): Promise<ImportSession> {
+ const session = await ctx.db.query.importSessions.findFirst({
+ where: and(
+ eq(importSessions.id, importSessionId),
+ eq(importSessions.userId, ctx.user.id),
+ ),
+ });
+
+ if (!session) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Import session not found",
+ });
+ }
+
+ return new ImportSession(ctx, session);
+ }
+
+ static async create(
+ ctx: AuthedContext,
+ input: z.infer<typeof zCreateImportSessionRequestSchema>,
+ ): Promise<ImportSession> {
+ const [session] = await ctx.db
+ .insert(importSessions)
+ .values({
+ name: input.name,
+ userId: ctx.user.id,
+ rootListId: input.rootListId,
+ })
+ .returning();
+
+ return new ImportSession(ctx, session);
+ }
+
+ static async getAll(ctx: AuthedContext): Promise<ImportSession[]> {
+ const sessions = await ctx.db.query.importSessions.findMany({
+ where: eq(importSessions.userId, ctx.user.id),
+ orderBy: (importSessions, { desc }) => [desc(importSessions.createdAt)],
+ limit: 50,
+ });
+
+ return sessions.map((session) => new ImportSession(ctx, session));
+ }
+
+ static async getAllWithStats(
+ ctx: AuthedContext,
+ ): Promise<ZImportSessionWithStats[]> {
+ const sessions = await this.getAll(ctx);
+
+ return await Promise.all(
+ sessions.map(async (session) => {
+ return await session.getWithStats();
+ }),
+ );
+ }
+
+ ensureCanAccess(ctx: AuthedContext): void {
+ if (this.session.userId !== ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access this import session",
+ });
+ }
+ }
+
+ async attachBookmark(bookmarkId: string): Promise<void> {
+ await this.ctx.db.insert(importSessionBookmarks).values({
+ importSessionId: this.session.id,
+ bookmarkId,
+ });
+ }
+
+ async getWithStats(): Promise<ZImportSessionWithStats> {
+ // Get bookmark counts by status
+ const statusCounts = await this.ctx.db
+ .select({
+ crawlStatus: bookmarkLinks.crawlStatus,
+ taggingStatus: bookmarks.taggingStatus,
+ count: count(),
+ })
+ .from(importSessionBookmarks)
+ .innerJoin(
+ importSessions,
+ eq(importSessions.id, importSessionBookmarks.importSessionId),
+ )
+ .leftJoin(bookmarks, eq(bookmarks.id, importSessionBookmarks.bookmarkId))
+ .leftJoin(
+ bookmarkLinks,
+ eq(bookmarkLinks.id, importSessionBookmarks.bookmarkId),
+ )
+ .where(
+ and(
+ eq(importSessionBookmarks.importSessionId, this.session.id),
+ eq(importSessions.userId, this.ctx.user.id),
+ ),
+ )
+ .groupBy(bookmarkLinks.crawlStatus, bookmarks.taggingStatus);
+
+ const stats = {
+ totalBookmarks: 0,
+ completedBookmarks: 0,
+ failedBookmarks: 0,
+ pendingBookmarks: 0,
+ processingBookmarks: 0,
+ };
+
+ statusCounts.forEach((statusCount) => {
+ stats.totalBookmarks += statusCount.count;
+ if (
+ statusCount.crawlStatus === "success" &&
+ statusCount.taggingStatus === "success"
+ ) {
+ stats.completedBookmarks += statusCount.count;
+ } else if (
+ statusCount.crawlStatus === "failure" ||
+ statusCount.taggingStatus === "failure"
+ ) {
+ stats.failedBookmarks += statusCount.count;
+ } else if (
+ statusCount.crawlStatus === "pending" ||
+ statusCount.taggingStatus === "pending"
+ ) {
+ stats.pendingBookmarks += statusCount.count;
+ }
+ });
+
+ return {
+ ...this.session,
+ status: stats.pendingBookmarks > 0 ? "in_progress" : "completed",
+ ...stats,
+ };
+ }
+
+ async delete(): Promise<void> {
+ // Delete the session (cascade will handle the bookmarks)
+ const result = await this.ctx.db
+ .delete(importSessions)
+ .where(
+ and(
+ eq(importSessions.id, this.session.id),
+ eq(importSessions.userId, this.ctx.user.id),
+ ),
+ );
+
+ if (result.changes === 0) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Import session not found",
+ });
+ }
+ }
+}
diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts
index 651c8d88..1d548ee4 100644
--- a/packages/trpc/routers/_app.ts
+++ b/packages/trpc/routers/_app.ts
@@ -5,6 +5,7 @@ import { assetsAppRouter } from "./assets";
import { bookmarksAppRouter } from "./bookmarks";
import { feedsAppRouter } from "./feeds";
import { highlightsAppRouter } from "./highlights";
+import { importSessionsRouter } from "./importSessions";
import { invitesAppRouter } from "./invites";
import { listsAppRouter } from "./lists";
import { promptsAppRouter } from "./prompts";
@@ -25,6 +26,7 @@ export const appRouter = router({
admin: adminAppRouter,
feeds: feedsAppRouter,
highlights: highlightsAppRouter,
+ importSessions: importSessionsRouter,
webhooks: webhooksAppRouter,
assets: assetsAppRouter,
rules: rulesAppRouter,
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index 3399bf19..be3664b3 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -59,6 +59,7 @@ import { authedProcedure, createRateLimitMiddleware, router } from "../index";
import { mapDBAssetTypeToUserType } from "../lib/attachments";
import { getBookmarkIdsFromMatcher } from "../lib/search";
import { Bookmark } from "../models/bookmarks";
+import { ImportSession } from "../models/importSessions";
import { ensureAssetOwnership } from "./assets";
export const ensureBookmarkOwnership = experimental_trpcMiddleware<{
@@ -272,6 +273,13 @@ export const bookmarksAppRouter = router({
// This doesn't 100% protect from duplicates because of races, but it's more than enough for this usecase.
const alreadyExists = await attemptToDedupLink(ctx, input.url);
if (alreadyExists) {
+ if (input.importSessionId) {
+ const session = await ImportSession.fromId(
+ ctx,
+ input.importSessionId,
+ );
+ await session.attachBookmark(alreadyExists.id);
+ }
return { ...alreadyExists, alreadyExists: true };
}
}
@@ -416,12 +424,16 @@ export const bookmarksAppRouter = router({
};
});
+ if (input.importSessionId) {
+ const session = await ImportSession.fromId(ctx, input.importSessionId);
+ await session.attachBookmark(bookmark.id);
+ }
+
const enqueueOpts: EnqueueOptions = {
// The lower the priority number, the sooner the job will be processed
priority: input.crawlPriority === "low" ? 50 : 0,
};
- // Enqueue crawling request
switch (bookmark.content.type) {
case BookmarkTypes.LINK: {
// The crawling job triggers openai when it's done
@@ -454,6 +466,7 @@ export const bookmarksAppRouter = router({
break;
}
}
+
await triggerRuleEngineOnEvent(
bookmark.id,
[
diff --git a/packages/trpc/routers/importSessions.test.ts b/packages/trpc/routers/importSessions.test.ts
new file mode 100644
index 00000000..b28d1421
--- /dev/null
+++ b/packages/trpc/routers/importSessions.test.ts
@@ -0,0 +1,204 @@
+import { beforeEach, describe, expect, test } from "vitest";
+import { z } from "zod";
+
+import {
+ BookmarkTypes,
+ zNewBookmarkRequestSchema,
+} from "@karakeep/shared/types/bookmarks";
+import {
+ zCreateImportSessionRequestSchema,
+ zDeleteImportSessionRequestSchema,
+ zGetImportSessionStatsRequestSchema,
+} from "@karakeep/shared/types/importSessions";
+import { zNewBookmarkListSchema } from "@karakeep/shared/types/lists";
+
+import type { APICallerType, CustomTestContext } from "../testUtils";
+import { defaultBeforeEach } from "../testUtils";
+
+beforeEach<CustomTestContext>(defaultBeforeEach(true));
+
+describe("ImportSessions Routes", () => {
+ async function createTestBookmark(api: APICallerType, sessionId: string) {
+ const newBookmarkInput: z.infer<typeof zNewBookmarkRequestSchema> = {
+ type: BookmarkTypes.TEXT,
+ text: "Test bookmark text",
+ importSessionId: sessionId,
+ };
+ const createdBookmark =
+ await api.bookmarks.createBookmark(newBookmarkInput);
+ return createdBookmark.id;
+ }
+
+ async function createTestList(api: APICallerType) {
+ const newListInput: z.infer<typeof zNewBookmarkListSchema> = {
+ name: "Test Import List",
+ description: "A test list for imports",
+ icon: "📋",
+ type: "manual",
+ };
+ const createdList = await api.lists.create(newListInput);
+ return createdList.id;
+ }
+
+ test<CustomTestContext>("create import session", async ({ apiCallers }) => {
+ const api = apiCallers[0].importSessions;
+ const listId = await createTestList(apiCallers[0]);
+
+ const newSessionInput: z.infer<typeof zCreateImportSessionRequestSchema> = {
+ name: "Test Import Session",
+ rootListId: listId,
+ };
+
+ const createdSession = await api.createImportSession(newSessionInput);
+
+ expect(createdSession).toMatchObject({
+ id: expect.any(String),
+ });
+
+ // Verify session appears in list
+ const sessions = await api.listImportSessions({});
+ const sessionFromList = sessions.sessions.find(
+ (s) => s.id === createdSession.id,
+ );
+ expect(sessionFromList).toBeDefined();
+ expect(sessionFromList?.name).toEqual(newSessionInput.name);
+ expect(sessionFromList?.rootListId).toEqual(listId);
+ });
+
+ test<CustomTestContext>("create import session without rootListId", async ({
+ apiCallers,
+ }) => {
+ const api = apiCallers[0].importSessions;
+
+ const newSessionInput: z.infer<typeof zCreateImportSessionRequestSchema> = {
+ name: "Test Import Session",
+ };
+
+ const createdSession = await api.createImportSession(newSessionInput);
+
+ expect(createdSession).toMatchObject({
+ id: expect.any(String),
+ });
+
+ // Verify session appears in list
+ const sessions = await api.listImportSessions({});
+ const sessionFromList = sessions.sessions.find(
+ (s) => s.id === createdSession.id,
+ );
+ expect(sessionFromList?.rootListId).toBeNull();
+ });
+
+ test<CustomTestContext>("get import session stats", async ({
+ apiCallers,
+ }) => {
+ const api = apiCallers[0];
+
+ const session = await api.importSessions.createImportSession({
+ name: "Test Import Session",
+ });
+ await createTestBookmark(api, session.id);
+ await createTestBookmark(api, session.id);
+
+ const statsInput: z.infer<typeof zGetImportSessionStatsRequestSchema> = {
+ importSessionId: session.id,
+ };
+
+ const stats = await api.importSessions.getImportSessionStats(statsInput);
+
+ expect(stats).toMatchObject({
+ id: session.id,
+ name: "Test Import Session",
+ status: "in_progress",
+ totalBookmarks: 2,
+ pendingBookmarks: 2,
+ completedBookmarks: 0,
+ failedBookmarks: 0,
+ processingBookmarks: 0,
+ });
+ });
+
+ test<CustomTestContext>("list import sessions returns all sessions", async ({
+ apiCallers,
+ }) => {
+ const api = apiCallers[0].importSessions;
+
+ const sessionNames = ["Session 1", "Session 2", "Session 3"];
+ for (const name of sessionNames) {
+ await api.createImportSession({ name });
+ }
+
+ const result = await api.listImportSessions({});
+
+ expect(result.sessions).toHaveLength(3);
+ expect(result.sessions.map((session) => session.name)).toEqual(
+ sessionNames,
+ );
+ expect(
+ result.sessions.every((session) => session.totalBookmarks === 0),
+ ).toBe(true);
+ });
+
+ test<CustomTestContext>("delete import session", async ({ apiCallers }) => {
+ const api = apiCallers[0].importSessions;
+
+ const session = await api.createImportSession({
+ name: "Session to Delete",
+ });
+
+ const deleteInput: z.infer<typeof zDeleteImportSessionRequestSchema> = {
+ importSessionId: session.id,
+ };
+
+ const result = await api.deleteImportSession(deleteInput);
+ expect(result.success).toBe(true);
+
+ // Verify session no longer exists
+ await expect(
+ api.getImportSessionStats({
+ importSessionId: session.id,
+ }),
+ ).rejects.toThrow("Import session not found");
+ });
+
+ test<CustomTestContext>("cannot access other user's session", async ({
+ apiCallers,
+ }) => {
+ const api1 = apiCallers[0].importSessions;
+ const api2 = apiCallers[1].importSessions;
+
+ // User 1 creates a session
+ const session = await api1.createImportSession({
+ name: "User 1 Session",
+ });
+
+ // User 2 tries to access it
+ await expect(
+ api2.getImportSessionStats({
+ importSessionId: session.id,
+ }),
+ ).rejects.toThrow("Import session not found");
+
+ await expect(
+ api2.deleteImportSession({
+ importSessionId: session.id,
+ }),
+ ).rejects.toThrow("Import session not found");
+ });
+
+ test<CustomTestContext>("cannot attach other user's bookmark", async ({
+ apiCallers,
+ }) => {
+ const api1 = apiCallers[0];
+ const api2 = apiCallers[1];
+
+ // User 1 creates session and bookmark
+ const session = await api1.importSessions.createImportSession({
+ name: "User 1 Session",
+ });
+
+ // User 1 tries to attach User 2's bookmark
+ await expect(
+ createTestBookmark(api2, session.id), // User 2's bookmark
+ ).rejects.toThrow("Import session not found");
+ });
+});
diff --git a/packages/trpc/routers/importSessions.ts b/packages/trpc/routers/importSessions.ts
new file mode 100644
index 00000000..4bdc4f29
--- /dev/null
+++ b/packages/trpc/routers/importSessions.ts
@@ -0,0 +1,48 @@
+import { z } from "zod";
+
+import {
+ zCreateImportSessionRequestSchema,
+ zDeleteImportSessionRequestSchema,
+ zGetImportSessionStatsRequestSchema,
+ zImportSessionWithStatsSchema,
+ zListImportSessionsRequestSchema,
+ zListImportSessionsResponseSchema,
+} from "@karakeep/shared/types/importSessions";
+
+import { authedProcedure, router } from "../index";
+import { ImportSession } from "../models/importSessions";
+
+export const importSessionsRouter = router({
+ createImportSession: authedProcedure
+ .input(zCreateImportSessionRequestSchema)
+ .output(z.object({ id: z.string() }))
+ .mutation(async ({ input, ctx }) => {
+ const session = await ImportSession.create(ctx, input);
+ return { id: session.session.id };
+ }),
+
+ getImportSessionStats: authedProcedure
+ .input(zGetImportSessionStatsRequestSchema)
+ .output(zImportSessionWithStatsSchema)
+ .query(async ({ input, ctx }) => {
+ const session = await ImportSession.fromId(ctx, input.importSessionId);
+ return await session.getWithStats();
+ }),
+
+ listImportSessions: authedProcedure
+ .input(zListImportSessionsRequestSchema)
+ .output(zListImportSessionsResponseSchema)
+ .query(async ({ ctx }) => {
+ const sessions = await ImportSession.getAllWithStats(ctx);
+ return { sessions };
+ }),
+
+ deleteImportSession: authedProcedure
+ .input(zDeleteImportSessionRequestSchema)
+ .output(z.object({ success: z.boolean() }))
+ .mutation(async ({ input, ctx }) => {
+ const session = await ImportSession.fromId(ctx, input.importSessionId);
+ await session.delete();
+ return { success: true };
+ }),
+});