From 05bfe009ec205b8cd1fafc3f75f14ec425837ecd Mon Sep 17 00:00:00 2001 From: Wesley Finck Date: Fri, 6 Feb 2026 17:35:08 -0800 Subject: [PATCH] implement fan-out feed activity to user and collection followers --- .../tests/test-utils/createTestSchema.ts | 13 + .../commands/AddActivityToFeedUseCase.ts | 85 +- src/modules/feeds/domain/IFeedRepository.ts | 41 + .../repositories/DrizzleFeedRepository.ts | 150 ++ .../schema/followingFeedItem.sql.ts | 29 + .../infrastructure/InMemoryFeedRepository.ts | 87 + .../migrations/0016_ancient_cerise.sql | 19 + .../migrations/meta/0016_snapshot.json | 1558 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + .../http/factories/RepositoryFactory.ts | 7 + .../http/factories/UseCaseFactory.ts | 4 + 11 files changed, 1999 insertions(+), 1 deletion(-) create mode 100644 src/modules/feeds/infrastructure/repositories/schema/followingFeedItem.sql.ts create mode 100644 src/shared/infrastructure/database/migrations/0016_ancient_cerise.sql create mode 100644 src/shared/infrastructure/database/migrations/meta/0016_snapshot.json diff --git a/src/modules/cards/tests/test-utils/createTestSchema.ts b/src/modules/cards/tests/test-utils/createTestSchema.ts index 39494b6f..0bc165fb 100644 --- a/src/modules/cards/tests/test-utils/createTestSchema.ts +++ b/src/modules/cards/tests/test-utils/createTestSchema.ts @@ -118,6 +118,14 @@ export async function createTestSchema(db: PostgresJsDatabase) { created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), PRIMARY KEY (follower_id, target_id, target_type) )`, + + // Following feed items table (references feed_activities) + sql`CREATE TABLE IF NOT EXISTS following_feed_items ( + user_id TEXT NOT NULL, + activity_id UUID NOT NULL REFERENCES feed_activities(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY (user_id, activity_id) + )`, ]; // Execute table creation queries in order @@ -256,4 +264,9 @@ export async function createTestSchema(db: PostgresJsDatabase) { await db.execute(sql` CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_id, target_type); `); + + // Following feed items indexes + await db.execute(sql` + CREATE INDEX IF NOT EXISTS idx_following_feed_user_time ON following_feed_items(user_id, created_at DESC); + `); } diff --git a/src/modules/feeds/application/useCases/commands/AddActivityToFeedUseCase.ts b/src/modules/feeds/application/useCases/commands/AddActivityToFeedUseCase.ts index e96cf97e..ac646fc4 100644 --- a/src/modules/feeds/application/useCases/commands/AddActivityToFeedUseCase.ts +++ b/src/modules/feeds/application/useCases/commands/AddActivityToFeedUseCase.ts @@ -10,6 +10,12 @@ import { FeedService } from 'src/modules/feeds/domain/services/FeedService'; import { ICardRepository } from '../../../../cards/domain/ICardRepository'; import { SourceTypeEnum } from '../../../domain/value-objects/SourceType'; import { ATPROTO_NSID } from '../../../../../shared/constants/atproto'; +import { IFollowsRepository } from '../../../../user/domain/repositories/IFollowsRepository'; +import { IFeedRepository } from '../../../domain/IFeedRepository'; +import { + FollowTargetType, + FollowTargetTypeEnum, +} from '../../../../user/domain/value-objects/FollowTargetType'; export interface AddCardCollectedActivityDTO { type: ActivityTypeEnum.CARD_COLLECTED; @@ -44,6 +50,8 @@ export class AddActivityToFeedUseCase constructor( private feedService: FeedService, private cardRepository: ICardRepository, + private followsRepository: IFollowsRepository, + private feedRepository: IFeedRepository, ) {} async execute( @@ -153,8 +161,83 @@ export class AddActivityToFeedUseCase return err(new ValidationError(activityResult.error.message)); } + const activity = activityResult.value; + + // ======================================== + // PHASE 4: GET FOLLOWERS + // ======================================== + + // 4a. Get followers of the actor (user who created activity) + const targetTypeResult = FollowTargetType.create( + FollowTargetTypeEnum.USER, + ); + if (targetTypeResult.isErr()) { + console.error( + 'Failed to create FollowTargetType:', + targetTypeResult.error, + ); + return ok({ + activityId: activity.activityId.getStringValue(), + }); + } + + const userFollowersResult = await this.followsRepository.getFollowers( + actorId.value, + targetTypeResult.value, + ); + + const userFollowers = userFollowersResult.isOk() + ? userFollowersResult.value.map((f) => f.followerId.value) + : []; + + // 4b. Get followers of collections (if any) + let collectionFollowers: string[] = []; + if (collectionIds && collectionIds.length > 0) { + const collectionIdStrings = collectionIds.map((id) => + id.getStringValue(), + ); + const collectionFollowersResult = + await this.followsRepository.getFollowersOfCollections( + collectionIdStrings, + ); + + collectionFollowers = collectionFollowersResult.isOk() + ? collectionFollowersResult.value.map((f) => f.followerId.value) + : []; + } + + // 4c. Combine and deduplicate follower IDs + const allFollowerIds = new Set([ + ...userFollowers, + ...collectionFollowers, + ]); + + // ======================================== + // PHASE 5: FAN-OUT + // ======================================== + + if (allFollowerIds.size > 0) { + const fanOutResult = + await this.feedRepository.fanOutActivityToFollowers( + activity.activityId, + Array.from(allFollowerIds), + activity.createdAt, + ); + + // Error handling: Log but don't fail the use case + // Activity already exists in global feed + // Event retries will eventually distribute it + if (fanOutResult.isErr()) { + console.error( + 'Fan-out failed (will retry on event retry):', + fanOutResult.error, + ); + // Note: We do NOT return err here - activity was created successfully + } + } + return ok({ - activityId: activityResult.value.activityId.getStringValue(), + activityId: activity.activityId.getStringValue(), }); } catch (error) { return err(AppError.UnexpectedError.create(error)); diff --git a/src/modules/feeds/domain/IFeedRepository.ts b/src/modules/feeds/domain/IFeedRepository.ts index 4903227b..fb09f8b3 100644 --- a/src/modules/feeds/domain/IFeedRepository.ts +++ b/src/modules/feeds/domain/IFeedRepository.ts @@ -36,4 +36,45 @@ export interface IFeedRepository { withinMinutes: number, ): Promise>; updateActivity(activity: FeedActivity): Promise>; + + /** + * Fan-out an activity to multiple followers' following feeds. + * + * @param activityId - Activity to distribute + * @param followerIds - User DIDs to receive this activity (deduplicated by caller) + * @param createdAt - Activity timestamp (denormalized for sorting) + * @returns Success or error + * + * Idempotency guarantee: + * - Uses ON CONFLICT DO NOTHING on primary key (user_id, activity_id) + * - Safe to call multiple times with same inputs + * - Retries are silent (no error on duplicate) + * + * Performance: + * - Bulk insert operation (single query) + * - Returns immediately if followerIds is empty (no-op) + */ + fanOutActivityToFollowers( + activityId: ActivityId, + followerIds: string[], + createdAt: Date, + ): Promise>; + + /** + * Get a user's following feed (paginated). + * + * @param userId - User DID whose feed to fetch + * @param options - Pagination, filters (urlType, source, beforeActivityId) + * @returns Paginated feed activities + * + * Query pattern: + * - Filters by user_id on following_feed_items + * - JOINs to feed_activities for full activity data + * - Supports same filters as global feed (urlType, source) + * - Cursor-based pagination via beforeActivityId + */ + getFollowingFeed( + userId: string, + options: FeedQueryOptions, + ): Promise>; } diff --git a/src/modules/feeds/infrastructure/repositories/DrizzleFeedRepository.ts b/src/modules/feeds/infrastructure/repositories/DrizzleFeedRepository.ts index 5a06ca38..f1736bf3 100644 --- a/src/modules/feeds/infrastructure/repositories/DrizzleFeedRepository.ts +++ b/src/modules/feeds/infrastructure/repositories/DrizzleFeedRepository.ts @@ -8,6 +8,7 @@ import { import { FeedActivity } from '../../domain/FeedActivity'; import { ActivityId } from '../../domain/value-objects/ActivityId'; import { feedActivities } from './schema/feedActivity.sql'; +import { followingFeedItems } from './schema/followingFeedItem.sql'; import { FeedActivityMapper, FeedActivityDTO, @@ -424,4 +425,153 @@ export class DrizzleFeedRepository implements IFeedRepository { return err(error as Error); } } + + async fanOutActivityToFollowers( + activityId: ActivityId, + followerIds: string[], + createdAt: Date, + ): Promise> { + try { + if (followerIds.length === 0) { + return ok(undefined); + } + + const values = followerIds.map((userId) => ({ + userId: userId, + activityId: activityId.getStringValue(), + createdAt: createdAt, + })); + + await this.db + .insert(followingFeedItems) + .values(values) + .onConflictDoNothing(); + + return ok(undefined); + } catch (error) { + return err(error as Error); + } + } + + async getFollowingFeed( + userId: string, + options: FeedQueryOptions, + ): Promise> { + try { + const { page, limit, beforeActivityId } = options; + const offset = (page - 1) * limit; + + // Build where conditions + const whereConditions = [eq(followingFeedItems.userId, userId)]; + + if (options.urlType) { + whereConditions.push(eq(feedActivities.urlType, options.urlType)); + } + + if (options.source) { + if (options.source === ActivitySource.SEMBLE) { + whereConditions.push(sql`${feedActivities.source} IS NULL`); + } else { + whereConditions.push(eq(feedActivities.source, options.source)); + } + } + + // Cursor-based pagination + if (beforeActivityId) { + const beforeActivity = await this.db + .select({ createdAt: followingFeedItems.createdAt }) + .from(followingFeedItems) + .where( + and( + eq(followingFeedItems.userId, userId), + eq( + followingFeedItems.activityId, + beforeActivityId.getStringValue(), + ), + ), + ) + .limit(1); + + if (beforeActivity.length > 0) { + whereConditions.push( + lt(followingFeedItems.createdAt, beforeActivity[0]!.createdAt), + ); + } + } + + // Main query with JOIN + const activitiesResult = await this.db + .select({ + id: feedActivities.id, + actorId: feedActivities.actorId, + cardId: feedActivities.cardId, + type: feedActivities.type, + metadata: feedActivities.metadata, + urlType: feedActivities.urlType, + source: feedActivities.source, + createdAt: followingFeedItems.createdAt, // Use denormalized timestamp + }) + .from(followingFeedItems) + .innerJoin( + feedActivities, + eq(feedActivities.id, followingFeedItems.activityId), + ) + .where(and(...whereConditions)) + .orderBy( + desc(followingFeedItems.createdAt), + desc(followingFeedItems.activityId), + ) + .limit(limit) + .offset(offset); + + // Count total (with same filters) + const totalCountResult = await this.db + .select({ count: count() }) + .from(followingFeedItems) + .innerJoin( + feedActivities, + eq(feedActivities.id, followingFeedItems.activityId), + ) + .where(and(...whereConditions)); + + const totalCount = totalCountResult[0]?.count || 0; + + // Map to domain objects + const activities: FeedActivity[] = []; + for (const activityData of activitiesResult) { + const dto: FeedActivityDTO = { + id: activityData.id, + actorId: activityData.actorId, + cardId: activityData.cardId || undefined, + type: activityData.type, + metadata: activityData.metadata as any, + urlType: activityData.urlType || undefined, + source: activityData.source || undefined, + createdAt: activityData.createdAt, + }; + + const domainResult = FeedActivityMapper.toDomain(dto); + if (domainResult.isErr()) { + return err(domainResult.error); + } + + activities.push(domainResult.value); + } + + const hasMore = offset + activities.length < totalCount; + const nextCursor = + hasMore && activities.length > 0 + ? activities[activities.length - 1]!.activityId + : undefined; + + return ok({ + activities, + totalCount, + hasMore, + nextCursor, + }); + } catch (error) { + return err(error as Error); + } + } } diff --git a/src/modules/feeds/infrastructure/repositories/schema/followingFeedItem.sql.ts b/src/modules/feeds/infrastructure/repositories/schema/followingFeedItem.sql.ts new file mode 100644 index 00000000..5d8432a3 --- /dev/null +++ b/src/modules/feeds/infrastructure/repositories/schema/followingFeedItem.sql.ts @@ -0,0 +1,29 @@ +import { + pgTable, + text, + timestamp, + uuid, + index, + primaryKey, +} from 'drizzle-orm/pg-core'; +import { feedActivities } from './feedActivity.sql'; + +export const followingFeedItems = pgTable( + 'following_feed_items', + { + userId: text('user_id').notNull(), // DID of feed owner + activityId: uuid('activity_id') + .notNull() + .references(() => feedActivities.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').notNull(), // Denormalized from activity for sorting + }, + (table) => ({ + // Composite primary key + pk: primaryKey({ columns: [table.userId, table.activityId] }), + // Index for efficient user feed queries sorted by time + userTimeIdx: index('idx_following_feed_user_time').on( + table.userId, + table.createdAt.desc(), + ), + }), +); diff --git a/src/modules/feeds/tests/infrastructure/InMemoryFeedRepository.ts b/src/modules/feeds/tests/infrastructure/InMemoryFeedRepository.ts index d3699c77..2dd3d42b 100644 --- a/src/modules/feeds/tests/infrastructure/InMemoryFeedRepository.ts +++ b/src/modules/feeds/tests/infrastructure/InMemoryFeedRepository.ts @@ -11,6 +11,8 @@ import { CollectionId } from '../../../cards/domain/value-objects/CollectionId'; export class InMemoryFeedRepository implements IFeedRepository { private static instance: InMemoryFeedRepository | null = null; private activities: FeedActivity[] = []; + // Store following feed items as Map> + private followingFeedItems: Map> = new Map(); private constructor() {} @@ -212,9 +214,94 @@ export class InMemoryFeedRepository implements IFeedRepository { } } + async fanOutActivityToFollowers( + activityId: ActivityId, + followerIds: string[], + createdAt: Date, + ): Promise> { + try { + if (followerIds.length === 0) { + return ok(undefined); + } + + const activityIdString = activityId.getStringValue(); + + for (const userId of followerIds) { + if (!this.followingFeedItems.has(userId)) { + this.followingFeedItems.set(userId, new Set()); + } + this.followingFeedItems.get(userId)!.add(activityIdString); + } + + return ok(undefined); + } catch (error) { + return err(error as Error); + } + } + + async getFollowingFeed( + userId: string, + options: FeedQueryOptions, + ): Promise> { + try { + const { page, limit, beforeActivityId, urlType } = options; + + // Get activity IDs for this user's following feed + const userActivityIds = this.followingFeedItems.get(userId) || new Set(); + + // Filter activities that are in this user's following feed + let filteredActivities = this.activities.filter((activity) => + userActivityIds.has(activity.activityId.getStringValue()), + ); + + // Filter by URL type if provided + if (urlType) { + filteredActivities = filteredActivities.filter( + (activity) => activity.urlType === urlType, + ); + } + + // Filter by cursor if provided + if (beforeActivityId) { + const beforeIndex = filteredActivities.findIndex((activity) => + activity.activityId.equals(beforeActivityId), + ); + if (beforeIndex >= 0) { + filteredActivities = filteredActivities.slice(beforeIndex + 1); + } + } + + // Paginate + const offset = (page - 1) * limit; + const paginatedActivities = filteredActivities.slice( + offset, + offset + limit, + ); + + const totalCount = filteredActivities.length; + const hasMore = offset + paginatedActivities.length < totalCount; + + let nextCursor: ActivityId | undefined; + if (hasMore && paginatedActivities.length > 0) { + nextCursor = + paginatedActivities[paginatedActivities.length - 1]!.activityId; + } + + return ok({ + activities: paginatedActivities, + totalCount, + hasMore, + nextCursor, + }); + } catch (error) { + return err(error as Error); + } + } + // Test helper methods clear(): void { this.activities = []; + this.followingFeedItems.clear(); } getAll(): FeedActivity[] { diff --git a/src/shared/infrastructure/database/migrations/0016_ancient_cerise.sql b/src/shared/infrastructure/database/migrations/0016_ancient_cerise.sql new file mode 100644 index 00000000..3c5fbbb9 --- /dev/null +++ b/src/shared/infrastructure/database/migrations/0016_ancient_cerise.sql @@ -0,0 +1,19 @@ +CREATE TABLE "following_feed_items" ( + "user_id" text NOT NULL, + "activity_id" uuid NOT NULL, + "created_at" timestamp NOT NULL, + CONSTRAINT "following_feed_items_user_id_activity_id_pk" PRIMARY KEY("user_id","activity_id") +); +--> statement-breakpoint +CREATE TABLE "follows" ( + "follower_id" text NOT NULL, + "target_id" text NOT NULL, + "target_type" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "follows_follower_id_target_id_target_type_pk" PRIMARY KEY("follower_id","target_id","target_type") +); +--> statement-breakpoint +ALTER TABLE "following_feed_items" ADD CONSTRAINT "following_feed_items_activity_id_feed_activities_id_fk" FOREIGN KEY ("activity_id") REFERENCES "public"."feed_activities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_following_feed_user_time" ON "following_feed_items" USING btree ("user_id","created_at" DESC NULLS LAST);--> statement-breakpoint +CREATE INDEX "idx_follows_follower" ON "follows" USING btree ("follower_id");--> statement-breakpoint +CREATE INDEX "idx_follows_target" ON "follows" USING btree ("target_id","target_type"); \ No newline at end of file diff --git a/src/shared/infrastructure/database/migrations/meta/0016_snapshot.json b/src/shared/infrastructure/database/migrations/meta/0016_snapshot.json new file mode 100644 index 00000000..479c6297 --- /dev/null +++ b/src/shared/infrastructure/database/migrations/meta/0016_snapshot.json @@ -0,0 +1,1558 @@ +{ + "id": "1186ff74-4783-4cc7-94c4-ae3e8f93ac59", + "prevId": "a6374d24-e08b-4e73-9589-82060d95ba81", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_password_sessions": { + "name": "app_password_sessions", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_data": { + "name": "session_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "app_password": { + "name": "app_password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cards": { + "name": "cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_data": { + "name": "content_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url_type": { + "name": "url_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_card_id": { + "name": "parent_card_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "via_card_id": { + "name": "via_card_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_record_id": { + "name": "published_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "library_count": { + "name": "library_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cards_author_url_idx": { + "name": "cards_author_url_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cards_author_id_idx": { + "name": "cards_author_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cards_type_updated_at": { + "name": "idx_cards_type_updated_at", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cards_url_type": { + "name": "idx_cards_url_type", + "columns": [ + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cards_url_type_filter": { + "name": "idx_cards_url_type_filter", + "columns": [ + { + "expression": "url_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cards_parent_type": { + "name": "idx_cards_parent_type", + "columns": [ + { + "expression": "parent_card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "type = 'NOTE'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cards_parent_card_id_cards_id_fk": { + "name": "cards_parent_card_id_cards_id_fk", + "tableFrom": "cards", + "tableTo": "cards", + "columnsFrom": ["parent_card_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cards_via_card_id_cards_id_fk": { + "name": "cards_via_card_id_cards_id_fk", + "tableFrom": "cards", + "tableTo": "cards", + "columnsFrom": ["via_card_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cards_published_record_id_published_records_id_fk": { + "name": "cards_published_record_id_published_records_id_fk", + "tableFrom": "cards", + "tableTo": "published_records", + "columnsFrom": ["published_record_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_cards": { + "name": "collection_cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "card_id": { + "name": "card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "via_card_id": { + "name": "via_card_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_record_id": { + "name": "published_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collection_cards_card_id_idx": { + "name": "collection_cards_card_id_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "collection_cards_collection_id_idx": { + "name": "collection_cards_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_collection_cards_collection_added": { + "name": "idx_collection_cards_collection_added", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "added_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_collection_cards_card_collection": { + "name": "idx_collection_cards_card_collection", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_collection_cards_added_by_added_at": { + "name": "idx_collection_cards_added_by_added_at", + "columns": [ + { + "expression": "added_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "added_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_cards_collection_id_collections_id_fk": { + "name": "collection_cards_collection_id_collections_id_fk", + "tableFrom": "collection_cards", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_cards_card_id_cards_id_fk": { + "name": "collection_cards_card_id_cards_id_fk", + "tableFrom": "collection_cards", + "tableTo": "cards", + "columnsFrom": ["card_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_cards_via_card_id_cards_id_fk": { + "name": "collection_cards_via_card_id_cards_id_fk", + "tableFrom": "collection_cards", + "tableTo": "cards", + "columnsFrom": ["via_card_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "collection_cards_published_record_id_published_records_id_fk": { + "name": "collection_cards_published_record_id_published_records_id_fk", + "tableFrom": "collection_cards", + "tableTo": "published_records", + "columnsFrom": ["published_record_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_collaborators": { + "name": "collection_collaborators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "collaborator_id": { + "name": "collaborator_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "collection_collaborators_collection_id_collections_id_fk": { + "name": "collection_collaborators_collection_id_collections_id_fk", + "tableFrom": "collection_collaborators", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_type": { + "name": "access_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "card_count": { + "name": "card_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_record_id": { + "name": "published_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collections_author_id_idx": { + "name": "collections_author_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "collections_author_updated_at_idx": { + "name": "collections_author_updated_at_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_published_record_id_published_records_id_fk": { + "name": "collections_published_record_id_published_records_id_fk", + "tableFrom": "collections", + "tableTo": "published_records", + "columnsFrom": ["published_record_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.library_memberships": { + "name": "library_memberships", + "schema": "", + "columns": { + "card_id": { + "name": "card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_record_id": { + "name": "published_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_cards": { + "name": "idx_user_cards", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_card_users": { + "name": "idx_card_users", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_library_memberships_user_type_covering": { + "name": "idx_library_memberships_user_type_covering", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "added_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "library_memberships_card_id_cards_id_fk": { + "name": "library_memberships_card_id_cards_id_fk", + "tableFrom": "library_memberships", + "tableTo": "cards", + "columnsFrom": ["card_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "library_memberships_published_record_id_published_records_id_fk": { + "name": "library_memberships_published_record_id_published_records_id_fk", + "tableFrom": "library_memberships", + "tableTo": "published_records", + "columnsFrom": ["published_record_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "library_memberships_card_id_user_id_pk": { + "name": "library_memberships_card_id_user_id_pk", + "columns": ["card_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.published_records": { + "name": "published_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uri_cid_unique_idx": { + "name": "uri_cid_unique_idx", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "published_records_uri_idx": { + "name": "published_records_uri_idx", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feed_activities": { + "name": "feed_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "url_type": { + "name": "url_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feed_activities_type_idx": { + "name": "feed_activities_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_url_type_idx": { + "name": "feed_activities_url_type_idx", + "columns": [ + { + "expression": "url_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_created_at_idx": { + "name": "feed_activities_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_type_created_at_idx": { + "name": "feed_activities_type_created_at_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_url_type_created_at_idx": { + "name": "feed_activities_url_type_created_at_idx", + "columns": [ + { + "expression": "url_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_type_url_type_created_at_idx": { + "name": "feed_activities_type_url_type_created_at_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_dedup_idx": { + "name": "feed_activities_dedup_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_card_id_idx": { + "name": "feed_activities_card_id_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_activities_source_idx": { + "name": "feed_activities_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.following_feed_items": { + "name": "following_feed_items", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_following_feed_user_time": { + "name": "idx_following_feed_user_time", + "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": {} + } + }, + "foreignKeys": { + "following_feed_items_activity_id_feed_activities_id_fk": { + "name": "following_feed_items_activity_id_feed_activities_id_fk", + "tableFrom": "following_feed_items", + "tableTo": "feed_activities", + "columnsFrom": ["activity_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "following_feed_items_user_id_activity_id_pk": { + "name": "following_feed_items_user_id_activity_id_pk", + "columns": ["user_id", "activity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_recipient_idx": { + "name": "notifications_recipient_idx", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_recipient_created_at_idx": { + "name": "notifications_recipient_created_at_idx", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_recipient_read_idx": { + "name": "notifications_recipient_read_idx", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_statuses": { + "name": "sync_statuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "curator_id": { + "name": "curator_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_attempt_at": { + "name": "last_sync_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_error_message": { + "name": "sync_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "records_processed": { + "name": "records_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sync_statuses_curator_id_unique": { + "name": "sync_statuses_curator_id_unique", + "nullsNotDistinct": false, + "columns": ["curator_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session": { + "name": "session", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_state": { + "name": "auth_state", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_refresh_tokens": { + "name": "auth_refresh_tokens", + "schema": "", + "columns": { + "token_id": { + "name": "token_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_did": { + "name": "user_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_refresh_tokens_user_did_users_id_fk": { + "name": "auth_refresh_tokens_user_did_users_id_fk", + "tableFrom": "auth_refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_did"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_follows_follower": { + "name": "idx_follows_follower", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_follows_target": { + "name": "idx_follows_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "follows_follower_id_target_id_target_type_pk": { + "name": "follows_follower_id_target_id_target_type_pk", + "columns": ["follower_id", "target_id", "target_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/shared/infrastructure/database/migrations/meta/_journal.json b/src/shared/infrastructure/database/migrations/meta/_journal.json index 7bbf6838..f28d20e4 100644 --- a/src/shared/infrastructure/database/migrations/meta/_journal.json +++ b/src/shared/infrastructure/database/migrations/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1770074100791, "tag": "0015_small_loki", "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1770427721556, + "tag": "0016_ancient_cerise", + "breakpoints": true } ] } diff --git a/src/shared/infrastructure/http/factories/RepositoryFactory.ts b/src/shared/infrastructure/http/factories/RepositoryFactory.ts index 5fa2b3a6..31649451 100644 --- a/src/shared/infrastructure/http/factories/RepositoryFactory.ts +++ b/src/shared/infrastructure/http/factories/RepositoryFactory.ts @@ -42,6 +42,9 @@ import { InMemoryNotificationRepository } from '../../../../modules/notification import { ISyncStatusRepository } from '../../../../modules/sync/domain/repositories/ISyncStatusRepository'; import { DrizzleSyncStatusRepository } from '../../../../modules/sync/infrastructure/repositories/DrizzleSyncStatusRepository'; import { InMemorySyncStatusRepository } from '../../../../modules/sync/tests/infrastructure/InMemorySyncStatusRepository'; +import { IFollowsRepository } from '../../../../modules/user/domain/repositories/IFollowsRepository'; +import { DrizzleFollowsRepository } from '../../../../modules/user/infrastructure/repositories/DrizzleFollowsRepository'; +import { InMemoryFollowsRepository } from '../../../../modules/user/tests/infrastructure/InMemoryFollowsRepository'; export interface Repositories { userRepository: IUserRepository; @@ -52,6 +55,7 @@ export interface Repositories { collectionQueryRepository: ICollectionQueryRepository; appPasswordSessionRepository: IAppPasswordSessionRepository; feedRepository: IFeedRepository; + followsRepository: IFollowsRepository; notificationRepository: INotificationRepository; syncStatusRepository: ISyncStatusRepository; atUriResolutionService: IAtUriResolutionService; @@ -80,6 +84,7 @@ export class RepositoryFactory { const appPasswordSessionRepository = InMemoryAppPasswordSessionRepository.getInstance(); const feedRepository = InMemoryFeedRepository.getInstance(); + const followsRepository = InMemoryFollowsRepository.getInstance(); const atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, @@ -101,6 +106,7 @@ export class RepositoryFactory { collectionQueryRepository, appPasswordSessionRepository, feedRepository, + followsRepository, notificationRepository, syncStatusRepository, atUriResolutionService, @@ -125,6 +131,7 @@ export class RepositoryFactory { collectionQueryRepository: new DrizzleCollectionQueryRepository(db), appPasswordSessionRepository: new DrizzleAppPasswordSessionRepository(db), feedRepository: new DrizzleFeedRepository(db), + followsRepository: new DrizzleFollowsRepository(db), notificationRepository: new DrizzleNotificationRepository(db), syncStatusRepository: new DrizzleSyncStatusRepository(db), atUriResolutionService: new DrizzleAtUriResolutionService(db), diff --git a/src/shared/infrastructure/http/factories/UseCaseFactory.ts b/src/shared/infrastructure/http/factories/UseCaseFactory.ts index 20b093c9..b8c1e55e 100644 --- a/src/shared/infrastructure/http/factories/UseCaseFactory.ts +++ b/src/shared/infrastructure/http/factories/UseCaseFactory.ts @@ -299,6 +299,8 @@ export class UseCaseFactory { addActivityToFeedUseCase: new AddActivityToFeedUseCase( services.feedService, repositories.cardRepository, + repositories.followsRepository, + repositories.feedRepository, ), // Search use cases getSimilarUrlsForUrlUseCase: new GetSimilarUrlsForUrlUseCase( @@ -349,6 +351,8 @@ export class UseCaseFactory { const addActivityToFeedUseCase = new AddActivityToFeedUseCase( services.feedService, repositories.cardRepository, + repositories.followsRepository, + repositories.feedRepository, ); // Search use cases -- 2.51.2