From c1ad9b6f41cf5ae148d8078e4e312c8b1cec4957 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 4 Sep 2023 15:03:04 +0800 Subject: [PATCH] cont --- packages/api/src/resolvers/api_key/index.ts | 8 +- packages/api/src/resolvers/article/index.ts | 166 +++++++++--------- packages/api/src/resolvers/filters/index.ts | 34 ++-- .../api/src/resolvers/function_resolvers.ts | 7 +- packages/api/src/resolvers/highlight/index.ts | 4 +- .../importers/uploadImportFileResolver.ts | 7 +- .../api/src/resolvers/newsletters/index.ts | 15 +- .../api/src/resolvers/recent_emails/index.ts | 1 - .../src/resolvers/recent_searches/index.ts | 14 +- .../src/resolvers/recommendations/index.ts | 14 +- packages/api/src/resolvers/rules/index.ts | 1 - packages/api/src/resolvers/save/index.ts | 4 +- .../send_install_instructions/index.ts | 5 +- .../api/src/resolvers/subscriptions/index.ts | 3 +- .../api/src/resolvers/upload_files/index.ts | 77 ++++---- .../resolvers/user_personalization/index.ts | 30 ++-- packages/api/src/resolvers/webhooks/index.ts | 126 +++++-------- packages/api/src/routers/auth/apple_auth.ts | 2 +- packages/api/src/routers/auth/google_auth.ts | 2 +- .../api/src/routers/auth/mobile/sign_in.ts | 1 - .../api/src/routers/auth/mobile/sign_up.ts | 2 +- packages/api/src/routers/svc/content.ts | 12 +- packages/api/src/services/groups.ts | 24 ++- .../src/services/integrations/integration.ts | 6 +- .../api/src/services/integrations/pocket.ts | 10 +- .../api/src/services/integrations/readwise.ts | 41 +++-- packages/api/src/services/library_item.ts | 34 ++-- packages/api/src/services/newsletters.ts | 81 +++++---- packages/api/src/services/reports.ts | 54 +++--- packages/api/src/services/save_email.ts | 103 ++++++----- packages/api/src/services/save_file.ts | 20 +-- packages/api/src/services/save_page.ts | 89 +++++----- packages/api/src/services/save_url.ts | 19 +- packages/api/src/services/upload_file.ts | 2 +- 34 files changed, 451 insertions(+), 567 deletions(-) diff --git a/packages/api/src/resolvers/api_key/index.ts b/packages/api/src/resolvers/api_key/index.ts index ce8de19c9..8d8246b7c 100644 --- a/packages/api/src/resolvers/api_key/index.ts +++ b/packages/api/src/resolvers/api_key/index.ts @@ -1,3 +1,4 @@ +import { ApiKey } from '../../entity/api_key' import { env } from '../../env' import { ApiKeysError, @@ -12,7 +13,6 @@ import { RevokeApiKeyErrorCode, RevokeApiKeySuccess, } from '../../generated/graphql' -import { apiKeyRepository } from '../../repository/api_key' import { analytics } from '../../utils/analytics' import { generateApiKey, hashApiKey } from '../../utils/auth' import { authorized } from '../../utils/helpers' @@ -21,7 +21,7 @@ export const apiKeysResolver = authorized( async (_, __, { log, authTrx }) => { try { const apiKeys = await authTrx(async (tx) => { - return tx.withRepository(apiKeyRepository).find({ + return tx.getRepository(ApiKey).find({ select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'], order: { usedAt: { direction: 'DESC', nulls: 'last' }, @@ -52,7 +52,7 @@ export const generateApiKeyResolver = authorized< const exp = new Date(expiresAt) const originalKey = generateApiKey() const apiKeyCreated = await authTrx(async (tx) => { - return tx.withRepository(apiKeyRepository).save({ + return tx.getRepository(ApiKey).save({ user: { id: uid }, name, key: hashApiKey(originalKey), @@ -90,7 +90,7 @@ export const revokeApiKeyResolver = authorized< >(async (_, { id }, { claims: { uid }, log, authTrx }) => { try { const deletedApiKey = await authTrx(async (tx) => { - const apiRepo = tx.withRepository(apiKeyRepository) + const apiRepo = tx.getRepository(ApiKey) const apiKey = await apiRepo.findOneBy({ id }) if (!apiKey) { return null diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 57e8cd39d..9d7fd56ca 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -143,13 +143,13 @@ export const createArticleResolver = authorized< CreateArticleError, MutationCreateArticleArgs >( - ;async ( + async ( _, { input: { url, preparedDocument, - articleSavingRequestId: pageId, + articleSavingRequestId, uploadFileId, skipParsing, source, @@ -176,7 +176,7 @@ export const createArticleResolver = authorized< errorCodes: [CreateArticleErrorCode.Unauthorized], }, uid, - pageId, + articleSavingRequestId, pubsub ) } @@ -189,7 +189,7 @@ export const createArticleResolver = authorized< errorCodes: [CreateArticleErrorCode.NotAllowedToParse], }, uid, - pageId, + articleSavingRequestId, pubsub ) } @@ -237,12 +237,12 @@ export const createArticleResolver = authorized< /* We do not trust the values from client, lookup upload file by querying * with filtering on user ID and URL to verify client's uploadFileId is valid. */ - const uploadFile = await findUploadFileById(uploadFileId, uid) + const uploadFile = await findUploadFileById(uploadFileId) if (!uploadFile) { return pageError( { errorCodes: [CreateArticleErrorCode.UploadFileMissing] }, uid, - pageId, + articleSavingRequestId, pubsub ) } @@ -295,7 +295,7 @@ export const createArticleResolver = authorized< title, parsedContent, userId: uid, - pageId, + itemId: articleSavingRequestId, slug, croppedPathname, originalHtml: domContent, @@ -320,14 +320,14 @@ export const createArticleResolver = authorized< }) if (uploadFileId) { - const uploadFileData = await setFileUploadComplete(uploadFileId, uid) + const uploadFileData = await setFileUploadComplete(uploadFileId) if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) { return pageError( { errorCodes: [CreateArticleErrorCode.UploadFileMissing], }, uid, - pageId, + articleSavingRequestId, pubsub ) } @@ -350,11 +350,11 @@ export const createArticleResolver = authorized< libraryItemToSave.originalUrl, uid ) - pageId = existingLibraryItem?.id || pageId - if (pageId) { + articleSavingRequestId = existingLibraryItem?.id || articleSavingRequestId + if (articleSavingRequestId) { // update existing page's state from processing to succeeded libraryItemToReturn = await updateLibraryItem( - pageId, + articleSavingRequestId, libraryItemToSave, uid, pubsub @@ -388,7 +388,7 @@ export const createArticleResolver = authorized< errorCodes: [CreateArticleErrorCode.ElasticError], }, uid, - pageId, + articleSavingRequestId, pubsub ) } @@ -654,92 +654,88 @@ export const saveArticleReadingProgressResolver = authorized< } ) -export const searchResolver = authorized( - async (_obj, params, { uid, log }) => { - const startCursor = params.after || '' - const first = params.first || 10 +export const searchResolver = authorized< + SearchSuccess, + SearchError, + QuerySearchArgs +>(async (_obj, params, { uid, log }) => { + const startCursor = params.after || '' + const first = params.first || 10 - // the query size is limited to 255 characters - if (params.query && params.query.length > 255) { - return { errorCodes: [SearchErrorCode.QueryTooLong] } + // the query size is limited to 255 characters + if (params.query && params.query.length > 255) { + return { errorCodes: [SearchErrorCode.QueryTooLong] } + } + + const searchQuery = parseSearchQuery(params.query || undefined) + + const { libraryItems, count } = await searchLibraryItems( + { + from: Number(startCursor), + size: first + 1, // fetch one more item to get next cursor + sort: searchQuery.sort, + includePending: true, + includeContent: params.includeContent ?? false, + ...searchQuery, + }, + uid + ) + + const start = + startCursor && !isNaN(Number(startCursor)) ? Number(startCursor) : 0 + const hasNextPage = libraryItems.length > first + const endCursor = String(start + libraryItems.length - (hasNextPage ? 1 : 0)) + + if (hasNextPage) { + // remove an extra if exists + libraryItems.pop() + } + + const edges = libraryItems.map((libraryItem) => { + if (libraryItem.siteIcon && !isBase64Image(libraryItem.siteIcon)) { + libraryItem.siteIcon = createImageProxyUrl(libraryItem.siteIcon, 128, 128) } - - const searchQuery = parseSearchQuery(params.query || undefined) - - const { libraryItems, count } = await searchLibraryItems( - { - from: Number(startCursor), - size: first + 1, // fetch one more item to get next cursor - sort: searchQuery.sort, - includePending: true, - includeContent: params.includeContent ?? false, - ...searchQuery, - }, - uid - ) - - const start = - startCursor && !isNaN(Number(startCursor)) ? Number(startCursor) : 0 - const hasNextPage = libraryItems.length > first - const endCursor = String( - start + libraryItems.length - (hasNextPage ? 1 : 0) - ) - - if (hasNextPage) { - // remove an extra if exists - libraryItems.pop() - } - - const edges = libraryItems.map((libraryItem) => { - if (libraryItem.siteIcon && !isBase64Image(libraryItem.siteIcon)) { - libraryItem.siteIcon = createImageProxyUrl( - libraryItem.siteIcon, - 128, - 128 - ) - } - if (params.includeContent && libraryItem.readableContent) { - // convert html to the requested format - const format = params.format || ArticleFormat.Html - try { - const converter = contentConverter(format) - if (converter) { - libraryItem.readableContent = converter( - libraryItem.readableContent, - libraryItem.highlights - ) - } - } catch (error) { - log.error('Error converting content', error) + if (params.includeContent && libraryItem.readableContent) { + // convert html to the requested format + const format = params.format || ArticleFormat.Html + try { + const converter = contentConverter(format) + if (converter) { + libraryItem.readableContent = converter( + libraryItem.readableContent, + libraryItem.highlights + ) } + } catch (error) { + log.error('Error converting content', error) } - - return { - node: libraryItemToSearchItem(libraryItem), - cursor: endCursor, - } - }) + } return { - edges, - pageInfo: { - hasPreviousPage: false, - startCursor, - hasNextPage: hasNextPage, - endCursor, - totalCount: count, - }, + node: libraryItemToSearchItem(libraryItem), + cursor: endCursor, } + }) + + return { + edges, + pageInfo: { + hasPreviousPage: false, + startCursor, + hasNextPage: hasNextPage, + endCursor, + totalCount: count, + }, } -) +}) export const typeaheadSearchResolver = authorized< TypeaheadSearchSuccess, TypeaheadSearchError, QueryTypeaheadSearchArgs ->(async (_obj, { query, first }, { uid, log }) => { +>(async (_obj, { query, first }, { log }) => { try { - const items = await findLibraryItemsByPrefix(query, uid, first || undefined) + const items = await findLibraryItemsByPrefix(query, first || undefined) return { items: items.map((item) => ({ diff --git a/packages/api/src/resolvers/filters/index.ts b/packages/api/src/resolvers/filters/index.ts index dc2bc46e3..2a8dcdbe0 100644 --- a/packages/api/src/resolvers/filters/index.ts +++ b/packages/api/src/resolvers/filters/index.ts @@ -33,15 +33,6 @@ export const saveFilterResolver = authorized< SaveFilterError, MutationSaveFilterArgs >(async (_, { input }, { authTrx, uid, log }) => { - log.info('Saving filters', { - input, - labels: { - source: 'resolver', - resolver: 'saveFilterResolver', - uid, - }, - }) - try { const filter = await authTrx(async (t) => { return t.withRepository(filterRepository).save({ @@ -72,27 +63,24 @@ export const deleteFilterResolver = authorized< DeleteFilterSuccess, DeleteFilterError, MutationDeleteFilterArgs ->(async (_, { id }, { authTrx, uid, log }) => { - log.info('Deleting filters', { - id, - labels: { - source: 'resolver', - resolver: 'deleteFilterResolver', - uid: claims.uid, - }, - }) - +>(async (_, { id }, { authTrx, log }) => { try { const filter = await authTrx(async (t) => { - const filter = await t.withRepository(filterRepository).findOne({ + const filter = await t.getRepository(Filter).findOneBy({ + id, + }) + if (!filter) { + throw new Error('Filter not found') + } + + return t.getRepository(Filter).remove(filter) + }) return { filter, } } catch (error) { - log.error('Error deleting filters', - error - ) + log.error('Error deleting filters', error) return { errorCodes: [DeleteFilterErrorCode.BadRequest], diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 45e7647ed..545f01aa5 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -375,10 +375,7 @@ export const functionResolvers = { ctx.claims && article.uploadFileId ) { - const upload = await findUploadFileById( - article.uploadFileId, - ctx.claims.uid - ) + const upload = await findUploadFileById(article.uploadFileId) if (!upload || !upload.fileName) { return undefined } @@ -486,7 +483,7 @@ export const functionResolvers = { ctx.uid && item.uploadFileId ) { - const upload = await findUploadFileById(item.uploadFileId, ctx.uid) + const upload = await findUploadFileById(item.uploadFileId) if (!upload || !upload.fileName) { return undefined } diff --git a/packages/api/src/resolvers/highlight/index.ts b/packages/api/src/resolvers/highlight/index.ts index 6fc7d1892..ab956e8dd 100644 --- a/packages/api/src/resolvers/highlight/index.ts +++ b/packages/api/src/resolvers/highlight/index.ts @@ -177,9 +177,9 @@ export const deleteHighlightResolver = authorized< DeleteHighlightSuccess, DeleteHighlightError, MutationDeleteHighlightArgs ->(async (_, { highlightId }, { uid, log }) => { +>(async (_, { highlightId }, { log }) => { try { - const deletedHighlight = await deleteHighlightById(highlightId, uid) + const deletedHighlight = await deleteHighlightById(highlightId) if (!deletedHighlight) { return { diff --git a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts index f73131c73..4f327ce54 100644 --- a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts +++ b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts @@ -1,6 +1,5 @@ import { DateTime } from 'luxon' import { v4 as uuidv4 } from 'uuid' -import { User } from '../../entity/user' import { env } from '../../env' import { MutationUploadImportFileArgs, @@ -8,7 +7,7 @@ import { UploadImportFileErrorCode, UploadImportFileSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' +import { userRepository } from '../../repository/user' import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/helpers' import { logger } from '../../utils/logger' @@ -35,15 +34,13 @@ export const uploadImportFileResolver = authorized< UploadImportFileError, MutationUploadImportFileArgs >(async (_, { type, contentType }, { claims: { uid }, log }) => { - log.info('uploadImportFileResolver') - if (!VALID_CONTENT_TYPES.includes(contentType)) { return { errorCodes: [UploadImportFileErrorCode.BadRequest], } } - const user = await getRepository(User).findOneBy({ id: uid }) + const user = await userRepository.findOneBy({ id: uid }) if (!user) { return { errorCodes: [UploadImportFileErrorCode.Unauthorized], diff --git a/packages/api/src/resolvers/newsletters/index.ts b/packages/api/src/resolvers/newsletters/index.ts index 7e461554f..2290499fc 100644 --- a/packages/api/src/resolvers/newsletters/index.ts +++ b/packages/api/src/resolvers/newsletters/index.ts @@ -13,7 +13,6 @@ import { NewsletterEmailsErrorCode, NewsletterEmailsSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' import { createNewsletterEmail, deleteNewsletterEmail, @@ -57,20 +56,10 @@ export const createNewsletterEmailResolver = authorized< export const newsletterEmailsResolver = authorized< NewsletterEmailsSuccess, NewsletterEmailsError ->(async (_parent, _args, { claims, log }) => { - log.info('newsletterEmailsResolver') +>(async (_parent, _args, { uid, log }) => { try { - const user = await getRepository(User).findOneBy({ - id: claims.uid, - }) - if (!user) { - return Promise.reject({ - errorCode: NewsletterEmailsErrorCode.Unauthorized, - }) - } - - const newsletterEmails = await getNewsletterEmails(user.id) + const newsletterEmails = await getNewsletterEmails(uid) return { newsletterEmails: newsletterEmails.map((newsletterEmail) => ({ diff --git a/packages/api/src/resolvers/recent_emails/index.ts b/packages/api/src/resolvers/recent_emails/index.ts index 203eebdbf..4d6934561 100644 --- a/packages/api/src/resolvers/recent_emails/index.ts +++ b/packages/api/src/resolvers/recent_emails/index.ts @@ -11,7 +11,6 @@ import { RecentEmailsErrorCode, RecentEmailsSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' import { updateReceivedEmail } from '../../services/received_emails' import { saveNewsletter } from '../../services/save_newsletter_email' import { authorized } from '../../utils/helpers' diff --git a/packages/api/src/resolvers/recent_searches/index.ts b/packages/api/src/resolvers/recent_searches/index.ts index e118c928d..952e97fd1 100644 --- a/packages/api/src/resolvers/recent_searches/index.ts +++ b/packages/api/src/resolvers/recent_searches/index.ts @@ -1,25 +1,15 @@ -import { User } from '../../entity/user' -import { env } from '../../env' import { RecentSearchesError, - RecentSearchesErrorCode, RecentSearchesSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' import { getRecentSearches } from '../../services/search_history' -import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/helpers' export const recentSearchesResolver = authorized< RecentSearchesSuccess, RecentSearchesError ->(async (_obj, _params, { claims: { uid }, log }) => { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { errorCodes: [RecentSearchesErrorCode.Unauthorized] } - } - - const searches = await getRecentSearches(uid) +>(async (_obj, _params) => { + const searches = await getRecentSearches() return { searches, } diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index 29e12b60e..930541938 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -28,7 +28,7 @@ import { RecommendHighlightsSuccess, RecommendSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' +import { userRepository } from '../../repository/user' import { createGroup, createLabelAndRuleForGroup, @@ -57,7 +57,7 @@ export const createGroupResolver = authorized< }) try { - const userData = await getRepository(User).findOne({ + const userData = await userRepository.findOne({ where: { id: uid }, relations: ['profile'], }) @@ -132,7 +132,7 @@ export const groupsResolver = authorized( }) try { - const user = await getRepository(User).findOneBy({ + const user = await userRepository.findOneBy({ id: uid, }) if (!user) { @@ -178,7 +178,7 @@ export const recommendResolver = authorized< }) try { - const user = await getRepository(User).findOne({ + const user = await userRepository.findOne({ where: { id: uid }, relations: ['profile'], }) @@ -272,7 +272,7 @@ export const joinGroupResolver = authorized< }) try { - const user = await getRepository(User).findOne({ + const user = await userRepository.findOne({ where: { id: uid }, relations: ['profile'], }) @@ -329,7 +329,7 @@ export const recommendHighlightsResolver = authorized< }) try { - const user = await getRepository(User).findOne({ + const user = await userRepository.findOne({ where: { id: uid }, relations: ['profile'], }) @@ -421,7 +421,7 @@ export const leaveGroupResolver = authorized< }) try { - const user = await getRepository(User).findOneBy({ + const user = await userRepository.findOneBy({ id: uid, }) if (!user) { diff --git a/packages/api/src/resolvers/rules/index.ts b/packages/api/src/resolvers/rules/index.ts index b17830b4a..efeef74c8 100644 --- a/packages/api/src/resolvers/rules/index.ts +++ b/packages/api/src/resolvers/rules/index.ts @@ -14,7 +14,6 @@ import { SetRuleErrorCode, SetRuleSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' import { authorized } from '../../utils/helpers' export const setRuleResolver = authorized< diff --git a/packages/api/src/resolvers/save/index.ts b/packages/api/src/resolvers/save/index.ts index 231181b59..6051ac042 100644 --- a/packages/api/src/resolvers/save/index.ts +++ b/packages/api/src/resolvers/save/index.ts @@ -37,7 +37,7 @@ export const savePageResolver = authorized< return { errorCodes: [SaveErrorCode.Unauthorized] } } - return savePage(ctx, user, input) + return savePage(input, user) }) export const saveUrlResolver = authorized< @@ -93,5 +93,5 @@ export const saveFileResolver = authorized< return { errorCodes: [SaveErrorCode.Unauthorized] } } - return saveFile(ctx, user, input) + return saveFile(input, user) }) diff --git a/packages/api/src/resolvers/send_install_instructions/index.ts b/packages/api/src/resolvers/send_install_instructions/index.ts index 3163b74eb..6435b807b 100644 --- a/packages/api/src/resolvers/send_install_instructions/index.ts +++ b/packages/api/src/resolvers/send_install_instructions/index.ts @@ -1,11 +1,10 @@ -import { appDataSource } from '../../data_source' -import { User } from '../../entity/user' import { env } from '../../env' import { SendInstallInstructionsError, SendInstallInstructionsErrorCode, SendInstallInstructionsSuccess, } from '../../generated/graphql' +import { userRepository } from '../../repository/user' import { authorized } from '../../utils/helpers' import { sendEmail } from '../../utils/sendEmail' @@ -17,7 +16,7 @@ export const sendInstallInstructionsResolver = authorized< SendInstallInstructionsError >(async (_parent, _args, { claims, log }) => { try { - const user = await appDataSource.getRepository(User).findOneBy({ + const user = await userRepository.findOneBy({ id: claims.uid, }) diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index 83a151681..6233eb284 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -26,8 +26,7 @@ import { UpdateSubscriptionErrorCode, UpdateSubscriptionSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' -import { getSubscribeHandler, unsubscribe } from '../../services/subscriptions' +import { unsubscribe } from '../../services/subscriptions' import { Merge } from '../../util' import { analytics } from '../../utils/analytics' import { enqueueRssFeedFetch } from '../../utils/createTask' diff --git a/packages/api/src/resolvers/upload_files/index.ts b/packages/api/src/resolvers/upload_files/index.ts index ee4fed892..e26e224e7 100644 --- a/packages/api/src/resolvers/upload_files/index.ts +++ b/packages/api/src/resolvers/upload_files/index.ts @@ -1,20 +1,22 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import normalizeUrl from 'normalize-url' import path from 'path' -import { createPage, getPageByParam, updatePage } from '../../elastic/pages' -import { LibraryItemType } from '../../entity/library_item' +import { LibraryItemState, LibraryItemType } from '../../entity/library_item' import { UploadFile } from '../../entity/upload_file' import { env } from '../../env' import { - ArticleSavingRequestStatus, MutationUploadFileRequestArgs, UploadFileRequestError, UploadFileRequestErrorCode, UploadFileRequestSuccess, UploadFileStatus, } from '../../generated/graphql' -import { uploadFileRepository } from '../../repository/upload_file' import { validateUrl } from '../../services/create_page_save_request' +import { + createLibraryItem, + findLibraryItemByUrl, + updateLibraryItem, +} from '../../services/library_item' import { analytics } from '../../utils/analytics' import { authorized, generateSlug } from '../../utils/helpers' import { @@ -87,13 +89,15 @@ export const uploadFileRequestResolver = authorized< return { errorCodes: [UploadFileRequestErrorCode.BadInput] } } - uploadFileData = await uploadFileRepository.save({ - url: input.url, - userId: uid, - fileName, - status: UploadFileStatus.Initialized, - contentType: input.contentType, - }) + uploadFileData = await authTrx((t) => + t.getRepository(UploadFile).save({ + url: input.url, + userId: uid, + fileName, + status: UploadFileStatus.Initialized, + contentType: input.contentType, + }) + ) if (uploadFileData.id) { const uploadFileId = uploadFileData.id @@ -118,63 +122,52 @@ export const uploadFileRequestResolver = authorized< }) } - let createdPageId: string | undefined = undefined + let createdItemId: string | undefined = undefined if (input.createPageEntry) { // If we have a file:// URL, don't try to match it // and create a copy of the page, just create a // new item. - const page = isFileUrl(input.url) - ? await getPageByParam({ - userId: uid, - url: input.url, - }) + const item = isFileUrl(input.url) + ? await findLibraryItemByUrl(input.url, uid) : undefined - if (page) { + if (item) { if ( - !(await updatePage( - page.id, + !(await updateLibraryItem( + item.id, { savedAt: new Date(), archivedAt: null, }, - ctx + uid )) ) { return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] } } - createdPageId = page.id + createdItemId = item.id } else { - const pageId = await createPage( + const item = await createLibraryItem( { - url: isFileUrl(input.url) ? publicUrl : input.url, - id: input.clientRequestId || '', - userId: uid, - title: title, - hash: uploadFilePathName, - content: '', - pageType: itemTypeForContentType(input.contentType), - uploadFileId: uploadFileData.id, + originalUrl: isFileUrl(input.url) ? publicUrl : input.url, + id: input.clientRequestId || undefined, + user: { id: uid }, + title, + readableContent: '', + itemType: itemTypeForContentType(input.contentType), + uploadFile: { id: uploadFileData.id }, slug: generateSlug(uploadFilePathName), - createdAt: new Date(), - savedAt: new Date(), - readingProgressPercent: 0, - readingProgressAnchorIndex: 0, - state: ArticleSavingRequestStatus.Succeeded, + state: LibraryItemState.Succeeded, }, - ctx + uid ) - if (!pageId) { - return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] } - } - createdPageId = pageId + createdItemId = item.id } } return { id: uploadFileData.id, uploadSignedUrl, - createdPageId: createdPageId, + createdPageId: createdItemId, } } else { return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] } diff --git a/packages/api/src/resolvers/user_personalization/index.ts b/packages/api/src/resolvers/user_personalization/index.ts index 6878ff4cd..ad0e8c306 100644 --- a/packages/api/src/resolvers/user_personalization/index.ts +++ b/packages/api/src/resolvers/user_personalization/index.ts @@ -1,4 +1,3 @@ -import { appDataSource } from '../../data_source' import { UserPersonalization } from '../../entity/user_personalization' import { GetUserPersonalizationError, @@ -9,20 +8,17 @@ import { SetUserPersonalizationSuccess, SortOrder, } from '../../generated/graphql' -import { getRepository, setClaims } from '../../repository' import { authorized } from '../../utils/helpers' export const setUserPersonalizationResolver = authorized< SetUserPersonalizationSuccess, SetUserPersonalizationError, MutationSetUserPersonalizationArgs ->(async (_, { input }, { claims: { uid }, log }) => { +>(async (_, { input }, { authTrx, claims: { uid }, log }) => { log.info('setUserPersonalizationResolver', { uid, input }) - const result = await appDataSource.transaction(async (entityManager) => { - await setClaims(entityManager, uid) - - return entityManager.getRepository(UserPersonalization).upsert( + const result = await authTrx(async (t) => { + return t.getRepository(UserPersonalization).upsert( { user: { id: uid }, ...input, @@ -37,9 +33,11 @@ export const setUserPersonalizationResolver = authorized< } } - const updatedUserPersonalization = await getRepository( - UserPersonalization - ).findOneBy({ id: result.identifiers[0].id as string }) + const updatedUserPersonalization = await authTrx((t) => + t + .getRepository(UserPersonalization) + .findOneBy({ id: result.identifiers[0].id as string }) + ) // Cast SortOrder from string to enum const librarySortOrder = updatedUserPersonalization?.librarySortOrder as @@ -58,12 +56,12 @@ export const setUserPersonalizationResolver = authorized< export const getUserPersonalizationResolver = authorized< GetUserPersonalizationResult, GetUserPersonalizationError ->(async (_parent, _args, { uid }) => { - const userPersonalization = await getRepository( - UserPersonalization - ).findOneBy({ - user: { id: uid }, - }) +>(async (_parent, _args, { authTrx, uid }) => { + const userPersonalization = await authTrx((t) => + t.getRepository(UserPersonalization).findOneBy({ + user: { id: uid }, + }) + ) // Cast SortOrder from string to enum const librarySortOrder = userPersonalization?.librarySortOrder as diff --git a/packages/api/src/resolvers/webhooks/index.ts b/packages/api/src/resolvers/webhooks/index.ts index cd0a3208d..19379a72a 100644 --- a/packages/api/src/resolvers/webhooks/index.ts +++ b/packages/api/src/resolvers/webhooks/index.ts @@ -1,4 +1,3 @@ -import { User } from '../../entity/user' import { Webhook } from '../../entity/webhook' import { env } from '../../env' import { @@ -20,25 +19,18 @@ import { WebhooksSuccess, WebhookSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' +import { authTrx } from '../../repository' import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/helpers' export const webhooksResolver = authorized( - async (_obj, _params, { claims: { uid }, log }) => { - log.info('webhooksResolver') - + async (_obj, _params, { uid, log }) => { try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [WebhooksErrorCode.Unauthorized], - } - } - - const webhooks = await getRepository(Webhook).findBy({ - user: { id: uid }, - }) + const webhooks = await authTrx((t) => + t.getRepository(Webhook).findBy({ + user: { id: uid }, + }) + ) return { webhooks: webhooks.map((webhook) => webhookDataToResponse(webhook)), @@ -57,21 +49,14 @@ export const webhookResolver = authorized< WebhookSuccess, WebhookError, QueryWebhookArgs ->(async (_, { id }, { claims: { uid }, log }) => { - log.info('webhookResolver') - +>(async (_, { id }, { authTrx, log }) => { try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [WebhookErrorCode.Unauthorized], - } - } - - const webhook = await getRepository(Webhook).findOne({ - where: { id }, - relations: ['user'], - }) + const webhook = await authTrx((t) => + t.getRepository(Webhook).findOne({ + where: { id }, + relations: ['user'], + }) + ) if (!webhook) { return { @@ -79,12 +64,6 @@ export const webhookResolver = authorized< } } - if (webhook.user.id !== uid) { - return { - errorCodes: [WebhookErrorCode.Unauthorized], - } - } - return { webhook: webhookDataToResponse(webhook), } @@ -101,42 +80,26 @@ export const deleteWebhookResolver = authorized< DeleteWebhookSuccess, DeleteWebhookError, MutationDeleteWebhookArgs ->(async (_, { id }, { claims: { uid }, log }) => { - log.info('deleteWebhookResolver') - +>(async (_, { id }, { authTrx, uid, log }) => { try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [DeleteWebhookErrorCode.Unauthorized], - } - } + const deletedWebhook = await authTrx(async (t) => { + const webhook = await t.getRepository(Webhook).findOne({ + where: { id }, + relations: ['user'], + }) - const webhook = await getRepository(Webhook).findOne({ - where: { id }, - relations: ['user'], + if (!webhook) { + throw new Error('Webhook not found') + } + + return t.getRepository(Webhook).remove(webhook) }) - if (!webhook) { - return { - errorCodes: [DeleteWebhookErrorCode.NotFound], - } - } - - if (webhook.user.id !== uid) { - return { - errorCodes: [DeleteWebhookErrorCode.Unauthorized], - } - } - - const deletedWebhook = await getRepository(Webhook).remove(webhook) - deletedWebhook.id = id - analytics.track({ userId: uid, event: 'webhook_delete', properties: { - webhookId: webhook.id, + webhookId: id, env: env.server.apiEnv, }, }) @@ -145,8 +108,7 @@ export const deleteWebhookResolver = authorized< webhook: webhookDataToResponse(deletedWebhook), } } catch (error) { - log.error(error) - + log.error('Error deleting webhook', error) return { errorCodes: [DeleteWebhookErrorCode.BadRequest], } @@ -157,17 +119,10 @@ export const setWebhookResolver = authorized< SetWebhookSuccess, SetWebhookError, MutationSetWebhookArgs ->(async (_, { input }, { claims: { uid }, log }) => { +>(async (_, { input }, { authTrx, claims: { uid }, log }) => { log.info('setWebhookResolver') try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [SetWebhookErrorCode.Unauthorized], - } - } - const webhookToSave: Partial = { url: input.url, eventTypes: input.eventTypes as string[], @@ -178,27 +133,26 @@ export const setWebhookResolver = authorized< if (input.id) { // Update - const existingWebhook = await getRepository(Webhook).findOne({ - where: { id: input.id }, - relations: ['user'], - }) + const existingWebhook = await authTrx((t) => + t.getRepository(Webhook).findOne({ + where: { id: input.id || '' }, + relations: ['user'], + }) + ) if (!existingWebhook) { return { errorCodes: [SetWebhookErrorCode.NotFound], } } - if (existingWebhook.user.id !== uid) { - return { - errorCodes: [SetWebhookErrorCode.Unauthorized], - } - } webhookToSave.id = input.id } - const webhook = await getRepository(Webhook).save({ - user, - ...webhookToSave, - }) + const webhook = await authTrx((t) => + t.getRepository(Webhook).save({ + user: { id: uid }, + ...webhookToSave, + }) + ) analytics.track({ userId: uid, diff --git a/packages/api/src/routers/auth/apple_auth.ts b/packages/api/src/routers/auth/apple_auth.ts index cd75b3324..925e7d37e 100644 --- a/packages/api/src/routers/auth/apple_auth.ts +++ b/packages/api/src/routers/auth/apple_auth.ts @@ -5,7 +5,7 @@ import * as jwt from 'jsonwebtoken' import jwksClient from 'jwks-rsa' import { env, homePageURL } from '../../env' import { LoginErrorCode } from '../../generated/graphql' -import { userRepository } from '../../repository' +import { userRepository } from '../../repository/user' import { logger } from '../../utils/logger' import { createSsoToken, ssoRedirectURL } from '../../utils/sso' import { DecodeTokenResult } from './auth_types' diff --git a/packages/api/src/routers/auth/google_auth.ts b/packages/api/src/routers/auth/google_auth.ts index ae500165b..5c849cd8b 100644 --- a/packages/api/src/routers/auth/google_auth.ts +++ b/packages/api/src/routers/auth/google_auth.ts @@ -3,7 +3,7 @@ import { OAuth2Client } from 'googleapis-common' import url from 'url' import { env, homePageURL } from '../../env' import { LoginErrorCode } from '../../generated/graphql' -import { userRepository } from '../../repository' +import { userRepository } from '../../repository/user' import { logger } from '../../utils/logger' import { createSsoToken, ssoRedirectURL } from '../../utils/sso' import { DecodeTokenResult } from './auth_types' diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 1d0c8523a..10d4f6203 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { StatusType } from '../../../entity/user' -import { userRepository } from '../../../repository' import { getUserByEmail } from '../../../services/create_user' import { sendConfirmationEmail } from '../../../services/send_emails' import { comparePassword } from '../../../utils/auth' diff --git a/packages/api/src/routers/auth/mobile/sign_up.ts b/packages/api/src/routers/auth/mobile/sign_up.ts index 7431b7c00..11bf3002d 100644 --- a/packages/api/src/routers/auth/mobile/sign_up.ts +++ b/packages/api/src/routers/auth/mobile/sign_up.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ -import { userRepository } from '../../../repository' +import { userRepository } from '../../../repository/user' import { createUser } from '../../../services/create_user' import { hashPassword } from '../../../utils/auth' import { logger } from '../../../utils/logger' diff --git a/packages/api/src/routers/svc/content.ts b/packages/api/src/routers/svc/content.ts index 28c64905f..d77445c34 100644 --- a/packages/api/src/routers/svc/content.ts +++ b/packages/api/src/routers/svc/content.ts @@ -2,13 +2,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import express from 'express' -import { appDataSource } from '../../data_source' import { getPageByParam, updatePage } from '../../elastic/pages' import { Page } from '../../elastic/types' import { ArticleSavingRequestStatus } from '../../generated/graphql' import { createPubSubClient, readPushSubscription } from '../../pubsub' -import { setClaims } from '../../repository' -import { setFileUploadComplete } from '../../services/save_file' +import { authTrx } from '../../repository' +import { setFileUploadComplete } from '../../services/upload_file' import { logger } from '../../utils/logger' interface UpdateContentMessage { @@ -73,10 +72,9 @@ export function contentServiceRouter() { pageToUpdate.state = ArticleSavingRequestStatus.Succeeded try { - const uploadFileData = await appDataSource.transaction(async (tx) => { - await setClaims(tx, page.userId) - return setFileUploadComplete(fileId, tx) - }) + const uploadFileData = await authTrx(async (t) => + setFileUploadComplete(fileId) + ) logger.info('updated uploadFileData', uploadFileData) } catch (error) { logger.info('error marking file upload as completed', error) diff --git a/packages/api/src/services/groups.ts b/packages/api/src/services/groups.ts index 399af9ff6..01f209e69 100644 --- a/packages/api/src/services/groups.ts +++ b/packages/api/src/services/groups.ts @@ -10,7 +10,7 @@ import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' import { authTrx } from '../repository' import { groupRepository } from '../repository/group' import { userDataToUser } from '../utils/helpers' -import { createLabel, getLabelByName } from './labels' +import { getLabelsAndCreateIfNotExist } from './labels' import { createRule } from './rules' export const createGroup = async (input: { @@ -135,12 +135,11 @@ export const joinGroup = async ( // Check if exceeded max members considering concurrent requests await t.query( - ` -insert into omnivore.group_membership (user_id, group_id, invite_id) -select $1, $2, $3 -from omnivore.group_membership -where group_id = $2 -having count(*) < $4`, + `insert into omnivore.group_membership (user_id, group_id, invite_id) + select $1, $2, $3 + from omnivore.group_membership + where group_id = $2 + having count(*) < $4`, [user.id, invite.group.id, invite.id, invite.maxMembers] ) @@ -231,11 +230,10 @@ export const createLabelAndRuleForGroup = async ( userId: string, groupName: string ) => { - let label = await getLabelByName(userId, groupName) - if (!label) { - // create a new label for the group - label = await createLabel(userId, { name: groupName }) - } + const labels = await getLabelsAndCreateIfNotExist( + [{ name: groupName }], + userId + ) // create a rule to add the label to all pages in the group const addLabelPromise = createRule(userId, { @@ -243,7 +241,7 @@ export const createLabelAndRuleForGroup = async ( actions: [ { type: RuleActionType.AddLabel, - params: [label.id], + params: [labels[0].id], }, ], // always add the label to pages in the group diff --git a/packages/api/src/services/integrations/integration.ts b/packages/api/src/services/integrations/integration.ts index 3c2a44a08..7c9454543 100644 --- a/packages/api/src/services/integrations/integration.ts +++ b/packages/api/src/services/integrations/integration.ts @@ -1,10 +1,10 @@ import { Integration } from '../../entity/integration' -import { ArticleSavingRequestStatus, Page } from '../../elastic/types' +import { LibraryItem, LibraryItemState } from '../../entity/library_item' export interface RetrievedData { url: string labels?: string[] - state?: ArticleSavingRequestStatus + state?: LibraryItemState } export interface RetrievedResult { data: RetrievedData[] @@ -27,7 +27,7 @@ export abstract class IntegrationService { } export = async ( integration: Integration, - pages: Page[] + items: LibraryItem[] ): Promise => { return Promise.resolve(false) } diff --git a/packages/api/src/services/integrations/pocket.ts b/packages/api/src/services/integrations/pocket.ts index c1a25477b..0a57bb9a6 100644 --- a/packages/api/src/services/integrations/pocket.ts +++ b/packages/api/src/services/integrations/pocket.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { ArticleSavingRequestStatus } from '../../elastic/types' +import { LibraryItemState } from '../../entity/library_item' import { env } from '../../env' import { logger } from '../../utils/logger' import { @@ -139,10 +139,10 @@ export class PocketIntegration extends IntegrationService { } const pocketItems = Object.values(pocketData.list) - const statusToState: Record = { - '0': ArticleSavingRequestStatus.Succeeded, - '1': ArticleSavingRequestStatus.Archived, - '2': ArticleSavingRequestStatus.Deleted, + const statusToState: Record = { + '0': LibraryItemState.Succeeded, + '1': LibraryItemState.Archived, + '2': LibraryItemState.Deleted, } const data = pocketItems.map((item) => ({ url: item.given_url, diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index d51aa7554..78e243dd5 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -1,8 +1,9 @@ import axios from 'axios' -import { HighlightType, Page } from '../../elastic/types' -import { getRepository } from '../../repository' +import { HighlightType } from '../../entity/highlight' import { Integration } from '../../entity/integration' +import { LibraryItem } from '../../entity/library_item' import { env } from '../../env' +import { authTrx } from '../../repository' import { wait } from '../../utils/helpers' import { logger } from '../../utils/logger' import { getHighlightUrl } from '../highlights' @@ -59,11 +60,11 @@ export class ReadwiseIntegration extends IntegrationService { } export = async ( integration: Integration, - pages: Page[] + items: LibraryItem[] ): Promise => { let result = true - const highlights = pages.flatMap(this.pageToReadwiseHighlight) + const highlights = items.flatMap(this.pageToReadwiseHighlight) // If there are no highlights, we will skip the sync if (highlights.length > 0) { result = await this.syncWithReadwise(integration.token, highlights) @@ -72,37 +73,41 @@ export class ReadwiseIntegration extends IntegrationService { // update integration syncedAt if successful if (result) { logger.info('updating integration syncedAt') - await getRepository(Integration).update(integration.id, { - syncedAt: new Date(), - }) + await authTrx((t) => + t.getRepository(Integration).update(integration.id, { + syncedAt: new Date(), + }) + ) } return result } - pageToReadwiseHighlight = (page: Page): ReadwiseHighlight[] => { - const { highlights } = page - if (!highlights) return [] - const category = page.siteName === 'Twitter' ? 'tweets' : 'articles' - return highlights + pageToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => { + if (!item.highlights) return [] + const category = item.siteName === 'Twitter' ? 'tweets' : 'articles' + return item.highlights .map((highlight) => { // filter out highlights that are not of type highlight or have no quote - if (highlight.type !== HighlightType.Highlight || !highlight.quote) { + if ( + highlight.highlightType !== HighlightType.Highlight || + !highlight.quote + ) { return undefined } return { text: highlight.quote, - title: page.title, - author: page.author || undefined, - highlight_url: getHighlightUrl(page.slug, highlight.id), + title: item.title, + author: item.author || undefined, + highlight_url: getHighlightUrl(item.slug, highlight.id), highlighted_at: new Date(highlight.createdAt).toISOString(), category, - image_url: page.image || undefined, + image_url: item.thumbnail || undefined, // location: highlight.highlightPositionAnchorIndex || undefined, location_type: 'order', note: highlight.annotation || undefined, source_type: 'omnivore', - source_url: page.url, + source_url: item.originalUrl, } }) .filter((highlight) => highlight !== undefined) as ReadwiseHighlight[] diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 52363398a..0307e0d63 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -7,7 +7,7 @@ import { LibraryItemType, } from '../entity/library_item' import { createPubSubClient, EntityType } from '../pubsub' -import { authTrx, setClaims } from '../repository' +import { authTrx } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { DateFilter, @@ -261,30 +261,30 @@ export const findLibraryItemById = async ( id: string, userId: string ): Promise => { - return authTrx(async (tx) => { - return tx + return authTrx(async (tx) => + tx .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.highlights', 'highlights') .where('library_item.user_id = :userId', { userId }) .andWhere('library_item.id = :id', { id }) .getOne() - }) + ) } export const findLibraryItemByUrl = async ( url: string, userId: string ): Promise => { - return authTrx(async (tx) => { - return tx + return authTrx(async (tx) => + tx .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.highlights', 'highlights') .where('library_item.user_id = :userId', { userId }) .andWhere('library_item.url = :url', { url }) .getOne() - }) + ) } export const updateLibraryItem = async ( @@ -293,9 +293,9 @@ export const updateLibraryItem = async ( userId: string, pubsub = createPubSubClient() ): Promise => { - const updatedLibraryItem = await authTrx(async (tx) => { - return tx.withRepository(libraryItemRepository).save({ id, ...libraryItem }) - }) + const updatedLibraryItem = await authTrx(async (tx) => + tx.withRepository(libraryItemRepository).save({ id, ...libraryItem }) + ) await pubsub.entityUpdated>( EntityType.PAGE, @@ -311,11 +311,9 @@ export const createLibraryItem = async ( userId: string, pubsub = createPubSubClient() ): Promise => { - const newLibraryItem = await authTrx(async (tx) => { - await setClaims(tx, userId) - - return tx.withRepository(libraryItemRepository).save(libraryItem) - }) + const newLibraryItem = await authTrx(async (tx) => + tx.withRepository(libraryItemRepository).save(libraryItem) + ) await pubsub.entityCreated( EntityType.PAGE, @@ -330,12 +328,12 @@ export const findLibraryItemsByPrefix = async ( prefix: string, limit = 5 ): Promise => { - return authTrx(async (tx) => { - return tx + return authTrx(async (tx) => + tx .createQueryBuilder(LibraryItem, 'library_item') .where('library_item.title ILIKE :prefix', { prefix: `${prefix}%` }) .orWhere('library_item.site_name ILIKE :prefix', { prefix: `${prefix}%` }) .limit(limit) .getMany() - }) + ) } diff --git a/packages/api/src/services/newsletters.ts b/packages/api/src/services/newsletters.ts index 3f8b8dd0a..36257e3f9 100644 --- a/packages/api/src/services/newsletters.ts +++ b/packages/api/src/services/newsletters.ts @@ -1,12 +1,12 @@ import { nanoid } from 'nanoid' import { NewsletterEmail } from '../entity/newsletter_email' -import { User } from '../entity/user' import { env } from '../env' import { CreateNewsletterEmailErrorCode, SubscriptionStatus, } from '../generated/graphql' -import { getRepository } from '../repository' +import { authTrx } from '../repository' +import { userRepository } from '../repository/user' import addressparser = require('nodemailer/lib/addressparser') const parsedAddress = (emailAddress: string): string | undefined => { @@ -20,7 +20,7 @@ const parsedAddress = (emailAddress: string): string | undefined => { export const createNewsletterEmail = async ( userId: string ): Promise => { - const user = await getRepository(User).findOne({ + const user = await userRepository.findOne({ where: { id: userId }, relations: ['profile'], }) @@ -32,33 +32,40 @@ export const createNewsletterEmail = async ( // generate a random email address with username prefix const emailAddress = createRandomEmailAddress(user.profile.username, 8) - return getRepository(NewsletterEmail).save({ - address: emailAddress, - user: user, - }) + return authTrx((t) => + t.getRepository(NewsletterEmail).save({ + address: emailAddress, + user: user, + }) + ) } export const getNewsletterEmails = async ( userId: string ): Promise => { - return getRepository(NewsletterEmail) - .createQueryBuilder('newsletter_email') - .leftJoinAndSelect('newsletter_email.user', 'user') - .leftJoinAndSelect( - 'newsletter_email.subscriptions', - 'subscriptions', - 'subscriptions.status = :status', - { - status: SubscriptionStatus.Active, - } - ) - .where('newsletter_email.user_id = :userId', { userId }) - .orderBy('newsletter_email.createdAt', 'DESC') - .getMany() + return authTrx((t) => + t + .getRepository(NewsletterEmail) + .createQueryBuilder('newsletter_email') + .leftJoinAndSelect('newsletter_email.user', 'user') + .leftJoinAndSelect( + 'newsletter_email.subscriptions', + 'subscriptions', + 'subscriptions.status = :status', + { + status: SubscriptionStatus.Active, + } + ) + .where('newsletter_email.user_id = :userId', { userId }) + .orderBy('newsletter_email.createdAt', 'DESC') + .getMany() + ) } export const deleteNewsletterEmail = async (id: string): Promise => { - const result = await getRepository(NewsletterEmail).delete(id) + const result = await authTrx((t) => + t.getRepository(NewsletterEmail).delete(id) + ) return !!result.affected } @@ -68,13 +75,16 @@ export const updateConfirmationCode = async ( confirmationCode: string ): Promise => { const address = parsedAddress(emailAddress) - const result = await getRepository(NewsletterEmail) - .createQueryBuilder() - .where('address ILIKE :address', { address }) - .update({ - confirmationCode: confirmationCode, - }) - .execute() + const result = await authTrx((t) => + t + .getRepository(NewsletterEmail) + .createQueryBuilder() + .where('address ILIKE :address', { address }) + .update({ + confirmationCode: confirmationCode, + }) + .execute() + ) return !!result.affected } @@ -83,11 +93,14 @@ export const getNewsletterEmail = async ( emailAddress: string ): Promise => { const address = parsedAddress(emailAddress) - return getRepository(NewsletterEmail) - .createQueryBuilder('newsletter_email') - .innerJoinAndSelect('newsletter_email.user', 'user') - .where('address ILIKE :address', { address }) - .getOne() + return authTrx((t) => + t + .getRepository(NewsletterEmail) + .createQueryBuilder('newsletter_email') + .innerJoinAndSelect('newsletter_email.user', 'user') + .where('address ILIKE :address', { address }) + .getOne() + ) } const createRandomEmailAddress = (userName: string, length: number): string => { diff --git a/packages/api/src/services/reports.ts b/packages/api/src/services/reports.ts index ad7572191..2a3b2c8f8 100644 --- a/packages/api/src/services/reports.ts +++ b/packages/api/src/services/reports.ts @@ -1,8 +1,7 @@ -import { getPageById } from '../elastic/pages' import { AbuseReport } from '../entity/reports/abuse_report' import { ContentDisplayReport } from '../entity/reports/content_display_report' import { ReportItemInput, ReportType } from '../generated/graphql' -import { getRepository } from '../repository' +import { authTrx } from '../repository' import { logger } from '../utils/logger' import { findLibraryItemById } from './library_item' @@ -10,23 +9,25 @@ export const saveContentDisplayReport = async ( uid: string, input: ReportItemInput ): Promise => { - const page = await findLibraryItemById(input.pageId) - - if (!page) { - logger.info('unable to submit report, page not found', input) + const item = await findLibraryItemById(input.pageId, uid) + if (!item) { + logger.info('unable to submit report, item not found', input) return false } // We capture the article content and original html now, in case it // reparsed or updated later, this gives us a view of exactly // what the user saw. - const result = await repo.save({ - user: { id: uid }, - content: page.content, - originalHtml: page.originalHtml || undefined, - originalUrl: page.url, - reportComment: input.reportComment, - }) + const result = await authTrx((tx) => + tx.getRepository(ContentDisplayReport).save({ + user: { id: uid }, + content: item.readableContent, + originalHtml: item.originalContent || undefined, + originalUrl: item.originalUrl, + reportComment: input.reportComment, + libraryItemId: item.id, + }) + ) return !!result } @@ -35,12 +36,9 @@ export const saveAbuseReport = async ( uid: string, input: ReportItemInput ): Promise => { - const repo = getRepository(AbuseReport) - - const page = await getPageById(input.pageId) - - if (!page) { - logger.info('unable to submit report, page not found', input) + const item = await findLibraryItemById(input.pageId, uid) + if (!item) { + logger.info('unable to submit report, item not found', input) return false } @@ -52,14 +50,16 @@ export const saveAbuseReport = async ( // We capture the article content and original html now, in case it // reparsed or updated later, this gives us a view of exactly // what the user saw. - const result = await repo.save({ - reportedBy: uid, - sharedBy: input.sharedBy, - elasticPageId: input.pageId, - itemUrl: input.itemUrl, - reportTypes: [ReportType.Abusive], - reportComment: input.reportComment, - }) + const result = await authTrx((tx) => + tx.getRepository(AbuseReport).save({ + reportedBy: uid, + sharedBy: input.sharedBy || undefined, + itemUrl: input.itemUrl, + reportTypes: [ReportType.Abusive], + reportComment: input.reportComment, + libraryItemId: item.id, + }) + ) return !!result } diff --git a/packages/api/src/services/save_email.ts b/packages/api/src/services/save_email.ts index 02ccdf12c..448db8a11 100644 --- a/packages/api/src/services/save_email.ts +++ b/packages/api/src/services/save_email.ts @@ -3,7 +3,9 @@ import { LibraryItemState, LibraryItemType, } from '../entity/library_item' -import { entityManager, libraryItemRepository } from '../repository' +import { authTrx } from '../repository' +import { getInternalLabelWithColor } from '../repository/label' +import { libraryItemRepository } from '../repository/library_item' import { enqueueThumbnailTask } from '../utils/createTask' import { cleanUrl, @@ -20,7 +22,6 @@ import { parsePreparedContent, parseUrlMetadata, } from '../utils/parser' -import { getInternalLabelWithColor } from './labels' import { createLibraryItem } from './library_item' import { updateReceivedEmail } from './received_emails' @@ -66,11 +67,12 @@ export const saveEmail = async ( siteIcon = await fetchFavicon(url) } - const existingLibraryItem = await libraryItemRepository.findOneBy({ - user: { id: input.userId }, - originalUrl: cleanedUrl, - state: LibraryItemState.Succeeded, - }) + const existingLibraryItem = await authTrx((t) => + t.withRepository(libraryItemRepository).findOneBy({ + originalUrl: cleanedUrl, + state: LibraryItemState.Succeeded, + }) + ) if (existingLibraryItem) { const updatedLibraryItem = await libraryItemRepository.save({ ...existingLibraryItem, @@ -84,55 +86,50 @@ export const saveEmail = async ( const newsletterLabel = getInternalLabelWithColor('newsletter') // start a transaction to create the library item and update the received email - const newLibraryItem = await entityManager.transaction(async (tx) => { - const newLibraryItem = await createLibraryItem( - { + const newLibraryItem = await createLibraryItem( + { + user: { id: input.userId }, + slug, + readableContent: content, + originalContent: input.originalContent, + description: metadata?.description || parseResult.parsedContent?.excerpt, + title: input.title, + author: input.author, + originalUrl: cleanedUrl, + itemType: parseResult.pageType as unknown as LibraryItemType, + textContentHash: stringToHash(content), + thumbnail: + metadata?.previewImage || + parseResult.parsedContent?.previewImage || + undefined, + publishedAt: validatedDate( + parseResult.parsedContent?.publishedDate ?? undefined + ), + subscription: { + name: input.author, + unsubscribeMailTo: input.unsubMailTo, + unsubscribeHttpUrl: input.unsubHttpUrl, user: { id: input.userId }, - slug, - readableContent: content, - originalContent: input.originalContent, - description: - metadata?.description || parseResult.parsedContent?.excerpt, - title: input.title, - author: input.author, - originalUrl: cleanedUrl, - itemType: parseResult.pageType as unknown as LibraryItemType, - textContentHash: stringToHash(content), - thumbnail: - metadata?.previewImage || - parseResult.parsedContent?.previewImage || - undefined, - publishedAt: validatedDate( - parseResult.parsedContent?.publishedDate ?? undefined - ), - subscription: { - name: input.author, - unsubscribeMailTo: input.unsubMailTo, - unsubscribeHttpUrl: input.unsubHttpUrl, - user: { id: input.userId }, - newsletterEmail: { id: input.newsletterEmailId }, - icon: siteIcon, - lastFetchedAt: new Date(), - }, - state: LibraryItemState.Succeeded, - siteIcon, - siteName: parseResult.parsedContent?.siteName ?? undefined, - wordCount: wordsCount(content), - labels: [ - { - ...newsletterLabel, - internal: true, - user: { id: input.userId }, - }, - ], + newsletterEmail: { id: input.newsletterEmailId }, + icon: siteIcon, + lastFetchedAt: new Date(), }, - tx - ) + state: LibraryItemState.Succeeded, + siteIcon, + siteName: parseResult.parsedContent?.siteName ?? undefined, + wordCount: wordsCount(content), + labels: [ + { + ...newsletterLabel, + internal: true, + user: { id: input.userId }, + }, + ], + }, + input.userId + ) - await updateReceivedEmail(input.receivedEmailId, 'article', tx) - - return newLibraryItem - }) + await updateReceivedEmail(input.receivedEmailId, 'article') // create a task to update thumbnail and pre-cache all images try { diff --git a/packages/api/src/services/save_file.ts b/packages/api/src/services/save_file.ts index 57a5132f9..f1b33637d 100644 --- a/packages/api/src/services/save_file.ts +++ b/packages/api/src/services/save_file.ts @@ -1,4 +1,3 @@ -import { UploadFile } from '../entity/upload_file' import { User } from '../entity/user' import { homePageURL } from '../env' import { @@ -7,24 +6,19 @@ import { SaveFileInput, SaveResult, } from '../generated/graphql' -import { entityManager, getRepository } from '../repository' -import { WithDataSourcesContext } from '../resolvers/types' import { logger } from '../utils/logger' import { getStorageFileDetails } from '../utils/uploads' import { getLabelsAndCreateIfNotExist } from './labels' -import { setFileUploadComplete } from './upload_file' +import { updateLibraryItem } from './library_item' +import { findUploadFileById, setFileUploadComplete } from './upload_file' export const saveFile = async ( - ctx: WithDataSourcesContext, - user: User, - input: SaveFileInput + input: SaveFileInput, + user: User ): Promise => { logger.info('saving file with input', input) const pageId = input.clientRequestId - const uploadFile = await getRepository(UploadFile).findOneBy({ - id: input.uploadFileId, - user: { id: ctx.uid }, - }) + const uploadFile = await findUploadFileById(input.uploadFileId) if (!uploadFile) { return { errorCodes: [SaveErrorCode.Unauthorized], @@ -49,13 +43,13 @@ export const saveFile = async ( ? await getLabelsAndCreateIfNotExist(input.labels, user.id) : undefined if (input.state || input.labels) { - const updated = await updatePage( + const updated = await updateLibraryItem( pageId, { archivedAt, labels, }, - ctx + user.id ) if (!updated) { logger.info('error updating page', pageId) diff --git a/packages/api/src/services/save_page.ts b/packages/api/src/services/save_page.ts index 16918855a..20531d378 100644 --- a/packages/api/src/services/save_page.ts +++ b/packages/api/src/services/save_page.ts @@ -16,8 +16,7 @@ import { SavePageInput, SaveResult, } from '../generated/graphql' -import { libraryItemRepository } from '../repository' -import { WithDataSourcesContext } from '../resolvers/types' +import { authTrx } from '../repository' import { enqueueThumbnailTask } from '../utils/createTask' import { cleanUrl, @@ -30,8 +29,9 @@ import { import { logger } from '../utils/logger' import { parsePreparedContent } from '../utils/parser' import { createPageSaveRequest } from './create_page_save_request' +import { saveHighlight } from './highlights' import { getLabelsAndCreateIfNotExist } from './labels' -import { createLibraryItem } from './library_item' +import { createLibraryItem, updateLibraryItem } from './library_item' // where we can use APIs to fetch their underlying content. const FORCE_PUPPETEER_URLS = [ @@ -60,9 +60,8 @@ const shouldParseInBackend = (input: SavePageInput): boolean => { } export const savePage = async ( - ctx: WithDataSourcesContext, - user: User, - input: SavePageInput + input: SavePageInput, + user: User ): Promise => { const parseResult = await parsePreparedContent( input.url, @@ -77,31 +76,23 @@ export const savePage = async ( ) const [newSlug, croppedPathname] = createSlug(input.url, input.title) let slug = newSlug - let pageId = input.clientRequestId + let clientRequestId = input.clientRequestId + const itemToSave = parsedContentToLibraryItem({ url: input.url, title: input.title, userId: user.id, - pageId, + itemId: clientRequestId, slug, croppedPathname, parsedContent: parseResult.parsedContent, itemType: parseResult.pageType as unknown as LibraryItemType, originalHtml: parseResult.domContent, canonicalUrl: parseResult.canonicalUrl, - rssFeedUrl: input.rssFeedUrl, saveTime: input.savedAt ? new Date(input.savedAt) : undefined, publishedAt: input.publishedAt ? new Date(input.publishedAt) : undefined, + state: input.state || undefined, }) - - // save state - const archivedAt = - input.state === ArticleSavingRequestStatus.Archived ? new Date() : null - // add labels to page - const labels = input.labels - ? await getLabelsAndCreateIfNotExist(ctx, input.labels) - : undefined - const isImported = input.source === 'csv-importer' // always parse in backend if the url is in the force puppeteer list @@ -110,10 +101,9 @@ export const savePage = async ( await createPageSaveRequest({ userId: user.id, url: itemToSave.originalUrl, - pubsub: ctx.pubsub, - articleSavingRequestId: input.clientRequestId, - archivedAt, - labels, + articleSavingRequestId: clientRequestId, + state: input.state || undefined, + labels: input.labels || undefined, }) } catch (e) { return { @@ -122,11 +112,21 @@ export const savePage = async ( } } } else { + // save state + itemToSave.archivedAt = + input.state === ArticleSavingRequestStatus.Archived ? new Date() : null + // add labels to page + itemToSave.labels = input.labels + ? await getLabelsAndCreateIfNotExist(input.labels, user.id) + : undefined + // check if the page already exists - const existingLibraryItem = await libraryItemRepository.findOne({ - where: { user: { id: user.id }, originalUrl: itemToSave.originalUrl }, - relations: ['subscriptions'], - }) + const existingLibraryItem = await authTrx((t) => + t.getRepository(LibraryItem).findOne({ + where: { user: { id: user.id }, originalUrl: itemToSave.originalUrl }, + relations: ['subscriptions'], + }) + ) if (existingLibraryItem) { // we don't want to update an rss feed page if rss-feeder is tring to re-save it if ( @@ -134,14 +134,14 @@ export const savePage = async ( existingLibraryItem.subscription.url === input.rssFeedUrl ) { return { - clientRequestId: pageId, + clientRequestId, url: `${homePageURL()}/${user.profile.username}/${slug}`, } } - pageId = existingLibraryItem.id + clientRequestId = existingLibraryItem.id slug = existingLibraryItem.slug - if (!(await libraryItemRepository.save(itemToSave))) { + if (!(await updateLibraryItem(clientRequestId, itemToSave, user.id))) { return { errorCodes: [SaveErrorCode.Unknown], message: 'Failed to update existing page', @@ -149,14 +149,8 @@ export const savePage = async ( } } else { // do not publish a pubsub event if the page is imported - const newPageId = await createLibraryItem(itemToSave) - if (!newPageId) { - return { - errorCodes: [SaveErrorCode.Unknown], - message: 'Failed to create new page', - } - } - pageId = newPageId + const newItem = await createLibraryItem(itemToSave, user.id) + clientRequestId = newItem.id } } @@ -175,13 +169,12 @@ export const savePage = async ( const highlight = { updatedAt: new Date(), createdAt: new Date(), - userId: ctx.uid, - elasticPageId: pageId, + userId: user.id, ...parseResult.highlightData, type: HighlightType.Highlight, } - if (!(await addHighlightToPage(pageId, highlight, ctx))) { + if (!(await saveHighlight(highlight, user.id))) { return { errorCodes: [SaveErrorCode.EmbeddedHighlightFailed], message: 'Failed to save highlight', @@ -190,7 +183,7 @@ export const savePage = async ( } return { - clientRequestId: pageId, + clientRequestId, url: `${homePageURL()}/${user.profile.username}/${slug}`, } } @@ -200,7 +193,7 @@ export const parsedContentToLibraryItem = ({ url, userId, originalHtml, - pageId, + itemId, parsedContent, slug, croppedPathname, @@ -211,8 +204,8 @@ export const parsedContentToLibraryItem = ({ uploadFileHash, uploadFileId, saveTime, - rssFeedUrl, publishedAt, + state, }: { url: string userId: string @@ -221,18 +214,18 @@ export const parsedContentToLibraryItem = ({ itemType: LibraryItemType parsedContent: Readability.ParseResult | null originalHtml?: string | null - pageId?: string | null + itemId?: string | null title?: string | null preparedDocument?: PreparedDocumentInput | null canonicalUrl?: string | null uploadFileHash?: string | null uploadFileId?: string | null saveTime?: Date - rssFeedUrl?: string | null publishedAt?: Date | null + state?: ArticleSavingRequestStatus | null }): DeepPartial & { originalUrl: string } => { return { - id: pageId ?? undefined, + id: itemId || undefined, slug, user: { id: userId }, originalContent: originalHtml, @@ -257,7 +250,9 @@ export const parsedContentToLibraryItem = ({ uploadFile: { id: uploadFileId ?? undefined }, readingProgressTopPercent: 0, readingProgressHighestReadAnchor: 0, - state: LibraryItemState.Succeeded, + state: state + ? (state as unknown as LibraryItemState) + : LibraryItemState.Succeeded, createdAt: validatedDate(saveTime), savedAt: validatedDate(saveTime), siteName: parsedContent?.siteName, diff --git a/packages/api/src/services/save_url.ts b/packages/api/src/services/save_url.ts index 0ef42d120..419c726de 100644 --- a/packages/api/src/services/save_url.ts +++ b/packages/api/src/services/save_url.ts @@ -1,12 +1,10 @@ -import { ArticleSavingRequestStatus } from '../elastic/types' import { User } from '../entity/user' import { homePageURL } from '../env' import { SaveErrorCode, SaveResult, SaveUrlInput } from '../generated/graphql' import { PubsubClient } from '../pubsub' -import { getRepository } from '../repository' +import { userRepository } from '../repository/user' import { logger } from '../utils/logger' import { createPageSaveRequest } from './create_page_save_request' -import { getLabelsAndCreateIfNotExist } from './labels' interface SaveContext { pubsub: PubsubClient @@ -19,22 +17,13 @@ export const saveUrl = async ( input: SaveUrlInput ): Promise => { try { - // save state - const archivedAt = - input.state === ArticleSavingRequestStatus.Archived ? new Date() : null - // add labels to page - const labels = input.labels - ? await getLabelsAndCreateIfNotExist(ctx, input.labels) - : undefined - const pageSaveRequest = await createPageSaveRequest({ ...input, userId: ctx.uid, pubsub: ctx.pubsub, articleSavingRequestId: input.clientRequestId, - archivedAt, - labels, - user, + state: input.state || undefined, + labels: input.labels || undefined, locale: input.locale || undefined, timezone: input.timezone || undefined, savedAt: input.savedAt ? new Date(input.savedAt) : undefined, @@ -61,7 +50,7 @@ export const saveUrlFromEmail = async ( url: string, clientRequestId: string ): Promise => { - const user = await getRepository(User).findOneBy({ + const user = await userRepository.findOneBy({ id: ctx.uid, }) if (!user) { diff --git a/packages/api/src/services/upload_file.ts b/packages/api/src/services/upload_file.ts index 8c7f39fb7..865fb8022 100644 --- a/packages/api/src/services/upload_file.ts +++ b/packages/api/src/services/upload_file.ts @@ -2,7 +2,7 @@ import { UploadFile } from '../entity/upload_file' import { authTrx } from '../repository' export const findUploadFileById = async (id: string) => { - return authTrx(async (tx) => tx.getRepository(UploadFile).findBy({ id })) + return authTrx(async (tx) => tx.getRepository(UploadFile).findOneBy({ id })) } export const setFileUploadComplete = async (id: string) => {