save just read feed in redis sorted set

This commit is contained in:
Hongbo Wu
2024-05-24 14:52:21 +08:00
parent cb4fc23507
commit a48a614676
11 changed files with 184 additions and 113 deletions

View File

@ -209,5 +209,8 @@ export class LibraryItem {
seenAt?: Date
@Column('text')
topic!: string
topic?: string
@Column('timestamptz')
digestedAt?: Date
}

View File

@ -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
}

View File

@ -41,4 +41,7 @@ export class PublicItemInteraction {
@Column('timestamptz')
updated!: Date
@Column('timestamptz')
digested?: Date
}

View File

@ -1391,9 +1391,10 @@ export type JustReadFeedItem = {
source?: Maybe<JustReadFeedSource>;
thumbnail?: Maybe<Scalars['String']>;
title: Scalars['String'];
topic: Scalars['String'];
topic?: Maybe<Scalars['String']>;
updatedAt?: Maybe<Scalars['Date']>;
url: Scalars['String'];
wordCount?: Maybe<Scalars['Int']>;
};
export type JustReadFeedResult = JustReadFeedError | JustReadFeedSuccess;
@ -1416,7 +1417,7 @@ export type JustReadFeedSuccess = {
export type JustReadFeedTopic = {
__typename?: 'JustReadFeedTopic';
items: Array<JustReadFeedItem>;
name: Scalars['String'];
name?: Maybe<Scalars['String']>;
thumbnail?: Maybe<Scalars['String']>;
};
@ -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<Scalars['String']>;
integration: IntegrationResult;
integrations: IntegrationsResult;
justReadFeed: JustReadFeedResult;
labels: LabelsResult;
me?: Maybe<User>;
newsletterEmails: NewsletterEmailsResult;
@ -2270,14 +2275,6 @@ export type QueryIntegrationArgs = {
};
export type QueryJustReadFeedArgs = {
after?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
language?: InputMaybe<Scalars['String']>;
location?: InputMaybe<Scalars['String']>;
};
export type QueryRulesArgs = {
enabled?: InputMaybe<Scalars['Boolean']>;
};
@ -4317,6 +4314,7 @@ export type ResolversTypes = {
MoveToFolderResult: ResolversTypes['MoveToFolderError'] | ResolversTypes['MoveToFolderSuccess'];
MoveToFolderSuccess: ResolverTypeWrapper<MoveToFolderSuccess>;
Mutation: ResolverTypeWrapper<{}>;
MySubscriptionRootType: ResolverTypeWrapper<{}>;
NewsletterEmail: ResolverTypeWrapper<NewsletterEmail>;
NewsletterEmailsError: ResolverTypeWrapper<NewsletterEmailsError>;
NewsletterEmailsErrorCode: NewsletterEmailsErrorCode;
@ -4494,7 +4492,7 @@ export type ResolversTypes = {
SubscribeInput: SubscribeInput;
SubscribeResult: ResolversTypes['SubscribeError'] | ResolversTypes['SubscribeSuccess'];
SubscribeSuccess: ResolverTypeWrapper<SubscribeSuccess>;
Subscription: ResolverTypeWrapper<{}>;
Subscription: ResolverTypeWrapper<Subscription>;
SubscriptionStatus: SubscriptionStatus;
SubscriptionType: SubscriptionType;
SubscriptionsError: ResolverTypeWrapper<SubscriptionsError>;
@ -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<ContextType = ResolverContext, ParentType
source?: Resolver<Maybe<ResolversTypes['JustReadFeedSource']>, ParentType, ContextType>;
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
topic?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
topic?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
wordCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -6116,7 +6116,7 @@ export type JustReadFeedSuccessResolvers<ContextType = ResolverContext, ParentTy
export type JustReadFeedTopicResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedTopic'] = ResolversParentTypes['JustReadFeedTopic']> = {
items?: Resolver<Array<ResolversTypes['JustReadFeedItem']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -6357,6 +6357,10 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
uploadImportFile?: Resolver<ResolversTypes['UploadImportFileResult'], ParentType, ContextType, RequireFields<MutationUploadImportFileArgs, 'contentType' | 'type'>>;
};
export type MySubscriptionRootTypeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['MySubscriptionRootType'] = ResolversParentTypes['MySubscriptionRootType']> = {
justReadFeed?: SubscriptionResolver<ResolversTypes['JustReadFeedResult'], "justReadFeed", ParentType, ContextType>;
};
export type NewsletterEmailResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['NewsletterEmail'] = ResolversParentTypes['NewsletterEmail']> = {
address?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
confirmationCode?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@ -6448,7 +6452,6 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
hello?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
integration?: Resolver<ResolversTypes['IntegrationResult'], ParentType, ContextType, RequireFields<QueryIntegrationArgs, 'name'>>;
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
justReadFeed?: Resolver<ResolversTypes['JustReadFeedResult'], ParentType, ContextType, Partial<QueryJustReadFeedArgs>>;
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
newsletterEmails?: Resolver<ResolversTypes['NewsletterEmailsResult'], ParentType, ContextType>;
@ -7032,28 +7035,29 @@ export type SubscribeSuccessResolvers<ContextType = ResolverContext, ParentType
};
export type SubscriptionResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription']> = {
autoAddToLibrary?: SubscriptionResolver<Maybe<ResolversTypes['Boolean']>, "autoAddToLibrary", ParentType, ContextType>;
count?: SubscriptionResolver<ResolversTypes['Int'], "count", ParentType, ContextType>;
createdAt?: SubscriptionResolver<ResolversTypes['Date'], "createdAt", ParentType, ContextType>;
description?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "description", ParentType, ContextType>;
failedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "failedAt", ParentType, ContextType>;
fetchContent?: SubscriptionResolver<ResolversTypes['Boolean'], "fetchContent", ParentType, ContextType>;
fetchContentType?: SubscriptionResolver<ResolversTypes['FetchContentType'], "fetchContentType", ParentType, ContextType>;
folder?: SubscriptionResolver<ResolversTypes['String'], "folder", ParentType, ContextType>;
icon?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "icon", ParentType, ContextType>;
id?: SubscriptionResolver<ResolversTypes['ID'], "id", ParentType, ContextType>;
isPrivate?: SubscriptionResolver<Maybe<ResolversTypes['Boolean']>, "isPrivate", ParentType, ContextType>;
lastFetchedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "lastFetchedAt", ParentType, ContextType>;
mostRecentItemDate?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "mostRecentItemDate", ParentType, ContextType>;
name?: SubscriptionResolver<ResolversTypes['String'], "name", ParentType, ContextType>;
newsletterEmail?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "newsletterEmail", ParentType, ContextType>;
refreshedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "refreshedAt", ParentType, ContextType>;
status?: SubscriptionResolver<ResolversTypes['SubscriptionStatus'], "status", ParentType, ContextType>;
type?: SubscriptionResolver<ResolversTypes['SubscriptionType'], "type", ParentType, ContextType>;
unsubscribeHttpUrl?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "unsubscribeHttpUrl", ParentType, ContextType>;
unsubscribeMailTo?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "unsubscribeMailTo", ParentType, ContextType>;
updatedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "updatedAt", ParentType, ContextType>;
url?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "url", ParentType, ContextType>;
autoAddToLibrary?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
count?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
failedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
fetchContent?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
fetchContentType?: Resolver<ResolversTypes['FetchContentType'], ParentType, ContextType>;
folder?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
icon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isPrivate?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
lastFetchedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
mostRecentItemDate?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
newsletterEmail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
refreshedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
status?: Resolver<ResolversTypes['SubscriptionStatus'], ParentType, ContextType>;
type?: Resolver<ResolversTypes['SubscriptionType'], ParentType, ContextType>;
unsubscribeHttpUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
unsubscribeMailTo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SubscriptionsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SubscriptionsError'] = ResolversParentTypes['SubscriptionsError']> = {
@ -7676,6 +7680,7 @@ export type Resolvers<ContextType = ResolverContext> = {
MoveToFolderResult?: MoveToFolderResultResolvers<ContextType>;
MoveToFolderSuccess?: MoveToFolderSuccessResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
MySubscriptionRootType?: MySubscriptionRootTypeResolvers<ContextType>;
NewsletterEmail?: NewsletterEmailResolvers<ContextType>;
NewsletterEmailsError?: NewsletterEmailsErrorResolvers<ContextType>;
NewsletterEmailsResult?: NewsletterEmailsResultResolvers<ContextType>;

View File

@ -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!

View File

@ -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<JustReadFeedItem>
thumbnail: string
}
@ -35,7 +41,10 @@ interface JustReadFeed {
topics: Array<JustReadFeedTopic>
}
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<Array<JustReadFeedItem>> => {
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<JustReadFeedItem> = libraryItems.map(
libraryItemToFeedItem
(libraryItem) => libraryItemToFeedItem(user, libraryItem)
)
// get candidates from public inventory
@ -98,11 +119,12 @@ const rankFeedItems = async (
feedItems: Array<JustReadFeedItem>
): Promise<Array<JustReadFeedItem>> => {
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<Array<JustReadFeedItem>>
const scores = (await response.json()) as Array<FeedItemScore>
// 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<JustReadFeed> => {
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<JustReadFeedItem>,
const appendItemsToFeed = async (
feedItems: Array<JustReadFeedItem>,
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)
}

View File

@ -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}`)
}

View File

@ -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
})

View File

@ -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
}
`

View File

@ -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;

View File

@ -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;