This commit is contained in:
Hongbo Wu
2023-09-04 15:03:04 +08:00
parent f8f3ec0c56
commit c1ad9b6f41
34 changed files with 451 additions and 567 deletions

View File

@ -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<ApiKeysSuccess, ApiKeysError>(
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

View File

@ -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<SearchSuccess, SearchError, QuerySearchArgs>(
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) => ({

View File

@ -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],

View File

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

View File

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

View File

@ -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],

View File

@ -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) => ({

View File

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

View File

@ -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,
}

View File

@ -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<GroupsSuccess, GroupsError>(
})
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) {

View File

@ -14,7 +14,6 @@ import {
SetRuleErrorCode,
SetRuleSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import { authorized } from '../../utils/helpers'
export const setRuleResolver = authorized<

View File

@ -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)
})

View File

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

View File

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

View File

@ -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] }

View File

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

View File

@ -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<WebhooksSuccess, WebhooksError>(
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<Webhook> = {
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<boolean> => {
return Promise.resolve(false)
}

View File

@ -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<string, ArticleSavingRequestStatus> = {
'0': ArticleSavingRequestStatus.Succeeded,
'1': ArticleSavingRequestStatus.Archived,
'2': ArticleSavingRequestStatus.Deleted,
const statusToState: Record<string, LibraryItemState> = {
'0': LibraryItemState.Succeeded,
'1': LibraryItemState.Archived,
'2': LibraryItemState.Deleted,
}
const data = pocketItems.map((item) => ({
url: item.given_url,

View File

@ -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<boolean> => {
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[]

View File

@ -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<LibraryItem | null> => {
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<LibraryItem | null> => {
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<LibraryItem> => {
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<DeepPartial<LibraryItem>>(
EntityType.PAGE,
@ -311,11 +311,9 @@ export const createLibraryItem = async (
userId: string,
pubsub = createPubSubClient()
): Promise<LibraryItem> => {
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<LibraryItem>(
EntityType.PAGE,
@ -330,12 +328,12 @@ export const findLibraryItemsByPrefix = async (
prefix: string,
limit = 5
): Promise<LibraryItem[]> => {
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()
})
)
}

View File

@ -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<NewsletterEmail> => {
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<NewsletterEmail[]> => {
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<boolean> => {
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<boolean> => {
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<NewsletterEmail | null> => {
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 => {

View File

@ -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<boolean> => {
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<boolean> => {
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
}

View File

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

View File

@ -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<SaveResult> => {
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)

View File

@ -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<SaveResult> => {
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<LibraryItem> & { 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,

View File

@ -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<SaveResult> => {
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<boolean> => {
const user = await getRepository(User).findOneBy({
const user = await userRepository.findOneBy({
id: ctx.uid,
})
if (!user) {

View File

@ -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) => {