aboutsummaryrefslogtreecommitdiffstats
path: root/packages/db/schema.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/db/schema.ts')
-rw-r--r--packages/db/schema.ts139
1 files changed, 135 insertions, 4 deletions
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 8f259d04..2b237e25 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -1,12 +1,13 @@
import type { AdapterAccount } from "@auth/core/adapters";
import { createId } from "@paralleldrive/cuid2";
-import { relations } from "drizzle-orm";
+import { relations, sql, SQL } from "drizzle-orm";
import {
AnySQLiteColumn,
foreignKey,
index,
integer,
primaryKey,
+ real,
sqliteTable,
text,
unique,
@@ -69,6 +70,32 @@ export const users = sqliteTable("user", {
.notNull()
.default("weekly"),
backupsRetentionDays: integer("backupsRetentionDays").notNull().default(30),
+
+ // Reader view settings (nullable = opt-in, null means use client default)
+ readerFontSize: integer("readerFontSize"),
+ readerLineHeight: real("readerLineHeight"),
+ readerFontFamily: text("readerFontFamily", {
+ enum: ["serif", "sans", "mono"],
+ }),
+
+ // AI Settings (nullable = opt-in, null means use server default)
+ autoTaggingEnabled: integer("autoTaggingEnabled", { mode: "boolean" }),
+ autoSummarizationEnabled: integer("autoSummarizationEnabled", {
+ mode: "boolean",
+ }),
+ tagStyle: text("tagStyle", {
+ enum: [
+ "lowercase-hyphens",
+ "lowercase-spaces",
+ "lowercase-underscores",
+ "titlecase-spaces",
+ "titlecase-hyphens",
+ "camelCase",
+ "as-generated",
+ ],
+ }).default("titlecase-spaces"),
+ curatedTagIds: text("curatedTagIds", { mode: "json" }).$type<string[]>(),
+ inferredTagLang: text("inferredTagLang"),
});
export const accounts = sqliteTable(
@@ -142,6 +169,7 @@ export const apiKeys = sqliteTable(
.$defaultFn(() => createId()),
name: text("name").notNull(),
createdAt: createdAtField(),
+ lastUsedAt: integer("lastUsedAt", { mode: "timestamp" }),
keyId: text("keyId").notNull().unique(),
keyHash: text("keyHash").notNull(),
userId: text("userId")
@@ -194,9 +222,21 @@ export const bookmarks = sqliteTable(
},
(b) => [
index("bookmarks_userId_idx").on(b.userId),
- index("bookmarks_archived_idx").on(b.archived),
- index("bookmarks_favourited_idx").on(b.favourited),
index("bookmarks_createdAt_idx").on(b.createdAt),
+ // Composite indexes for optimized pagination queries
+ index("bookmarks_userId_createdAt_id_idx").on(b.userId, b.createdAt, b.id),
+ index("bookmarks_userId_archived_createdAt_id_idx").on(
+ b.userId,
+ b.archived,
+ b.createdAt,
+ b.id,
+ ),
+ index("bookmarks_userId_favourited_createdAt_id_idx").on(
+ b.userId,
+ b.favourited,
+ b.createdAt,
+ b.id,
+ ),
],
);
@@ -233,6 +273,7 @@ export const bookmarkLinks = sqliteTable(
export const enum AssetTypes {
LINK_BANNER_IMAGE = "linkBannerImage",
LINK_SCREENSHOT = "linkScreenshot",
+ LINK_PDF = "linkPdf",
ASSET_SCREENSHOT = "assetScreenshot",
LINK_FULL_PAGE_ARCHIVE = "linkFullPageArchive",
LINK_PRECRAWLED_ARCHIVE = "linkPrecrawledArchive",
@@ -240,6 +281,7 @@ export const enum AssetTypes {
LINK_HTML_CONTENT = "linkHtmlContent",
BOOKMARK_ASSET = "bookmarkAsset",
USER_UPLOADED = "userUploaded",
+ AVATAR = "avatar",
BACKUP = "backup",
UNKNOWN = "unknown",
}
@@ -253,6 +295,7 @@ export const assets = sqliteTable(
enum: [
AssetTypes.LINK_BANNER_IMAGE,
AssetTypes.LINK_SCREENSHOT,
+ AssetTypes.LINK_PDF,
AssetTypes.ASSET_SCREENSHOT,
AssetTypes.LINK_FULL_PAGE_ARCHIVE,
AssetTypes.LINK_PRECRAWLED_ARCHIVE,
@@ -260,6 +303,7 @@ export const assets = sqliteTable(
AssetTypes.LINK_HTML_CONTENT,
AssetTypes.BOOKMARK_ASSET,
AssetTypes.USER_UPLOADED,
+ AssetTypes.AVATAR,
AssetTypes.BACKUP,
AssetTypes.UNKNOWN,
],
@@ -346,6 +390,14 @@ export const bookmarkTags = sqliteTable(
.primaryKey()
.$defaultFn(() => createId()),
name: text("name").notNull(),
+ normalizedName: text("normalizedName").generatedAlwaysAs(
+ (): SQL =>
+ // This function needs to be in sync with the tagNormalizer function in tagging.ts
+ sql`lower(replace(replace(replace(${bookmarkTags.name}, ' ', ''), '-', ''), '_', ''))`,
+ {
+ mode: "virtual",
+ },
+ ),
createdAt: createdAtField(),
userId: text("userId")
.notNull()
@@ -356,6 +408,7 @@ export const bookmarkTags = sqliteTable(
unique("bookmarkTags_userId_id_idx").on(bt.userId, bt.id),
index("bookmarkTags_name_idx").on(bt.name),
index("bookmarkTags_userId_idx").on(bt.userId),
+ index("bookmarkTags_normalizedName_idx").on(bt.normalizedName),
],
);
@@ -378,6 +431,8 @@ export const tagsOnBookmarks = sqliteTable(
primaryKey({ columns: [tb.bookmarkId, tb.tagId] }),
index("tagsOnBookmarks_tagId_idx").on(tb.tagId),
index("tagsOnBookmarks_bookmarkId_idx").on(tb.bookmarkId),
+ // Composite index for tag-first queries (when filtering by tagId)
+ index("tagsOnBookmarks_tagId_bookmarkId_idx").on(tb.tagId, tb.bookmarkId),
],
);
@@ -437,6 +492,11 @@ export const bookmarksInLists = sqliteTable(
primaryKey({ columns: [tb.bookmarkId, tb.listId] }),
index("bookmarksInLists_bookmarkId_idx").on(tb.bookmarkId),
index("bookmarksInLists_listId_idx").on(tb.listId),
+ // Composite index for list-first queries (when filtering by listId)
+ index("bookmarksInLists_listId_bookmarkId_idx").on(
+ tb.listId,
+ tb.bookmarkId,
+ ),
],
);
@@ -584,6 +644,11 @@ export const rssFeedImportsTable = sqliteTable(
index("rssFeedImports_feedIdIdx_idx").on(bl.rssFeedId),
index("rssFeedImports_entryIdIdx_idx").on(bl.entryId),
unique().on(bl.rssFeedId, bl.entryId),
+ // Composite index for RSS feed filter queries (when filtering by rssFeedId)
+ index("rssFeedImports_rssFeedId_bookmarkId_idx").on(
+ bl.rssFeedId,
+ bl.bookmarkId,
+ ),
],
);
@@ -769,10 +834,19 @@ export const importSessions = sqliteTable(
rootListId: text("rootListId").references(() => bookmarkLists.id, {
onDelete: "set null",
}),
+ status: text("status", {
+ enum: ["staging", "pending", "running", "paused", "completed", "failed"],
+ })
+ .notNull()
+ .default("staging"),
+ lastProcessedAt: integer("lastProcessedAt", { mode: "timestamp" }),
createdAt: createdAtField(),
modifiedAt: modifiedAtField(),
},
- (is) => [index("importSessions_userId_idx").on(is.userId)],
+ (is) => [
+ index("importSessions_userId_idx").on(is.userId),
+ index("importSessions_status_idx").on(is.status),
+ ],
);
export const importSessionBookmarks = sqliteTable(
@@ -797,6 +871,63 @@ export const importSessionBookmarks = sqliteTable(
],
);
+export const importStagingBookmarks = sqliteTable(
+ "importStagingBookmarks",
+ {
+ id: text("id")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => createId()),
+ importSessionId: text("importSessionId")
+ .notNull()
+ .references(() => importSessions.id, { onDelete: "cascade" }),
+
+ // Bookmark data to create
+ type: text("type", { enum: ["link", "text", "asset"] }).notNull(),
+ url: text("url"),
+ title: text("title"),
+ content: text("content"),
+ note: text("note"),
+ tags: text("tags", { mode: "json" }).$type<string[]>(),
+ listIds: text("listIds", { mode: "json" }).$type<string[]>(),
+ sourceAddedAt: integer("sourceAddedAt", { mode: "timestamp" }),
+
+ // Processing state
+ status: text("status", {
+ enum: ["pending", "processing", "completed", "failed"],
+ })
+ .notNull()
+ .default("pending"),
+ processingStartedAt: integer("processingStartedAt", {
+ mode: "timestamp",
+ }),
+
+ // Result (for observability)
+ result: text("result", {
+ enum: ["accepted", "rejected", "skipped_duplicate"],
+ }),
+ resultReason: text("resultReason"),
+ resultBookmarkId: text("resultBookmarkId").references(() => bookmarks.id, {
+ onDelete: "set null",
+ }),
+
+ createdAt: createdAtField(),
+ completedAt: integer("completedAt", { mode: "timestamp" }),
+ },
+ (isb) => [
+ index("importStaging_session_status_idx").on(
+ isb.importSessionId,
+ isb.status,
+ ),
+ index("importStaging_completedAt_idx").on(isb.completedAt),
+ index("importStaging_status_idx").on(isb.status),
+ index("importStaging_status_processingStartedAt_idx").on(
+ isb.status,
+ isb.processingStartedAt,
+ ),
+ ],
+);
+
// Relations
export const userRelations = relations(users, ({ many, one }) => ({