This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / cards / tests / test-utils / createTestSchema.ts
12 kB 319 lines
1import { sql } from 'drizzle-orm'; 2import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 3 4export async function createTestSchema(db: PostgresJsDatabase) { 5 // Create extension 6 await db.execute(sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`); 7 8 // Create tables in dependency order using raw SQL with proper column names 9 const tableCreationQueries = [ 10 // Users table (no dependencies) 11 sql`CREATE TABLE IF NOT EXISTS users ( 12 id TEXT PRIMARY KEY, 13 handle TEXT, 14 linked_at TIMESTAMP WITH TIME ZONE NOT NULL, 15 last_login_at TIMESTAMP WITH TIME ZONE NOT NULL 16 )`, 17 18 // Published records table (no dependencies) 19 sql`CREATE TABLE IF NOT EXISTS published_records ( 20 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 21 uri TEXT NOT NULL, 22 cid TEXT NOT NULL, 23 recorded_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 24 UNIQUE(uri, cid) 25 )`, 26 27 // Cards table (references published_records and self-references) 28 sql`CREATE TABLE IF NOT EXISTS cards ( 29 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 30 author_id TEXT NOT NULL, 31 type TEXT NOT NULL, 32 content_data JSONB NOT NULL, 33 url TEXT, 34 url_type TEXT, 35 parent_card_id UUID REFERENCES cards(id), 36 via_card_id UUID REFERENCES cards(id), 37 published_record_id UUID REFERENCES published_records(id), 38 library_count INTEGER NOT NULL DEFAULT 0, 39 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 40 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 41 )`, 42 43 // Library memberships table (references cards and published_records) 44 sql`CREATE TABLE IF NOT EXISTS library_memberships ( 45 card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE, 46 user_id TEXT NOT NULL, 47 added_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 48 published_record_id UUID REFERENCES published_records(id), 49 PRIMARY KEY (card_id, user_id) 50 )`, 51 52 // Collections table (references published_records) 53 sql`CREATE TABLE IF NOT EXISTS collections ( 54 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 55 author_id TEXT NOT NULL, 56 name TEXT NOT NULL, 57 description TEXT, 58 access_type TEXT NOT NULL, 59 card_count INTEGER NOT NULL DEFAULT 0, 60 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 61 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 62 published_record_id UUID REFERENCES published_records(id) 63 )`, 64 65 // Collection collaborators table (references collections) 66 sql`CREATE TABLE IF NOT EXISTS collection_collaborators ( 67 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 68 collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, 69 collaborator_id TEXT NOT NULL, 70 UNIQUE(collection_id, collaborator_id) 71 )`, 72 73 // Collection cards table (references collections, cards, and published_records) 74 sql`CREATE TABLE IF NOT EXISTS collection_cards ( 75 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 76 collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, 77 card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE, 78 added_by TEXT NOT NULL, 79 added_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 80 via_card_id UUID REFERENCES cards(id), 81 published_record_id UUID REFERENCES published_records(id), 82 UNIQUE(collection_id, card_id) 83 )`, 84 sql` 85 CREATE TABLE IF NOT EXISTS feed_activities ( 86 id UUID PRIMARY KEY, 87 actor_id TEXT NOT NULL, 88 card_id TEXT, 89 connection_id TEXT, 90 type TEXT NOT NULL, 91 metadata JSONB NOT NULL, 92 url_type TEXT, 93 source TEXT, 94 created_at TIMESTAMP NOT NULL DEFAULT NOW() 95 )`, 96 97 // Notifications table (no dependencies) 98 sql`CREATE TABLE IF NOT EXISTS notifications ( 99 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 100 recipient_user_id TEXT NOT NULL, 101 actor_user_id TEXT NOT NULL, 102 type TEXT NOT NULL, 103 metadata JSONB NOT NULL, 104 read BOOLEAN NOT NULL DEFAULT false, 105 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 106 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 107 )`, 108 109 // Sync statuses table (no dependencies) 110 sql`CREATE TABLE IF NOT EXISTS sync_statuses ( 111 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 112 curator_id TEXT NOT NULL UNIQUE, 113 sync_state TEXT NOT NULL, 114 last_synced_at TIMESTAMP WITH TIME ZONE, 115 last_sync_attempt_at TIMESTAMP WITH TIME ZONE, 116 sync_error_message TEXT, 117 records_processed INTEGER, 118 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 119 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 120 )`, 121 122 // Follows table (references published_records) 123 sql`CREATE TABLE IF NOT EXISTS follows ( 124 follower_id TEXT NOT NULL, 125 target_id TEXT NOT NULL, 126 target_type TEXT NOT NULL, 127 published_record_id UUID REFERENCES published_records(id), 128 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 129 PRIMARY KEY (follower_id, target_id, target_type) 130 )`, 131 132 // Connections table (references published_records) 133 sql`CREATE TABLE IF NOT EXISTS connections ( 134 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 135 curator_id TEXT NOT NULL, 136 source_type TEXT NOT NULL, 137 source_value TEXT NOT NULL, 138 source_url_metadata JSONB, 139 target_type TEXT NOT NULL, 140 target_value TEXT NOT NULL, 141 target_url_metadata JSONB, 142 connection_type TEXT, 143 note TEXT, 144 published_record_id UUID REFERENCES published_records(id), 145 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 146 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 147 )`, 148 149 // Following feed items table (references feed_activities) 150 sql`CREATE TABLE IF NOT EXISTS following_feed_items ( 151 user_id TEXT NOT NULL, 152 activity_id UUID NOT NULL REFERENCES feed_activities(id) ON DELETE CASCADE, 153 created_at TIMESTAMP NOT NULL, 154 PRIMARY KEY (user_id, activity_id) 155 )`, 156 ]; 157 158 // Execute table creation queries in order 159 for (const query of tableCreationQueries) { 160 await db.execute(query); 161 } 162 163 // Create indexes 164 await db.execute( 165 sql`CREATE INDEX IF NOT EXISTS idx_user_cards ON library_memberships(user_id)`, 166 ); 167 await db.execute( 168 sql`CREATE INDEX IF NOT EXISTS idx_card_users ON library_memberships(card_id)`, 169 ); 170 171 // Performance indexes 172 // Covering index for getUrlCardsOfUser - avoids table lookups by including cardId 173 await db.execute(sql` 174 CREATE INDEX IF NOT EXISTS idx_library_memberships_user_type_covering 175 ON library_memberships(user_id, added_at DESC) 176 INCLUDE (card_id) 177 `); 178 179 // Optimizes sorting cards by type and update time in query results 180 await db.execute(sql` 181 CREATE INDEX IF NOT EXISTS idx_cards_type_updated_at 182 ON cards(type, updated_at DESC) 183 `); 184 185 // Index for getLibrariesForUrl and getCollectionsWithUrl - fast URL+type lookups with card ID included 186 await db.execute(sql` 187 CREATE INDEX IF NOT EXISTS idx_cards_url_type 188 ON cards(url, type) INCLUDE (id) 189 `); 190 191 // Index for filtering by URL type 192 await db.execute(sql` 193 CREATE INDEX IF NOT EXISTS idx_cards_url_type_filter 194 ON cards(url_type) 195 `); 196 197 // Covering index for getCardsInCollection - sorted by add time with cardId included 198 await db.execute(sql` 199 CREATE INDEX IF NOT EXISTS idx_collection_cards_collection_added 200 ON collection_cards(collection_id, added_at DESC) 201 INCLUDE (card_id) 202 `); 203 204 // Partial index for finding NOTE cards by parent - only indexes NOTE type cards 205 await db.execute(sql` 206 CREATE INDEX IF NOT EXISTS idx_cards_parent_type 207 ON cards(parent_card_id, type) WHERE type = 'NOTE' 208 `); 209 210 // Covering index for finding collections containing a card - avoids table lookups 211 await db.execute(sql` 212 CREATE INDEX IF NOT EXISTS idx_collection_cards_card_collection 213 ON collection_cards(card_id) INCLUDE (collection_id) 214 `); 215 // Feed activities indexes 216 await db.execute(sql` 217 CREATE INDEX IF NOT EXISTS feed_activities_type_idx ON feed_activities(type); 218 `); 219 await db.execute(sql` 220 CREATE INDEX IF NOT EXISTS feed_activities_url_type_idx ON feed_activities(url_type); 221 `); 222 await db.execute(sql` 223 CREATE INDEX IF NOT EXISTS feed_activities_created_at_idx ON feed_activities(created_at DESC); 224 `); 225 await db.execute(sql` 226 CREATE INDEX IF NOT EXISTS feed_activities_type_created_at_idx ON feed_activities(type, created_at DESC); 227 `); 228 await db.execute(sql` 229 CREATE INDEX IF NOT EXISTS feed_activities_url_type_created_at_idx ON feed_activities(url_type, created_at DESC); 230 `); 231 await db.execute(sql` 232 CREATE INDEX IF NOT EXISTS feed_activities_type_url_type_created_at_idx ON feed_activities(type, url_type, created_at DESC); 233 `); 234 await db.execute(sql` 235 CREATE INDEX IF NOT EXISTS feed_activities_dedup_idx ON feed_activities(actor_id, card_id, created_at DESC); 236 `); 237 await db.execute(sql` 238 CREATE INDEX IF NOT EXISTS feed_activities_card_id_idx ON feed_activities(card_id); 239 `); 240 await db.execute(sql` 241 CREATE INDEX IF NOT EXISTS feed_activities_connection_id_idx ON feed_activities(connection_id); 242 `); 243 await db.execute(sql` 244 CREATE INDEX IF NOT EXISTS idx_feed_activities_actor_id ON feed_activities(actor_id); 245 `); 246 await db.execute(sql` 247 CREATE INDEX IF NOT EXISTS feed_activities_source_idx ON feed_activities(source); 248 `); 249 250 // Index for efficient AT URI look ups 251 await db.execute(sql` 252 CREATE INDEX IF NOT EXISTS published_records_uri_idx ON published_records(uri); 253 `); 254 255 // Notifications table indexes 256 await db.execute(sql` 257 CREATE INDEX IF NOT EXISTS notifications_recipient_idx ON notifications(recipient_user_id); 258 `); 259 await db.execute(sql` 260 CREATE INDEX IF NOT EXISTS notifications_recipient_created_at_idx ON notifications(recipient_user_id, created_at DESC); 261 `); 262 await db.execute(sql` 263 CREATE INDEX IF NOT EXISTS notifications_recipient_read_idx ON notifications(recipient_user_id, read); 264 `); 265 266 // Cards table indexes 267 await db.execute(sql` 268 CREATE INDEX IF NOT EXISTS cards_author_url_idx ON cards(author_id, url); 269 `); 270 await db.execute(sql` 271 CREATE INDEX IF NOT EXISTS cards_author_id_idx ON cards(author_id); 272 `); 273 274 // Collections table indexes 275 await db.execute(sql` 276 CREATE INDEX IF NOT EXISTS collections_author_id_idx ON collections(author_id); 277 `); 278 await db.execute(sql` 279 CREATE INDEX IF NOT EXISTS collections_author_updated_at_idx ON collections(author_id, updated_at); 280 `); 281 282 // Collection cards table indexes 283 await db.execute(sql` 284 CREATE INDEX IF NOT EXISTS collection_cards_card_id_idx ON collection_cards(card_id); 285 `); 286 await db.execute(sql` 287 CREATE INDEX IF NOT EXISTS collection_cards_collection_id_idx ON collection_cards(collection_id); 288 `); 289 290 // Follows table indexes 291 await db.execute(sql` 292 CREATE INDEX IF NOT EXISTS idx_follows_follower ON follows(follower_id); 293 `); 294 await db.execute(sql` 295 CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_id, target_type); 296 `); 297 298 // Connections table indexes 299 await db.execute(sql` 300 CREATE INDEX IF NOT EXISTS connections_curator_id_idx ON connections(curator_id); 301 `); 302 await db.execute(sql` 303 CREATE INDEX IF NOT EXISTS connections_source_idx ON connections(source_type, source_value); 304 `); 305 await db.execute(sql` 306 CREATE INDEX IF NOT EXISTS connections_target_idx ON connections(target_type, target_value); 307 `); 308 await db.execute(sql` 309 CREATE INDEX IF NOT EXISTS connections_created_at_idx ON connections(created_at DESC); 310 `); 311 await db.execute(sql` 312 CREATE INDEX IF NOT EXISTS connections_curator_created_at_idx ON connections(curator_id, created_at DESC); 313 `); 314 315 // Following feed items indexes 316 await db.execute(sql` 317 CREATE INDEX IF NOT EXISTS idx_following_feed_user_time ON following_feed_items(user_id, created_at DESC); 318 `); 319}