Merge pull request #4086 from omnivore-app/fix/resolver

fix: library item is null in highlights api
This commit is contained in:
Hongbo Wu
2024-06-20 17:22:10 +08:00
committed by GitHub
9 changed files with 53 additions and 354 deletions

View File

@ -6,7 +6,7 @@
import { createHmac } from 'crypto'
import { isError } from 'lodash'
import { Highlight } from '../entity/highlight'
import { LibraryItem } from '../entity/library_item'
import { LibraryItem, LibraryItemState } from '../entity/library_item'
import {
EXISTING_NEWSLETTER_FOLDER,
NewsletterEmail,
@ -157,7 +157,7 @@ import {
} from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { subscriptionResolver } from './subscriptions'
import { WithDataSourcesContext } from './types'
import { ResolverContext } from './types'
import { updateEmailResolver } from './user'
/* eslint-disable @typescript-eslint/naming-convention */
@ -180,7 +180,7 @@ const readingProgressHandlers = {
async readingProgressPercent(
article: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
ctx: ResolverContext
) {
if (ctx.claims?.uid) {
const readingProgress =
@ -200,7 +200,7 @@ const readingProgressHandlers = {
async readingProgressAnchorIndex(
article: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
ctx: ResolverContext
) {
if (ctx.claims?.uid) {
const readingProgress =
@ -220,7 +220,7 @@ const readingProgressHandlers = {
async readingProgressTopPercent(
article: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
ctx: ResolverContext
) {
if (ctx.claims?.uid) {
const readingProgress =
@ -364,11 +364,7 @@ export const functionResolvers = {
}
return undefined
},
async features(
_: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
async features(_: User, __: Record<string, unknown>, ctx: ResolverContext) {
if (!ctx.claims?.uid) {
return undefined
}
@ -378,7 +374,7 @@ export const functionResolvers = {
async featureList(
_: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
ctx: ResolverContext
) {
if (!ctx.claims?.uid) {
return undefined
@ -398,7 +394,7 @@ export const functionResolvers = {
sharedNotesCount: () => 0,
},
Article: {
async url(article: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
async url(article: LibraryItem, _: unknown, ctx: ResolverContext) {
if (
(article.itemType == PageType.File ||
article.itemType == PageType.Book) &&
@ -439,20 +435,12 @@ export const functionResolvers = {
? wordsCount(article.readableContent)
: undefined
},
async labels(
article: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async labels(article: LibraryItem, _: unknown, ctx: ResolverContext) {
if (article.labels) return article.labels
return ctx.dataLoaders.labels.load(article.id)
},
async highlights(
article: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async highlights(article: LibraryItem, _: unknown, ctx: ResolverContext) {
if (article.highlights) return article.highlights
return ctx.dataLoaders.highlights.load(article.id)
@ -468,35 +456,27 @@ export const functionResolvers = {
reactions: () => [],
replies: () => [],
type: (highlight: Highlight) => highlight.highlightType,
async user(highlight: Highlight, __: unknown, ctx: WithDataSourcesContext) {
async user(highlight: Highlight, __: unknown, ctx: ResolverContext) {
return ctx.dataLoaders.users.load(highlight.userId)
},
createdByMe(
highlight: Highlight,
__: unknown,
ctx: WithDataSourcesContext
) {
return highlight.userId === ctx.uid
createdByMe(highlight: Highlight, __: unknown, ctx: ResolverContext) {
return highlight.userId === ctx.claims?.uid
},
libraryItem(highlight: Highlight, _: unknown, ctx: WithDataSourcesContext) {
libraryItem(highlight: Highlight, _: unknown, ctx: ResolverContext) {
if (highlight.libraryItem) {
return highlight.libraryItem
}
return ctx.dataLoaders.libraryItems.load(highlight.libraryItemId)
},
labels: async (
highlight: Highlight,
_: unknown,
ctx: WithDataSourcesContext
) => {
labels: async (highlight: Highlight, _: unknown, ctx: ResolverContext) => {
return (
highlight.labels || ctx.dataLoaders.highlightLabels.load(highlight.id)
)
},
},
SearchItem: {
async url(item: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
async url(item: LibraryItem, _: unknown, ctx: ResolverContext) {
if (
(item.itemType == PageType.File || item.itemType == PageType.Book) &&
ctx.claims &&
@ -528,47 +508,33 @@ export const functionResolvers = {
return item.siteIcon
},
async labels(item: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
async labels(item: LibraryItem, _: unknown, ctx: ResolverContext) {
if (item.labels) return item.labels
return ctx.dataLoaders.labels.load(item.id)
},
async recommendations(
item: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async recommendations(item: LibraryItem, _: unknown, ctx: ResolverContext) {
if (item.recommendations) return item.recommendations
return ctx.dataLoaders.recommendations.load(item.id)
},
async aiSummary(
item: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async aiSummary(item: LibraryItem, _: unknown, ctx: ResolverContext) {
if (!ctx.claims) return undefined
return (
await getAISummary({
userId: ctx.uid,
userId: ctx.claims.uid,
libraryItemId: item.id,
idx: 'latest',
})
)?.summary
},
async highlights(
item: LibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async highlights(item: LibraryItem, _: unknown, ctx: ResolverContext) {
if (item.highlights) return item.highlights
return ctx.dataLoaders.highlights.load(item.id)
},
async content(
item: PartialLibraryItem,
_: unknown,
ctx: WithDataSourcesContext
) {
async content(item: PartialLibraryItem, _: unknown, ctx: ResolverContext) {
// convert html to the requested format if requested
if (
item.format &&
@ -658,7 +624,7 @@ export const functionResolvers = {
}>
},
_: unknown,
ctx: WithDataSourcesContext
ctx: ResolverContext
) {
const items = section.items
@ -668,7 +634,14 @@ export const functionResolvers = {
const libraryItems = (
await ctx.dataLoaders.libraryItems.loadMany(libraryItemIds)
).filter(
(libraryItem) => !!libraryItem && !isError(libraryItem)
(libraryItem) =>
!!libraryItem &&
!isError(libraryItem) &&
[
LibraryItemState.Succeeded,
LibraryItemState.ContentNotFetched,
].includes(libraryItem.state) &&
!libraryItem.seenAt
) as Array<LibraryItem>
const publicItemIds = section.items
@ -745,7 +718,7 @@ export const functionResolvers = {
{ subscription?: string; siteName: string; siteIcon?: string }
>,
_: unknown,
ctx: WithDataSourcesContext
ctx: ResolverContext
): Promise<HomeItemSource> {
if (item.source) {
return item.source
@ -785,7 +758,7 @@ export const functionResolvers = {
ArticleSavingRequest: {
status: (item: LibraryItem) => item.state,
url: (item: LibraryItem) => item.originalUrl,
async user(_item: LibraryItem, __: unknown, ctx: WithDataSourcesContext) {
async user(_item: LibraryItem, __: unknown, ctx: ResolverContext) {
if (ctx.claims?.uid) {
return ctx.dataLoaders.users.load(ctx.claims.uid)
}

View File

@ -11,7 +11,7 @@ import {
saveContentDisplayReport,
} from '../../services/reports'
import { analytics } from '../../utils/analytics'
import { WithDataSourcesContext } from '../types'
import { ResolverContext } from '../types'
const SUCCESS_MESSAGE = `Your report has been submitted. Thank you.`
const FAILURE_MESSAGE =
@ -36,7 +36,7 @@ const isContentDisplayReport = (types: ReportType[]): boolean => {
export const reportItemResolver: ResolverFn<
ReportItemResult,
unknown,
WithDataSourcesContext,
ResolverContext,
MutationReportItemArgs
> = async (_obj, args, ctx) => {
const { sharedBy, reportTypes } = args.input

View File

@ -14,7 +14,6 @@ import { Recommendation } from '../entity/recommendation'
import { Subscription } from '../entity/subscription'
import { UploadFile } from '../entity/upload_file'
import { User } from '../entity/user'
import { HomeItem } from '../generated/graphql'
import { PubsubClient } from '../pubsub'
export interface Claims {
@ -65,7 +64,3 @@ export interface RequestContext {
}
export type ResolverContext = ApolloContext<RequestContext>
export type WithDataSourcesContext = {
uid: string
} & ResolverContext

View File

@ -45,7 +45,7 @@ import { softDeleteUser } from '../../services/user'
import { Merge } from '../../util'
import { authorized } from '../../utils/gql-utils'
import { validateUsername } from '../../utils/usernamePolicy'
import { WithDataSourcesContext } from '../types'
import { ResolverContext } from '../types'
export const updateUserResolver = authorized<
Merge<UpdateUserSuccess, { user: UserEntity }>,
@ -145,7 +145,7 @@ export const updateUserProfileResolver = authorized<
export const googleLoginResolver: ResolverFn<
Merge<LoginResult, { me?: UserEntity }>,
unknown,
WithDataSourcesContext,
ResolverContext,
MutationGoogleLoginArgs
> = async (_obj, { input }, { setAuth }) => {
const { email, secret } = input
@ -172,7 +172,7 @@ export const googleLoginResolver: ResolverFn<
export const validateUsernameResolver: ResolverFn<
boolean,
Record<string, unknown>,
WithDataSourcesContext,
ResolverContext,
QueryValidateUsernameArgs
> = async (_obj, { username }) => {
const lowerCasedUsername = username.toLowerCase()
@ -191,7 +191,7 @@ export const validateUsernameResolver: ResolverFn<
export const googleSignupResolver: ResolverFn<
Merge<GoogleSignupResult, { me?: UserEntity }>,
Record<string, unknown>,
WithDataSourcesContext,
ResolverContext,
MutationGoogleSignupArgs
> = async (_obj, { input }, { setAuth, log }) => {
const { email, username, name, bio, sourceUserId, pictureUrl, secret } = input
@ -231,7 +231,7 @@ export const googleSignupResolver: ResolverFn<
export const logOutResolver: ResolverFn<
LogOutResult,
unknown,
WithDataSourcesContext,
ResolverContext,
unknown
> = (_, __, { clearAuth, log }) => {
try {
@ -246,7 +246,7 @@ export const logOutResolver: ResolverFn<
export const getMeUserResolver: ResolverFn<
UserEntity | undefined,
unknown,
WithDataSourcesContext,
ResolverContext,
unknown
> = async (_obj, __, { claims }) => {
try {
@ -268,9 +268,9 @@ export const getMeUserResolver: ResolverFn<
export const getUserResolver: ResolverFn<
Merge<UserResult, { user?: UserEntity }>,
unknown,
WithDataSourcesContext,
ResolverContext,
QueryUserArgs
> = async (_obj, { userId: id, username }, { uid }) => {
> = async (_obj, { userId: id, username }) => {
if (!(id || username)) {
return { errorCodes: [UserErrorCode.BadRequest] }
}

View File

@ -1,150 +0,0 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { FeedArticle, PageInfo } from '../../generated/graphql'
export type PartialFeedArticle = Omit<
FeedArticle,
'sharedBy' | 'article' | 'reactions'
>
type PaginatedFeedArticlesSuccessPartial = {
edges: { cursor: string; node: PartialFeedArticle }[]
pageInfo: PageInfo
}
// export const getSharedArticleResolver: ResolverFn<
// SharedArticleSuccessPartial | SharedArticleError,
// Record<string, unknown>,
// WithDataSourcesContext,
// QuerySharedArticleArgs
// > = async (_obj, { username, slug, selectedHighlightId }, { kx, models }) => {
// try {
// const user = await models.user.getWhere({ username })
// if (!user) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
// const article = await models.userArticle.getBySlug(username, slug)
// if (!article || !article.sharedAt) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
// if (selectedHighlightId) {
// const highlightResult = await models.highlight.getWhereIn('shortId', [
// selectedHighlightId,
// ])
// if (!highlightResult || !highlightResult[0].sharedAt) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
// }
// const shareInfo = await getShareInfoForArticle(
// kx,
// user.id,
// article.id,
// models
// )
// return { article: { ...article, userId: user.id, shareInfo: shareInfo } }
// } catch (error) {
// return { errorCodes: [SharedArticleErrorCode.NotFound] }
// }
// }
// export const getUserFeedArticlesResolver: ResolverFn<
// PaginatedFeedArticlesSuccessPartial,
// unknown,
// WithDataSourcesContext,
// QueryFeedArticlesArgs
// > = async (
// _obj,
// { after: _startCursor, first: _first, sharedByUser },
// { models, claims, authTrx }
// ) => {
// if (!(sharedByUser || claims?.uid)) {
// return {
// edges: [],
// pageInfo: {
// startCursor: '',
// endCursor: '',
// hasNextPage: false,
// hasPreviousPage: false,
// },
// }
// }
// const first = _first || 0
// const startCursor = _startCursor || ''
// const feedArticles =
// (await authTrx((tx) =>
// models.userArticle.getUserFeedArticlesPaginatedWithHighlights(
// { cursor: startCursor, first: first + 1, sharedByUser }, // fetch one more item to get next cursor
// claims?.uid || '',
// tx
// )
// )) || []
// const endCursor = feedArticles[feedArticles.length - 1]?.sharedAt
// .getTime()
// ?.toString()
// const hasNextPage = feedArticles.length > first
// if (hasNextPage) {
// // remove an extra if exists
// feedArticles.pop()
// }
// const edges = feedArticles.map((fa) => {
// return {
// node: fa,
// cursor: fa.sharedAt.getTime()?.toString(),
// }
// })
// return {
// edges,
// pageInfo: {
// hasPreviousPage: false,
// startCursor: '',
// hasNextPage,
// endCursor,
// },
// }
// }
// export const updateSharedCommentResolver = authorized<
// UpdateSharedCommentSuccess,
// UpdateSharedCommentError,
// MutationUpdateSharedCommentArgs
// >(
// async (
// _,
// { input: { articleID, sharedComment } },
// { models, authTrx, claims: { uid } }
// ) => {
// const ua = await authTrx((tx) =>
// models.userArticle.getByParameters(uid, { articleId: articleID }, tx)
// )
// if (!ua) {
// return { errorCodes: [UpdateSharedCommentErrorCode.NotFound] }
// }
// await authTrx((tx) =>
// models.userArticle.updateByArticleId(
// uid,
// articleID,
// { sharedComment },
// tx
// )
// )
// return { articleID, sharedComment }
// }
// )

View File

@ -1,101 +0,0 @@
// export const setFollowResolver = authorized<
// SetFollowSuccess,
// SetFollowError,
// MutationSetFollowArgs
// >(
// async (
// _,
// { input: { userId: friendUserId, follow } },
// { models, authTrx, claims: { uid } }
// ) => {
// const user = await models.user.getUserDetails(uid, friendUserId)
// if (!user) return { errorCodes: [SetFollowErrorCode.NotFound] }
// const userFriendRecord = await authTrx((tx) =>
// models.userFriends.getByUserFriendId(uid, friendUserId, tx)
// )
// if (follow) {
// if (!userFriendRecord) {
// await authTrx((tx) =>
// models.userFriends.create({ friendUserId, userId: uid }, tx)
// )
// }
// } else if (userFriendRecord) {
// await authTrx((tx) => models.userFriends.delete(userFriendRecord.id, tx))
// }
// const updatedUser = await models.user.getUserDetails(uid, friendUserId)
// if (!updatedUser) return { errorCodes: [SetFollowErrorCode.NotFound] }
// return {
// updatedUser: {
// ...userDataToUser(updatedUser),
// isFriend: updatedUser.viewerIsFollowing,
// },
// }
// }
// )
// const getUserList = async (
// uid: string,
// users: UserData[],
// models: DataModels,
// authTrx: <TResult>(
// cb: (tx: Knex.Transaction) => TResult,
// userRole?: string
// ) => Promise<TResult>
// ): Promise<User[]> => {
// const usersIds = users.map(({ id }) => id)
// const friends = await authTrx((tx) =>
// models.userFriends.getByFriendIds(uid, usersIds, tx)
// )
// const friendsIds = friends.map(({ friendUserId }) => friendUserId)
// users = users.map((f) => ({
// ...f,
// isFriend: friendsIds.includes(f.id),
// viewerIsFollowing: friendsIds.includes(f.id),
// }))
// return users.map((u) => userDataToUser(u))
// }
// export const getFollowersResolver: ResolverFn<
// GetFollowersResult,
// unknown,
// WithDataSourcesContext,
// QueryGetFollowersArgs
// > = async (_parent, { userId }, { models, claims, authTrx }) => {
// const followers = userId
// ? await authTrx((tx) => models.user.getUserFollowersList(userId, tx))
// : []
// if (!claims?.uid) return { followers: usersWithNoFriends(followers) }
// return {
// followers: await getUserList(claims?.uid, followers, models, authTrx),
// }
// }
// export const getFollowingResolver: ResolverFn<
// GetFollowingResult,
// unknown,
// WithDataSourcesContext,
// QueryGetFollowingArgs
// > = async (_parent, { userId }, { models, claims, authTrx }) => {
// const following = userId
// ? await authTrx((tx) => models.user.getUserFollowingList(userId, tx))
// : []
// if (!claims?.uid) return { following: usersWithNoFriends(following) }
// return {
// following: await getUserList(claims?.uid, following, models, authTrx),
// }
// }
// const usersWithNoFriends = (users: UserData[]): User[] => {
// return users.map((f) =>
// userDataToUser({
// ...f,
// isFriend: false,
// } as UserData)
// )
// }

View File

@ -26,7 +26,6 @@ export const batchGetHighlightsFromLibraryItemIds = async (
const highlights = await authTrx(async (tx) =>
tx.getRepository(Highlight).find({
where: { libraryItem: { id: In(libraryItemIds as string[]) } },
relations: ['user'],
})
)

View File

@ -6,7 +6,6 @@ import {
EntityManager,
FindOptionsWhere,
In,
IsNull,
ObjectLiteral,
} from 'typeorm'
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
@ -135,31 +134,15 @@ export enum SortBy {
const readingProgressDataSource = new ReadingProgressDataSource()
export const batchGetLibraryItems = async (ids: readonly string[]) => {
const selectColumns: Array<keyof LibraryItem> = [
'id',
'title',
'author',
'thumbnail',
'wordCount',
'savedAt',
'originalUrl',
'directionality',
'description',
'subscription',
'siteName',
'siteIcon',
'archivedAt',
'deletedAt',
'slug',
'previewContent',
]
// select all columns except content
const select = getColumns(libraryItemRepository).filter(
(select) => ['originalContent', 'readableContent'].indexOf(select) === -1
)
const items = await authTrx(async (tx) =>
tx.getRepository(LibraryItem).find({
select: selectColumns,
select,
where: {
id: In(ids as string[]),
state: LibraryItemState.Succeeded,
seenAt: IsNull(),
},
})
)

View File

@ -1,5 +1,5 @@
import { ResolverFn } from '../generated/graphql'
import { Claims, WithDataSourcesContext } from '../resolvers/types'
import { Claims, ResolverContext } from '../resolvers/types'
export function authorized<
TSuccess,
@ -12,10 +12,10 @@ export function authorized<
resolver: ResolverFn<
TSuccess | TError,
TParent,
WithDataSourcesContext & { claims: Claims },
ResolverContext & { claims: Claims; uid: string },
TArgs
>
): ResolverFn<TSuccess | TError, TParent, WithDataSourcesContext, TArgs> {
): ResolverFn<TSuccess | TError, TParent, ResolverContext, TArgs> {
return (parent, args, ctx, info) => {
const { claims } = ctx
if (claims?.uid) {