From c620ef38a2f1ffc50ab73c7b80e549826e72d81e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 6 Jun 2024 12:08:15 +0800 Subject: [PATCH] clean up helpers --- packages/api/src/entity/library_item.ts | 3 + packages/api/src/resolvers/article/index.ts | 150 +++++------------ .../resolvers/article_saving_request/index.ts | 19 +-- .../api/src/resolvers/function_resolvers.ts | 132 ++++++++------- .../src/resolvers/recommendations/index.ts | 74 +++++---- packages/api/src/resolvers/update/index.ts | 8 +- packages/api/src/resolvers/user/index.ts | 46 +++--- packages/api/src/routers/article_router.ts | 19 +-- .../src/services/create_page_save_request.ts | 17 +- packages/api/src/services/groups.ts | 25 ++- packages/api/src/utils/helpers.ts | 152 +----------------- 11 files changed, 225 insertions(+), 420 deletions(-) diff --git a/packages/api/src/entity/library_item.ts b/packages/api/src/entity/library_item.ts index 8fb0edc94..ed8bc47ff 100644 --- a/packages/api/src/entity/library_item.ts +++ b/packages/api/src/entity/library_item.ts @@ -46,6 +46,9 @@ export class LibraryItem { @JoinColumn({ name: 'user_id' }) user!: User + @Column('uuid') + userId!: string + @Column('enum', { enum: LibraryItemState, default: LibraryItemState.Succeeded, diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 026cb7988..99e1d29cd 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -5,7 +5,12 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { Readability } from '@omnivore/readability' import graphqlFields from 'graphql-fields' -import { LibraryItem, LibraryItemState } from '../../entity/library_item' +import { + ContentReaderType, + LibraryItem, + LibraryItemState, +} from '../../entity/library_item' +import { User } from '../../entity/user' import { env } from '../../env' import { ArticleError, @@ -43,6 +48,7 @@ import { SaveArticleReadingProgressSuccess, SearchError, SearchErrorCode, + SearchItemEdge, SearchSuccess, SetBookmarkArticleError, SetBookmarkArticleErrorCode, @@ -87,6 +93,7 @@ import { setFileUploadComplete, } from '../../services/upload_file' import { traceAs } from '../../tracing' +import { Merge } from '../../util' import { analytics } from '../../utils/analytics' import { isSiteBlockedForParse } from '../../utils/blocked' import { enqueueBulkAction } from '../../utils/createTask' @@ -96,10 +103,7 @@ import { errorHandler, generateSlug, isParsingTimeout, - libraryItemToArticle, - libraryItemToSearchItem, titleForFilePath, - userDataToUser, } from '../../utils/helpers' import { getDistillerResult, @@ -126,7 +130,10 @@ const FORCE_PUPPETEER_URLS = [ const UNPARSEABLE_CONTENT = '

We were unable to parse this page.

' export const createArticleResolver = authorized< - CreateArticleSuccess, + Merge< + CreateArticleSuccess, + { user: User; createdArticle: Partial } + >, CreateArticleError, MutationCreateArticleArgs >( @@ -160,8 +167,8 @@ export const createArticleResolver = authorized< }, }) - const userData = await userRepository.findById(uid) - if (!userData) { + const user = await userRepository.findById(uid) + if (!user) { return errorHandler( { errorCodes: [CreateArticleErrorCode.Unauthorized], @@ -171,7 +178,6 @@ export const createArticleResolver = authorized< pubsub ) } - const user = userDataToUser(userData) try { if (isSiteBlockedForParse(url)) { @@ -203,25 +209,22 @@ export const createArticleResolver = authorized< let domContent = null let itemType = PageType.Unknown - const DUMMY_RESPONSE: CreateArticleSuccess = { + const DUMMY_RESPONSE = { user, created: false, createdArticle: { id: '', slug: '', createdAt: new Date(), - originalHtml: domContent, - content: '', + originalContent: domContent, + readableContent: '', description: '', title: '', - pageType: itemType, - contentReader: ContentReader.Web, + itemType, + contentReader: ContentReaderType.WEB, author: '', - url, - hash: '', - isArchived: false, - readingProgressAnchorIndex: 0, - readingProgressPercent: 0, + originalUrl: url, + textContentHash: '', highlights: [], savedAt: savedAt || new Date(), updatedAt: new Date(), @@ -257,7 +260,7 @@ export const createArticleResolver = authorized< FORCE_PUPPETEER_URLS.some((regex) => regex.test(url)) ) { await createPageSaveRequest({ - user: userData, + user: user, url, state: state || undefined, labels: inputLabels || undefined, @@ -282,7 +285,7 @@ export const createArticleResolver = authorized< // We have a URL but no document, so we try to send this to puppeteer // and return a dummy response. await createPageSaveRequest({ - user: userData, + user, url, state: state || undefined, labels: inputLabels || undefined, @@ -353,7 +356,7 @@ export const createArticleResolver = authorized< return { user, created: true, - createdArticle: libraryItemToArticle(libraryItemToReturn), + createdArticle: libraryItemToReturn, } } catch (error) { log.error('Error creating article', error) @@ -370,7 +373,7 @@ export const createArticleResolver = authorized< ) export const getArticleResolver = authorized< - ArticleSuccess, + Merge, ArticleError, QueryArticleArgs >(async (_obj, { slug, format }, { authTrx, uid, log }, info) => { @@ -439,7 +442,7 @@ export const getArticleResolver = authorized< } return { - article: libraryItemToArticle(libraryItem), + article: libraryItem, } } catch (error) { log.error(error) @@ -447,88 +450,8 @@ export const getArticleResolver = authorized< } }) -// type PaginatedPartialArticles = { -// edges: { cursor: string; node: PartialArticle }[] -// pageInfo: PageInfo -// } - -// export type SetShareArticleSuccessPartial = Merge< -// SetShareArticleSuccess, -// { -// updatedFeedArticle?: Omit< -// FeedArticle, -// | 'sharedBy' -// | 'article' -// | 'highlightsCount' -// | 'annotationsCount' -// | 'reactions' -// > -// updatedFeedArticleId?: string -// updatedArticle: PartialArticle -// } -// > - -// export const setShareArticleResolver = authorized< -// SetShareArticleSuccessPartial, -// SetShareArticleError, -// MutationSetShareArticleArgs -// >( -// async ( -// _, -// { input: { articleID, share, sharedComment, sharedWithHighlights } }, -// { models, authTrx, claims: { uid }, log } -// ) => { -// const article = await models.article.get(articleID) -// if (!article) { -// return { errorCodes: [SetShareArticleErrorCode.NotFound] } -// } - -// const sharedAt = share ? new Date() : null - -// log.info(`${share ? 'S' : 'Uns'}haring an article`, { -// article: Object.assign({}, article, { -// content: undefined, -// originalHtml: undefined, -// sharedAt, -// }), -// labels: { -// source: 'resolver', -// resolver: 'setShareArticleResolver', -// articleId: article.id, -// distinctId: uid, -// }, -// }) - -// const result = await authTrx((tx) => -// models.userArticle.updateByArticleId( -// uid, -// articleID, -// { sharedAt, sharedComment, sharedWithHighlights }, -// tx -// ) -// ) - -// if (!result) { -// return { errorCodes: [SetShareArticleErrorCode.NotFound] } -// } - -// // Make sure article.id instead of userArticle.id has passed. We use it for cache updates -// const updatedArticle = { -// ...result, -// ...article, -// postedByViewer: !!sharedAt, -// } -// const updatedFeedArticle = sharedAt ? { ...result, sharedAt } : undefined -// return { -// updatedFeedArticleId: result.id, -// updatedFeedArticle, -// updatedArticle, -// } -// } -// ) - export const setBookmarkArticleResolver = authorized< - SetBookmarkArticleSuccess, + Merge, SetBookmarkArticleError, MutationSetBookmarkArticleArgs >(async (_, { input: { articleID } }, { uid, log, pubsub }) => { @@ -556,12 +479,12 @@ export const setBookmarkArticleResolver = authorized< }) // Make sure article.id instead of userArticle.id has passed. We use it for cache updates return { - bookmarkedArticle: libraryItemToArticle(deletedLibraryItem), + bookmarkedArticle: deletedLibraryItem, } }) export const saveArticleReadingProgressResolver = authorized< - SaveArticleReadingProgressSuccess, + Merge, SaveArticleReadingProgressError, MutationSaveArticleReadingProgressArgs >( @@ -661,13 +584,15 @@ export const saveArticleReadingProgressResolver = authorized< } return { - updatedArticle: libraryItemToArticle(updatedItem), + updatedArticle: updatedItem, } } ) +export type PartialLibraryItem = Merge +type PartialSearchItemEdge = Merge export const searchResolver = authorized< - SearchSuccess, + Merge }>, SearchError, QuerySearchArgs >(async (_obj, params, { uid }) => { @@ -704,7 +629,10 @@ export const searchResolver = authorized< return { edges: libraryItems.map((item) => ({ - node: libraryItemToSearchItem(item, params.format as ArticleFormat), + node: { + ...item, + format: params.format || undefined, + }, cursor: endCursor, })), pageInfo: { @@ -738,7 +666,7 @@ export const typeaheadSearchResolver = authorized< }) export const updatesSinceResolver = authorized< - UpdatesSinceSuccess, + Merge }>, UpdatesSinceError, QueryUpdatesSinceArgs >(async (_obj, { since, first, after, sort: sortParams, folder }, { uid }) => { @@ -781,7 +709,7 @@ export const updatesSinceResolver = authorized< const edges = libraryItems.map((item) => { const updateReason = getUpdateReason(item, startDate) return { - node: libraryItemToSearchItem(item), + node: item, cursor: endCursor, itemID: item.id, updateReason, diff --git a/packages/api/src/resolvers/article_saving_request/index.ts b/packages/api/src/resolvers/article_saving_request/index.ts index 07d40aea5..3d99a8109 100644 --- a/packages/api/src/resolvers/article_saving_request/index.ts +++ b/packages/api/src/resolvers/article_saving_request/index.ts @@ -17,17 +17,17 @@ import { findLibraryItemById, findLibraryItemByUrl, } from '../../services/library_item' +import { Merge } from '../../util' import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/gql-utils' -import { - cleanUrl, - isParsingTimeout, - libraryItemToArticleSavingRequest, -} from '../../utils/helpers' +import { cleanUrl, isParsingTimeout } from '../../utils/helpers' import { isErrorWithCode } from '../user' export const createArticleSavingRequestResolver = authorized< - CreateArticleSavingRequestSuccess, + Merge< + CreateArticleSavingRequestSuccess, + { articleSavingRequest: LibraryItem } + >, CreateArticleSavingRequestError, MutationCreateArticleSavingRequestArgs >(async (_, { input: { url } }, { uid, pubsub, log }) => { @@ -67,7 +67,7 @@ export const createArticleSavingRequestResolver = authorized< }) export const articleSavingRequestResolver = authorized< - ArticleSavingRequestSuccess, + Merge, ArticleSavingRequestError, QueryArticleSavingRequestArgs >(async (_, { id, url }, { uid, log }) => { @@ -109,10 +109,7 @@ export const articleSavingRequestResolver = authorized< libraryItem.state = LibraryItemState.Succeeded } return { - articleSavingRequest: libraryItemToArticleSavingRequest( - user, - libraryItem - ), + articleSavingRequest: libraryItem, } } catch (error) { log.error('articleSavingRequestResolver error', error) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 1a7d0ba40..a4fc71cf8 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -6,12 +6,14 @@ import { createHmac } from 'crypto' import { isError } from 'lodash' import { Highlight } from '../entity/highlight' +import { Label } from '../entity/label' import { LibraryItem } from '../entity/library_item' import { EXISTING_NEWSLETTER_FOLDER, NewsletterEmail, } from '../entity/newsletter_email' import { PublicItem } from '../entity/public_item' +import { Recommendation } from '../entity/recommendation' import { DEFAULT_SUBSCRIPTION_FOLDER, Subscription, @@ -19,25 +21,16 @@ import { import { User as UserEntity } from '../entity/user' import { env } from '../env' import { - Article, HomeItem, HomeItemSource, HomeItemSourceType, - Label, PageType, - Recommendation, - SearchItem, User, } from '../generated/graphql' import { getAISummary } from '../services/ai-summaries' import { findUserFeatures } from '../services/features' import { Merge } from '../util' -import { - isBase64Image, - recommandationDataToRecommendation, - validatedDate, - wordsCount, -} from '../utils/helpers' +import { isBase64Image, validatedDate, wordsCount } from '../utils/helpers' import { createImageProxyUrl } from '../utils/imageproxy' import { contentConverter } from '../utils/parser' import { @@ -48,6 +41,7 @@ import { ArticleFormat, emptyTrashResolver, fetchContentResolver, + PartialLibraryItem, } from './article' import { addDiscoverFeedResolver, @@ -192,7 +186,7 @@ const resultResolveTypeResolver = ( const readingProgressHandlers = { async readingProgressPercent( - article: { id: string; readingProgressPercent?: number }, + article: LibraryItem, _: unknown, ctx: WithDataSourcesContext ) { @@ -204,15 +198,15 @@ const readingProgressHandlers = { ) if (readingProgress) { return Math.max( - article.readingProgressPercent ?? 0, + article.readingProgressBottomPercent ?? 0, readingProgress.readingProgressPercent ) } } - return article.readingProgressPercent + return article.readingProgressBottomPercent }, async readingProgressAnchorIndex( - article: { id: string; readingProgressAnchorIndex?: number }, + article: LibraryItem, _: unknown, ctx: WithDataSourcesContext ) { @@ -224,15 +218,15 @@ const readingProgressHandlers = { ) if (readingProgress && readingProgress.readingProgressAnchorIndex) { return Math.max( - article.readingProgressAnchorIndex ?? 0, + article.readingProgressHighestReadAnchor ?? 0, readingProgress.readingProgressAnchorIndex ) } } - return article.readingProgressAnchorIndex + return article.readingProgressHighestReadAnchor }, async readingProgressTopPercent( - article: { id: string; readingProgressTopPercent?: number }, + article: LibraryItem, _: unknown, ctx: WithDataSourcesContext ) { @@ -427,10 +421,10 @@ export const functionResolvers = { sharedNotesCount: () => 0, }, Article: { - async url(article: Article, _: unknown, ctx: WithDataSourcesContext) { + async url(article: LibraryItem, _: unknown, ctx: WithDataSourcesContext) { if ( - (article.pageType == PageType.File || - article.pageType == PageType.Book) && + (article.itemType == PageType.File || + article.itemType == PageType.Book) && ctx.claims && article.uploadFileId ) { @@ -443,29 +437,33 @@ export const functionResolvers = { const filePath = generateUploadFilePathName(upload.id, upload.fileName) return generateDownloadSignedUrl(filePath) } - return article.url + return article.originalUrl }, - originalArticleUrl(article: { url: string }) { - return article.url + originalArticleUrl(article: LibraryItem) { + return article.originalUrl }, - hasContent(article: { - content: string | null - originalHtml: string | null - }) { - return !!article.originalHtml && !!article.content + hasContent(article: LibraryItem) { + return !!article.originalContent && !!article.readableContent }, publishedAt(article: { publishedAt: Date }) { return validatedDate(article.publishedAt) }, - image(article: { image?: string }): string | undefined { - return article.image && createImageProxyUrl(article.image, 320, 320) + image(article: LibraryItem): string | undefined { + if (article.thumbnail) { + return createImageProxyUrl(article.thumbnail, 320, 320) + } + + return undefined }, - wordsCount(article: { wordCount?: number; content?: string }) { + wordsCount(article: LibraryItem): number | undefined { if (article.wordCount) return article.wordCount - return article.content ? wordsCount(article.content) : undefined + + return article.readableContent + ? wordsCount(article.readableContent) + : undefined }, async labels( - article: { id: string; labels?: Label[] }, + article: LibraryItem, _: unknown, ctx: WithDataSourcesContext ) { @@ -473,6 +471,11 @@ export const functionResolvers = { return ctx.dataLoaders.labels.load(article.id) }, + content: (item: LibraryItem) => item.readableContent, + hash: (item: LibraryItem) => item.textContentHash || '', + isArchived: (item: LibraryItem) => !!item.archivedAt, + uploadFileId: (item: LibraryItem) => item.uploadFile?.id, + pageType: (item: LibraryItem) => item.itemType, ...readingProgressHandlers, }, Highlight: { @@ -491,9 +494,9 @@ export const functionResolvers = { }, }, SearchItem: { - async url(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) { + async url(item: LibraryItem, _: unknown, ctx: WithDataSourcesContext) { if ( - (item.pageType == PageType.File || item.pageType == PageType.Book) && + (item.itemType == PageType.File || item.itemType == PageType.Book) && ctx.claims && item.uploadFileId ) { @@ -504,19 +507,19 @@ export const functionResolvers = { const filePath = generateUploadFilePathName(upload.id, upload.fileName) return generateDownloadSignedUrl(filePath) } - return item.url + return item.originalUrl }, - image(item: SearchItem) { - return item.image && createImageProxyUrl(item.image, 320, 320) + image(item: LibraryItem) { + return item.thumbnail && createImageProxyUrl(item.thumbnail, 320, 320) }, - originalArticleUrl(item: { url: string }) { - return item.url + originalArticleUrl(item: LibraryItem) { + return item.originalUrl }, - wordsCount(item: { wordCount?: number; content?: string }) { + wordsCount(item: LibraryItem) { if (item.wordCount) return item.wordCount - return item.content ? wordsCount(item.content) : undefined + return item.readableContent ? wordsCount(item.readableContent) : undefined }, - siteIcon(item: { siteIcon?: string }) { + siteIcon(item: LibraryItem) { if (item.siteIcon && !isBase64Image(item.siteIcon)) { return createImageProxyUrl(item.siteIcon, 128, 128) } @@ -546,9 +549,13 @@ export const functionResolvers = { const recommendations = await ctx.dataLoaders.recommendations.load( item.id ) - return recommendations.map(recommandationDataToRecommendation) + return recommendations }, - async aiSummary(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) { + async aiSummary( + item: LibraryItem, + _: unknown, + ctx: WithDataSourcesContext + ) { return ( await getAISummary({ userId: ctx.uid, @@ -572,17 +579,16 @@ export const functionResolvers = { }, ...readingProgressHandlers, async content( - item: { - id: string - content?: string - highlightAnnotations?: string[] - format?: ArticleFormat - }, + item: PartialLibraryItem, _: unknown, ctx: WithDataSourcesContext ) { // convert html to the requested format if requested - if (item.format && item.format !== ArticleFormat.Html && item.content) { + if ( + item.format && + item.format !== ArticleFormat.Html && + item.readableContent + ) { let highlights: Highlight[] = [] // load highlights if needed if ( @@ -598,15 +604,17 @@ export const functionResolvers = { // convert html to the requested format const converter = contentConverter(item.format) if (converter) { - return converter(item.content, highlights) + return converter(item.readableContent, highlights) } } catch (error) { ctx.log.error('Error converting content', error) } } - return item.content + return item.readableContent }, + isArchived: (item: LibraryItem) => !!item.archivedAt, + pageType: (item: LibraryItem) => item.itemType, }, Subscription: { newsletterEmail(subscription: Subscription) { @@ -781,6 +789,22 @@ export const functionResolvers = { } }, }, + ArticleSavingRequest: { + status: (item: LibraryItem) => item.state, + url: (item: LibraryItem) => item.originalUrl, + }, + Recommendation: { + user: (recommendation: Recommendation) => { + return { + userId: recommendation.recommender.id, + username: recommendation.recommender.profile.username, + profileImageURL: recommendation.recommender.profile.pictureUrl, + name: recommendation.recommender.name, + } + }, + name: (recommendation: Recommendation) => recommendation.group.name, + recommendedAt: (recommendation: Recommendation) => recommendation.createdAt, + }, ...resultResolveTypeResolver('Login'), ...resultResolveTypeResolver('LogOut'), ...resultResolveTypeResolver('GoogleSignup'), diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index ef8b1b430..d1bd121eb 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -1,5 +1,6 @@ import { In } from 'typeorm' import { Group } from '../../entity/groups/group' +import { User } from '../../entity/user' import { env } from '../../env' import { CreateGroupError, @@ -19,6 +20,7 @@ import { MutationLeaveGroupArgs, MutationRecommendArgs, MutationRecommendHighlightsArgs, + RecommendationGroup, RecommendError, RecommendErrorCode, RecommendHighlightsError, @@ -38,26 +40,30 @@ import { leaveGroup, } from '../../services/groups' import { findLibraryItemById } from '../../services/library_item' +import { Merge } from '../../util' import { analytics } from '../../utils/analytics' import { enqueueRecommendation } from '../../utils/createTask' import { authorized } from '../../utils/gql-utils' -import { userDataToUser } from '../../utils/helpers' +export type PartialRecommendationGroup = Merge< + RecommendationGroup, + { admins: Array; members: Array } +> export const createGroupResolver = authorized< - CreateGroupSuccess, + Merge, CreateGroupError, MutationCreateGroupArgs >(async (_, { input }, { uid, log }) => { try { - const userData = await userRepository.findById(uid) - if (!userData) { + const user = await userRepository.findById(uid) + if (!user) { return { errorCodes: [CreateGroupErrorCode.Unauthorized], } } const [group, invite] = await createGroup({ - admin: userData, + admin: user, name: input.name, maxMembers: input.maxMembers, expiresInDays: input.expiresInDays, @@ -80,7 +86,6 @@ export const createGroupResolver = authorized< await createLabelAndRuleForGroup(uid, group.name) const inviteUrl = getInviteUrl(invite) - const user = userDataToUser(userData) return { group: { @@ -103,37 +108,38 @@ export const createGroupResolver = authorized< } }) -export const groupsResolver = authorized( - async (_, __, { uid, log }) => { - try { - const user = await userRepository.findById(uid) - if (!user) { - return { - errorCodes: [GroupsErrorCode.Unauthorized], - } - } - - const groups = await getRecommendationGroups(user) - +export const groupsResolver = authorized< + Merge }>, + GroupsError +>(async (_, __, { uid, log }) => { + try { + const user = await userRepository.findById(uid) + if (!user) { return { - groups, - } - } catch (error) { - log.error('Error getting groups', { - error, - labels: { - source: 'resolver', - resolver: 'groupsResolver', - uid, - }, - }) - - return { - errorCodes: [GroupsErrorCode.BadRequest], + errorCodes: [GroupsErrorCode.Unauthorized], } } + + const groups = await getRecommendationGroups(user) + + return { + groups, + } + } catch (error) { + log.error('Error getting groups', { + error, + labels: { + source: 'resolver', + resolver: 'groupsResolver', + uid, + }, + }) + + return { + errorCodes: [GroupsErrorCode.BadRequest], + } } -) +}) export const recommendResolver = authorized< RecommendSuccess, @@ -206,7 +212,7 @@ export const recommendResolver = authorized< }) export const joinGroupResolver = authorized< - JoinGroupSuccess, + Merge, JoinGroupError, MutationJoinGroupArgs >(async (_, { inviteCode }, { uid, log }) => { diff --git a/packages/api/src/resolvers/update/index.ts b/packages/api/src/resolvers/update/index.ts index c38019d8b..d17830505 100644 --- a/packages/api/src/resolvers/update/index.ts +++ b/packages/api/src/resolvers/update/index.ts @@ -1,15 +1,15 @@ -import { LibraryItemState } from '../../entity/library_item' +import { LibraryItem, LibraryItemState } from '../../entity/library_item' import { MutationUpdatePageArgs, UpdatePageError, UpdatePageSuccess, } from '../../generated/graphql' import { updateLibraryItem } from '../../services/library_item' -import { libraryItemToArticle } from '../../utils/helpers' +import { Merge } from '../../util' import { authorized } from '../../utils/gql-utils' export const updatePageResolver = authorized< - UpdatePageSuccess, + Merge, UpdatePageError, MutationUpdatePageArgs >(async (_, { input }, { uid }) => { @@ -29,6 +29,6 @@ export const updatePageResolver = authorized< uid ) return { - updatedPage: libraryItemToArticle(updatedPage), + updatedPage: updatedPage, } }) diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 5f794aff0..6572c1f10 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -33,7 +33,6 @@ import { UpdateUserProfileErrorCode, UpdateUserProfileSuccess, UpdateUserSuccess, - User, UserErrorCode, UserResult, UsersError, @@ -43,13 +42,13 @@ import { userRepository } from '../../repository/user' import { createUser } from '../../services/create_user' import { sendAccountChangeEmail } from '../../services/send_emails' import { softDeleteUser } from '../../services/user' -import { userDataToUser } from '../../utils/helpers' +import { Merge } from '../../util' +import { authorized } from '../../utils/gql-utils' import { validateUsername } from '../../utils/usernamePolicy' import { WithDataSourcesContext } from '../types' -import { authorized } from '../../utils/gql-utils' export const updateUserResolver = authorized< - UpdateUserSuccess, + Merge, UpdateUserError, MutationUpdateUserArgs >(async (_, { input: { name, bio } }, { uid, authTrx }) => { @@ -83,11 +82,11 @@ export const updateUserResolver = authorized< }) ) - return { user: userDataToUser(updatedUser) } + return { user: updatedUser } }) export const updateUserProfileResolver = authorized< - UpdateUserProfileSuccess, + Merge, UpdateUserProfileError, MutationUpdateUserProfileArgs >(async (_, { input: { userId, username, pictureUrl } }, { uid, authTrx }) => { @@ -140,11 +139,11 @@ export const updateUserProfileResolver = authorized< }) ) - return { user: userDataToUser(updatedUser) } + return { user: updatedUser } }) export const googleLoginResolver: ResolverFn< - LoginResult, + Merge, unknown, WithDataSourcesContext, MutationGoogleLoginArgs @@ -167,7 +166,7 @@ export const googleLoginResolver: ResolverFn< // set auth cookie in response header await setAuth({ uid: user.id }) - return { me: userDataToUser(user) } + return { me: user } } export const validateUsernameResolver: ResolverFn< @@ -190,7 +189,7 @@ export const validateUsernameResolver: ResolverFn< } export const googleSignupResolver: ResolverFn< - GoogleSignupResult, + Merge, Record, WithDataSourcesContext, MutationGoogleSignupArgs @@ -205,7 +204,7 @@ export const googleSignupResolver: ResolverFn< } try { - const [user, profile] = await createUser({ + const [user] = await createUser({ email, sourceUserId, provider: 'GOOGLE', @@ -218,7 +217,7 @@ export const googleSignupResolver: ResolverFn< await setAuth({ uid: user.id }) return { - me: userDataToUser({ ...user, profile: { ...profile, private: false } }), + me: user, } } catch (err) { log.info('error signing up with google', err) @@ -245,7 +244,7 @@ export const logOutResolver: ResolverFn< } export const getMeUserResolver: ResolverFn< - User | undefined, + UserEntity | undefined, unknown, WithDataSourcesContext, unknown @@ -260,14 +259,14 @@ export const getMeUserResolver: ResolverFn< return undefined } - return userDataToUser(user) + return user } catch (error) { return undefined } } export const getUserResolver: ResolverFn< - UserResult, + Merge, unknown, WithDataSourcesContext, QueryUserArgs @@ -294,16 +293,17 @@ export const getUserResolver: ResolverFn< return { errorCodes: [UserErrorCode.UserNotFound] } } - return { user: userDataToUser(userRecord) } + return { user: userRecord } } -export const getAllUsersResolver = authorized( - async (_obj, _params) => { - const users = await userRepository.findTopUsers() - const result = { users: users.map((userData) => userDataToUser(userData)) } - return result - } -) +export const getAllUsersResolver = authorized< + Merge }>, + UsersError +>(async (_obj, _params) => { + const users = await userRepository.findTopUsers() + const result = { users } + return result +}) type ErrorWithCode = { errorCode: string diff --git a/packages/api/src/routers/article_router.ts b/packages/api/src/routers/article_router.ts index fbe9ea4d5..1adda960a 100644 --- a/packages/api/src/routers/article_router.ts +++ b/packages/api/src/routers/article_router.ts @@ -49,22 +49,23 @@ export function articleRouter() { return res.status(400).send('Bad Request') } - const result = await createPageSaveRequest({ user, url }) - if (isSiteBlockedForParse(url)) { return res .status(400) .send({ errorCode: CreateArticleErrorCode.NotAllowedToParse }) } - if (result.errorCode) { - return res.status(400).send({ errorCode: result.errorCode }) - } + try { + const result = await createPageSaveRequest({ user, url }) - return res.send({ - articleSavingRequestId: result.id, - url: result.url, - }) + return res.send({ + articleSavingRequestId: result.id, + url: result.originalUrl, + }) + } catch (error) { + logger.error('Error saving article:', error) + return res.status(500).send({ errorCode: 'INTERNAL_ERROR' }) + } }) router.get( diff --git a/packages/api/src/services/create_page_save_request.ts b/packages/api/src/services/create_page_save_request.ts index ce4ce137b..8e2470e2c 100644 --- a/packages/api/src/services/create_page_save_request.ts +++ b/packages/api/src/services/create_page_save_request.ts @@ -1,20 +1,16 @@ import * as privateIpLib from 'private-ip' -import { LibraryItemState } from '../entity/library_item' +import { LibraryItem, LibraryItemState } from '../entity/library_item' import { User } from '../entity/user' import { - ArticleSavingRequest, ArticleSavingRequestStatus, CreateArticleSavingRequestErrorCode, CreateLabelInput, PageType, } from '../generated/graphql' import { createPubSubClient, PubsubClient } from '../pubsub' +import { Merge } from '../util' import { enqueueParseRequest } from '../utils/createTask' -import { - cleanUrl, - generateSlug, - libraryItemToArticleSavingRequest, -} from '../utils/helpers' +import { cleanUrl, generateSlug } from '../utils/helpers' import { logger } from '../utils/logger' import { countBySavedAt, createOrUpdateLibraryItem } from './library_item' @@ -88,11 +84,12 @@ export const createPageSaveRequest = async ({ publishedAt, folder, subscription, -}: PageSaveRequest): Promise => { +}: PageSaveRequest): Promise => { try { validateUrl(url) } catch (error) { - logger.info('invalid url', { url, error }) + logger.error('invalid url', { url, error }) + return Promise.reject({ errorCode: CreateArticleSavingRequestErrorCode.BadData, }) @@ -140,5 +137,5 @@ export const createPageSaveRequest = async ({ rssFeedUrl: subscription, }) - return libraryItemToArticleSavingRequest(user, libraryItem) + return libraryItem } diff --git a/packages/api/src/services/groups.ts b/packages/api/src/services/groups.ts index cedfecd5a..ef1f61325 100644 --- a/packages/api/src/services/groups.ts +++ b/packages/api/src/services/groups.ts @@ -6,9 +6,8 @@ import { Invite } from '../entity/groups/invite' import { RuleActionType } from '../entity/rule' import { User } from '../entity/user' import { homePageURL } from '../env' -import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' import { getRepository } from '../repository' -import { userDataToUser } from '../utils/helpers' +import { PartialRecommendationGroup } from '../resolvers' import { findOrCreateLabels } from './labels' import { createRule } from './rules' @@ -70,22 +69,21 @@ export const createGroup = async (input: { export const getRecommendationGroups = async ( user: User -): Promise => { +): Promise> => { const groupMembers = await getRepository(GroupMembership).find({ where: { user: { id: user.id } }, relations: ['invite', 'group.members.user.profile'], }) return groupMembers.map((gm) => { - const admins: GraphqlUser[] = [] - const members: GraphqlUser[] = [] + const admins: Array = [] + const members: Array = [] // Return all members gm.group.members.forEach((m) => { - const user = userDataToUser(m.user) if (m.isAdmin) { - admins.push(user) + admins.push(m.user) } - members.push(user) + members.push(m.user) }) const canSeeMembers = gm.group.onlyAdminCanSeeMembers ? gm.isAdmin : true @@ -113,7 +111,7 @@ export const getInviteUrl = (invite: Invite) => { export const joinGroup = async ( user: User, inviteCode: string -): Promise => { +): Promise => { const invite = await appDataSource.transaction(async (t) => { // Check if the invite exists const invite = await t @@ -147,15 +145,14 @@ export const joinGroup = async ( where: { id: invite.group.id }, relations: ['members', 'members.user.profile'], }) - const admins: GraphqlUser[] = [] - const members: GraphqlUser[] = [] + const admins: Array = [] + const members: Array = [] // Return all members group.members.forEach((m) => { - const user = userDataToUser(m.user) if (m.isAdmin) { - admins.push(user) + admins.push(m.user) } - members.push(user) + members.push(m.user) }) return { diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index 30a821a5e..97f495859 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -7,30 +7,11 @@ import path from 'path' import _ from 'underscore' import slugify from 'voca/slugify' import wordsCounter from 'word-counting' -import { Highlight as HighlightData } from '../entity/highlight' import { LibraryItem, LibraryItemState } from '../entity/library_item' -import { Recommendation as RecommendationData } from '../entity/recommendation' -import { RegistrationType, User } from '../entity/user' -import { - Article, - ArticleSavingRequest, - ArticleSavingRequestStatus, - ContentReader, - CreateArticleError, - CreateArticleSuccess, - DirectionalityType, - FeedArticle, - Highlight, - PageType, - Profile, - Recommendation, - SearchItem, -} from '../generated/graphql' +import { CreateArticleError } from '../generated/graphql' import { createPubSubClient } from '../pubsub' -import { ArticleFormat } from '../resolvers' import { validateUrl } from '../services/create_page_save_request' import { updateLibraryItem } from '../services/library_item' -import { Merge } from '../util' import { logger } from './logger' interface InputObject { @@ -101,55 +82,6 @@ export const findDelimiter = ( return delimiter || defaultDelimiter } -// FIXME: Remove this Date stub after nullable types will be fixed -export const userDataToUser = ( - user: Merge< - User, - { - isFriend?: boolean - followersCount?: number - friendsCount?: number - sharedArticlesCount?: number - sharedHighlightsCount?: number - sharedNotesCount?: number - viewerIsFollowing?: boolean - } - > -): { - id: string - name: string - source: RegistrationType - email?: string | null - phone?: string | null - picture?: string | null - googleId?: string | null - createdAt: Date - isFriend?: boolean | null - isFullUser: boolean - viewerIsFollowing?: boolean | null - sourceUserId: string - friendsCount?: number - followersCount?: number - sharedArticles: FeedArticle[] - sharedArticlesCount?: number - sharedHighlightsCount?: number - sharedNotesCount?: number - profile: Profile -} => ({ - ...user, - source: user.source as RegistrationType, - createdAt: user.createdAt, - friendsCount: user.friendsCount || 0, - followersCount: user.followersCount || 0, - isFullUser: true, - viewerIsFollowing: user.viewerIsFollowing || user.isFriend || false, - picture: user.profile.pictureUrl, - sharedArticles: [], - sharedArticlesCount: user.sharedArticlesCount || 0, - sharedHighlightsCount: user.sharedHighlightsCount || 0, - sharedNotesCount: user.sharedNotesCount || 0, -}) - export const generateSlug = (title: string): string => { return slugify(title).substring(0, 64) + '-' + Date.now().toString(16) } @@ -161,7 +93,7 @@ export const errorHandler = async ( userId: string, pageId?: string | null, pubsub = createPubSubClient() -): Promise => { +): Promise => { if (!pageId) return result await updateLibraryItem( @@ -176,86 +108,6 @@ export const errorHandler = async ( return result } -export const highlightDataToHighlight = ( - highlight: HighlightData -): Highlight => ({ - ...highlight, - createdByMe: false, - reactions: [], - replies: [], - type: highlight.highlightType, - user: userDataToUser(highlight.user), -}) - -export const recommandationDataToRecommendation = ( - recommendation: RecommendationData -): Recommendation => ({ - ...recommendation, - user: { - userId: recommendation.recommender.id, - username: recommendation.recommender.profile.username, - profileImageURL: recommendation.recommender.profile.pictureUrl, - name: recommendation.recommender.name, - }, - name: recommendation.group.name, - recommendedAt: recommendation.createdAt, -}) - -export const libraryItemToArticleSavingRequest = ( - user: User, - item: LibraryItem -): ArticleSavingRequest => ({ - ...item, - user: userDataToUser(user), - status: item.state as unknown as ArticleSavingRequestStatus, - url: item.originalUrl, - userId: user.id, -}) - -export const libraryItemToArticle = (item: LibraryItem): Article => ({ - ...item, - url: item.originalUrl, - state: item.state as unknown as ArticleSavingRequestStatus, - content: item.readableContent, - hash: item.textContentHash || '', - isArchived: !!item.archivedAt, - recommendations: item.recommendations?.map( - recommandationDataToRecommendation - ), - image: item.thumbnail, - contentReader: item.contentReader as unknown as ContentReader, - readingProgressAnchorIndex: item.readingProgressHighestReadAnchor, - readingProgressPercent: item.readingProgressBottomPercent, - highlights: item.highlights?.map(highlightDataToHighlight) || [], - uploadFileId: item.uploadFile?.id, - pageType: item.itemType as unknown as PageType, - wordsCount: item.wordCount, - directionality: item.directionality as unknown as DirectionalityType, -}) - -export const libraryItemToSearchItem = ( - item: LibraryItem, - format?: ArticleFormat -): SearchItem => ({ - ...item, - url: item.originalUrl, - state: item.state as unknown as ArticleSavingRequestStatus, - content: item.readableContent, - isArchived: !!item.archivedAt, - pageType: item.itemType as unknown as PageType, - readingProgressPercent: item.readingProgressBottomPercent, - contentReader: item.contentReader as unknown as ContentReader, - readingProgressAnchorIndex: item.readingProgressHighestReadAnchor, - recommendations: item.recommendations?.map( - recommandationDataToRecommendation - ), - image: item.thumbnail, - highlights: item.highlights?.map(highlightDataToHighlight), - wordsCount: item.wordCount, - directionality: item.directionality as unknown as DirectionalityType, - format, -}) - export const isParsingTimeout = (libraryItem: LibraryItem): boolean => { return ( // item processed more than 30 seconds ago