add pagination

This commit is contained in:
Hongbo Wu 2024-05-25 11:47:45 +08:00
parent 880a907679
commit 1ba6221bc4
5 changed files with 219 additions and 196 deletions

View file

@ -1370,6 +1370,7 @@ export type JustReadFeedError = {
export enum JustReadFeedErrorCode {
BadRequest = 'BAD_REQUEST',
Pending = 'PENDING',
Unauthorized = 'UNAUTHORIZED'
}
@ -1377,49 +1378,47 @@ export type JustReadFeedItem = {
__typename?: 'JustReadFeedItem';
author?: Maybe<Scalars['String']>;
broadcastCount?: Maybe<Scalars['Int']>;
comments?: Maybe<Array<Scalars['String']>>;
createdAt: Scalars['Date'];
canArchive?: Maybe<Scalars['Boolean']>;
canComment?: Maybe<Scalars['Boolean']>;
canDelete?: Maybe<Scalars['Boolean']>;
canSave?: Maybe<Scalars['Boolean']>;
canShare?: Maybe<Scalars['Boolean']>;
date: Scalars['Date'];
dir?: Maybe<Scalars['String']>;
highlights?: Maybe<Scalars['String']>;
id: Scalars['ID'];
languageCode?: Maybe<Scalars['String']>;
likedCount?: Maybe<Scalars['Int']>;
previewContent?: Maybe<Scalars['String']>;
savedCount?: Maybe<Scalars['Int']>;
seen_at?: Maybe<Scalars['Date']>;
siteName?: Maybe<Scalars['String']>;
sourceIcon?: Maybe<Scalars['String']>;
sourceName: Scalars['String'];
subscription: JustReadFeedSubscription;
thumbnail?: Maybe<Scalars['String']>;
title: Scalars['String'];
topic?: Maybe<Scalars['String']>;
updatedAt: Scalars['Date'];
url: Scalars['String'];
wordCount?: Maybe<Scalars['Int']>;
};
export type JustReadFeedResult = JustReadFeedError | JustReadFeedSuccess;
export type JustReadFeedSource = {
__typename?: 'JustReadFeedSource';
id: Scalars['ID'];
languageCodes?: Maybe<Array<Scalars['String']>>;
name: Scalars['String'];
export type JustReadFeedSection = {
__typename?: 'JustReadFeedSection';
items: Array<JustReadFeedItem>;
layout?: Maybe<Scalars['String']>;
thumbnail?: Maybe<Scalars['String']>;
topics?: Maybe<Array<Scalars['String']>>;
url: Scalars['String'];
title?: Maybe<Scalars['String']>;
};
export type JustReadFeedSubscription = {
__typename?: 'JustReadFeedSubscription';
icon?: Maybe<Scalars['String']>;
id: Scalars['ID'];
name: Scalars['String'];
url?: Maybe<Scalars['String']>;
};
export type JustReadFeedSuccess = {
__typename?: 'JustReadFeedSuccess';
topics: Array<JustReadFeedTopic>;
};
export type JustReadFeedTopic = {
__typename?: 'JustReadFeedTopic';
items: Array<JustReadFeedItem>;
name?: Maybe<Scalars['String']>;
thumbnail?: Maybe<Scalars['String']>;
sections: Array<JustReadFeedSection>;
};
export type Label = {
@ -2074,11 +2073,6 @@ export type MutationUploadImportFileArgs = {
type: UploadImportFileType;
};
export type MySubscriptionRootType = {
__typename?: 'MySubscriptionRootType';
hello?: Maybe<Scalars['String']>;
};
export type NewsletterEmail = {
__typename?: 'NewsletterEmail';
address: Scalars['String'];
@ -2277,6 +2271,12 @@ export type QueryIntegrationArgs = {
};
export type QueryJustReadFeedArgs = {
after?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
};
export type QueryRulesArgs = {
enabled?: InputMaybe<Scalars['Boolean']>;
};
@ -3285,6 +3285,11 @@ export type Subscription = {
url?: Maybe<Scalars['String']>;
};
export type SubscriptionRootType = {
__typename?: 'SubscriptionRootType';
hello?: Maybe<Scalars['String']>;
};
export enum SubscriptionStatus {
Active = 'ACTIVE',
Deleted = 'DELETED',
@ -4270,9 +4275,9 @@ export type ResolversTypes = {
JustReadFeedErrorCode: JustReadFeedErrorCode;
JustReadFeedItem: ResolverTypeWrapper<JustReadFeedItem>;
JustReadFeedResult: ResolversTypes['JustReadFeedError'] | ResolversTypes['JustReadFeedSuccess'];
JustReadFeedSource: ResolverTypeWrapper<JustReadFeedSource>;
JustReadFeedSection: ResolverTypeWrapper<JustReadFeedSection>;
JustReadFeedSubscription: ResolverTypeWrapper<JustReadFeedSubscription>;
JustReadFeedSuccess: ResolverTypeWrapper<JustReadFeedSuccess>;
JustReadFeedTopic: ResolverTypeWrapper<JustReadFeedTopic>;
Label: ResolverTypeWrapper<Label>;
LabelsError: ResolverTypeWrapper<LabelsError>;
LabelsErrorCode: LabelsErrorCode;
@ -4316,7 +4321,6 @@ export type ResolversTypes = {
MoveToFolderResult: ResolversTypes['MoveToFolderError'] | ResolversTypes['MoveToFolderSuccess'];
MoveToFolderSuccess: ResolverTypeWrapper<MoveToFolderSuccess>;
Mutation: ResolverTypeWrapper<{}>;
MySubscriptionRootType: ResolverTypeWrapper<{}>;
NewsletterEmail: ResolverTypeWrapper<NewsletterEmail>;
NewsletterEmailsError: ResolverTypeWrapper<NewsletterEmailsError>;
NewsletterEmailsErrorCode: NewsletterEmailsErrorCode;
@ -4495,6 +4499,7 @@ export type ResolversTypes = {
SubscribeResult: ResolversTypes['SubscribeError'] | ResolversTypes['SubscribeSuccess'];
SubscribeSuccess: ResolverTypeWrapper<SubscribeSuccess>;
Subscription: ResolverTypeWrapper<Subscription>;
SubscriptionRootType: ResolverTypeWrapper<{}>;
SubscriptionStatus: SubscriptionStatus;
SubscriptionType: SubscriptionType;
SubscriptionsError: ResolverTypeWrapper<SubscriptionsError>;
@ -4819,9 +4824,9 @@ export type ResolversParentTypes = {
JustReadFeedError: JustReadFeedError;
JustReadFeedItem: JustReadFeedItem;
JustReadFeedResult: ResolversParentTypes['JustReadFeedError'] | ResolversParentTypes['JustReadFeedSuccess'];
JustReadFeedSource: JustReadFeedSource;
JustReadFeedSection: JustReadFeedSection;
JustReadFeedSubscription: JustReadFeedSubscription;
JustReadFeedSuccess: JustReadFeedSuccess;
JustReadFeedTopic: JustReadFeedTopic;
Label: Label;
LabelsError: LabelsError;
LabelsResult: ResolversParentTypes['LabelsError'] | ResolversParentTypes['LabelsSuccess'];
@ -4856,7 +4861,6 @@ export type ResolversParentTypes = {
MoveToFolderResult: ResolversParentTypes['MoveToFolderError'] | ResolversParentTypes['MoveToFolderSuccess'];
MoveToFolderSuccess: MoveToFolderSuccess;
Mutation: {};
MySubscriptionRootType: {};
NewsletterEmail: NewsletterEmail;
NewsletterEmailsError: NewsletterEmailsError;
NewsletterEmailsResult: ResolversParentTypes['NewsletterEmailsError'] | ResolversParentTypes['NewsletterEmailsSuccess'];
@ -4996,6 +5000,7 @@ export type ResolversParentTypes = {
SubscribeResult: ResolversParentTypes['SubscribeError'] | ResolversParentTypes['SubscribeSuccess'];
SubscribeSuccess: SubscribeSuccess;
Subscription: Subscription;
SubscriptionRootType: {};
SubscriptionsError: SubscriptionsError;
SubscriptionsResult: ResolversParentTypes['SubscriptionsError'] | ResolversParentTypes['SubscriptionsSuccess'];
SubscriptionsSuccess: SubscriptionsSuccess;
@ -6076,23 +6081,22 @@ export type JustReadFeedErrorResolvers<ContextType = ResolverContext, ParentType
export type JustReadFeedItemResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedItem'] = ResolversParentTypes['JustReadFeedItem']> = {
author?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
broadcastCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
comments?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
canArchive?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
canComment?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
canDelete?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
canSave?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
canShare?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
date?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
dir?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
highlights?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
languageCode?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
likedCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
previewContent?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
savedCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
seen_at?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
siteName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
sourceIcon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
sourceName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
subscription?: Resolver<ResolversTypes['JustReadFeedSubscription'], ParentType, ContextType>;
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
topic?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
wordCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -6102,25 +6106,24 @@ export type JustReadFeedResultResolvers<ContextType = ResolverContext, ParentTyp
__resolveType: TypeResolveFn<'JustReadFeedError' | 'JustReadFeedSuccess', ParentType, ContextType>;
};
export type JustReadFeedSourceResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedSource'] = ResolversParentTypes['JustReadFeedSource']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
languageCodes?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
export type JustReadFeedSectionResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedSection'] = ResolversParentTypes['JustReadFeedSection']> = {
items?: Resolver<Array<ResolversTypes['JustReadFeedItem']>, ParentType, ContextType>;
layout?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
topics?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
title?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type JustReadFeedSubscriptionResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedSubscription'] = ResolversParentTypes['JustReadFeedSubscription']> = {
icon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type JustReadFeedSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedSuccess'] = ResolversParentTypes['JustReadFeedSuccess']> = {
topics?: Resolver<Array<ResolversTypes['JustReadFeedTopic']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type JustReadFeedTopicResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JustReadFeedTopic'] = ResolversParentTypes['JustReadFeedTopic']> = {
items?: Resolver<Array<ResolversTypes['JustReadFeedItem']>, ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
sections?: Resolver<Array<ResolversTypes['JustReadFeedSection']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -6360,10 +6363,6 @@ 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']> = {
hello?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "hello", 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>;
@ -6455,7 +6454,7 @@ 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>;
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>;
@ -7064,6 +7063,10 @@ export type SubscriptionResolvers<ContextType = ResolverContext, ParentType exte
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SubscriptionRootTypeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SubscriptionRootType'] = ResolversParentTypes['SubscriptionRootType']> = {
hello?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "hello", ParentType, ContextType>;
};
export type SubscriptionsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SubscriptionsError'] = ResolversParentTypes['SubscriptionsError']> = {
errorCodes?: Resolver<Array<ResolversTypes['SubscriptionsErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -7650,9 +7653,9 @@ export type Resolvers<ContextType = ResolverContext> = {
JustReadFeedError?: JustReadFeedErrorResolvers<ContextType>;
JustReadFeedItem?: JustReadFeedItemResolvers<ContextType>;
JustReadFeedResult?: JustReadFeedResultResolvers<ContextType>;
JustReadFeedSource?: JustReadFeedSourceResolvers<ContextType>;
JustReadFeedSection?: JustReadFeedSectionResolvers<ContextType>;
JustReadFeedSubscription?: JustReadFeedSubscriptionResolvers<ContextType>;
JustReadFeedSuccess?: JustReadFeedSuccessResolvers<ContextType>;
JustReadFeedTopic?: JustReadFeedTopicResolvers<ContextType>;
Label?: LabelResolvers<ContextType>;
LabelsError?: LabelsErrorResolvers<ContextType>;
LabelsResult?: LabelsResultResolvers<ContextType>;
@ -7684,7 +7687,6 @@ 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>;
@ -7795,6 +7797,7 @@ export type Resolvers<ContextType = ResolverContext> = {
SubscribeResult?: SubscribeResultResolvers<ContextType>;
SubscribeSuccess?: SubscribeSuccessResolvers<ContextType>;
Subscription?: SubscriptionResolvers<ContextType>;
SubscriptionRootType?: SubscriptionRootTypeResolvers<ContextType>;
SubscriptionsError?: SubscriptionsErrorResolvers<ContextType>;
SubscriptionsResult?: SubscriptionsResultResolvers<ContextType>;
SubscriptionsSuccess?: SubscriptionsSuccessResolvers<ContextType>;

View file

@ -1,7 +1,7 @@
schema {
query: Query
mutation: Mutation
subscription: MySubscriptionRootType
subscription: SubscriptionRootType
}
directive @sanitize(allowedTags: [String], maxLength: Int, minLength: Int, pattern: String) on INPUT_FIELD_DEFINITION
@ -1234,52 +1234,51 @@ type JustReadFeedError {
enum JustReadFeedErrorCode {
BAD_REQUEST
PENDING
UNAUTHORIZED
}
type JustReadFeedItem {
author: String
broadcastCount: Int
comments: [String!]
createdAt: Date!
canArchive: Boolean
canComment: Boolean
canDelete: Boolean
canSave: Boolean
canShare: Boolean
date: Date!
dir: String
highlights: String
id: ID!
languageCode: String
likedCount: Int
previewContent: String
savedCount: Int
seen_at: Date
siteName: String
sourceIcon: String
sourceName: String!
subscription: JustReadFeedSubscription!
thumbnail: String
title: String!
topic: String
updatedAt: Date!
url: String!
wordCount: Int
}
union JustReadFeedResult = JustReadFeedError | JustReadFeedSuccess
type JustReadFeedSource {
id: ID!
languageCodes: [String!]
name: String!
type JustReadFeedSection {
items: [JustReadFeedItem!]!
layout: String
thumbnail: String
topics: [String!]
url: String!
title: String
}
type JustReadFeedSubscription {
icon: String
id: ID!
name: String!
url: String
}
type JustReadFeedSuccess {
topics: [JustReadFeedTopic!]!
}
type JustReadFeedTopic {
items: [JustReadFeedItem!]!
name: String
thumbnail: String
sections: [JustReadFeedSection!]!
}
type Label {
@ -1563,10 +1562,6 @@ type Mutation {
uploadImportFile(contentType: String!, type: UploadImportFileType!): UploadImportFileResult!
}
type MySubscriptionRootType {
hello: String
}
type NewsletterEmail {
address: String!
confirmationCode: String
@ -1704,7 +1699,7 @@ type Query {
hello: String
integration(name: String!): IntegrationResult!
integrations: IntegrationsResult!
justReadFeed: JustReadFeedResult!
justReadFeed(after: String, first: Int): JustReadFeedResult!
labels: LabelsResult!
me: User
newsletterEmails: NewsletterEmailsResult!
@ -2606,6 +2601,10 @@ type Subscription {
url: String
}
type SubscriptionRootType {
hello: String
}
enum SubscriptionStatus {
ACTIVE
DELETED

View file

@ -2,6 +2,11 @@ import languages from '@cospired/i18n-iso-languages'
import { LibraryItem } from '../entity/library_item'
import { PublicItem } from '../entity/public_item'
import { User } from '../entity/user'
import {
JustReadFeedItem,
JustReadFeedSection,
JustReadFeedSuccess,
} from '../generated/graphql'
import { redisDataSource } from '../redis_data_source'
import { findUnseenPublicItems } from '../services/just_read_feed'
import { searchLibraryItems } from '../services/library_item'
@ -14,37 +19,44 @@ export interface UpdateJustReadFeedJobData {
userId: string
}
interface JustReadFeedItem {
interface Feature {
title: string
has_thumbnail: boolean
has_site_icon: boolean
saved_at: Date
site?: string
language?: string
author?: string
directionality: string
word_count?: number
subscription_type: string
folder: string
}
interface ScoreApiRequestBody {
user_id: string
item_features: Record<string, Feature> // item_id -> feature
}
interface Candidate {
id: string
title: string
url: string
createdAt: Date
updatedAt: Date
sourceName: string
siteName?: string
topic?: string
thumbnail?: string
previewContent?: string
languageCode?: string
languageCode: string
author?: string
dir?: string
dir: string
date: Date
topic?: string
wordCount?: number
sourceIcon?: string
saveCount?: number
likeCount?: number
broadcastCount?: number
}
interface JustReadFeedTopic {
name?: string
items: Array<JustReadFeedItem>
thumbnail: string
}
interface JustReadFeed {
topics: Array<JustReadFeedTopic>
siteName?: string
subscription: {
id: string
name: string
icon?: string
}
}
const lanaugeToCode = (language: string): string =>
@ -62,13 +74,16 @@ const libraryItemToFeedItem = (
languageCode: lanaugeToCode(item.itemLanguage || 'English'),
author: item.author || undefined,
dir: item.directionality || 'ltr',
createdAt: item.createdAt,
date: item.createdAt,
topic: item.topic,
wordCount: item.wordCount || undefined,
sourceIcon: user.profile.pictureUrl || undefined,
sourceName: user.name,
siteName: item.siteName || undefined,
updatedAt: item.updatedAt,
subscription: {
id: user.id,
name: user.name,
icon: user.profile.pictureUrl || undefined,
},
})
const publicItemToFeedItem = (item: PublicItem): JustReadFeedItem => ({
@ -80,11 +95,9 @@ const publicItemToFeedItem = (item: PublicItem): JustReadFeedItem => ({
languageCode: item.languageCode || 'en',
author: item.author,
dir: item.dir || 'ltr',
createdAt: item.createdAt,
date: item.createdAt,
topic: item.topic,
wordCount: item.wordCount || undefined,
sourceIcon: item.sourceIcon,
sourceName: item.sourceName,
siteName: item.siteName,
updatedAt: item.updatedAt,
saveCount: item.stats.saveCount,
@ -92,10 +105,7 @@ const publicItemToFeedItem = (item: PublicItem): JustReadFeedItem => ({
broadcastCount: item.stats.broadcastCount,
})
interface FeedItemScore {
id: string
score: number
}
type ScoreApiResponse = Record<string, number> // item_id -> score
const selectCandidates = async (
user: User
@ -138,6 +148,7 @@ const selectCandidates = async (
}
const rankFeedItems = async (
userId: string,
feedItems: Array<JustReadFeedItem>
): Promise<Array<JustReadFeedItem>> => {
if (feedItems.length <= 10) {
@ -146,32 +157,46 @@ const rankFeedItems = async (
}
// TODO: get score of candidates
// const API_URL = 'https://score.omnivore.app' // fake URL
const API_URL = 'http://127.0.0.1:5000/predictions'
const requestBody: ScoreApiRequestBody = {
user_id: userId,
item_features: feedItems.reduce((acc, item) => {
acc[item.id] = {
title: item.title,
has_thumbnail: !!item.thumbnail,
has_site_icon: !!item.sourceIcon,
saved_at: item.date,
site: item.siteName,
language: item.languageCode,
} as Feature
return acc
}, {} as Record<string, Feature>),
}
// const response = await fetch(API_URL, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ candidates: feedItems }),
// })
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
// if (!response.ok) {
// throw new Error(`Failed to score candidates: ${response.statusText}`)
// }
if (!response.ok) {
throw new Error(`Failed to score candidates: ${response.statusText}`)
}
// const scores = (await response.json()) as Array<FeedItemScore>
const scores = (await response.json()) as ScoreApiResponse
// fake scores
const scores: Array<FeedItemScore> = feedItems.map((item) => ({
id: item.id,
score: Math.random(),
}))
// // fake scores
// const scores: Array<FeedItemScore> = feedItems.map((item) => ({
// id: item.id,
// score: Math.random(),
// }))
// rank candidates by score in ascending order
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
const scoreA = scores[a.id] || 0
const scoreB = scores[b.id] || 0
return scoreA - scoreB
})
@ -183,8 +208,10 @@ const redisKey = (userId: string) => `just-read-feed:${userId}`
const MAX_FEED_ITEMS = 500
export const getJustReadFeed = async (
userId: string
): Promise<JustReadFeed> => {
userId: string,
first = 10,
after?: string
): Promise<JustReadFeedSuccess> => {
const redisClient = redisDataSource.redisClient
if (!redisClient) {
throw new Error('Redis client not available')
@ -192,31 +219,16 @@ export const getJustReadFeed = async (
const key = redisKey(userId)
const results = await redisClient.zrevrange(key, 0, MAX_FEED_ITEMS)
const results = await redisClient.zrevrange(key, 0, first - 1, 'WITHSCORES')
const feedItems = results.map((item) => JSON.parse(item) as JustReadFeedItem)
const sections = JSON.parse(results.join(',')) as JustReadFeedSection[]
const topics: Array<JustReadFeedTopic> = []
feedItems.forEach((item) => {
const topic = topics.find((topic) => topic.name === item.topic)
if (topic) {
topic.items.push(item)
} else {
topics.push({
name: item.topic,
thumbnail: item.thumbnail || '',
items: [item],
})
}
})
return { topics }
return { sections }
}
const appendItemsToFeed = async (
feedItems: Array<JustReadFeedItem>,
userId: string
const appendSectionsToFeed = async (
userId: string,
sections: Array<JustReadFeedSection>
) => {
const redisClient = redisDataSource.redisClient
if (!redisClient) {
@ -228,18 +240,18 @@ const appendItemsToFeed = async (
// store candidates in redis sorted set
const pipeline = redisClient.pipeline()
const scoreMembers = feedItems.flatMap((item) => [
Date.now() + 86_400_000, // items expire in 24 hours
const scoreMembers = sections.flatMap((item) => [
Date.now() + 86_400_000, // sections expire in 24 hours
JSON.stringify(item),
])
// add candidates to the sorted set
// add section to the sorted set
pipeline.zadd(key, ...scoreMembers)
// remove expired items and keep only the top 500
// remove expired sections and keep only the top 500
pipeline.zremrangebyrank(key, 0, -(MAX_FEED_ITEMS + 1))
pipeline.zremrangebyscore(key, '-inf', Date.now())
logger.info('Adding feed items to redis')
logger.info('Adding feed sections to redis')
await pipeline.exec()
}
@ -259,7 +271,7 @@ export const updateJustReadFeed = async (data: UpdateJustReadFeedJobData) => {
// TODO: integrity check on candidates?
logger.info('Ranking feed items')
const rankedFeedItems = await rankFeedItems(feedItems)
const rankedFeedItems = await rankFeedItems(userId, feedItems)
if (rankedFeedItems.length === 0) {
logger.info('No feed items to append')
return
@ -271,5 +283,5 @@ export const updateJustReadFeed = async (data: UpdateJustReadFeedJobData) => {
const filteredFeedItems = rankedFeedItems.slice(0, 100)
logger.info('Appending feed items to feed')
await appendItemsToFeed(filteredFeedItems, userId)
await appendSectionsToFeed(userId, filteredFeedItems)
}

View file

@ -1,4 +1,8 @@
import { JustReadFeedError, JustReadFeedSuccess } from '../../generated/graphql'
import {
JustReadFeedError,
JustReadFeedErrorCode,
JustReadFeedSuccess
} from '../../generated/graphql'
import { getJustReadFeed } from '../../jobs/update_just_read_feed'
import { enqueueUpdateJustReadFeed } from '../../utils/createTask'
import { authorized } from '../../utils/gql-utils'
@ -6,14 +10,20 @@ import { authorized } from '../../utils/gql-utils'
export const justReadFeedResolver = authorized<
JustReadFeedSuccess,
JustReadFeedError
>(async (_, __, { uid, log }) => {
const feed = await getJustReadFeed(uid)
>(async (_, { first, after }, { uid, log }) => {
const feed = await getJustReadFeed(uid, first, after)
log.info('Just read feed fetched')
if (feed.topics.length === 0) {
if (feed.sections.length === 0) {
await enqueueUpdateJustReadFeed({
userId: uid,
})
log.info('Just read feed update enqueued')
return {
errorCodes: [JustReadFeedErrorCode.Pending],
}
}
return feed

View file

@ -3101,19 +3101,16 @@ const schema = gql`
SUBSCRIBE
}
type JustReadFeedSource {
type JustReadFeedSubscription {
id: ID!
name: String!
url: String!
topics: [String!]
thumbnail: String
languageCodes: [String!]
url: String
icon: String
}
type JustReadFeedItem {
id: ID!
title: String!
topic: String
url: String!
thumbnail: String
previewContent: String
@ -3121,32 +3118,34 @@ const schema = gql`
savedCount: Int
likedCount: Int
broadcastCount: Int
createdAt: Date!
updatedAt: Date!
comments: [String!]
date: Date!
author: String
languageCode: String
dir: String
seen_at: Date
wordCount: Int
sourceName: String!
sourceIcon: String
siteName: String
subscription: JustReadFeedSubscription!
canSave: Boolean
canComment: Boolean
canShare: Boolean
canArchive: Boolean
canDelete: Boolean
}
type JustReadFeedTopic {
name: String
type JustReadFeedSection {
title: String
layout: String
items: [JustReadFeedItem!]!
thumbnail: String
}
type JustReadFeedSuccess {
topics: [JustReadFeedTopic!]!
sections: [JustReadFeedSection!]!
}
enum JustReadFeedErrorCode {
UNAUTHORIZED
BAD_REQUEST
PENDING
}
type JustReadFeedError {
@ -3155,7 +3154,7 @@ const schema = gql`
union JustReadFeedResult = JustReadFeedSuccess | JustReadFeedError
type MySubscriptionRootType {
type SubscriptionRootType {
hello: String # for testing only
}
@ -3354,13 +3353,13 @@ const schema = gql`
feeds(input: FeedsInput!): FeedsResult!
discoverFeeds: DiscoverFeedResult!
scanFeeds(input: ScanFeedsInput!): ScanFeedsResult!
justReadFeed: JustReadFeedResult!
justReadFeed(first: Int, after: String): JustReadFeedResult!
}
schema {
query: Query
mutation: Mutation
subscription: MySubscriptionRootType
subscription: SubscriptionRootType
}
`