ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

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

refactor: new postgres schema, kysely client, seed data + test

byarielm.fyi e84404cc 3800a81a

verified
+756 -1
+5 -1
packages/api/package.json
··· 4 4 "description": "", 5 5 "main": "index.js", 6 6 "scripts": { 7 - "test": "echo \"Error: no test specified\" && exit 1" 7 + "dev": "tsx watch src/server.ts", 8 + "build": "tsc", 9 + "start": "node dist/server.js", 10 + "test": "echo \"Error: no test specified\" && exit 1", 11 + "test:db": "tsx src/db/test-connection.ts" 8 12 }, 9 13 "keywords": [], 10 14 "author": "",
+79
packages/api/src/db/client.ts
··· 1 + /** 2 + * Kysely Database Client 3 + * Connection pooling and query builder for PostgreSQL 4 + */ 5 + 6 + import { Kysely, PostgresDialect, sql } from "kysely"; 7 + import { Pool } from "pg"; 8 + import type { Database } from "./types"; 9 + 10 + /** 11 + * PostgreSQL connection pool 12 + */ 13 + const pool = new Pool({ 14 + connectionString: process.env.DATABASE_URL, 15 + max: 10, 16 + idleTimeoutMillis: 30000, 17 + connectionTimeoutMillis: 2000, 18 + }); 19 + 20 + /** 21 + * Kysely database instance 22 + */ 23 + export const db = new Kysely<Database>({ 24 + dialect: new PostgresDialect({ pool }), 25 + }); 26 + 27 + /** 28 + * Fuzzy search helper using pg_trgm extension 29 + * 30 + * Searches for usernames similar to the provided username using trigram similarity. 31 + * The % operator calculates similarity, and results are ordered by similarity score. 32 + * 33 + * @param username - The username to search for 34 + * @param limit - Maximum number of results to return (default: 100) 35 + * @returns Array of matching source accounts ordered by similarity 36 + * 37 + * @example 38 + * ```ts 39 + * const matches = await fuzzySearchUsernames('johndoe', 10); 40 + * // Returns up to 10 accounts similar to 'johndoe' 41 + * ``` 42 + */ 43 + export async function fuzzySearchUsernames( 44 + username: string, 45 + limit: number = 100, 46 + ) { 47 + return db 48 + .selectFrom("source_accounts") 49 + .selectAll() 50 + .where(sql<boolean>`normalized_username % ${username}`) // % is similarity operator 51 + .orderBy(sql`similarity(normalized_username, ${username})`, "desc") 52 + .limit(limit) 53 + .execute(); 54 + } 55 + 56 + /** 57 + * Test database connection 58 + * 59 + * @returns Promise that resolves if connection is successful 60 + * @throws Error if connection fails 61 + */ 62 + export async function testConnection(): Promise<void> { 63 + try { 64 + await sql`SELECT 1`.execute(db); 65 + console.log("✅ Database connection successful"); 66 + } catch (error) { 67 + console.error("❌ Database connection failed:", error); 68 + throw error; 69 + } 70 + } 71 + 72 + /** 73 + * Close database connection pool 74 + * Use this for graceful shutdown 75 + */ 76 + export async function closeConnection(): Promise<void> { 77 + await db.destroy(); 78 + console.log("Database connection pool closed"); 79 + }
+6
packages/api/src/db/index.ts
··· 1 + /** 2 + * Database exports 3 + */ 4 + 5 + export { db, fuzzySearchUsernames, testConnection, closeConnection } from './client'; 6 + export type * from './types';
+93
packages/api/src/db/test-connection.ts
··· 1 + /** 2 + * Database Connection Test Script 3 + * 4 + * Run this script to verify database connectivity and schema. 5 + * Usage: tsx src/db/test-connection.ts 6 + */ 7 + 8 + import { db, testConnection } from './client'; 9 + import { sql } from 'kysely'; 10 + 11 + async function main() { 12 + console.log('🔍 Testing database connection...\n'); 13 + 14 + try { 15 + // Test basic connection 16 + await testConnection(); 17 + 18 + // Check if extensions are installed 19 + console.log('\n📦 Checking extensions...'); 20 + const extensions = await sql<{ extname: string }>` 21 + SELECT extname FROM pg_extension 22 + WHERE extname IN ('uuid-ossp', 'pg_trgm') 23 + `.execute(db); 24 + 25 + console.log(' Installed extensions:', extensions.rows.map(e => e.extname).join(', ')); 26 + 27 + // Check if all tables exist 28 + console.log('\n📋 Checking tables...'); 29 + const tables = await sql<{ tablename: string }>` 30 + SELECT tablename FROM pg_tables 31 + WHERE schemaname = 'public' 32 + ORDER BY tablename 33 + `.execute(db); 34 + 35 + console.log(' Tables found:', tables.rows.length); 36 + tables.rows.forEach(t => console.log(' -', t.tablename)); 37 + 38 + // Check indexes 39 + console.log('\n🔍 Checking indexes...'); 40 + const indexes = await sql<{ indexname: string, tablename: string }>` 41 + SELECT indexname, tablename 42 + FROM pg_indexes 43 + WHERE schemaname = 'public' 44 + AND indexname LIKE 'idx_%' 45 + ORDER BY tablename, indexname 46 + `.execute(db); 47 + 48 + console.log(' Indexes found:', indexes.rows.length); 49 + const groupedIndexes = indexes.rows.reduce((acc, idx) => { 50 + if (!acc[idx.tablename]) acc[idx.tablename] = []; 51 + acc[idx.tablename].push(idx.indexname); 52 + return acc; 53 + }, {} as Record<string, string[]>); 54 + 55 + Object.entries(groupedIndexes).forEach(([table, idxs]) => { 56 + console.log(` ${table}:`); 57 + idxs.forEach(idx => console.log(` - ${idx}`)); 58 + }); 59 + 60 + // Test fuzzy matching capability 61 + console.log('\n🔎 Testing fuzzy matching (pg_trgm)...'); 62 + const testResult = await sql<{ similarity: number }>` 63 + SELECT similarity('johndoe', 'john_doe') as similarity 64 + `.execute(db); 65 + 66 + console.log(' Similarity score:', testResult.rows[0].similarity); 67 + console.log(' ✅ Fuzzy matching is working!'); 68 + 69 + // Count records in each table 70 + console.log('\n📊 Record counts:'); 71 + const counts = await Promise.all([ 72 + db.selectFrom('user_sessions').select(sql`COUNT(*)`.as('count')).executeTakeFirst(), 73 + db.selectFrom('user_uploads').select(sql`COUNT(*)`.as('count')).executeTakeFirst(), 74 + db.selectFrom('source_accounts').select(sql`COUNT(*)`.as('count')).executeTakeFirst(), 75 + db.selectFrom('atproto_matches').select(sql`COUNT(*)`.as('count')).executeTakeFirst(), 76 + ]); 77 + 78 + console.log(' user_sessions:', counts[0]?.count ?? 0); 79 + console.log(' user_uploads:', counts[1]?.count ?? 0); 80 + console.log(' source_accounts:', counts[2]?.count ?? 0); 81 + console.log(' atproto_matches:', counts[3]?.count ?? 0); 82 + 83 + console.log('\n✅ All database checks passed!\n'); 84 + process.exit(0); 85 + } catch (error) { 86 + console.error('\n❌ Database test failed:', error); 87 + process.exit(1); 88 + } finally { 89 + await db.destroy(); 90 + } 91 + } 92 + 93 + main();
+151
packages/api/src/db/types.ts
··· 1 + /** 2 + * Database Schema Types 3 + * Generated from scripts/init-db.sql 4 + */ 5 + 6 + import type { ColumnType } from 'kysely'; 7 + 8 + /** 9 + * Timestamp columns that are auto-generated 10 + */ 11 + export type Generated<T> = T extends ColumnType<infer S, infer I, infer U> 12 + ? ColumnType<S, I | undefined, U> 13 + : ColumnType<T, T | undefined, T>; 14 + 15 + export type Timestamp = ColumnType<Date, Date | string, Date | string>; 16 + 17 + /** 18 + * OAuth State Storage (transient) 19 + */ 20 + export interface OAuthStatesTable { 21 + state: string; 22 + data: Record<string, unknown>; 23 + created_at: Generated<Timestamp>; 24 + } 25 + 26 + /** 27 + * OAuth Sessions (transient) 28 + */ 29 + export interface OAuthSessionsTable { 30 + did: string; 31 + session_data: Record<string, unknown>; 32 + updated_at: Generated<Timestamp>; 33 + } 34 + 35 + /** 36 + * User Sessions (transient) 37 + */ 38 + export interface UserSessionsTable { 39 + session_id: string; 40 + did: string; 41 + fingerprint: string; 42 + created_at: Generated<Timestamp>; 43 + expires_at: Timestamp; 44 + } 45 + 46 + /** 47 + * User Uploads (persistent) 48 + */ 49 + export interface UserUploadsTable { 50 + upload_id: string; 51 + user_did: string; 52 + source_platform: string; 53 + created_at: Generated<Timestamp>; 54 + total_users: Generated<number>; 55 + matched_users: Generated<number>; 56 + unmatched_users: Generated<number>; 57 + } 58 + 59 + /** 60 + * Source Accounts (persistent) 61 + */ 62 + export interface SourceAccountsTable { 63 + id: Generated<number>; 64 + source_platform: string; 65 + original_username: string; 66 + normalized_username: string; 67 + date_on_source: Timestamp | null; 68 + created_at: Generated<Timestamp>; 69 + } 70 + 71 + /** 72 + * User-Source Follows (join table) 73 + */ 74 + export interface UserSourceFollowsTable { 75 + user_did: string; 76 + upload_id: string; 77 + source_account_id: number; 78 + found_at: Generated<Timestamp>; 79 + } 80 + 81 + /** 82 + * AT Protocol Matches (persistent) 83 + */ 84 + export interface AtprotoMatchesTable { 85 + id: Generated<number>; 86 + source_account_id: number; 87 + atproto_did: string; 88 + atproto_handle: string; 89 + display_name: string | null; 90 + match_score: number; 91 + post_count: number | null; 92 + follower_count: number | null; 93 + follow_status: Generated<Record<string, unknown>>; 94 + found_at: Generated<Timestamp>; 95 + } 96 + 97 + /** 98 + * User Match Status (persistent) 99 + */ 100 + export interface UserMatchStatusTable { 101 + user_did: string; 102 + match_id: number; 103 + viewed: Generated<boolean>; 104 + dismissed: Generated<boolean>; 105 + followed: Generated<boolean>; 106 + notified: Generated<boolean>; 107 + updated_at: Generated<Timestamp>; 108 + } 109 + 110 + /** 111 + * Notification Queue (transient - for Phase 2) 112 + */ 113 + export interface NotificationQueueTable { 114 + id: Generated<number>; 115 + user_did: string; 116 + match_id: number; 117 + notification_type: 'in_app' | 'bluesky_dm' | 'partner_api'; 118 + status: Generated<'pending' | 'sent' | 'failed'>; 119 + attempts: Generated<number>; 120 + last_attempt: Timestamp | null; 121 + error_message: string | null; 122 + created_at: Generated<Timestamp>; 123 + } 124 + 125 + /** 126 + * Partner API Keys (for Phase 2) 127 + */ 128 + export interface PartnerApiKeysTable { 129 + id: Generated<number>; 130 + partner_name: string; 131 + api_key_hash: string; 132 + created_at: Generated<Timestamp>; 133 + last_used: Timestamp | null; 134 + is_active: Generated<boolean>; 135 + } 136 + 137 + /** 138 + * Database schema interface 139 + */ 140 + export interface Database { 141 + oauth_states: OAuthStatesTable; 142 + oauth_sessions: OAuthSessionsTable; 143 + user_sessions: UserSessionsTable; 144 + user_uploads: UserUploadsTable; 145 + source_accounts: SourceAccountsTable; 146 + user_source_follows: UserSourceFollowsTable; 147 + atproto_matches: AtprotoMatchesTable; 148 + user_match_status: UserMatchStatusTable; 149 + notification_queue: NotificationQueueTable; 150 + partner_api_keys: PartnerApiKeysTable; 151 + }
+20
packages/api/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "lib": ["ES2022"], 7 + "outDir": "./dist", 8 + "rootDir": "./src", 9 + "strict": true, 10 + "esModuleInterop": true, 11 + "skipLibCheck": true, 12 + "forceConsistentCasingInFileNames": true, 13 + "resolveJsonModule": true, 14 + "isolatedModules": true, 15 + "allowSyntheticDefaultImports": true, 16 + "types": ["node"] 17 + }, 18 + "include": ["src/**/*"], 19 + "exclude": ["node_modules", "dist"] 20 + }
+155
scripts/README.md
··· 1 + # Database Scripts 2 + 3 + This directory contains database schema and utility scripts for the ATlast migration. 4 + 5 + ## Files 6 + 7 + - **`init-db.sql`** - PostgreSQL schema definition (tables, indexes, functions) 8 + - **`seed-test-data.sql`** - Test data for local development 9 + - **`generate-encryption-key.ts`** - OAuth key generation utility 10 + - **`keygen.js`** - Legacy key generation script 11 + 12 + ## Database Setup 13 + 14 + ### Local Development (Docker) 15 + 16 + 1. **Start PostgreSQL container:** 17 + ```bash 18 + cd docker 19 + docker compose up -d database 20 + ``` 21 + 22 + 2. **Initialize schema:** 23 + ```bash 24 + docker compose exec database psql -U atlast -d atlast -f /docker-entrypoint-initdb.d/init.sql 25 + ``` 26 + 27 + Or connect and run manually: 28 + ```bash 29 + docker compose exec -i database psql -U atlast -d atlast < scripts/init-db.sql 30 + ``` 31 + 32 + 3. **Seed test data (optional):** 33 + ```bash 34 + docker compose exec -i database psql -U atlast -d atlast < scripts/seed-test-data.sql 35 + ``` 36 + 37 + 4. **Verify setup:** 38 + ```bash 39 + cd packages/api 40 + DATABASE_URL=postgresql://atlast:password@localhost:5432/atlast pnpm run test:db 41 + ``` 42 + 43 + ### Production Setup 44 + 45 + The database will be automatically initialized when the Docker container starts, as `init-db.sql` is mounted to `/docker-entrypoint-initdb.d/init.sql` in the compose file. 46 + 47 + ## Schema Overview 48 + 49 + ### Transient Tables (Session Data) 50 + - `oauth_states` - OAuth flow state storage 51 + - `oauth_sessions` - OAuth session data 52 + - `user_sessions` - User authentication sessions 53 + - `notification_queue` - Pending notifications (Phase 2) 54 + 55 + **Note:** Transient data is cleaned up daily via the `cleanup_transient_data()` function. 56 + 57 + ### Persistent Tables (User Data) 58 + - `user_uploads` - Upload history and metadata 59 + - `source_accounts` - Usernames from source platforms (Instagram, TikTok, etc.) 60 + - `user_source_follows` - Links users to their source account follows 61 + - `atproto_matches` - Matched AT Protocol accounts 62 + - `user_match_status` - User interaction with matches (viewed, followed, etc.) 63 + - `partner_api_keys` - API keys for partner integrations (Phase 2) 64 + 65 + ## Key Features 66 + 67 + ### Fuzzy Matching 68 + The schema includes the `pg_trgm` extension for fuzzy username matching. This enables: 69 + - Similarity-based searches (`%` operator) 70 + - Trigram GIN indexes for fast fuzzy lookups 71 + - Essential for Phase 2 Tap server matching 72 + 73 + ### Indexes 74 + All tables are indexed for common query patterns: 75 + - Foreign key indexes for joins 76 + - Partial indexes for filtered queries (e.g., unnotified matches) 77 + - GIN indexes for fuzzy text matching 78 + 79 + ### Cleanup Function 80 + The `cleanup_transient_data()` function automatically removes: 81 + - Expired OAuth states (>1 hour old) 82 + - Expired user sessions 83 + - Old notification records (>7 days sent, >30 days failed) 84 + 85 + This runs daily via BullMQ worker in production. 86 + 87 + ## Testing 88 + 89 + ### Test Connection 90 + ```bash 91 + cd packages/api 92 + DATABASE_URL=postgresql://atlast:password@localhost:5432/atlast pnpm run test:db 93 + ``` 94 + 95 + This script verifies: 96 + - Database connectivity 97 + - Required extensions are installed 98 + - All tables exist 99 + - Indexes are created 100 + - Fuzzy matching works 101 + - Displays record counts 102 + 103 + ### Manual Testing 104 + ```bash 105 + # Connect to database 106 + docker compose exec database psql -U atlast 107 + 108 + # List tables 109 + \dt 110 + 111 + # List indexes 112 + \di 113 + 114 + # Check extensions 115 + SELECT * FROM pg_extension WHERE extname IN ('uuid-ossp', 'pg_trgm'); 116 + 117 + # Test fuzzy matching 118 + SELECT similarity('johndoe', 'john_doe'); 119 + 120 + # Run cleanup function 121 + SELECT cleanup_transient_data(); 122 + ``` 123 + 124 + ## Migration Notes 125 + 126 + ### Phase 1 (Current) 127 + - No periodic checking features 128 + - Notification queue exists but is not used until Phase 2 129 + - Partner API keys table exists but is not used until Phase 2 130 + 131 + ### Phase 2 (Future) 132 + - Tap server will use fuzzy matching to detect new accounts 133 + - Notification system will use the notification_queue table 134 + - Partner integrations will use the partner_api_keys table 135 + 136 + ## Troubleshooting 137 + 138 + ### Extensions Not Found 139 + ```sql 140 + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 141 + CREATE EXTENSION IF NOT EXISTS "pg_trgm"; 142 + ``` 143 + 144 + ### Permission Issues 145 + Ensure the database user has necessary permissions: 146 + ```sql 147 + GRANT ALL PRIVILEGES ON DATABASE atlast TO atlast; 148 + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO atlast; 149 + ``` 150 + 151 + ### Connection Refused 152 + Check that: 153 + - PostgreSQL is running: `docker compose ps database` 154 + - Port is exposed: `docker compose port database 5432` 155 + - DATABASE_URL is correct: `postgresql://atlast:password@localhost:5432/atlast`
+151
scripts/init-db.sql
··· 1 + -- ATlast Database Schema 2 + -- Migration Plan v2.0 - Phase 1 3 + -- Self-hosted PostgreSQL schema with fuzzy matching support 4 + 5 + -- Enable extensions 6 + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 7 + CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fuzzy matching 8 + 9 + -- OAuth state storage (transient) 10 + CREATE TABLE oauth_states ( 11 + state TEXT PRIMARY KEY, 12 + data JSONB NOT NULL, 13 + created_at TIMESTAMP DEFAULT NOW() 14 + ); 15 + CREATE INDEX idx_oauth_states_created ON oauth_states(created_at); 16 + 17 + -- OAuth sessions (transient) 18 + CREATE TABLE oauth_sessions ( 19 + did TEXT PRIMARY KEY, 20 + session_data JSONB NOT NULL, 21 + updated_at TIMESTAMP DEFAULT NOW() 22 + ); 23 + 24 + -- User sessions (transient) 25 + CREATE TABLE user_sessions ( 26 + session_id TEXT PRIMARY KEY, 27 + did TEXT NOT NULL, 28 + fingerprint TEXT NOT NULL, 29 + created_at TIMESTAMP DEFAULT NOW(), 30 + expires_at TIMESTAMP NOT NULL 31 + ); 32 + CREATE INDEX idx_user_sessions_did ON user_sessions(did); 33 + CREATE INDEX idx_user_sessions_expires ON user_sessions(expires_at); 34 + 35 + -- User uploads (persistent) 36 + CREATE TABLE user_uploads ( 37 + upload_id TEXT PRIMARY KEY, 38 + user_did TEXT NOT NULL, 39 + source_platform TEXT NOT NULL, 40 + created_at TIMESTAMP DEFAULT NOW(), 41 + total_users INTEGER DEFAULT 0, 42 + matched_users INTEGER DEFAULT 0, 43 + unmatched_users INTEGER DEFAULT 0 44 + ); 45 + CREATE INDEX idx_user_uploads_user_did ON user_uploads(user_did); 46 + -- Note: check_frequency and last_checked removed - no periodic checking in Phase 1 47 + 48 + -- Source accounts (persistent) 49 + CREATE TABLE source_accounts ( 50 + id SERIAL PRIMARY KEY, 51 + source_platform TEXT NOT NULL, 52 + original_username TEXT NOT NULL, 53 + normalized_username TEXT NOT NULL, 54 + date_on_source TIMESTAMP, 55 + created_at TIMESTAMP DEFAULT NOW(), 56 + UNIQUE(source_platform, normalized_username) 57 + ); 58 + CREATE INDEX idx_source_accounts_normalized ON source_accounts 59 + USING gin(normalized_username gin_trgm_ops); -- Fuzzy matching! 60 + CREATE INDEX idx_source_accounts_platform ON source_accounts(source_platform); 61 + 62 + -- User-source follows (join table) 63 + CREATE TABLE user_source_follows ( 64 + user_did TEXT NOT NULL, 65 + upload_id TEXT NOT NULL REFERENCES user_uploads(upload_id) ON DELETE CASCADE, 66 + source_account_id INTEGER NOT NULL REFERENCES source_accounts(id), 67 + found_at TIMESTAMP DEFAULT NOW(), 68 + PRIMARY KEY (upload_id, source_account_id) 69 + ); 70 + CREATE INDEX idx_user_source_follows_user ON user_source_follows(user_did); 71 + CREATE INDEX idx_user_source_follows_source ON user_source_follows(source_account_id); 72 + 73 + -- AT Protocol matches (persistent) 74 + CREATE TABLE atproto_matches ( 75 + id SERIAL PRIMARY KEY, 76 + source_account_id INTEGER NOT NULL REFERENCES source_accounts(id), 77 + atproto_did TEXT NOT NULL, 78 + atproto_handle TEXT NOT NULL, 79 + display_name TEXT, 80 + match_score INTEGER NOT NULL, 81 + post_count INTEGER, 82 + follower_count INTEGER, 83 + follow_status JSONB DEFAULT '{}', 84 + found_at TIMESTAMP DEFAULT NOW(), 85 + UNIQUE(source_account_id, atproto_did) 86 + ); 87 + CREATE INDEX idx_atproto_matches_source ON atproto_matches(source_account_id); 88 + CREATE INDEX idx_atproto_matches_did ON atproto_matches(atproto_did); 89 + CREATE INDEX idx_atproto_matches_score ON atproto_matches(match_score DESC); 90 + 91 + -- User match status (persistent) 92 + CREATE TABLE user_match_status ( 93 + user_did TEXT NOT NULL, 94 + match_id INTEGER NOT NULL REFERENCES atproto_matches(id), 95 + viewed BOOLEAN DEFAULT FALSE, 96 + dismissed BOOLEAN DEFAULT FALSE, 97 + followed BOOLEAN DEFAULT FALSE, 98 + notified BOOLEAN DEFAULT FALSE, 99 + updated_at TIMESTAMP DEFAULT NOW(), 100 + PRIMARY KEY (user_did, match_id) 101 + ); 102 + CREATE INDEX idx_user_match_status_user ON user_match_status(user_did); 103 + CREATE INDEX idx_user_match_status_notified ON user_match_status(user_did, notified) 104 + WHERE notified = FALSE; 105 + 106 + -- Notification queue (transient - for Phase 2) 107 + CREATE TABLE notification_queue ( 108 + id SERIAL PRIMARY KEY, 109 + user_did TEXT NOT NULL, 110 + match_id INTEGER NOT NULL REFERENCES atproto_matches(id), 111 + notification_type TEXT NOT NULL, -- 'in_app', 'bluesky_dm', 'partner_api' 112 + status TEXT DEFAULT 'pending', -- 'pending', 'sent', 'failed' 113 + attempts INTEGER DEFAULT 0, 114 + last_attempt TIMESTAMP, 115 + error_message TEXT, -- Store error details for debugging 116 + created_at TIMESTAMP DEFAULT NOW() 117 + ); 118 + CREATE INDEX idx_notification_queue_status ON notification_queue(status) 119 + WHERE status = 'pending'; 120 + CREATE INDEX idx_notification_queue_user ON notification_queue(user_did); 121 + 122 + -- Partner API keys (for Phase 2) 123 + CREATE TABLE partner_api_keys ( 124 + id SERIAL PRIMARY KEY, 125 + partner_name TEXT NOT NULL, -- 'skylight', 'spark', etc. 126 + api_key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hashed API key 127 + created_at TIMESTAMP DEFAULT NOW(), 128 + last_used TIMESTAMP, 129 + is_active BOOLEAN DEFAULT TRUE 130 + ); 131 + CREATE INDEX idx_partner_api_keys_hash ON partner_api_keys(api_key_hash) 132 + WHERE is_active = TRUE; 133 + 134 + -- Cleanup function for old transient data 135 + CREATE OR REPLACE FUNCTION cleanup_transient_data() RETURNS void AS $$ 136 + BEGIN 137 + -- Clean expired OAuth states (1 hour) 138 + DELETE FROM oauth_states WHERE created_at < NOW() - INTERVAL '1 hour'; 139 + 140 + -- Clean expired sessions 141 + DELETE FROM user_sessions WHERE expires_at < NOW(); 142 + 143 + -- Clean old sent notifications (7 days) 144 + DELETE FROM notification_queue 145 + WHERE status = 'sent' AND created_at < NOW() - INTERVAL '7 days'; 146 + 147 + -- Clean old failed notifications (30 days) 148 + DELETE FROM notification_queue 149 + WHERE status = 'failed' AND created_at < NOW() - INTERVAL '30 days'; 150 + END; 151 + $$ LANGUAGE plpgsql;
+96
scripts/seed-test-data.sql
··· 1 + -- Test Data Seeding Script 2 + -- Use this for local development and testing 3 + 4 + -- Clean existing test data (optional) 5 + DELETE FROM user_match_status WHERE user_did LIKE 'did:plc:test%'; 6 + DELETE FROM atproto_matches WHERE source_account_id IN (SELECT id FROM source_accounts WHERE source_platform = 'test'); 7 + DELETE FROM user_source_follows WHERE user_did LIKE 'did:plc:test%'; 8 + DELETE FROM source_accounts WHERE source_platform = 'test'; 9 + DELETE FROM user_uploads WHERE upload_id LIKE 'test-%'; 10 + DELETE FROM user_sessions WHERE session_id LIKE 'test-%'; 11 + 12 + -- Test user session 13 + INSERT INTO user_sessions (session_id, did, fingerprint, expires_at) 14 + VALUES ( 15 + 'test-session-123', 16 + 'did:plc:test', 17 + 'test-fingerprint', 18 + NOW() + INTERVAL '7 days' 19 + ); 20 + 21 + -- Test upload 22 + INSERT INTO user_uploads (upload_id, user_did, source_platform, total_users, matched_users, unmatched_users) 23 + VALUES ( 24 + 'test-upload-1', 25 + 'did:plc:test', 26 + 'instagram', 27 + 10, 28 + 5, 29 + 5 30 + ); 31 + 32 + -- Test source accounts 33 + INSERT INTO source_accounts (source_platform, original_username, normalized_username) 34 + VALUES 35 + ('instagram', 'test_user', 'testuser'), 36 + ('instagram', 'john.doe', 'johndoe'), 37 + ('instagram', 'jane_smith', 'janesmith'), 38 + ('tiktok', '@cool_person', 'coolperson'), 39 + ('twitter', 'example_account', 'exampleaccount') 40 + ON CONFLICT (source_platform, normalized_username) DO NOTHING; 41 + 42 + -- Link source accounts to upload 43 + INSERT INTO user_source_follows (user_did, upload_id, source_account_id) 44 + SELECT 45 + 'did:plc:test', 46 + 'test-upload-1', 47 + id 48 + FROM source_accounts 49 + WHERE source_platform IN ('instagram', 'tiktok', 'twitter') 50 + AND normalized_username IN ('testuser', 'johndoe', 'janesmith', 'coolperson', 'exampleaccount') 51 + ON CONFLICT DO NOTHING; 52 + 53 + -- Test AT Protocol matches 54 + INSERT INTO atproto_matches ( 55 + source_account_id, 56 + atproto_did, 57 + atproto_handle, 58 + display_name, 59 + match_score, 60 + post_count, 61 + follower_count, 62 + follow_status 63 + ) 64 + SELECT 65 + sa.id, 66 + 'did:plc:matched-' || sa.id, 67 + sa.normalized_username || '.bsky.social', 68 + INITCAP(REPLACE(sa.normalized_username, '_', ' ')), 69 + 100, 70 + 42, 71 + 128, 72 + '{}'::jsonb 73 + FROM source_accounts sa 74 + WHERE sa.source_platform IN ('instagram', 'tiktok') 75 + AND sa.normalized_username IN ('testuser', 'johndoe') 76 + ON CONFLICT (source_account_id, atproto_did) DO NOTHING; 77 + 78 + -- Test user match status 79 + INSERT INTO user_match_status (user_did, match_id, viewed, dismissed, followed, notified) 80 + SELECT 81 + 'did:plc:test', 82 + am.id, 83 + false, 84 + false, 85 + false, 86 + false 87 + FROM atproto_matches am 88 + WHERE am.atproto_did LIKE 'did:plc:matched-%' 89 + ON CONFLICT DO NOTHING; 90 + 91 + -- Display summary 92 + SELECT 'Test data seeded successfully!' as message; 93 + SELECT COUNT(*) as session_count FROM user_sessions WHERE session_id LIKE 'test-%'; 94 + SELECT COUNT(*) as upload_count FROM user_uploads WHERE upload_id LIKE 'test-%'; 95 + SELECT COUNT(*) as source_account_count FROM source_accounts WHERE source_platform IN ('test', 'instagram', 'tiktok', 'twitter'); 96 + SELECT COUNT(*) as match_count FROM atproto_matches WHERE atproto_did LIKE 'did:plc:matched-%';