{sourceTitle}
+ ) : null} ++ + {item.title} + +
+ {item.excerpt ? ( +{item.excerpt}
+ ) : null} +{publishedAt}
: null} + {showActions ?diff --git a/CHANGELOG.md b/CHANGELOG.md
index 205b00d..3598b49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+- Add user-curated RSS and Atom subscriptions with staged Review, Saved, and Dismissed collections.
+- Add source filtering, newest/oldest sorting, cursor-based infinite loading, and responsive feed management.
+- Add per-feed pause, future-only Auto-save, health warnings, and confirmed deletion that preserves saved articles.
+- Add autonomous conditional polling with failure backoff, bounded retention, SSRF-safe fetching, and parser workload limits.
- Add admin dashboard with dedicated layout for account management and service health visibility.
- Add admin account management: list, detail, update name/role, reset password, soft delete, and restore users.
- Expose user role in current-user response; client role gates admin navigation affordance only.
diff --git a/README.md b/README.md
index 4884e49..355e500 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Loreo is a read-it-later app for saving articles worth revisiting, built with se
### Saving & Organization
- Save links with automatic article extraction
+- Subscribe to RSS/Atom feeds and review new items by source before saving
- Favorite articles for quick access
- Archive articles to hide them from your reading list
- Organize with tag groups and tags
@@ -43,6 +44,7 @@ Loreo is a read-it-later app for saving articles worth revisiting, built with se
### Utilities
- CSV import with field mapping
+- RSS feed polling through the existing Redis/BullMQ worker stack
- Docker Compose support, run locally or as separate web/server processes
## Why Loreo?
diff --git a/apps/server/.env.example b/apps/server/.env.example
index f674623..8b8dfce 100644
--- a/apps/server/.env.example
+++ b/apps/server/.env.example
@@ -34,6 +34,10 @@ RATE_LIMIT_MAX=50
# Body size limit (in bytes)
BODY_SIZE_LIMIT=102400
+# RSS polling scheduler
+FEED_POLL_SCAN_INTERVAL_MS=60000
+FEED_POLL_SCAN_BATCH_SIZE=100
+
# Public/storage URLs
PUBLIC_URL=http://localhost:3000
STORAGE_PROVIDER=local
diff --git a/apps/server/scripts/migrate-test.ts b/apps/server/scripts/migrate-test.ts
index 0d35bc0..79b44c8 100644
--- a/apps/server/scripts/migrate-test.ts
+++ b/apps/server/scripts/migrate-test.ts
@@ -23,6 +23,7 @@ if (!env.DATABASE_DB.includes('test')) {
console.log('Resetting test database schema...');
await pool.query('DROP SCHEMA IF EXISTS public CASCADE;');
+await pool.query('DROP SCHEMA IF EXISTS drizzle CASCADE;');
await pool.query('CREATE SCHEMA public;');
console.log('Test database schema reset complete.');
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index fb6d4aa..d9020ea 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -5,6 +5,8 @@ import createApp from './lib/create-app.js';
import type { Repos } from './lib/types.js';
import { createDrizzleAuthAdapter } from './repositories/auth.repository.js';
+import { createDrizzleFeedItemsAdapter } from './repositories/feed-items.repository.js';
+import { createDrizzleFeedSubscriptionsAdapter } from './repositories/feed-subscriptions.repository.js';
import { createDrizzleHighlightsAdapter } from './repositories/highlights.repository.js';
import { createDrizzleImportSessionsAdapter } from './repositories/import-sessions.repository.js';
import { createDrizzleLinksAdapter } from './repositories/links.repository.js';
@@ -12,6 +14,7 @@ import { createDrizzleTagsAdapter } from './repositories/tags.repository.js';
import admin from './routes/admin/admin.index.js';
import auth from './routes/auth/auth.index.js';
+import feeds from './routes/feeds/feeds.index.js';
import files from './routes/files/files.index.js';
import health from './routes/health/health.index.js';
import highlights from './routes/highlights/highlights.index.js';
@@ -24,6 +27,8 @@ const app = createApp();
const repos: Repos = {
auth: createDrizzleAuthAdapter(db),
+ feedItems: createDrizzleFeedItemsAdapter(db),
+ feedSubscriptions: createDrizzleFeedSubscriptionsAdapter(db),
highlights: createDrizzleHighlightsAdapter(db),
importSessions: createDrizzleImportSessionsAdapter(db),
links: createDrizzleLinksAdapter(db),
@@ -43,6 +48,7 @@ const router = app
.route('/', auth)
.route('/', home)
.route('/', links)
+ .route('/', feeds)
.route('/', highlights)
.route('/', tags)
.route('/', files)
diff --git a/apps/server/src/db/migrations/0001_create_rss_feed_tables.sql b/apps/server/src/db/migrations/0001_create_rss_feed_tables.sql
new file mode 100644
index 0000000..636b670
--- /dev/null
+++ b/apps/server/src/db/migrations/0001_create_rss_feed_tables.sql
@@ -0,0 +1,57 @@
+CREATE TABLE "feed_items" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "subscription_id" uuid NOT NULL,
+ "user_id" uuid NOT NULL,
+ "link_id" uuid,
+ "guid" text,
+ "url" text NOT NULL,
+ "normalized_url" text NOT NULL,
+ "title" text NOT NULL,
+ "excerpt" text,
+ "author" text,
+ "published_at" timestamp with time zone,
+ "image_url" text,
+ "state" varchar(20) DEFAULT 'new' NOT NULL,
+ "discovered_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "saved_at" timestamp with time zone,
+ "dismissed_at" timestamp with time zone,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "feed_subscriptions" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" uuid NOT NULL,
+ "feed_url" text NOT NULL,
+ "normalized_feed_url" text NOT NULL,
+ "site_url" text,
+ "title" text NOT NULL,
+ "description" text,
+ "image_url" text,
+ "auto_save" boolean DEFAULT false NOT NULL,
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
+ "last_fetched_at" timestamp with time zone,
+ "last_successful_fetch_at" timestamp with time zone,
+ "next_fetch_after" timestamp with time zone,
+ "last_error" text,
+ "failure_count" integer DEFAULT 0 NOT NULL,
+ "etag" text,
+ "last_modified" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_subscription_id_feed_subscriptions_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."feed_subscriptions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_link_id_links_id_fk" FOREIGN KEY ("link_id") REFERENCES "public"."links"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "feed_subscriptions" ADD CONSTRAINT "feed_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE UNIQUE INDEX "uq_feed_items_subscription_guid" ON "feed_items" USING btree ("subscription_id","guid");--> statement-breakpoint
+CREATE UNIQUE INDEX "uq_feed_items_subscription_normalized_url" ON "feed_items" USING btree ("subscription_id","normalized_url");--> statement-breakpoint
+CREATE INDEX "idx_feed_items_user_state_published" ON "feed_items" USING btree ("user_id","state","published_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "idx_feed_items_user_normalized_url" ON "feed_items" USING btree ("user_id","normalized_url");--> statement-breakpoint
+CREATE INDEX "idx_feed_items_subscription_discovered" ON "feed_items" USING btree ("subscription_id","discovered_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "idx_feed_items_link_id" ON "feed_items" USING btree ("link_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "uq_feed_subscriptions_user_normalized_url" ON "feed_subscriptions" USING btree ("user_id","normalized_feed_url");--> statement-breakpoint
+CREATE INDEX "idx_feed_subscriptions_user_created" ON "feed_subscriptions" USING btree ("user_id","created_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "idx_feed_subscriptions_status_next_fetch" ON "feed_subscriptions" USING btree ("status","next_fetch_after");--> statement-breakpoint
+CREATE INDEX "idx_feed_subscriptions_user_status" ON "feed_subscriptions" USING btree ("user_id","status");
diff --git a/apps/server/src/db/migrations/0002_smooth_rss_feed_tables.sql b/apps/server/src/db/migrations/0002_smooth_rss_feed_tables.sql
new file mode 100644
index 0000000..62eab87
--- /dev/null
+++ b/apps/server/src/db/migrations/0002_smooth_rss_feed_tables.sql
@@ -0,0 +1,86 @@
+CREATE TABLE IF NOT EXISTS "feed_subscriptions" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" uuid NOT NULL,
+ "feed_url" text NOT NULL,
+ "normalized_feed_url" text NOT NULL,
+ "site_url" text,
+ "title" text NOT NULL,
+ "description" text,
+ "image_url" text,
+ "auto_save" boolean DEFAULT false NOT NULL,
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
+ "last_fetched_at" timestamp with time zone,
+ "last_successful_fetch_at" timestamp with time zone,
+ "next_fetch_after" timestamp with time zone,
+ "last_error" text,
+ "failure_count" integer DEFAULT 0 NOT NULL,
+ "etag" text,
+ "last_modified" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "feed_items" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "subscription_id" uuid NOT NULL,
+ "user_id" uuid NOT NULL,
+ "link_id" uuid,
+ "guid" text,
+ "url" text NOT NULL,
+ "normalized_url" text NOT NULL,
+ "title" text NOT NULL,
+ "excerpt" text,
+ "author" text,
+ "published_at" timestamp with time zone,
+ "image_url" text,
+ "state" varchar(20) DEFAULT 'new' NOT NULL,
+ "discovered_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "saved_at" timestamp with time zone,
+ "dismissed_at" timestamp with time zone,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "feed_subscriptions" ADD CONSTRAINT "feed_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_subscription_id_feed_subscriptions_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."feed_subscriptions"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_link_id_links_id_fk" FOREIGN KEY ("link_id") REFERENCES "public"."links"("id") ON DELETE set null ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS "uq_feed_subscriptions_user_normalized_url" ON "feed_subscriptions" USING btree ("user_id","normalized_feed_url");
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_subscriptions_user_created" ON "feed_subscriptions" USING btree ("user_id","created_at" DESC NULLS LAST);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_subscriptions_status_next_fetch" ON "feed_subscriptions" USING btree ("status","next_fetch_after");
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_subscriptions_user_status" ON "feed_subscriptions" USING btree ("user_id","status");
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS "uq_feed_items_subscription_guid" ON "feed_items" USING btree ("subscription_id","guid");
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS "uq_feed_items_subscription_normalized_url" ON "feed_items" USING btree ("subscription_id","normalized_url");
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_items_user_state_published" ON "feed_items" USING btree ("user_id","state","published_at" DESC NULLS LAST);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_items_user_normalized_url" ON "feed_items" USING btree ("user_id","normalized_url");
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_items_subscription_discovered" ON "feed_items" USING btree ("subscription_id","discovered_at" DESC NULLS LAST);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_feed_items_link_id" ON "feed_items" USING btree ("link_id");
diff --git a/apps/server/src/db/migrations/0003_feed_item_pagination_indexes.sql b/apps/server/src/db/migrations/0003_feed_item_pagination_indexes.sql
new file mode 100644
index 0000000..198dd27
--- /dev/null
+++ b/apps/server/src/db/migrations/0003_feed_item_pagination_indexes.sql
@@ -0,0 +1,16 @@
+CREATE INDEX IF NOT EXISTS "idx_feed_items_user_state_effective_date_id"
+ ON "feed_items" USING btree (
+ "user_id",
+ "state",
+ (coalesce("published_at", "discovered_at")),
+ "id"
+ );
+
+CREATE INDEX IF NOT EXISTS "idx_feed_items_user_state_subscription_effective_date_id"
+ ON "feed_items" USING btree (
+ "user_id",
+ "state",
+ "subscription_id",
+ (coalesce("published_at", "discovered_at")),
+ "id"
+ );
diff --git a/apps/server/src/db/migrations/0004_feed_item_pagination_query_shapes.sql b/apps/server/src/db/migrations/0004_feed_item_pagination_query_shapes.sql
new file mode 100644
index 0000000..11f6771
--- /dev/null
+++ b/apps/server/src/db/migrations/0004_feed_item_pagination_query_shapes.sql
@@ -0,0 +1,2 @@
+CREATE INDEX "idx_feed_items_user_effective_date_id" ON "feed_items" USING btree ("user_id",coalesce("published_at", "discovered_at"),"id");--> statement-breakpoint
+CREATE INDEX "idx_feed_items_user_subscription_effective_date_id" ON "feed_items" USING btree ("user_id","subscription_id",coalesce("published_at", "discovered_at"),"id");
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/0005_feed_owner_constraints.sql b/apps/server/src/db/migrations/0005_feed_owner_constraints.sql
new file mode 100644
index 0000000..c7f7da9
--- /dev/null
+++ b/apps/server/src/db/migrations/0005_feed_owner_constraints.sql
@@ -0,0 +1,6 @@
+CREATE UNIQUE INDEX "uq_feed_subscriptions_id_user" ON "feed_subscriptions" USING btree ("id","user_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "uq_links_id_user" ON "links" USING btree ("id","user_id");--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "fk_feed_items_subscription_owner" FOREIGN KEY ("subscription_id","user_id") REFERENCES "public"."feed_subscriptions"("id","user_id") ON DELETE cascade ON UPDATE no action NOT VALID;--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "fk_feed_items_link_owner" FOREIGN KEY ("link_id","user_id") REFERENCES "public"."links"("id","user_id") ON DELETE no action ON UPDATE no action NOT VALID;--> statement-breakpoint
+ALTER TABLE "feed_items" ADD CONSTRAINT "chk_feed_items_state" CHECK ("feed_items"."state" in ('new', 'dismissed', 'saved')) NOT VALID;--> statement-breakpoint
+ALTER TABLE "feed_subscriptions" ADD CONSTRAINT "chk_feed_subscriptions_status" CHECK ("feed_subscriptions"."status" in ('active', 'paused')) NOT VALID;
diff --git a/apps/server/src/db/migrations/meta/0001_snapshot.json b/apps/server/src/db/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000..bc5e58f
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0001_snapshot.json
@@ -0,0 +1,1870 @@
+{
+ "id": "54310944-236e-44a4-9def-127912959169",
+ "prevId": "97687d4b-242c-4e04-ad33-051de3e6bd9e",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.feed_items": {
+ "name": "feed_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_url": {
+ "name": "normalized_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'new'"
+ },
+ "discovered_at": {
+ "name": "discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "saved_at": {
+ "name": "saved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_items_subscription_guid": {
+ "name": "uq_feed_items_subscription_guid",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "guid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_feed_items_subscription_normalized_url": {
+ "name": "uq_feed_items_subscription_normalized_url",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_published": {
+ "name": "idx_feed_items_user_state_published",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_normalized_url": {
+ "name": "idx_feed_items_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_subscription_discovered": {
+ "name": "idx_feed_items_subscription_discovered",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "discovered_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_link_id": {
+ "name": "idx_feed_items_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_items_subscription_id_feed_subscriptions_id_fk": {
+ "name": "feed_items_subscription_id_feed_subscriptions_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "feed_subscriptions",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_user_id_users_id_fk": {
+ "name": "feed_items_user_id_users_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_link_id_links_id_fk": {
+ "name": "feed_items_link_id_links_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.feed_subscriptions": {
+ "name": "feed_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feed_url": {
+ "name": "feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_feed_url": {
+ "name": "normalized_feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_save": {
+ "name": "auto_save",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_successful_fetch_at": {
+ "name": "last_successful_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_fetch_after": {
+ "name": "next_fetch_after",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "etag": {
+ "name": "etag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_subscriptions_user_normalized_url": {
+ "name": "uq_feed_subscriptions_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_feed_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_created": {
+ "name": "idx_feed_subscriptions_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_status_next_fetch": {
+ "name": "idx_feed_subscriptions_status_next_fetch",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_fetch_after",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_status": {
+ "name": "idx_feed_subscriptions_user_status",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_subscriptions_user_id_users_id_fk": {
+ "name": "feed_subscriptions_user_id_users_id_fk",
+ "tableFrom": "feed_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.highlights": {
+ "name": "highlights",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_highlights_link_id": {
+ "name": "idx_highlights_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_user_id": {
+ "name": "idx_highlights_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_link_user": {
+ "name": "idx_highlights_link_user",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_start_offset": {
+ "name": "idx_highlights_start_offset",
+ "columns": [
+ {
+ "expression": "start_offset",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "highlights_link_id_links_id_fk": {
+ "name": "highlights_link_id_links_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "highlights_user_id_users_id_fk": {
+ "name": "highlights_user_id_users_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.import_sessions": {
+ "name": "import_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_rows": {
+ "name": "total_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imported_count": {
+ "name": "imported_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped_count": {
+ "name": "skipped_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extraction_status": {
+ "name": "extraction_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "extraction_progress": {
+ "name": "extraction_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_completed": {
+ "name": "extraction_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_failed": {
+ "name": "extraction_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_import_sessions_user_id": {
+ "name": "idx_import_sessions_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_status": {
+ "name": "idx_import_sessions_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_created_at": {
+ "name": "idx_import_sessions_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "import_sessions_user_id_users_id_fk": {
+ "name": "import_sessions_user_id_users_id_fk",
+ "tableFrom": "import_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.link_tags": {
+ "name": "link_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_link_tags_link_id": {
+ "name": "idx_link_tags_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_tag_id": {
+ "name": "idx_link_tags_tag_id",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_user_id": {
+ "name": "idx_link_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_link_tags_link_tag": {
+ "name": "uq_link_tags_link_tag",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "link_tags_link_id_links_id_fk": {
+ "name": "link_tags_link_id_links_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_tag_id_tags_id_fk": {
+ "name": "link_tags_tag_id_tags_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_user_id_users_id_fk": {
+ "name": "link_tags_user_id_users_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.links": {
+ "name": "links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_content": {
+ "name": "text_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon": {
+ "name": "favicon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reading_time": {
+ "name": "reading_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_progress": {
+ "name": "reading_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "time_spent_reading": {
+ "name": "time_spent_reading",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_favorite": {
+ "name": "is_favorite",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_paywalled": {
+ "name": "is_paywalled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "import_session_id": {
+ "name": "import_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_links_user_id": {
+ "name": "idx_links_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_url": {
+ "name": "idx_links_url",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_title": {
+ "name": "idx_links_title",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_read": {
+ "name": "idx_links_is_read",
+ "columns": [
+ {
+ "expression": "is_read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_favorite": {
+ "name": "idx_links_is_favorite",
+ "columns": [
+ {
+ "expression": "is_favorite",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_archived": {
+ "name": "idx_links_is_archived",
+ "columns": [
+ {
+ "expression": "is_archived",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_priority": {
+ "name": "idx_links_priority",
+ "columns": [
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_status": {
+ "name": "idx_links_processing_status",
+ "columns": [
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_created_at": {
+ "name": "idx_links_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_last_read_at": {
+ "name": "idx_links_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_created": {
+ "name": "idx_links_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_last_read_at": {
+ "name": "idx_links_user_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_import_session_id": {
+ "name": "idx_links_import_session_id",
+ "columns": [
+ {
+ "expression": "import_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_started_at": {
+ "name": "idx_links_processing_started_at",
+ "columns": [
+ {
+ "expression": "processing_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "links_user_id_users_id_fk": {
+ "name": "links_user_id_users_id_fk",
+ "tableFrom": "links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "links_import_session_id_import_sessions_id_fk": {
+ "name": "links_import_session_id_import_sessions_id_fk",
+ "tableFrom": "links",
+ "tableTo": "import_sessions",
+ "columnsFrom": ["import_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_groups": {
+ "name": "tag_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tag_groups_name": {
+ "name": "idx_tag_groups_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tag_groups_user_id": {
+ "name": "idx_tag_groups_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tag_groups_user_id_name": {
+ "name": "uq_tag_groups_user_id_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_groups_user_id_users_id_fk": {
+ "name": "tag_groups_user_id_users_id_fk",
+ "tableFrom": "tag_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tags_user_id": {
+ "name": "idx_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_group_id": {
+ "name": "idx_tags_group_id",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_name": {
+ "name": "idx_tags_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_user_group": {
+ "name": "idx_tags_user_group",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tags_user_group_name": {
+ "name": "uq_tags_user_group_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_group_id_tag_groups_id_fk": {
+ "name": "tags_group_id_tag_groups_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "tag_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tags_user_id_users_id_fk": {
+ "name": "tags_user_id_users_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar": {
+ "name": "avatar",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_users_email": {
+ "name": "idx_users_email",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_settings": {
+ "name": "idx_users_settings",
+ "columns": [
+ {
+ "expression": "settings",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_users_role": {
+ "name": "idx_users_role",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_deleted_at": {
+ "name": "idx_users_deleted_at",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/server/src/db/migrations/meta/0003_snapshot.json b/apps/server/src/db/migrations/meta/0003_snapshot.json
new file mode 100644
index 0000000..d0e3f45
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0003_snapshot.json
@@ -0,0 +1,1942 @@
+{
+ "id": "2f8bb313-f535-462b-a510-1c2b71ab6c3c",
+ "prevId": "54310944-236e-44a4-9def-127912959169",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.feed_items": {
+ "name": "feed_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_url": {
+ "name": "normalized_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'new'"
+ },
+ "discovered_at": {
+ "name": "discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "saved_at": {
+ "name": "saved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_items_subscription_guid": {
+ "name": "uq_feed_items_subscription_guid",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "guid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_feed_items_subscription_normalized_url": {
+ "name": "uq_feed_items_subscription_normalized_url",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_published": {
+ "name": "idx_feed_items_user_state_published",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_effective_date_id": {
+ "name": "idx_feed_items_user_state_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_subscription_effective_date_id": {
+ "name": "idx_feed_items_user_state_subscription_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_normalized_url": {
+ "name": "idx_feed_items_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_subscription_discovered": {
+ "name": "idx_feed_items_subscription_discovered",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "discovered_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_link_id": {
+ "name": "idx_feed_items_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_items_subscription_id_feed_subscriptions_id_fk": {
+ "name": "feed_items_subscription_id_feed_subscriptions_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "feed_subscriptions",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_user_id_users_id_fk": {
+ "name": "feed_items_user_id_users_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_link_id_links_id_fk": {
+ "name": "feed_items_link_id_links_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.feed_subscriptions": {
+ "name": "feed_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feed_url": {
+ "name": "feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_feed_url": {
+ "name": "normalized_feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_save": {
+ "name": "auto_save",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_successful_fetch_at": {
+ "name": "last_successful_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_fetch_after": {
+ "name": "next_fetch_after",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "etag": {
+ "name": "etag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_subscriptions_user_normalized_url": {
+ "name": "uq_feed_subscriptions_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_feed_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_created": {
+ "name": "idx_feed_subscriptions_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_status_next_fetch": {
+ "name": "idx_feed_subscriptions_status_next_fetch",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_fetch_after",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_status": {
+ "name": "idx_feed_subscriptions_user_status",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_subscriptions_user_id_users_id_fk": {
+ "name": "feed_subscriptions_user_id_users_id_fk",
+ "tableFrom": "feed_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.highlights": {
+ "name": "highlights",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_highlights_link_id": {
+ "name": "idx_highlights_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_user_id": {
+ "name": "idx_highlights_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_link_user": {
+ "name": "idx_highlights_link_user",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_start_offset": {
+ "name": "idx_highlights_start_offset",
+ "columns": [
+ {
+ "expression": "start_offset",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "highlights_link_id_links_id_fk": {
+ "name": "highlights_link_id_links_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "highlights_user_id_users_id_fk": {
+ "name": "highlights_user_id_users_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.import_sessions": {
+ "name": "import_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_rows": {
+ "name": "total_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imported_count": {
+ "name": "imported_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped_count": {
+ "name": "skipped_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extraction_status": {
+ "name": "extraction_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "extraction_progress": {
+ "name": "extraction_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_completed": {
+ "name": "extraction_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_failed": {
+ "name": "extraction_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_import_sessions_user_id": {
+ "name": "idx_import_sessions_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_status": {
+ "name": "idx_import_sessions_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_created_at": {
+ "name": "idx_import_sessions_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "import_sessions_user_id_users_id_fk": {
+ "name": "import_sessions_user_id_users_id_fk",
+ "tableFrom": "import_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.link_tags": {
+ "name": "link_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_link_tags_link_id": {
+ "name": "idx_link_tags_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_tag_id": {
+ "name": "idx_link_tags_tag_id",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_user_id": {
+ "name": "idx_link_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_link_tags_link_tag": {
+ "name": "uq_link_tags_link_tag",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "link_tags_link_id_links_id_fk": {
+ "name": "link_tags_link_id_links_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_tag_id_tags_id_fk": {
+ "name": "link_tags_tag_id_tags_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_user_id_users_id_fk": {
+ "name": "link_tags_user_id_users_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.links": {
+ "name": "links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_content": {
+ "name": "text_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon": {
+ "name": "favicon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reading_time": {
+ "name": "reading_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_progress": {
+ "name": "reading_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "time_spent_reading": {
+ "name": "time_spent_reading",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_favorite": {
+ "name": "is_favorite",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_paywalled": {
+ "name": "is_paywalled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "import_session_id": {
+ "name": "import_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_links_user_id": {
+ "name": "idx_links_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_url": {
+ "name": "idx_links_url",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_title": {
+ "name": "idx_links_title",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_read": {
+ "name": "idx_links_is_read",
+ "columns": [
+ {
+ "expression": "is_read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_favorite": {
+ "name": "idx_links_is_favorite",
+ "columns": [
+ {
+ "expression": "is_favorite",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_archived": {
+ "name": "idx_links_is_archived",
+ "columns": [
+ {
+ "expression": "is_archived",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_priority": {
+ "name": "idx_links_priority",
+ "columns": [
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_status": {
+ "name": "idx_links_processing_status",
+ "columns": [
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_created_at": {
+ "name": "idx_links_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_last_read_at": {
+ "name": "idx_links_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_created": {
+ "name": "idx_links_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_last_read_at": {
+ "name": "idx_links_user_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_import_session_id": {
+ "name": "idx_links_import_session_id",
+ "columns": [
+ {
+ "expression": "import_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_started_at": {
+ "name": "idx_links_processing_started_at",
+ "columns": [
+ {
+ "expression": "processing_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "links_user_id_users_id_fk": {
+ "name": "links_user_id_users_id_fk",
+ "tableFrom": "links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "links_import_session_id_import_sessions_id_fk": {
+ "name": "links_import_session_id_import_sessions_id_fk",
+ "tableFrom": "links",
+ "tableTo": "import_sessions",
+ "columnsFrom": ["import_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_groups": {
+ "name": "tag_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tag_groups_name": {
+ "name": "idx_tag_groups_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tag_groups_user_id": {
+ "name": "idx_tag_groups_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tag_groups_user_id_name": {
+ "name": "uq_tag_groups_user_id_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_groups_user_id_users_id_fk": {
+ "name": "tag_groups_user_id_users_id_fk",
+ "tableFrom": "tag_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tags_user_id": {
+ "name": "idx_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_group_id": {
+ "name": "idx_tags_group_id",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_name": {
+ "name": "idx_tags_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_user_group": {
+ "name": "idx_tags_user_group",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tags_user_group_name": {
+ "name": "uq_tags_user_group_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_group_id_tag_groups_id_fk": {
+ "name": "tags_group_id_tag_groups_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "tag_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tags_user_id_users_id_fk": {
+ "name": "tags_user_id_users_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar": {
+ "name": "avatar",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_users_email": {
+ "name": "idx_users_email",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_settings": {
+ "name": "idx_users_settings",
+ "columns": [
+ {
+ "expression": "settings",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_users_role": {
+ "name": "idx_users_role",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_deleted_at": {
+ "name": "idx_users_deleted_at",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/server/src/db/migrations/meta/0004_snapshot.json b/apps/server/src/db/migrations/meta/0004_snapshot.json
new file mode 100644
index 0000000..fc57034
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0004_snapshot.json
@@ -0,0 +1,2002 @@
+{
+ "id": "db71409b-99e9-449b-ae09-9e34f87b19e6",
+ "prevId": "2f8bb313-f535-462b-a510-1c2b71ab6c3c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.feed_items": {
+ "name": "feed_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_url": {
+ "name": "normalized_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'new'"
+ },
+ "discovered_at": {
+ "name": "discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "saved_at": {
+ "name": "saved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_items_subscription_guid": {
+ "name": "uq_feed_items_subscription_guid",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "guid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_feed_items_subscription_normalized_url": {
+ "name": "uq_feed_items_subscription_normalized_url",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_published": {
+ "name": "idx_feed_items_user_state_published",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_effective_date_id": {
+ "name": "idx_feed_items_user_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_effective_date_id": {
+ "name": "idx_feed_items_user_state_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_subscription_effective_date_id": {
+ "name": "idx_feed_items_user_subscription_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_subscription_effective_date_id": {
+ "name": "idx_feed_items_user_state_subscription_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_normalized_url": {
+ "name": "idx_feed_items_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_subscription_discovered": {
+ "name": "idx_feed_items_subscription_discovered",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "discovered_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_link_id": {
+ "name": "idx_feed_items_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_items_subscription_id_feed_subscriptions_id_fk": {
+ "name": "feed_items_subscription_id_feed_subscriptions_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "feed_subscriptions",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_user_id_users_id_fk": {
+ "name": "feed_items_user_id_users_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_link_id_links_id_fk": {
+ "name": "feed_items_link_id_links_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.feed_subscriptions": {
+ "name": "feed_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feed_url": {
+ "name": "feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_feed_url": {
+ "name": "normalized_feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_save": {
+ "name": "auto_save",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_successful_fetch_at": {
+ "name": "last_successful_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_fetch_after": {
+ "name": "next_fetch_after",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "etag": {
+ "name": "etag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_subscriptions_user_normalized_url": {
+ "name": "uq_feed_subscriptions_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_feed_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_created": {
+ "name": "idx_feed_subscriptions_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_status_next_fetch": {
+ "name": "idx_feed_subscriptions_status_next_fetch",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_fetch_after",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_status": {
+ "name": "idx_feed_subscriptions_user_status",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_subscriptions_user_id_users_id_fk": {
+ "name": "feed_subscriptions_user_id_users_id_fk",
+ "tableFrom": "feed_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.highlights": {
+ "name": "highlights",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_highlights_link_id": {
+ "name": "idx_highlights_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_user_id": {
+ "name": "idx_highlights_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_link_user": {
+ "name": "idx_highlights_link_user",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_start_offset": {
+ "name": "idx_highlights_start_offset",
+ "columns": [
+ {
+ "expression": "start_offset",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "highlights_link_id_links_id_fk": {
+ "name": "highlights_link_id_links_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "highlights_user_id_users_id_fk": {
+ "name": "highlights_user_id_users_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.import_sessions": {
+ "name": "import_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_rows": {
+ "name": "total_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imported_count": {
+ "name": "imported_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped_count": {
+ "name": "skipped_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extraction_status": {
+ "name": "extraction_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "extraction_progress": {
+ "name": "extraction_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_completed": {
+ "name": "extraction_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_failed": {
+ "name": "extraction_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_import_sessions_user_id": {
+ "name": "idx_import_sessions_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_status": {
+ "name": "idx_import_sessions_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_created_at": {
+ "name": "idx_import_sessions_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "import_sessions_user_id_users_id_fk": {
+ "name": "import_sessions_user_id_users_id_fk",
+ "tableFrom": "import_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.link_tags": {
+ "name": "link_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_link_tags_link_id": {
+ "name": "idx_link_tags_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_tag_id": {
+ "name": "idx_link_tags_tag_id",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_user_id": {
+ "name": "idx_link_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_link_tags_link_tag": {
+ "name": "uq_link_tags_link_tag",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "link_tags_link_id_links_id_fk": {
+ "name": "link_tags_link_id_links_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_tag_id_tags_id_fk": {
+ "name": "link_tags_tag_id_tags_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_user_id_users_id_fk": {
+ "name": "link_tags_user_id_users_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.links": {
+ "name": "links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_content": {
+ "name": "text_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon": {
+ "name": "favicon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reading_time": {
+ "name": "reading_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_progress": {
+ "name": "reading_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "time_spent_reading": {
+ "name": "time_spent_reading",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_favorite": {
+ "name": "is_favorite",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_paywalled": {
+ "name": "is_paywalled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "import_session_id": {
+ "name": "import_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_links_user_id": {
+ "name": "idx_links_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_url": {
+ "name": "idx_links_url",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_title": {
+ "name": "idx_links_title",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_read": {
+ "name": "idx_links_is_read",
+ "columns": [
+ {
+ "expression": "is_read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_favorite": {
+ "name": "idx_links_is_favorite",
+ "columns": [
+ {
+ "expression": "is_favorite",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_archived": {
+ "name": "idx_links_is_archived",
+ "columns": [
+ {
+ "expression": "is_archived",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_priority": {
+ "name": "idx_links_priority",
+ "columns": [
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_status": {
+ "name": "idx_links_processing_status",
+ "columns": [
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_created_at": {
+ "name": "idx_links_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_last_read_at": {
+ "name": "idx_links_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_created": {
+ "name": "idx_links_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_last_read_at": {
+ "name": "idx_links_user_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_import_session_id": {
+ "name": "idx_links_import_session_id",
+ "columns": [
+ {
+ "expression": "import_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_started_at": {
+ "name": "idx_links_processing_started_at",
+ "columns": [
+ {
+ "expression": "processing_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "links_user_id_users_id_fk": {
+ "name": "links_user_id_users_id_fk",
+ "tableFrom": "links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "links_import_session_id_import_sessions_id_fk": {
+ "name": "links_import_session_id_import_sessions_id_fk",
+ "tableFrom": "links",
+ "tableTo": "import_sessions",
+ "columnsFrom": ["import_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_groups": {
+ "name": "tag_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tag_groups_name": {
+ "name": "idx_tag_groups_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tag_groups_user_id": {
+ "name": "idx_tag_groups_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tag_groups_user_id_name": {
+ "name": "uq_tag_groups_user_id_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_groups_user_id_users_id_fk": {
+ "name": "tag_groups_user_id_users_id_fk",
+ "tableFrom": "tag_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tags_user_id": {
+ "name": "idx_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_group_id": {
+ "name": "idx_tags_group_id",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_name": {
+ "name": "idx_tags_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_user_group": {
+ "name": "idx_tags_user_group",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tags_user_group_name": {
+ "name": "uq_tags_user_group_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_group_id_tag_groups_id_fk": {
+ "name": "tags_group_id_tag_groups_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "tag_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tags_user_id_users_id_fk": {
+ "name": "tags_user_id_users_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar": {
+ "name": "avatar",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_users_email": {
+ "name": "idx_users_email",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_settings": {
+ "name": "idx_users_settings",
+ "columns": [
+ {
+ "expression": "settings",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_users_role": {
+ "name": "idx_users_role",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_deleted_at": {
+ "name": "idx_users_deleted_at",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/server/src/db/migrations/meta/0005_snapshot.json b/apps/server/src/db/migrations/meta/0005_snapshot.json
new file mode 100644
index 0000000..e7a2cc3
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0005_snapshot.json
@@ -0,0 +1,2072 @@
+{
+ "id": "5ac123a5-d46d-4537-a3c5-98d178865d36",
+ "prevId": "db71409b-99e9-449b-ae09-9e34f87b19e6",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.feed_items": {
+ "name": "feed_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_url": {
+ "name": "normalized_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'new'"
+ },
+ "discovered_at": {
+ "name": "discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "saved_at": {
+ "name": "saved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_items_subscription_guid": {
+ "name": "uq_feed_items_subscription_guid",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "guid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_feed_items_subscription_normalized_url": {
+ "name": "uq_feed_items_subscription_normalized_url",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_published": {
+ "name": "idx_feed_items_user_state_published",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_effective_date_id": {
+ "name": "idx_feed_items_user_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_effective_date_id": {
+ "name": "idx_feed_items_user_state_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_subscription_effective_date_id": {
+ "name": "idx_feed_items_user_subscription_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_state_subscription_effective_date_id": {
+ "name": "idx_feed_items_user_state_subscription_effective_date_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"published_at\", \"discovered_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_user_normalized_url": {
+ "name": "idx_feed_items_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_subscription_discovered": {
+ "name": "idx_feed_items_subscription_discovered",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "discovered_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_items_link_id": {
+ "name": "idx_feed_items_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_items_subscription_id_feed_subscriptions_id_fk": {
+ "name": "feed_items_subscription_id_feed_subscriptions_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "feed_subscriptions",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_user_id_users_id_fk": {
+ "name": "feed_items_user_id_users_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feed_items_link_id_links_id_fk": {
+ "name": "feed_items_link_id_links_id_fk",
+ "tableFrom": "feed_items",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "fk_feed_items_subscription_owner": {
+ "name": "fk_feed_items_subscription_owner",
+ "tableFrom": "feed_items",
+ "tableTo": "feed_subscriptions",
+ "columnsFrom": ["subscription_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_feed_items_link_owner": {
+ "name": "fk_feed_items_link_owner",
+ "tableFrom": "feed_items",
+ "tableTo": "links",
+ "columnsFrom": ["link_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "chk_feed_items_state": {
+ "name": "chk_feed_items_state",
+ "value": "\"feed_items\".\"state\" in ('new', 'dismissed', 'saved')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.feed_subscriptions": {
+ "name": "feed_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feed_url": {
+ "name": "feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_feed_url": {
+ "name": "normalized_feed_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_save": {
+ "name": "auto_save",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_successful_fetch_at": {
+ "name": "last_successful_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_fetch_after": {
+ "name": "next_fetch_after",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "etag": {
+ "name": "etag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_feed_subscriptions_id_user": {
+ "name": "uq_feed_subscriptions_id_user",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_feed_subscriptions_user_normalized_url": {
+ "name": "uq_feed_subscriptions_user_normalized_url",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_feed_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_created": {
+ "name": "idx_feed_subscriptions_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_status_next_fetch": {
+ "name": "idx_feed_subscriptions_status_next_fetch",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_fetch_after",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_feed_subscriptions_user_status": {
+ "name": "idx_feed_subscriptions_user_status",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_subscriptions_user_id_users_id_fk": {
+ "name": "feed_subscriptions_user_id_users_id_fk",
+ "tableFrom": "feed_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "chk_feed_subscriptions_status": {
+ "name": "chk_feed_subscriptions_status",
+ "value": "\"feed_subscriptions\".\"status\" in ('active', 'paused')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.highlights": {
+ "name": "highlights",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_highlights_link_id": {
+ "name": "idx_highlights_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_user_id": {
+ "name": "idx_highlights_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_link_user": {
+ "name": "idx_highlights_link_user",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_highlights_start_offset": {
+ "name": "idx_highlights_start_offset",
+ "columns": [
+ {
+ "expression": "start_offset",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "highlights_link_id_links_id_fk": {
+ "name": "highlights_link_id_links_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "highlights_user_id_users_id_fk": {
+ "name": "highlights_user_id_users_id_fk",
+ "tableFrom": "highlights",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.import_sessions": {
+ "name": "import_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_rows": {
+ "name": "total_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imported_count": {
+ "name": "imported_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped_count": {
+ "name": "skipped_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extraction_status": {
+ "name": "extraction_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "extraction_progress": {
+ "name": "extraction_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_completed": {
+ "name": "extraction_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "extraction_failed": {
+ "name": "extraction_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_import_sessions_user_id": {
+ "name": "idx_import_sessions_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_status": {
+ "name": "idx_import_sessions_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_import_sessions_created_at": {
+ "name": "idx_import_sessions_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "import_sessions_user_id_users_id_fk": {
+ "name": "import_sessions_user_id_users_id_fk",
+ "tableFrom": "import_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.link_tags": {
+ "name": "link_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "link_id": {
+ "name": "link_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_link_tags_link_id": {
+ "name": "idx_link_tags_link_id",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_tag_id": {
+ "name": "idx_link_tags_tag_id",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_link_tags_user_id": {
+ "name": "idx_link_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_link_tags_link_tag": {
+ "name": "uq_link_tags_link_tag",
+ "columns": [
+ {
+ "expression": "link_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "link_tags_link_id_links_id_fk": {
+ "name": "link_tags_link_id_links_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "links",
+ "columnsFrom": ["link_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_tag_id_tags_id_fk": {
+ "name": "link_tags_tag_id_tags_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "link_tags_user_id_users_id_fk": {
+ "name": "link_tags_user_id_users_id_fk",
+ "tableFrom": "link_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.links": {
+ "name": "links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_content": {
+ "name": "text_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon": {
+ "name": "favicon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reading_time": {
+ "name": "reading_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_progress": {
+ "name": "reading_progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "time_spent_reading": {
+ "name": "time_spent_reading",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_favorite": {
+ "name": "is_favorite",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_paywalled": {
+ "name": "is_paywalled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "import_session_id": {
+ "name": "import_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uq_links_id_user": {
+ "name": "uq_links_id_user",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_id": {
+ "name": "idx_links_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_url": {
+ "name": "idx_links_url",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_title": {
+ "name": "idx_links_title",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_read": {
+ "name": "idx_links_is_read",
+ "columns": [
+ {
+ "expression": "is_read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_favorite": {
+ "name": "idx_links_is_favorite",
+ "columns": [
+ {
+ "expression": "is_favorite",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_is_archived": {
+ "name": "idx_links_is_archived",
+ "columns": [
+ {
+ "expression": "is_archived",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_priority": {
+ "name": "idx_links_priority",
+ "columns": [
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_status": {
+ "name": "idx_links_processing_status",
+ "columns": [
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_created_at": {
+ "name": "idx_links_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_last_read_at": {
+ "name": "idx_links_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_created": {
+ "name": "idx_links_user_created",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_user_last_read_at": {
+ "name": "idx_links_user_last_read_at",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_import_session_id": {
+ "name": "idx_links_import_session_id",
+ "columns": [
+ {
+ "expression": "import_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_links_processing_started_at": {
+ "name": "idx_links_processing_started_at",
+ "columns": [
+ {
+ "expression": "processing_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "links_user_id_users_id_fk": {
+ "name": "links_user_id_users_id_fk",
+ "tableFrom": "links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "links_import_session_id_import_sessions_id_fk": {
+ "name": "links_import_session_id_import_sessions_id_fk",
+ "tableFrom": "links",
+ "tableTo": "import_sessions",
+ "columnsFrom": ["import_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_groups": {
+ "name": "tag_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tag_groups_name": {
+ "name": "idx_tag_groups_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tag_groups_user_id": {
+ "name": "idx_tag_groups_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tag_groups_user_id_name": {
+ "name": "uq_tag_groups_user_id_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_groups_user_id_users_id_fk": {
+ "name": "tag_groups_user_id_users_id_fk",
+ "tableFrom": "tag_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_tags_user_id": {
+ "name": "idx_tags_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_group_id": {
+ "name": "idx_tags_group_id",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_name": {
+ "name": "idx_tags_name",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_tags_user_group": {
+ "name": "idx_tags_user_group",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uq_tags_user_group_name": {
+ "name": "uq_tags_user_group_name",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_group_id_tag_groups_id_fk": {
+ "name": "tags_group_id_tag_groups_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "tag_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tags_user_id_users_id_fk": {
+ "name": "tags_user_id_users_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar": {
+ "name": "avatar",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_users_email": {
+ "name": "idx_users_email",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_settings": {
+ "name": "idx_users_settings",
+ "columns": [
+ {
+ "expression": "settings",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_users_role": {
+ "name": "idx_users_role",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_users_deleted_at": {
+ "name": "idx_users_deleted_at",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json
index 2533832..f00fc66 100644
--- a/apps/server/src/db/migrations/meta/_journal.json
+++ b/apps/server/src/db/migrations/meta/_journal.json
@@ -8,6 +8,41 @@
"when": 1777355318209,
"tag": "0000_melted_mandroid",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1782600940254,
+ "tag": "0001_create_rss_feed_tables",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1782648000000,
+ "tag": "0002_smooth_rss_feed_tables",
+ "breakpoints": true
+ },
+ {
+ "idx": 3,
+ "version": "7",
+ "when": 1783706400000,
+ "tag": "0003_feed_item_pagination_indexes",
+ "breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "7",
+ "when": 1784366874258,
+ "tag": "0004_feed_item_pagination_query_shapes",
+ "breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1784379225121,
+ "tag": "0005_feed_owner_constraints",
+ "breakpoints": true
}
]
}
diff --git a/apps/server/src/db/migrations/rss-feed-upgrade.test.ts b/apps/server/src/db/migrations/rss-feed-upgrade.test.ts
new file mode 100644
index 0000000..bdc70d4
--- /dev/null
+++ b/apps/server/src/db/migrations/rss-feed-upgrade.test.ts
@@ -0,0 +1,198 @@
+import { readFile } from 'node:fs/promises';
+
+import { sql } from 'drizzle-orm';
+import { describe, expect, it } from 'vitest';
+
+import { db } from '@/db/index.js';
+
+async function applyMigration(filename: string) {
+ const migration = await readFile(new URL(filename, import.meta.url), 'utf8');
+ await db.execute(
+ sql.raw(
+ migration
+ .replaceAll('--> statement-breakpoint', '')
+ .replaceAll('"public"."feed_subscriptions"', '"rss_feed_upgrade_test"."feed_subscriptions"')
+ .replaceAll('"public"."links"', '"rss_feed_upgrade_test"."links"')
+ )
+ );
+}
+
+describe('RSS feed forward migrations', () => {
+ it('upgrades a database where migration 0001 was recorded before feed tables existed', async () => {
+ await db.execute(sql`drop schema if exists rss_feed_upgrade_test cascade`);
+ await db.execute(sql`create schema rss_feed_upgrade_test`);
+ await db.execute(sql`set local search_path to rss_feed_upgrade_test, public`);
+ await db.execute(sql`
+ create table rss_feed_upgrade_test.links (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid not null references public.users(id) on delete cascade
+ )
+ `);
+
+ await applyMigration('./0002_smooth_rss_feed_tables.sql');
+ await applyMigration('./0003_feed_item_pagination_indexes.sql');
+ await applyMigration('./0004_feed_item_pagination_query_shapes.sql');
+
+ const userA = '30000000-0000-0000-0000-000000000001';
+ const userB = '30000000-0000-0000-0000-000000000002';
+ await db.execute(sql`
+ insert into public.users (id, email, password_hash, name, settings)
+ values
+ (${userA}::uuid, 'rss-upgrade-a@example.com', 'hash', 'Upgrade A', '{}'::jsonb),
+ (${userB}::uuid, 'rss-upgrade-b@example.com', 'hash', 'Upgrade B', '{}'::jsonb)
+ on conflict (id) do nothing
+ `);
+ await db.execute(sql`
+ insert into rss_feed_upgrade_test.feed_subscriptions (
+ id, user_id, feed_url, normalized_feed_url, title, status
+ ) values
+ ('30000000-0000-0000-0000-000000000010', ${userA}::uuid,
+ 'https://example.com/feed', 'https://example.com/feed', 'Valid legacy feed', 'active'),
+ ('30000000-0000-0000-0000-000000000011', ${userA}::uuid,
+ 'https://example.com/invalid', 'https://example.com/invalid', 'Invalid legacy feed', 'legacy')
+ `);
+ await db.execute(sql`
+ insert into rss_feed_upgrade_test.links (id, user_id)
+ values ('30000000-0000-0000-0000-000000000020', ${userB}::uuid)
+ `);
+ await db.execute(sql`
+ insert into rss_feed_upgrade_test.feed_items (
+ id, subscription_id, user_id, link_id, url, normalized_url, title, state
+ ) values
+ ('30000000-0000-0000-0000-000000000030',
+ '30000000-0000-0000-0000-000000000010', ${userB}::uuid, null,
+ 'https://example.com/cross-sub', 'https://example.com/cross-sub',
+ 'Legacy cross-owner subscription', 'new'),
+ ('30000000-0000-0000-0000-000000000031',
+ '30000000-0000-0000-0000-000000000010', ${userA}::uuid,
+ '30000000-0000-0000-0000-000000000020',
+ 'https://example.com/cross-link', 'https://example.com/cross-link',
+ 'Legacy cross-owner link', 'saved'),
+ ('30000000-0000-0000-0000-000000000032',
+ '30000000-0000-0000-0000-000000000010', ${userA}::uuid, null,
+ 'https://example.com/invalid-state', 'https://example.com/invalid-state',
+ 'Legacy invalid state', 'legacy')
+ `);
+
+ await applyMigration('./0005_feed_owner_constraints.sql');
+
+ const tableResult = await db.execute<{
+ feedItems: string | null;
+ feedSubscriptions: string | null;
+ }>(sql`
+ select
+ to_regclass('rss_feed_upgrade_test.feed_items')::text as "feedItems",
+ to_regclass('rss_feed_upgrade_test.feed_subscriptions')::text as "feedSubscriptions"
+ `);
+ expect(tableResult.rows[0]).toEqual({
+ feedItems: 'feed_items',
+ feedSubscriptions: 'feed_subscriptions'
+ });
+
+ const indexResult = await db.execute<{ indexname: string }>(sql`
+ select indexname
+ from pg_indexes
+ where schemaname = 'rss_feed_upgrade_test' and tablename = 'feed_items'
+ `);
+ const indexes = indexResult.rows.map(({ indexname }) => indexname);
+
+ expect(indexes).toEqual(
+ expect.arrayContaining([
+ 'idx_feed_items_user_effective_date_id',
+ 'idx_feed_items_user_state_effective_date_id',
+ 'idx_feed_items_user_subscription_effective_date_id',
+ 'idx_feed_items_user_state_subscription_effective_date_id'
+ ])
+ );
+
+ const constraintResult = await db.execute<{ conname: string }>(sql`
+ select constraint_name as conname
+ from information_schema.table_constraints
+ where table_schema = 'rss_feed_upgrade_test'
+ and table_name in ('feed_items', 'feed_subscriptions')
+ `);
+ const constraints = constraintResult.rows.map(({ conname }) => conname);
+ expect(constraints).toEqual(
+ expect.arrayContaining([
+ 'chk_feed_items_state',
+ 'chk_feed_subscriptions_status',
+ 'fk_feed_items_link_owner',
+ 'fk_feed_items_subscription_owner'
+ ])
+ );
+
+ const validationResult = await db.execute<{ conname: string; validated: boolean }>(sql`
+ select conname, convalidated as validated
+ from pg_constraint
+ where conname in (
+ 'chk_feed_items_state',
+ 'chk_feed_subscriptions_status',
+ 'fk_feed_items_link_owner',
+ 'fk_feed_items_subscription_owner'
+ )
+ and conrelid in (
+ 'rss_feed_upgrade_test.feed_items'::regclass,
+ 'rss_feed_upgrade_test.feed_subscriptions'::regclass
+ )
+ order by conname
+ `);
+ expect(validationResult.rows).toEqual([
+ { conname: 'chk_feed_items_state', validated: false },
+ { conname: 'chk_feed_subscriptions_status', validated: false },
+ { conname: 'fk_feed_items_link_owner', validated: false },
+ { conname: 'fk_feed_items_subscription_owner', validated: false }
+ ]);
+
+ const legacyViolations = await db.execute<{ violations: number }>(sql`
+ select count(*)::int as violations
+ from rss_feed_upgrade_test.feed_items item
+ left join rss_feed_upgrade_test.feed_subscriptions subscription
+ on subscription.id = item.subscription_id and subscription.user_id = item.user_id
+ left join rss_feed_upgrade_test.links link
+ on link.id = item.link_id and link.user_id = item.user_id
+ where subscription.id is null
+ or (item.link_id is not null and link.id is null)
+ or item.state not in ('new', 'dismissed', 'saved')
+ `);
+ expect(legacyViolations.rows[0]?.violations).toBe(3);
+
+ await db.execute(sql`
+ delete from rss_feed_upgrade_test.feed_items
+ where id in (
+ '30000000-0000-0000-0000-000000000030',
+ '30000000-0000-0000-0000-000000000031',
+ '30000000-0000-0000-0000-000000000032'
+ )
+ `);
+ await db.execute(sql`
+ delete from rss_feed_upgrade_test.feed_subscriptions
+ where id = '30000000-0000-0000-0000-000000000011'
+ `);
+ await db.execute(sql`
+ alter table rss_feed_upgrade_test.feed_items
+ validate constraint fk_feed_items_subscription_owner;
+ alter table rss_feed_upgrade_test.feed_items
+ validate constraint fk_feed_items_link_owner;
+ alter table rss_feed_upgrade_test.feed_items
+ validate constraint chk_feed_items_state;
+ alter table rss_feed_upgrade_test.feed_subscriptions
+ validate constraint chk_feed_subscriptions_status;
+ `);
+
+ const validatedResult = await db.execute<{ validated: boolean }>(sql`
+ select bool_and(convalidated) as validated
+ from pg_constraint
+ where conname in (
+ 'chk_feed_items_state',
+ 'chk_feed_subscriptions_status',
+ 'fk_feed_items_link_owner',
+ 'fk_feed_items_subscription_owner'
+ )
+ and conrelid in (
+ 'rss_feed_upgrade_test.feed_items'::regclass,
+ 'rss_feed_upgrade_test.feed_subscriptions'::regclass
+ )
+ `);
+ expect(validatedResult.rows[0]?.validated).toBe(true);
+ });
+});
diff --git a/apps/server/src/db/schemas/feed-items.ts b/apps/server/src/db/schemas/feed-items.ts
new file mode 100644
index 0000000..7fdeed4
--- /dev/null
+++ b/apps/server/src/db/schemas/feed-items.ts
@@ -0,0 +1,108 @@
+import { sql } from 'drizzle-orm';
+import {
+ check,
+ foreignKey,
+ index,
+ pgTable,
+ text,
+ timestamp,
+ uniqueIndex,
+ uuid,
+ varchar
+} from 'drizzle-orm/pg-core';
+import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
+
+import { feedSubscriptionsTable } from './feed-subscriptions.js';
+import { linksTable } from './links.js';
+import { usersTable } from './users.js';
+
+export const feedItemsTable = pgTable(
+ 'feed_items',
+ {
+ id: uuid('id').primaryKey().notNull().defaultRandom(),
+ subscriptionId: uuid('subscription_id')
+ .notNull()
+ .references(() => feedSubscriptionsTable.id, { onDelete: 'cascade' }),
+ userId: uuid('user_id')
+ .notNull()
+ .references(() => usersTable.id, { onDelete: 'cascade' }),
+ linkId: uuid('link_id').references(() => linksTable.id, { onDelete: 'set null' }),
+ guid: text('guid'),
+ url: text('url').notNull(),
+ normalizedUrl: text('normalized_url').notNull(),
+ title: text('title').notNull(),
+ excerpt: text('excerpt'),
+ author: text('author'),
+ publishedAt: timestamp('published_at', { withTimezone: true }),
+ imageUrl: text('image_url'),
+ state: varchar('state', { enum: ['new', 'dismissed', 'saved'], length: 20 })
+ .notNull()
+ .default('new'),
+ discoveredAt: timestamp('discovered_at', { withTimezone: true }).notNull().defaultNow(),
+ savedAt: timestamp('saved_at', { withTimezone: true }),
+ dismissedAt: timestamp('dismissed_at', { withTimezone: true }),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
+ },
+ (table) => [
+ check('chk_feed_items_state', sql`${table.state} in ('new', 'dismissed', 'saved')`),
+ foreignKey({
+ columns: [table.subscriptionId, table.userId],
+ foreignColumns: [feedSubscriptionsTable.id, feedSubscriptionsTable.userId],
+ name: 'fk_feed_items_subscription_owner'
+ }).onDelete('cascade'),
+ foreignKey({
+ columns: [table.linkId, table.userId],
+ foreignColumns: [linksTable.id, linksTable.userId],
+ name: 'fk_feed_items_link_owner'
+ }),
+ uniqueIndex('uq_feed_items_subscription_guid').on(table.subscriptionId, table.guid),
+ uniqueIndex('uq_feed_items_subscription_normalized_url').on(
+ table.subscriptionId,
+ table.normalizedUrl
+ ),
+ index('idx_feed_items_user_state_published').on(
+ table.userId,
+ table.state,
+ table.publishedAt.desc()
+ ),
+ index('idx_feed_items_user_effective_date_id').on(
+ table.userId,
+ sql`coalesce(${table.publishedAt}, ${table.discoveredAt})`,
+ table.id
+ ),
+ index('idx_feed_items_user_state_effective_date_id').on(
+ table.userId,
+ table.state,
+ sql`coalesce(${table.publishedAt}, ${table.discoveredAt})`,
+ table.id
+ ),
+ index('idx_feed_items_user_subscription_effective_date_id').on(
+ table.userId,
+ table.subscriptionId,
+ sql`coalesce(${table.publishedAt}, ${table.discoveredAt})`,
+ table.id
+ ),
+ index('idx_feed_items_user_state_subscription_effective_date_id').on(
+ table.userId,
+ table.state,
+ table.subscriptionId,
+ sql`coalesce(${table.publishedAt}, ${table.discoveredAt})`,
+ table.id
+ ),
+ index('idx_feed_items_user_normalized_url').on(table.userId, table.normalizedUrl),
+ index('idx_feed_items_subscription_discovered').on(
+ table.subscriptionId,
+ table.discoveredAt.desc()
+ ),
+ index('idx_feed_items_link_id').on(table.linkId)
+ ]
+);
+
+export const selectFeedItemsSchema = createSelectSchema(feedItemsTable);
+
+export const insertFeedItemsSchema = createInsertSchema(feedItemsTable).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true
+});
diff --git a/apps/server/src/db/schemas/feed-subscriptions.ts b/apps/server/src/db/schemas/feed-subscriptions.ts
new file mode 100644
index 0000000..fc45654
--- /dev/null
+++ b/apps/server/src/db/schemas/feed-subscriptions.ts
@@ -0,0 +1,64 @@
+import { sql } from 'drizzle-orm';
+import {
+ boolean,
+ check,
+ index,
+ integer,
+ pgTable,
+ text,
+ timestamp,
+ uniqueIndex,
+ uuid,
+ varchar
+} from 'drizzle-orm/pg-core';
+import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
+
+import { usersTable } from './users.js';
+
+export const feedSubscriptionsTable = pgTable(
+ 'feed_subscriptions',
+ {
+ id: uuid('id').primaryKey().notNull().defaultRandom(),
+ userId: uuid('user_id')
+ .notNull()
+ .references(() => usersTable.id, { onDelete: 'cascade' }),
+ feedUrl: text('feed_url').notNull(),
+ normalizedFeedUrl: text('normalized_feed_url').notNull(),
+ siteUrl: text('site_url'),
+ title: text('title').notNull(),
+ description: text('description'),
+ imageUrl: text('image_url'),
+ autoSave: boolean('auto_save').notNull().default(false),
+ status: varchar('status', { enum: ['active', 'paused'], length: 20 })
+ .notNull()
+ .default('active'),
+ lastFetchedAt: timestamp('last_fetched_at', { withTimezone: true }),
+ lastSuccessfulFetchAt: timestamp('last_successful_fetch_at', { withTimezone: true }),
+ nextFetchAfter: timestamp('next_fetch_after', { withTimezone: true }),
+ lastError: text('last_error'),
+ failureCount: integer('failure_count').notNull().default(0),
+ etag: text('etag'),
+ lastModified: text('last_modified'),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
+ },
+ (table) => [
+ check('chk_feed_subscriptions_status', sql`${table.status} in ('active', 'paused')`),
+ uniqueIndex('uq_feed_subscriptions_id_user').on(table.id, table.userId),
+ uniqueIndex('uq_feed_subscriptions_user_normalized_url').on(
+ table.userId,
+ table.normalizedFeedUrl
+ ),
+ index('idx_feed_subscriptions_user_created').on(table.userId, table.createdAt.desc()),
+ index('idx_feed_subscriptions_status_next_fetch').on(table.status, table.nextFetchAfter),
+ index('idx_feed_subscriptions_user_status').on(table.userId, table.status)
+ ]
+);
+
+export const selectFeedSubscriptionsSchema = createSelectSchema(feedSubscriptionsTable);
+
+export const insertFeedSubscriptionsSchema = createInsertSchema(feedSubscriptionsTable).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true
+});
diff --git a/apps/server/src/db/schemas/index.ts b/apps/server/src/db/schemas/index.ts
index 206e1f5..1a044d6 100644
--- a/apps/server/src/db/schemas/index.ts
+++ b/apps/server/src/db/schemas/index.ts
@@ -1,11 +1,15 @@
import { relations } from 'drizzle-orm';
+import { feedItemsTable } from './feed-items.js';
+import { feedSubscriptionsTable } from './feed-subscriptions.js';
import { highlightsTable } from './highlights.js';
import { importSessionsTable } from './import-sessions.js';
import { linksTable, linkTagsTable } from './links.js';
import { tagGroupsTable, tagsTable } from './tags.js';
import { usersTable } from './users.js';
+export * from './feed-items.js';
+export * from './feed-subscriptions.js';
export * from './highlights.js';
export * from './import-sessions.js';
export * from './links.js';
@@ -14,6 +18,8 @@ export * from './user-settings.js';
export * from './users.js';
export const userRelations = relations(usersTable, ({ many }) => ({
+ feedItems: many(feedItemsTable),
+ feedSubscriptions: many(feedSubscriptionsTable),
highlights: many(highlightsTable),
links: many(linksTable),
linkTags: many(linkTagsTable),
@@ -41,10 +47,34 @@ export const linksRelations = relations(linksTable, ({ many, one }) => ({
fields: [linksTable.importSessionId],
references: [importSessionsTable.id]
}),
+ feedItems: many(feedItemsTable),
highlights: many(highlightsTable),
linkTags: many(linkTagsTable)
}));
+export const feedSubscriptionsRelations = relations(feedSubscriptionsTable, ({ many, one }) => ({
+ feedItems: many(feedItemsTable),
+ user: one(usersTable, {
+ fields: [feedSubscriptionsTable.userId],
+ references: [usersTable.id]
+ })
+}));
+
+export const feedItemsRelations = relations(feedItemsTable, ({ one }) => ({
+ link: one(linksTable, {
+ fields: [feedItemsTable.linkId],
+ references: [linksTable.id]
+ }),
+ subscription: one(feedSubscriptionsTable, {
+ fields: [feedItemsTable.subscriptionId],
+ references: [feedSubscriptionsTable.id]
+ }),
+ user: one(usersTable, {
+ fields: [feedItemsTable.userId],
+ references: [usersTable.id]
+ })
+}));
+
export const linkTagRelations = relations(linkTagsTable, ({ one }) => ({
link: one(linksTable, {
fields: [linkTagsTable.linkId],
diff --git a/apps/server/src/db/schemas/links.ts b/apps/server/src/db/schemas/links.ts
index 8504565..86c0a12 100644
--- a/apps/server/src/db/schemas/links.ts
+++ b/apps/server/src/db/schemas/links.ts
@@ -68,6 +68,7 @@ export const linksTable = pgTable(
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [
+ uniqueIndex('uq_links_id_user').on(table.id, table.userId),
index('idx_links_user_id').on(table.userId),
index('idx_links_url').on(table.url),
index('idx_links_title').on(table.title),
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index 65ef136..4693881 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -10,8 +10,15 @@ import { browserService } from './services/browser.service.js';
import app from './app.js';
import { enqueueContentExtraction } from './queues/content-extraction.queue.js';
+import {
+ feedPollSchedulerQueue,
+ registerFeedPollScheduler
+} from './queues/feed-poll-scheduler.queue.js';
+import { enqueueFeedPoll } from './queues/feed-poll.queue.js';
import contentExtractionWorker from './workers/content-extraction.worker.js';
import csvImportWorker from './workers/csv-import.worker.js';
+import feedPollSchedulerWorker from './workers/feed-poll-scheduler.worker.js';
+import feedPollWorker from './workers/feed-poll.worker.js';
if (env.isDevelopment) {
logger.info('Available routes:');
@@ -28,6 +35,11 @@ const server = serve(
}
);
+void registerFeedPollScheduler().catch((error: unknown) => {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.error(`[Queue] Failed to register feed poll scheduler: ${message}`);
+});
+
let isShuttingDown = false;
function onCloseSignal() {
@@ -41,7 +53,11 @@ function onCloseSignal() {
await Promise.all([
contentExtractionWorker.close(),
csvImportWorker.close(),
+ feedPollSchedulerWorker.close(),
+ feedPollWorker.close(),
enqueueContentExtraction.close(),
+ feedPollSchedulerQueue.close(),
+ enqueueFeedPoll.close(),
browserService.close()
]);
diff --git a/apps/server/src/lib/api-client.test.ts b/apps/server/src/lib/api-client.test.ts
index bc7031a..8db017c 100644
--- a/apps/server/src/lib/api-client.test.ts
+++ b/apps/server/src/lib/api-client.test.ts
@@ -1,81 +1,189 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createServer } from 'node:http';
+import { gzipSync } from 'node:zlib';
-import { fetchWithValidatedRedirects } from './api-client.js';
+import { afterEach, describe, expect, it, vi } from 'vitest';
-const isValidUrlMock = vi.hoisted(() => vi.fn());
+import { createValidatedRedirectFetcher } from './api-client.js';
-vi.mock('./url-validator.js', () => ({
- isValidUrl: isValidUrlMock
-}));
+const openServers: ReturnType
+ {errorMessage} +
+ ) : null} +{sourceTitle}
+ ) : null} +{item.excerpt}
+ ) : null} +{publishedAt}
: null} + {showActions ?