diff --git a/packages/api/src/entity/library_item.ts b/packages/api/src/entity/library_item.ts index 0e9d7c259..2950ff9a6 100644 --- a/packages/api/src/entity/library_item.ts +++ b/packages/api/src/entity/library_item.ts @@ -209,5 +209,8 @@ export class LibraryItem { seenAt?: Date @Column('text') - topic!: string + topic?: string + + @Column('timestamptz') + digestedAt?: Date } diff --git a/packages/api/src/entity/public_item.ts b/packages/api/src/entity/public_item.ts index 30e318a3a..df82e1713 100644 --- a/packages/api/src/entity/public_item.ts +++ b/packages/api/src/entity/public_item.ts @@ -12,10 +12,10 @@ export class PublicItem { id!: string @Column('text') - source_name!: string + sourceName!: string @Column('text') - source_icon!: string + sourceIcon?: string @Column('text') type!: string @@ -54,5 +54,11 @@ export class PublicItem { updatedAt!: Date @Column('text') - topic!: string + topic?: string + + @Column('integer') + wordCount?: number + + @Column('text') + siteName?: string } diff --git a/packages/api/src/entity/public_item_interaction.ts b/packages/api/src/entity/public_item_interaction.ts index efb370d58..dc192e72a 100644 --- a/packages/api/src/entity/public_item_interaction.ts +++ b/packages/api/src/entity/public_item_interaction.ts @@ -41,4 +41,7 @@ export class PublicItemInteraction { @Column('timestamptz') updated!: Date + + @Column('timestamptz') + digested?: Date } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 55db3b4a0..a3d09f2eb 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1391,9 +1391,10 @@ export type JustReadFeedItem = { source?: Maybe; thumbnail?: Maybe; title: Scalars['String']; - topic: Scalars['String']; + topic?: Maybe; updatedAt?: Maybe; url: Scalars['String']; + wordCount?: Maybe; }; export type JustReadFeedResult = JustReadFeedError | JustReadFeedSuccess; @@ -1416,7 +1417,7 @@ export type JustReadFeedSuccess = { export type JustReadFeedTopic = { __typename?: 'JustReadFeedTopic'; items: Array; - name: Scalars['String']; + name?: Maybe; thumbnail?: Maybe; }; @@ -2072,6 +2073,11 @@ export type MutationUploadImportFileArgs = { type: UploadImportFileType; }; +export type MySubscriptionRootType = { + __typename?: 'MySubscriptionRootType'; + justReadFeed: JustReadFeedResult; +}; + export type NewsletterEmail = { __typename?: 'NewsletterEmail'; address: Scalars['String']; @@ -2218,7 +2224,6 @@ export type Query = { hello?: Maybe; integration: IntegrationResult; integrations: IntegrationsResult; - justReadFeed: JustReadFeedResult; labels: LabelsResult; me?: Maybe; newsletterEmails: NewsletterEmailsResult; @@ -2270,14 +2275,6 @@ export type QueryIntegrationArgs = { }; -export type QueryJustReadFeedArgs = { - after?: InputMaybe; - first?: InputMaybe; - language?: InputMaybe; - location?: InputMaybe; -}; - - export type QueryRulesArgs = { enabled?: InputMaybe; }; @@ -4317,6 +4314,7 @@ export type ResolversTypes = { MoveToFolderResult: ResolversTypes['MoveToFolderError'] | ResolversTypes['MoveToFolderSuccess']; MoveToFolderSuccess: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; + MySubscriptionRootType: ResolverTypeWrapper<{}>; NewsletterEmail: ResolverTypeWrapper; NewsletterEmailsError: ResolverTypeWrapper; NewsletterEmailsErrorCode: NewsletterEmailsErrorCode; @@ -4494,7 +4492,7 @@ export type ResolversTypes = { SubscribeInput: SubscribeInput; SubscribeResult: ResolversTypes['SubscribeError'] | ResolversTypes['SubscribeSuccess']; SubscribeSuccess: ResolverTypeWrapper; - Subscription: ResolverTypeWrapper<{}>; + Subscription: ResolverTypeWrapper; SubscriptionStatus: SubscriptionStatus; SubscriptionType: SubscriptionType; SubscriptionsError: ResolverTypeWrapper; @@ -4856,6 +4854,7 @@ export type ResolversParentTypes = { MoveToFolderResult: ResolversParentTypes['MoveToFolderError'] | ResolversParentTypes['MoveToFolderSuccess']; MoveToFolderSuccess: MoveToFolderSuccess; Mutation: {}; + MySubscriptionRootType: {}; NewsletterEmail: NewsletterEmail; NewsletterEmailsError: NewsletterEmailsError; NewsletterEmailsResult: ResolversParentTypes['NewsletterEmailsError'] | ResolversParentTypes['NewsletterEmailsSuccess']; @@ -4994,7 +4993,7 @@ export type ResolversParentTypes = { SubscribeInput: SubscribeInput; SubscribeResult: ResolversParentTypes['SubscribeError'] | ResolversParentTypes['SubscribeSuccess']; SubscribeSuccess: SubscribeSuccess; - Subscription: {}; + Subscription: Subscription; SubscriptionsError: SubscriptionsError; SubscriptionsResult: ResolversParentTypes['SubscriptionsError'] | ResolversParentTypes['SubscriptionsSuccess']; SubscriptionsSuccess: SubscriptionsSuccess; @@ -6089,9 +6088,10 @@ export type JustReadFeedItemResolvers, ParentType, ContextType>; thumbnail?: Resolver, ParentType, ContextType>; title?: Resolver; - topic?: Resolver; + topic?: Resolver, ParentType, ContextType>; updatedAt?: Resolver, ParentType, ContextType>; url?: Resolver; + wordCount?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -6116,7 +6116,7 @@ export type JustReadFeedSuccessResolvers = { items?: Resolver, ParentType, ContextType>; - name?: Resolver; + name?: Resolver, ParentType, ContextType>; thumbnail?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -6357,6 +6357,10 @@ export type MutationResolvers>; }; +export type MySubscriptionRootTypeResolvers = { + justReadFeed?: SubscriptionResolver; +}; + export type NewsletterEmailResolvers = { address?: Resolver; confirmationCode?: Resolver, ParentType, ContextType>; @@ -6448,7 +6452,6 @@ export type QueryResolvers, ParentType, ContextType>; integration?: Resolver>; integrations?: Resolver; - justReadFeed?: Resolver>; labels?: Resolver; me?: Resolver, ParentType, ContextType>; newsletterEmails?: Resolver; @@ -7032,28 +7035,29 @@ export type SubscribeSuccessResolvers = { - autoAddToLibrary?: SubscriptionResolver, "autoAddToLibrary", ParentType, ContextType>; - count?: SubscriptionResolver; - createdAt?: SubscriptionResolver; - description?: SubscriptionResolver, "description", ParentType, ContextType>; - failedAt?: SubscriptionResolver, "failedAt", ParentType, ContextType>; - fetchContent?: SubscriptionResolver; - fetchContentType?: SubscriptionResolver; - folder?: SubscriptionResolver; - icon?: SubscriptionResolver, "icon", ParentType, ContextType>; - id?: SubscriptionResolver; - isPrivate?: SubscriptionResolver, "isPrivate", ParentType, ContextType>; - lastFetchedAt?: SubscriptionResolver, "lastFetchedAt", ParentType, ContextType>; - mostRecentItemDate?: SubscriptionResolver, "mostRecentItemDate", ParentType, ContextType>; - name?: SubscriptionResolver; - newsletterEmail?: SubscriptionResolver, "newsletterEmail", ParentType, ContextType>; - refreshedAt?: SubscriptionResolver, "refreshedAt", ParentType, ContextType>; - status?: SubscriptionResolver; - type?: SubscriptionResolver; - unsubscribeHttpUrl?: SubscriptionResolver, "unsubscribeHttpUrl", ParentType, ContextType>; - unsubscribeMailTo?: SubscriptionResolver, "unsubscribeMailTo", ParentType, ContextType>; - updatedAt?: SubscriptionResolver, "updatedAt", ParentType, ContextType>; - url?: SubscriptionResolver, "url", ParentType, ContextType>; + autoAddToLibrary?: Resolver, ParentType, ContextType>; + count?: Resolver; + createdAt?: Resolver; + description?: Resolver, ParentType, ContextType>; + failedAt?: Resolver, ParentType, ContextType>; + fetchContent?: Resolver; + fetchContentType?: Resolver; + folder?: Resolver; + icon?: Resolver, ParentType, ContextType>; + id?: Resolver; + isPrivate?: Resolver, ParentType, ContextType>; + lastFetchedAt?: Resolver, ParentType, ContextType>; + mostRecentItemDate?: Resolver, ParentType, ContextType>; + name?: Resolver; + newsletterEmail?: Resolver, ParentType, ContextType>; + refreshedAt?: Resolver, ParentType, ContextType>; + status?: Resolver; + type?: Resolver; + unsubscribeHttpUrl?: Resolver, ParentType, ContextType>; + unsubscribeMailTo?: Resolver, ParentType, ContextType>; + updatedAt?: Resolver, ParentType, ContextType>; + url?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; }; export type SubscriptionsErrorResolvers = { @@ -7676,6 +7680,7 @@ export type Resolvers = { MoveToFolderResult?: MoveToFolderResultResolvers; MoveToFolderSuccess?: MoveToFolderSuccessResolvers; Mutation?: MutationResolvers; + MySubscriptionRootType?: MySubscriptionRootTypeResolvers; NewsletterEmail?: NewsletterEmailResolvers; NewsletterEmailsError?: NewsletterEmailsErrorResolvers; NewsletterEmailsResult?: NewsletterEmailsResultResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 5dc0b0dc9..0474deea6 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1,3 +1,9 @@ +schema { + query: Query + mutation: Mutation + subscription: MySubscriptionRootType +} + directive @sanitize(allowedTags: [String], maxLength: Int, minLength: Int, pattern: String) on INPUT_FIELD_DEFINITION type AddDiscoverFeedError { @@ -1248,9 +1254,10 @@ type JustReadFeedItem { source: JustReadFeedSource thumbnail: String title: String! - topic: String! + topic: String updatedAt: Date url: String! + wordCount: Int } union JustReadFeedResult = JustReadFeedError | JustReadFeedSuccess @@ -1270,7 +1277,7 @@ type JustReadFeedSuccess { type JustReadFeedTopic { items: [JustReadFeedItem!]! - name: String! + name: String thumbnail: String } @@ -1555,6 +1562,10 @@ type Mutation { uploadImportFile(contentType: String!, type: UploadImportFileType!): UploadImportFileResult! } +type MySubscriptionRootType { + justReadFeed: JustReadFeedResult! +} + type NewsletterEmail { address: String! confirmationCode: String @@ -1692,7 +1703,6 @@ type Query { hello: String integration(name: String!): IntegrationResult! integrations: IntegrationsResult! - justReadFeed(after: String, first: Int, language: String, location: String): JustReadFeedResult! labels: LabelsResult! me: User newsletterEmails: NewsletterEmailsResult! diff --git a/packages/api/src/jobs/update_just_read_feed.ts b/packages/api/src/jobs/update_just_read_feed.ts index 33ff23d65..a72cf61de 100644 --- a/packages/api/src/jobs/update_just_read_feed.ts +++ b/packages/api/src/jobs/update_just_read_feed.ts @@ -1,8 +1,10 @@ import { LibraryItem } from '../entity/library_item' import { PublicItem } from '../entity/public_item' +import { User } from '../entity/user' import { redisDataSource } from '../redis_data_source' import { findUnseenPublicItems } from '../services/just_read_feed' import { searchLibraryItems } from '../services/library_item' +import { findActiveUser } from '../services/user' import { logger } from '../utils/logger' export const UPDATE_JUST_READ_FEED_JOB = 'UPDATE_JUST_READ_FEED_JOB' @@ -15,18 +17,22 @@ interface JustReadFeedItem { id: string title: string url: string - topic: string + createdAt: Date + sourceName: string + + siteName?: string + topic?: string thumbnail?: string previewContent?: string languageCode?: string author?: string dir?: string - publishedAt?: Date - subscription?: string + wordCount?: number + sourceIcon?: string } interface JustReadFeedTopic { - name: string + name?: string items: Array thumbnail: string } @@ -35,7 +41,10 @@ interface JustReadFeed { topics: Array } -const libraryItemToFeedItem = (item: LibraryItem): JustReadFeedItem => ({ +const libraryItemToFeedItem = ( + user: User, + item: LibraryItem +): JustReadFeedItem => ({ id: item.id, title: item.title, url: item.originalUrl, @@ -44,9 +53,12 @@ const libraryItemToFeedItem = (item: LibraryItem): JustReadFeedItem => ({ languageCode: item.itemLanguage || undefined, // TODO: map to language code author: item.author || undefined, dir: item.directionality || undefined, - publishedAt: item.publishedAt || undefined, - subscription: item.subscription || undefined, + createdAt: item.createdAt, topic: item.topic, + wordCount: item.wordCount || undefined, + sourceIcon: user.profile.pictureUrl || undefined, + sourceName: user.name, + siteName: item.siteName || undefined, }) const publicItemToFeedItem = (item: PublicItem): JustReadFeedItem => ({ @@ -58,14 +70,23 @@ const publicItemToFeedItem = (item: PublicItem): JustReadFeedItem => ({ languageCode: item.languageCode, author: item.author, dir: item.dir, - publishedAt: item.publishedAt, - subscription: item.source_name, + createdAt: item.createdAt, topic: item.topic, + wordCount: item.wordCount || undefined, + sourceIcon: item.sourceIcon, + sourceName: item.sourceName, + siteName: item.siteName, }) +interface FeedItemScore { + id: string + score: number +} + const selectCandidates = async ( - userId: string + user: User ): Promise> => { + const userId = user.id // get last 100 library items saved and not seen by user const libraryItems = await searchLibraryItems( { @@ -78,7 +99,7 @@ const selectCandidates = async ( // map library items to candidates const privateCandidates: Array = libraryItems.map( - libraryItemToFeedItem + (libraryItem) => libraryItemToFeedItem(user, libraryItem) ) // get candidates from public inventory @@ -98,11 +119,12 @@ const rankFeedItems = async ( feedItems: Array ): Promise> => { if (feedItems.length <= 10) { + // no need to rank if there are less than 10 candidates return feedItems } - // TODO: rank candidates - const API_URL = 'https://rank.omnivore.app' + // TODO: get score of candidates + const API_URL = 'https://score.omnivore.app' // fake URL const response = await fetch(API_URL, { method: 'POST', @@ -113,18 +135,25 @@ const rankFeedItems = async ( }) if (!response.ok) { - throw new Error(`Failed to rank candidates: ${response.statusText}`) + throw new Error(`Failed to score candidates: ${response.statusText}`) } - return response.json() as Promise> + const scores = (await response.json()) as Array + + // rank candidates by score in ascending order + return feedItems.sort((a, b) => { + const scoreA = scores.find((score) => score.id === a.id)?.score || 0 + const scoreB = scores.find((score) => score.id === b.id)?.score || 0 + + return scoreA - scoreB + }) } const redisKey = (userId: string) => `just-read-feed:${userId}` +const MAX_FEED_ITEMS = 500 export const getJustReadFeed = async ( - userId: string, - limit: number, - offset: number + userId: string ): Promise => { const redisClient = redisDataSource.redisClient if (!redisClient) { @@ -133,7 +162,7 @@ export const getJustReadFeed = async ( const key = redisKey(userId) - const results = await redisClient.lrange(key, offset, offset + limit - 1) + const results = await redisClient.zrevrange(key, 0, MAX_FEED_ITEMS) const feedItems = results.map((item) => JSON.parse(item) as JustReadFeedItem) @@ -155,8 +184,8 @@ export const getJustReadFeed = async ( return { topics } } -const prependItemsToFeed = async ( - candidates: Array, +const appendItemsToFeed = async ( + feedItems: Array, userId: string ) => { const redisClient = redisDataSource.redisClient @@ -166,26 +195,43 @@ const prependItemsToFeed = async ( const key = redisKey(userId) + // store candidates in redis sorted set const pipeline = redisClient.pipeline() - candidates.forEach((candidate) => - pipeline.lpush(key, JSON.stringify(candidate)) - ) - // keep only the first 100 items - pipeline.ltrim(key, 0, 99) + + const scoreMembers = feedItems.flatMap((item) => [ + Date.now() + 86_400_000, // items expire in 24 hours + JSON.stringify(item), + ]) + // add candidates to the sorted set + pipeline.zadd(key, ...scoreMembers) + + // remove expired items and keep only the top 500 + pipeline.zremrangebyrank(key, 0, -(MAX_FEED_ITEMS + 1)) + pipeline.zremrangebyscore(key, '-inf', Date.now()) await pipeline.exec() } -const updateJustReadFeed = async (data: UpdateJustReadFeedJobData) => { +export const updateJustReadFeed = async (data: UpdateJustReadFeedJobData) => { const { userId } = data + const user = await findActiveUser(userId) + if (!user) { + logger.error(`User ${userId} not found`) + return + } + logger.info(`Updating just read feed for user ${userId}`) - const feedItems = await selectCandidates(userId) + const feedItems = await selectCandidates(user) logger.info(`Found ${feedItems.length} candidates`) // TODO: integrity check on candidates? const rankedFeedItems = await rankFeedItems(feedItems) - await prependItemsToFeed(rankedFeedItems, userId) + // TODO: filtering + // get top 100 ranked feed items + const filteredFeedItems = rankedFeedItems.slice(0, 100) + + await appendItemsToFeed(filteredFeedItems, userId) } diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index 359bdfcf1..edb634e70 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -59,6 +59,10 @@ import { UPDATE_HIGHLIGHT_JOB, UPDATE_LABELS_JOB, } from './jobs/update_db' +import { + updateJustReadFeed, + UPDATE_JUST_READ_FEED_JOB, +} from './jobs/update_just_read_feed' import { updatePDFContentJob } from './jobs/update_pdf_content' import { uploadContentJob, UPLOAD_CONTENT_JOB } from './jobs/upload_content' import { redisDataSource } from './redis_data_source' @@ -185,6 +189,8 @@ export const createWorker = (connection: ConnectionOptions) => return createDigest(job.data) case UPLOAD_CONTENT_JOB: return uploadContentJob(job.data) + case UPDATE_JUST_READ_FEED_JOB: + return updateJustReadFeed(job.data) default: logger.warning(`[queue-processor] unhandled job: ${job.name}`) } diff --git a/packages/api/src/resolvers/just_read_feed/index.ts b/packages/api/src/resolvers/just_read_feed/index.ts index 92e78a417..c907d1ddb 100644 --- a/packages/api/src/resolvers/just_read_feed/index.ts +++ b/packages/api/src/resolvers/just_read_feed/index.ts @@ -1,31 +1,13 @@ -import { - JustReadFeedError, - JustReadFeedSuccess, - QueryJustReadFeedArgs, -} from '../../generated/graphql' +import { JustReadFeedError, JustReadFeedSuccess } from '../../generated/graphql' import { getJustReadFeed } from '../../jobs/update_just_read_feed' -import { enqueueUpdateJustReadFeed } from '../../utils/createTask' import { authorized } from '../../utils/gql-utils' export const justReadFeedResolver = authorized< JustReadFeedSuccess, - JustReadFeedError, - QueryJustReadFeedArgs ->(async (_, { first, after }, { uid, log }) => { - first = first || 10 - after = after || '0' - const offset = parseInt(after, 10) - - const feed = await getJustReadFeed(uid, first, offset) - if (feed.topics.length === 0) { - log.info('No feed items found, updating feed') - - await enqueueUpdateJustReadFeed({ userId: uid }) - - return { - topics: [], - } - } + JustReadFeedError +>(async (_, __, { uid, log }) => { + const feed = await getJustReadFeed(uid) + log.info('Just read feed fetched') return feed }) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 9eabe1a73..ad6d04cbc 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3114,7 +3114,7 @@ const schema = gql` type JustReadFeedItem { id: ID! title: String! - topic: String! + topic: String url: String! thumbnail: String publishedAt: Date @@ -3131,10 +3131,11 @@ const schema = gql` languageCode: String dir: String seen_at: Date + wordCount: Int } type JustReadFeedTopic { - name: String! + name: String items: [JustReadFeedItem!]! thumbnail: String } @@ -3154,6 +3155,10 @@ const schema = gql` union JustReadFeedResult = JustReadFeedSuccess | JustReadFeedError + type MySubscriptionRootType { + justReadFeed: JustReadFeedResult! + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -3349,12 +3354,12 @@ const schema = gql` feeds(input: FeedsInput!): FeedsResult! discoverFeeds: DiscoverFeedResult! scanFeeds(input: ScanFeedsInput!): ScanFeedsResult! - justReadFeed( - location: String - language: String - first: Int - after: String - ): JustReadFeedResult! + } + + schema { + query: Query + mutation: Mutation + subscription: MySubscriptionRootType } ` diff --git a/packages/db/migrations/0177.do.public_item.sql b/packages/db/migrations/0177.do.public_item.sql index d5c3b31d4..59df6453a 100755 --- a/packages/db/migrations/0177.do.public_item.sql +++ b/packages/db/migrations/0177.do.public_item.sql @@ -12,8 +12,8 @@ CREATE TABLE omnivore.public_item_source ( url TEXT NOT NULL, language_codes TEXT[] NOT NULL, approved BOOLEAN NOT NULL DEFAULT FALSE, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER update_public_item_source_modtime BEFORE UPDATE ON omnivore.public_item_source FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); @@ -26,7 +26,7 @@ CREATE TABLE omnivore.public_item ( type TEXT NOT NULL, -- public feeds, newsletters, or user recommended title TEXT NOT NULL, url TEXT NOT NULL, - topic TEXT NOT NULL, + topic TEXT, approved BOOLEAN NOT NULL DEFAULT FALSE, thumbnail TEXT, preview_content TEXT, @@ -34,8 +34,10 @@ CREATE TABLE omnivore.public_item ( author TEXT, dir TEXT, published_at timestamptz, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + word_count INT, + site_name TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER update_public_item_modtime BEFORE UPDATE ON omnivore.public_item FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); @@ -47,8 +49,8 @@ CREATE TABLE omnivore.public_item_stats ( save_count INT NOT NULL DEFAULT 0, like_count INT NOT NULL DEFAULT 0, broadcast_count INT NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX public_item_stats_public_item_id_idx ON omnivore.public_item_stats(public_item_id); @@ -63,6 +65,7 @@ CREATE TABLE omnivore.public_item_interactions ( liked_at TIMESTAMPTZ, broadcasted_at TIMESTAMPTZ, seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + digested_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -74,6 +77,7 @@ CREATE TRIGGER update_public_item_interactions_modtime BEFORE UPDATE ON omnivore ALTER TABLE omnivore.library_item ADD COLUMN seen_at timestamptz, - ADD COLUMN topic TEXT NOT NULL; + ADD COLUMN digested_at timestamptz, + ADD COLUMN topic TEXT; COMMIT; diff --git a/packages/db/migrations/0177.undo.public_item.sql b/packages/db/migrations/0177.undo.public_item.sql index 5b9704f4c..a58f38333 100755 --- a/packages/db/migrations/0177.undo.public_item.sql +++ b/packages/db/migrations/0177.undo.public_item.sql @@ -11,6 +11,7 @@ DROP TABLE omnivore.public_item_source; ALTER TABLE omnivore.library_item DROP COLUMN seen_at, + DROP COLUMN digested_at, DROP COLUMN topic; COMMIT;