aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/db/drizzle/0019_many_vertigo.sql2
-rw-r--r--packages/db/drizzle/meta/0019_snapshot.json986
-rw-r--r--packages/db/drizzle/meta/_journal.json7
-rw-r--r--packages/db/schema.ts8
-rw-r--r--packages/shared-react/hooks/bookmarks.ts10
-rw-r--r--packages/shared-react/hooks/lists.ts78
-rw-r--r--packages/shared/utils/listUtils.ts55
-rw-r--r--packages/trpc/routers/lists.ts79
8 files changed, 1187 insertions, 38 deletions
diff --git a/packages/db/drizzle/0019_many_vertigo.sql b/packages/db/drizzle/0019_many_vertigo.sql
new file mode 100644
index 00000000..7b58bf85
--- /dev/null
+++ b/packages/db/drizzle/0019_many_vertigo.sql
@@ -0,0 +1,2 @@
+DROP INDEX IF EXISTS `bookmarkLists_name_userId_unique`;--> statement-breakpoint
+ALTER TABLE bookmarkLists ADD `parentId` text REFERENCES bookmarkLists(id) ON DELETE SET NULL;
diff --git a/packages/db/drizzle/meta/0019_snapshot.json b/packages/db/drizzle/meta/0019_snapshot.json
new file mode 100644
index 00000000..b60ba498
--- /dev/null
+++ b/packages/db/drizzle/meta/0019_snapshot.json
@@ -0,0 +1,986 @@
+{
+ "version": "5",
+ "dialect": "sqlite",
+ "id": "3e975d6c-1289-487f-8e78-6f4eda5243d2",
+ "prevId": "ca4ff2bd-aed3-474d-92a1-68e3a8349e01",
+ "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": {}
+ },
+ "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": {}
+ },
+ "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
+ }
+ },
+ "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": {}
+ },
+ "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
+ },
+ "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'"
+ }
+ },
+ "indexes": {},
+ "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": {}
+ },
+ "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
+ },
+ "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
+ },
+ "parentId": {
+ "name": "parentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "bookmarkLists_userId_idx": {
+ "name": "bookmarkLists_userId_idx",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ }
+ },
+ "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": {}
+ },
+ "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
+ }
+ },
+ "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": {}
+ },
+ "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
+ }
+ },
+ "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": {}
+ },
+ "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
+ },
+ "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'"
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "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": {}
+ },
+ "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": {}
+ },
+ "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": {}
+ },
+ "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": [
+ "bookmarkId"
+ ],
+ "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": {}
+ },
+ "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
+ },
+ "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": {}
+ },
+ "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": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+} \ No newline at end of file
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index 89dc6610..67a48b68 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -134,6 +134,13 @@
"when": 1713183014188,
"tag": "0018_bright_infant_terrible",
"breakpoints": true
+ },
+ {
+ "idx": 19,
+ "version": "5",
+ "when": 1713432890859,
+ "tag": "0019_many_vertigo",
+ "breakpoints": true
}
]
} \ No newline at end of file
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 6a7128c0..037be814 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -2,6 +2,7 @@ import type { AdapterAccount } from "@auth/core/adapters";
import { createId } from "@paralleldrive/cuid2";
import { relations } from "drizzle-orm";
import {
+ AnySQLiteColumn,
index,
integer,
primaryKey,
@@ -223,9 +224,10 @@ export const bookmarkLists = sqliteTable(
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
+ parentId: text("parentId")
+ .references((): AnySQLiteColumn => bookmarkLists.id, { onDelete: "set null" }),
},
(bl) => ({
- unq: unique().on(bl.name, bl.userId),
userIdIdx: index("bookmarkLists_userId_idx").on(bl.userId),
}),
);
@@ -318,6 +320,10 @@ export const bookmarkListsRelations = relations(
fields: [bookmarkLists.userId],
references: [users.id],
}),
+ parent: one(bookmarkLists, {
+ fields: [bookmarkLists.parentId],
+ references: [bookmarkLists.id],
+ }),
}),
);
diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts
index 5f246b38..2a549d1f 100644
--- a/packages/shared-react/hooks/bookmarks.ts
+++ b/packages/shared-react/hooks/bookmarks.ts
@@ -8,7 +8,7 @@ export function useCreateBookmarkWithPostHook(
const apiUtils = api.useUtils();
const postCreationCB = useBookmarkPostCreationHook();
return api.bookmarks.createBookmark.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: async (res, req, meta) => {
apiUtils.bookmarks.getBookmarks.invalidate();
apiUtils.bookmarks.searchBookmarks.invalidate();
@@ -23,7 +23,7 @@ export function useDeleteBookmark(
) {
const apiUtils = api.useUtils();
return api.bookmarks.deleteBookmark.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmarks.invalidate();
apiUtils.bookmarks.searchBookmarks.invalidate();
@@ -37,7 +37,7 @@ export function useUpdateBookmark(
) {
const apiUtils = api.useUtils();
return api.bookmarks.updateBookmark.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmarks.invalidate();
apiUtils.bookmarks.searchBookmarks.invalidate();
@@ -52,7 +52,7 @@ export function useRecrawlBookmark(
) {
const apiUtils = api.useUtils();
return api.bookmarks.recrawlBookmark.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
return opts[0]?.onSuccess?.(res, req, meta);
@@ -65,7 +65,7 @@ export function useUpdateBookmarkTags(
) {
const apiUtils = api.useUtils();
return api.bookmarks.updateTags.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts
index d5654f9f..61e50689 100644
--- a/packages/shared-react/hooks/lists.ts
+++ b/packages/shared-react/hooks/lists.ts
@@ -1,11 +1,44 @@
+import { ZBookmarkList } from "@hoarder/shared/types/lists";
+import {
+ listsToTree,
+ ZBookmarkListRoot,
+} from "@hoarder/shared/utils/listUtils";
+
import { api } from "../trpc";
+export function useCreateBookmarkList(
+ ...opts: Parameters<typeof api.lists.create.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.lists.create.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.lists.list.invalidate();
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
+export function useEditBookmarkList(
+ ...opts: Parameters<typeof api.lists.edit.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.lists.edit.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.lists.list.invalidate();
+ apiUtils.lists.get.invalidate({ listId: req.listId });
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
export function useAddBookmarkToList(
...opts: Parameters<typeof api.lists.addToList.useMutation>
) {
const apiUtils = api.useUtils();
return api.lists.addToList.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId });
return opts[0]?.onSuccess?.(res, req, meta);
@@ -18,10 +51,51 @@ export function useRemoveBookmarkFromList(
) {
const apiUtils = api.useUtils();
return api.lists.removeFromList.useMutation({
- ...opts,
+ ...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId });
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
+
+export function useDeleteBookmarkList(
+ ...opts: Parameters<typeof api.lists.delete.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.lists.delete.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.lists.list.invalidate();
+ apiUtils.lists.get.invalidate({ listId: req.listId });
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
+export function useBookmarkLists(
+ ...opts: Parameters<typeof api.lists.list.useQuery>
+) {
+ return api.lists.list.useQuery(opts[0], {
+ ...opts[1],
+ select: (data) => {
+ return { data: data.lists, ...listsToTree(data.lists) };
+ },
+ });
+}
+
+export function augmentBookmarkListsWithInitialData(
+ data:
+ | {
+ data: ZBookmarkList[];
+ root: ZBookmarkListRoot;
+ allPaths: ZBookmarkList[][];
+ }
+ | undefined,
+ initialData: ZBookmarkList[],
+) {
+ if (data) {
+ return data;
+ }
+ return { data: initialData, ...listsToTree(initialData) };
+}
diff --git a/packages/shared/utils/listUtils.ts b/packages/shared/utils/listUtils.ts
new file mode 100644
index 00000000..1dd3d476
--- /dev/null
+++ b/packages/shared/utils/listUtils.ts
@@ -0,0 +1,55 @@
+import { ZBookmarkList } from "../types/lists";
+
+export interface ZBookmarkListTreeNode {
+ item: ZBookmarkList;
+ children: ZBookmarkListTreeNode[];
+}
+
+export type ZBookmarkListRoot = Record<string, ZBookmarkListTreeNode>;
+
+export function listsToTree(lists: ZBookmarkList[]) {
+ const idToList = lists.reduce<Record<string, ZBookmarkList>>((acc, list) => {
+ acc[list.id] = list;
+ return acc;
+ }, {});
+
+ const root: ZBookmarkListRoot = {};
+
+ // Prepare all refs
+ const refIdx = lists.reduce<Record<string, ZBookmarkListTreeNode>>(
+ (acc, l) => {
+ acc[l.id] = {
+ item: l,
+ children: [],
+ };
+ return acc;
+ },
+ {},
+ );
+
+ // Build the tree
+ lists.forEach((list) => {
+ const node = refIdx[list.id];
+ if (list.parentId) {
+ refIdx[list.parentId].children.push(node);
+ } else {
+ root[list.id] = node;
+ }
+ });
+
+ const allPaths: ZBookmarkList[][] = [];
+ const dfs = (node: ZBookmarkListTreeNode, path: ZBookmarkList[]) => {
+ const list = idToList[node.item.id];
+ const newPath = [...path, list];
+ allPaths.push(newPath);
+ node.children.forEach((child) => {
+ dfs(child, newPath);
+ });
+ };
+
+ Object.values(root).forEach((node) => {
+ dfs(node, []);
+ });
+
+ return { allPaths, root };
+}
diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts
index 74b4f737..5cab0ac3 100644
--- a/packages/trpc/routers/lists.ts
+++ b/packages/trpc/routers/lists.ts
@@ -10,6 +10,15 @@ import type { Context } from "../index";
import { authedProcedure, router } from "../index";
import { ensureBookmarkOwnership } from "./bookmarks";
+const zNewBookmarkListSchema = z.object({
+ name: z
+ .string()
+ .min(1, "List name can't be empty")
+ .max(20, "List name is at most 20 chars"),
+ icon: z.string(),
+ parentId: z.string().nullish(),
+});
+
export const ensureListOwnership = experimental_trpcMiddleware<{
ctx: Context;
input: { listId: string };
@@ -44,41 +53,50 @@ export const ensureListOwnership = experimental_trpcMiddleware<{
export const listsAppRouter = router({
create: authedProcedure
+ .input(zNewBookmarkListSchema)
+ .output(zBookmarkListSchema)
+ .mutation(async ({ input, ctx }) => {
+ const [result] = await ctx.db
+ .insert(bookmarkLists)
+ .values({
+ name: input.name,
+ icon: input.icon,
+ userId: ctx.user.id,
+ parentId: input.parentId,
+ })
+ .returning();
+ return result;
+ }),
+ edit: authedProcedure
.input(
- z.object({
- name: z
- .string()
- .min(1, "List name can't be empty")
- .max(20, "List name is at most 20 chars"),
- icon: z.string(),
- }),
+ zNewBookmarkListSchema
+ .partial()
+ .merge(z.object({ listId: z.string() }))
+ .refine((val) => val.parentId != val.listId, {
+ message: "List can't be its own parent",
+ path: ["parentId"],
+ }),
)
.output(zBookmarkListSchema)
.mutation(async ({ input, ctx }) => {
- try {
- const result = await ctx.db
- .insert(bookmarkLists)
- .values({
- name: input.name,
- icon: input.icon,
- userId: ctx.user.id,
- })
- .returning();
- return result[0];
- } catch (e) {
- if (e instanceof SqliteError) {
- if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "List already exists",
- });
- }
- }
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Something went wrong",
- });
+ const result = await ctx.db
+ .update(bookmarkLists)
+ .set({
+ name: input.name,
+ icon: input.icon,
+ parentId: input.parentId,
+ })
+ .where(
+ and(
+ eq(bookmarkLists.id, input.listId),
+ eq(bookmarkLists.userId, ctx.user.id),
+ ),
+ )
+ .returning();
+ if (result.length == 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
}
+ return result[0];
}),
delete: authedProcedure
.input(
@@ -187,6 +205,7 @@ export const listsAppRouter = router({
id: res.id,
name: res.name,
icon: res.icon,
+ parentId: res.parentId,
bookmarks: res.bookmarksInLists.map((b) => b.bookmarkId),
};
}),