clean up helpers

This commit is contained in:
Hongbo Wu
2024-06-06 12:08:15 +08:00
parent 0013150c26
commit c620ef38a2
11 changed files with 225 additions and 420 deletions

View File

@ -46,6 +46,9 @@ export class LibraryItem {
@JoinColumn({ name: 'user_id' })
user!: User
@Column('uuid')
userId!: string
@Column('enum', {
enum: LibraryItemState,
default: LibraryItemState.Succeeded,

View File

@ -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 = '<p>We were unable to parse this page.</p>'
export const createArticleResolver = authorized<
CreateArticleSuccess,
Merge<
CreateArticleSuccess,
{ user: User; createdArticle: Partial<LibraryItem> }
>,
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<ArticleSuccess, { article: LibraryItem }>,
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<SetBookmarkArticleSuccess, { bookmarkedArticle: LibraryItem }>,
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<SaveArticleReadingProgressSuccess, { updatedArticle: LibraryItem }>,
SaveArticleReadingProgressError,
MutationSaveArticleReadingProgressArgs
>(
@ -661,13 +584,15 @@ export const saveArticleReadingProgressResolver = authorized<
}
return {
updatedArticle: libraryItemToArticle(updatedItem),
updatedArticle: updatedItem,
}
}
)
export type PartialLibraryItem = Merge<LibraryItem, { format?: string }>
type PartialSearchItemEdge = Merge<SearchItemEdge, { node: PartialLibraryItem }>
export const searchResolver = authorized<
SearchSuccess,
Merge<SearchSuccess, { edges: Array<PartialSearchItemEdge> }>,
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<UpdatesSinceSuccess, { edges: Array<PartialSearchItemEdge> }>,
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,

View File

@ -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<ArticleSavingRequestSuccess, { articleSavingRequest: LibraryItem }>,
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)

View File

@ -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'),

View File

@ -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<User>; members: Array<User> }
>
export const createGroupResolver = authorized<
CreateGroupSuccess,
Merge<CreateGroupSuccess, { group: PartialRecommendationGroup }>,
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<GroupsSuccess, GroupsError>(
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<GroupsSuccess, { groups: Array<PartialRecommendationGroup> }>,
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<JoinGroupSuccess, { group: PartialRecommendationGroup }>,
JoinGroupError,
MutationJoinGroupArgs
>(async (_, { inviteCode }, { uid, log }) => {

View File

@ -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<UpdatePageSuccess, { updatedPage: LibraryItem }>,
UpdatePageError,
MutationUpdatePageArgs
>(async (_, { input }, { uid }) => {
@ -29,6 +29,6 @@ export const updatePageResolver = authorized<
uid
)
return {
updatedPage: libraryItemToArticle(updatedPage),
updatedPage: updatedPage,
}
})

View File

@ -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<UpdateUserSuccess, { user: UserEntity }>,
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<UpdateUserProfileSuccess, { user: UserEntity }>,
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<LoginResult, { me?: UserEntity }>,
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<GoogleSignupResult, { me?: UserEntity }>,
Record<string, unknown>,
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<UserResult, { user?: UserEntity }>,
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<UsersSuccess, UsersError>(
async (_obj, _params) => {
const users = await userRepository.findTopUsers()
const result = { users: users.map((userData) => userDataToUser(userData)) }
return result
}
)
export const getAllUsersResolver = authorized<
Merge<UsersSuccess, { users: Array<UserEntity> }>,
UsersError
>(async (_obj, _params) => {
const users = await userRepository.findTopUsers()
const result = { users }
return result
})
type ErrorWithCode = {
errorCode: string

View File

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

View File

@ -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<ArticleSavingRequest> => {
}: PageSaveRequest): Promise<LibraryItem> => {
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
}

View File

@ -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<RecommendationGroup[]> => {
): Promise<Array<PartialRecommendationGroup>> => {
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<User> = []
const members: Array<User> = []
// 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<RecommendationGroup> => {
): Promise<PartialRecommendationGroup> => {
const invite = await appDataSource.transaction<Invite>(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<User> = []
const members: Array<User> = []
// 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 {

View File

@ -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<CreateArticleError | CreateArticleSuccess> => {
): Promise<CreateArticleError> => {
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