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 { env } from '../../env'
import { import {
ApiKeysError, ApiKeysError,
@ -12,7 +13,6 @@ import {
RevokeApiKeyErrorCode, RevokeApiKeyErrorCode,
RevokeApiKeySuccess, RevokeApiKeySuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { apiKeyRepository } from '../../repository/api_key'
import { analytics } from '../../utils/analytics' import { analytics } from '../../utils/analytics'
import { generateApiKey, hashApiKey } from '../../utils/auth' import { generateApiKey, hashApiKey } from '../../utils/auth'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
@ -21,7 +21,7 @@ export const apiKeysResolver = authorized<ApiKeysSuccess, ApiKeysError>(
async (_, __, { log, authTrx }) => { async (_, __, { log, authTrx }) => {
try { try {
const apiKeys = await authTrx(async (tx) => { const apiKeys = await authTrx(async (tx) => {
return tx.withRepository(apiKeyRepository).find({ return tx.getRepository(ApiKey).find({
select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'], select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'],
order: { order: {
usedAt: { direction: 'DESC', nulls: 'last' }, usedAt: { direction: 'DESC', nulls: 'last' },
@ -52,7 +52,7 @@ export const generateApiKeyResolver = authorized<
const exp = new Date(expiresAt) const exp = new Date(expiresAt)
const originalKey = generateApiKey() const originalKey = generateApiKey()
const apiKeyCreated = await authTrx(async (tx) => { const apiKeyCreated = await authTrx(async (tx) => {
return tx.withRepository(apiKeyRepository).save({ return tx.getRepository(ApiKey).save({
user: { id: uid }, user: { id: uid },
name, name,
key: hashApiKey(originalKey), key: hashApiKey(originalKey),
@ -90,7 +90,7 @@ export const revokeApiKeyResolver = authorized<
>(async (_, { id }, { claims: { uid }, log, authTrx }) => { >(async (_, { id }, { claims: { uid }, log, authTrx }) => {
try { try {
const deletedApiKey = await authTrx(async (tx) => { const deletedApiKey = await authTrx(async (tx) => {
const apiRepo = tx.withRepository(apiKeyRepository) const apiRepo = tx.getRepository(ApiKey)
const apiKey = await apiRepo.findOneBy({ id }) const apiKey = await apiRepo.findOneBy({ id })
if (!apiKey) { if (!apiKey) {
return null return null

View File

@ -143,13 +143,13 @@ export const createArticleResolver = authorized<
CreateArticleError, CreateArticleError,
MutationCreateArticleArgs MutationCreateArticleArgs
>( >(
;async ( async (
_, _,
{ {
input: { input: {
url, url,
preparedDocument, preparedDocument,
articleSavingRequestId: pageId, articleSavingRequestId,
uploadFileId, uploadFileId,
skipParsing, skipParsing,
source, source,
@ -176,7 +176,7 @@ export const createArticleResolver = authorized<
errorCodes: [CreateArticleErrorCode.Unauthorized], errorCodes: [CreateArticleErrorCode.Unauthorized],
}, },
uid, uid,
pageId, articleSavingRequestId,
pubsub pubsub
) )
} }
@ -189,7 +189,7 @@ export const createArticleResolver = authorized<
errorCodes: [CreateArticleErrorCode.NotAllowedToParse], errorCodes: [CreateArticleErrorCode.NotAllowedToParse],
}, },
uid, uid,
pageId, articleSavingRequestId,
pubsub pubsub
) )
} }
@ -237,12 +237,12 @@ export const createArticleResolver = authorized<
/* We do not trust the values from client, lookup upload file by querying /* 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. * 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) { if (!uploadFile) {
return pageError( return pageError(
{ errorCodes: [CreateArticleErrorCode.UploadFileMissing] }, { errorCodes: [CreateArticleErrorCode.UploadFileMissing] },
uid, uid,
pageId, articleSavingRequestId,
pubsub pubsub
) )
} }
@ -295,7 +295,7 @@ export const createArticleResolver = authorized<
title, title,
parsedContent, parsedContent,
userId: uid, userId: uid,
pageId, itemId: articleSavingRequestId,
slug, slug,
croppedPathname, croppedPathname,
originalHtml: domContent, originalHtml: domContent,
@ -320,14 +320,14 @@ export const createArticleResolver = authorized<
}) })
if (uploadFileId) { if (uploadFileId) {
const uploadFileData = await setFileUploadComplete(uploadFileId, uid) const uploadFileData = await setFileUploadComplete(uploadFileId)
if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) { if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) {
return pageError( return pageError(
{ {
errorCodes: [CreateArticleErrorCode.UploadFileMissing], errorCodes: [CreateArticleErrorCode.UploadFileMissing],
}, },
uid, uid,
pageId, articleSavingRequestId,
pubsub pubsub
) )
} }
@ -350,11 +350,11 @@ export const createArticleResolver = authorized<
libraryItemToSave.originalUrl, libraryItemToSave.originalUrl,
uid uid
) )
pageId = existingLibraryItem?.id || pageId articleSavingRequestId = existingLibraryItem?.id || articleSavingRequestId
if (pageId) { if (articleSavingRequestId) {
// update existing page's state from processing to succeeded // update existing page's state from processing to succeeded
libraryItemToReturn = await updateLibraryItem( libraryItemToReturn = await updateLibraryItem(
pageId, articleSavingRequestId,
libraryItemToSave, libraryItemToSave,
uid, uid,
pubsub pubsub
@ -388,7 +388,7 @@ export const createArticleResolver = authorized<
errorCodes: [CreateArticleErrorCode.ElasticError], errorCodes: [CreateArticleErrorCode.ElasticError],
}, },
uid, uid,
pageId, articleSavingRequestId,
pubsub pubsub
) )
} }
@ -654,92 +654,88 @@ export const saveArticleReadingProgressResolver = authorized<
} }
) )
export const searchResolver = authorized<SearchSuccess, SearchError, QuerySearchArgs>( export const searchResolver = authorized<
async (_obj, params, { uid, log }) => { SearchSuccess,
const startCursor = params.after || '' SearchError,
const first = params.first || 10 QuerySearchArgs
>(async (_obj, params, { uid, log }) => {
const startCursor = params.after || ''
const first = params.first || 10
// the query size is limited to 255 characters // the query size is limited to 255 characters
if (params.query && params.query.length > 255) { if (params.query && params.query.length > 255) {
return { errorCodes: [SearchErrorCode.QueryTooLong] } 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)
} }
if (params.includeContent && libraryItem.readableContent) {
const searchQuery = parseSearchQuery(params.query || undefined) // convert html to the requested format
const format = params.format || ArticleFormat.Html
const { libraryItems, count } = await searchLibraryItems( try {
{ const converter = contentConverter(format)
from: Number(startCursor), if (converter) {
size: first + 1, // fetch one more item to get next cursor libraryItem.readableContent = converter(
sort: searchQuery.sort, libraryItem.readableContent,
includePending: true, libraryItem.highlights
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)
} }
} catch (error) {
log.error('Error converting content', error)
} }
}
return {
node: libraryItemToSearchItem(libraryItem),
cursor: endCursor,
}
})
return { return {
edges, node: libraryItemToSearchItem(libraryItem),
pageInfo: { cursor: endCursor,
hasPreviousPage: false,
startCursor,
hasNextPage: hasNextPage,
endCursor,
totalCount: count,
},
} }
})
return {
edges,
pageInfo: {
hasPreviousPage: false,
startCursor,
hasNextPage: hasNextPage,
endCursor,
totalCount: count,
},
} }
) })
export const typeaheadSearchResolver = authorized< export const typeaheadSearchResolver = authorized<
TypeaheadSearchSuccess, TypeaheadSearchSuccess,
TypeaheadSearchError, TypeaheadSearchError,
QueryTypeaheadSearchArgs QueryTypeaheadSearchArgs
>(async (_obj, { query, first }, { uid, log }) => { >(async (_obj, { query, first }, { log }) => {
try { try {
const items = await findLibraryItemsByPrefix(query, uid, first || undefined) const items = await findLibraryItemsByPrefix(query, first || undefined)
return { return {
items: items.map((item) => ({ items: items.map((item) => ({

View File

@ -33,15 +33,6 @@ export const saveFilterResolver = authorized<
SaveFilterError, SaveFilterError,
MutationSaveFilterArgs MutationSaveFilterArgs
>(async (_, { input }, { authTrx, uid, log }) => { >(async (_, { input }, { authTrx, uid, log }) => {
log.info('Saving filters', {
input,
labels: {
source: 'resolver',
resolver: 'saveFilterResolver',
uid,
},
})
try { try {
const filter = await authTrx(async (t) => { const filter = await authTrx(async (t) => {
return t.withRepository(filterRepository).save({ return t.withRepository(filterRepository).save({
@ -72,27 +63,24 @@ export const deleteFilterResolver = authorized<
DeleteFilterSuccess, DeleteFilterSuccess,
DeleteFilterError, DeleteFilterError,
MutationDeleteFilterArgs MutationDeleteFilterArgs
>(async (_, { id }, { authTrx, uid, log }) => { >(async (_, { id }, { authTrx, log }) => {
log.info('Deleting filters', {
id,
labels: {
source: 'resolver',
resolver: 'deleteFilterResolver',
uid: claims.uid,
},
})
try { try {
const filter = await authTrx(async (t) => { 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 { return {
filter, filter,
} }
} catch (error) { } catch (error) {
log.error('Error deleting filters', log.error('Error deleting filters', error)
error
)
return { return {
errorCodes: [DeleteFilterErrorCode.BadRequest], errorCodes: [DeleteFilterErrorCode.BadRequest],

View File

@ -375,10 +375,7 @@ export const functionResolvers = {
ctx.claims && ctx.claims &&
article.uploadFileId article.uploadFileId
) { ) {
const upload = await findUploadFileById( const upload = await findUploadFileById(article.uploadFileId)
article.uploadFileId,
ctx.claims.uid
)
if (!upload || !upload.fileName) { if (!upload || !upload.fileName) {
return undefined return undefined
} }
@ -486,7 +483,7 @@ export const functionResolvers = {
ctx.uid && ctx.uid &&
item.uploadFileId item.uploadFileId
) { ) {
const upload = await findUploadFileById(item.uploadFileId, ctx.uid) const upload = await findUploadFileById(item.uploadFileId)
if (!upload || !upload.fileName) { if (!upload || !upload.fileName) {
return undefined return undefined
} }

View File

@ -177,9 +177,9 @@ export const deleteHighlightResolver = authorized<
DeleteHighlightSuccess, DeleteHighlightSuccess,
DeleteHighlightError, DeleteHighlightError,
MutationDeleteHighlightArgs MutationDeleteHighlightArgs
>(async (_, { highlightId }, { uid, log }) => { >(async (_, { highlightId }, { log }) => {
try { try {
const deletedHighlight = await deleteHighlightById(highlightId, uid) const deletedHighlight = await deleteHighlightById(highlightId)
if (!deletedHighlight) { if (!deletedHighlight) {
return { return {

View File

@ -1,6 +1,5 @@
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { User } from '../../entity/user'
import { env } from '../../env' import { env } from '../../env'
import { import {
MutationUploadImportFileArgs, MutationUploadImportFileArgs,
@ -8,7 +7,7 @@ import {
UploadImportFileErrorCode, UploadImportFileErrorCode,
UploadImportFileSuccess, UploadImportFileSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository' import { userRepository } from '../../repository/user'
import { analytics } from '../../utils/analytics' import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
@ -35,15 +34,13 @@ export const uploadImportFileResolver = authorized<
UploadImportFileError, UploadImportFileError,
MutationUploadImportFileArgs MutationUploadImportFileArgs
>(async (_, { type, contentType }, { claims: { uid }, log }) => { >(async (_, { type, contentType }, { claims: { uid }, log }) => {
log.info('uploadImportFileResolver')
if (!VALID_CONTENT_TYPES.includes(contentType)) { if (!VALID_CONTENT_TYPES.includes(contentType)) {
return { return {
errorCodes: [UploadImportFileErrorCode.BadRequest], errorCodes: [UploadImportFileErrorCode.BadRequest],
} }
} }
const user = await getRepository(User).findOneBy({ id: uid }) const user = await userRepository.findOneBy({ id: uid })
if (!user) { if (!user) {
return { return {
errorCodes: [UploadImportFileErrorCode.Unauthorized], errorCodes: [UploadImportFileErrorCode.Unauthorized],

View File

@ -13,7 +13,6 @@ import {
NewsletterEmailsErrorCode, NewsletterEmailsErrorCode,
NewsletterEmailsSuccess, NewsletterEmailsSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository'
import { import {
createNewsletterEmail, createNewsletterEmail,
deleteNewsletterEmail, deleteNewsletterEmail,
@ -57,20 +56,10 @@ export const createNewsletterEmailResolver = authorized<
export const newsletterEmailsResolver = authorized< export const newsletterEmailsResolver = authorized<
NewsletterEmailsSuccess, NewsletterEmailsSuccess,
NewsletterEmailsError NewsletterEmailsError
>(async (_parent, _args, { claims, log }) => { >(async (_parent, _args, { uid, log }) => {
log.info('newsletterEmailsResolver')
try { try {
const user = await getRepository(User).findOneBy({ const newsletterEmails = await getNewsletterEmails(uid)
id: claims.uid,
})
if (!user) {
return Promise.reject({
errorCode: NewsletterEmailsErrorCode.Unauthorized,
})
}
const newsletterEmails = await getNewsletterEmails(user.id)
return { return {
newsletterEmails: newsletterEmails.map((newsletterEmail) => ({ newsletterEmails: newsletterEmails.map((newsletterEmail) => ({

View File

@ -11,7 +11,6 @@ import {
RecentEmailsErrorCode, RecentEmailsErrorCode,
RecentEmailsSuccess, RecentEmailsSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository'
import { updateReceivedEmail } from '../../services/received_emails' import { updateReceivedEmail } from '../../services/received_emails'
import { saveNewsletter } from '../../services/save_newsletter_email' import { saveNewsletter } from '../../services/save_newsletter_email'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'

View File

@ -1,25 +1,15 @@
import { User } from '../../entity/user'
import { env } from '../../env'
import { import {
RecentSearchesError, RecentSearchesError,
RecentSearchesErrorCode,
RecentSearchesSuccess, RecentSearchesSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository'
import { getRecentSearches } from '../../services/search_history' import { getRecentSearches } from '../../services/search_history'
import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
export const recentSearchesResolver = authorized< export const recentSearchesResolver = authorized<
RecentSearchesSuccess, RecentSearchesSuccess,
RecentSearchesError RecentSearchesError
>(async (_obj, _params, { claims: { uid }, log }) => { >(async (_obj, _params) => {
const user = await getRepository(User).findOneBy({ id: uid }) const searches = await getRecentSearches()
if (!user) {
return { errorCodes: [RecentSearchesErrorCode.Unauthorized] }
}
const searches = await getRecentSearches(uid)
return { return {
searches, searches,
} }

View File

@ -28,7 +28,7 @@ import {
RecommendHighlightsSuccess, RecommendHighlightsSuccess,
RecommendSuccess, RecommendSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository' import { userRepository } from '../../repository/user'
import { import {
createGroup, createGroup,
createLabelAndRuleForGroup, createLabelAndRuleForGroup,
@ -57,7 +57,7 @@ export const createGroupResolver = authorized<
}) })
try { try {
const userData = await getRepository(User).findOne({ const userData = await userRepository.findOne({
where: { id: uid }, where: { id: uid },
relations: ['profile'], relations: ['profile'],
}) })
@ -132,7 +132,7 @@ export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
}) })
try { try {
const user = await getRepository(User).findOneBy({ const user = await userRepository.findOneBy({
id: uid, id: uid,
}) })
if (!user) { if (!user) {
@ -178,7 +178,7 @@ export const recommendResolver = authorized<
}) })
try { try {
const user = await getRepository(User).findOne({ const user = await userRepository.findOne({
where: { id: uid }, where: { id: uid },
relations: ['profile'], relations: ['profile'],
}) })
@ -272,7 +272,7 @@ export const joinGroupResolver = authorized<
}) })
try { try {
const user = await getRepository(User).findOne({ const user = await userRepository.findOne({
where: { id: uid }, where: { id: uid },
relations: ['profile'], relations: ['profile'],
}) })
@ -329,7 +329,7 @@ export const recommendHighlightsResolver = authorized<
}) })
try { try {
const user = await getRepository(User).findOne({ const user = await userRepository.findOne({
where: { id: uid }, where: { id: uid },
relations: ['profile'], relations: ['profile'],
}) })
@ -421,7 +421,7 @@ export const leaveGroupResolver = authorized<
}) })
try { try {
const user = await getRepository(User).findOneBy({ const user = await userRepository.findOneBy({
id: uid, id: uid,
}) })
if (!user) { if (!user) {

View File

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

View File

@ -37,7 +37,7 @@ export const savePageResolver = authorized<
return { errorCodes: [SaveErrorCode.Unauthorized] } return { errorCodes: [SaveErrorCode.Unauthorized] }
} }
return savePage(ctx, user, input) return savePage(input, user)
}) })
export const saveUrlResolver = authorized< export const saveUrlResolver = authorized<
@ -93,5 +93,5 @@ export const saveFileResolver = authorized<
return { errorCodes: [SaveErrorCode.Unauthorized] } 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 { env } from '../../env'
import { import {
SendInstallInstructionsError, SendInstallInstructionsError,
SendInstallInstructionsErrorCode, SendInstallInstructionsErrorCode,
SendInstallInstructionsSuccess, SendInstallInstructionsSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { userRepository } from '../../repository/user'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
import { sendEmail } from '../../utils/sendEmail' import { sendEmail } from '../../utils/sendEmail'
@ -17,7 +16,7 @@ export const sendInstallInstructionsResolver = authorized<
SendInstallInstructionsError SendInstallInstructionsError
>(async (_parent, _args, { claims, log }) => { >(async (_parent, _args, { claims, log }) => {
try { try {
const user = await appDataSource.getRepository(User).findOneBy({ const user = await userRepository.findOneBy({
id: claims.uid, id: claims.uid,
}) })

View File

@ -26,8 +26,7 @@ import {
UpdateSubscriptionErrorCode, UpdateSubscriptionErrorCode,
UpdateSubscriptionSuccess, UpdateSubscriptionSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository' import { unsubscribe } from '../../services/subscriptions'
import { getSubscribeHandler, unsubscribe } from '../../services/subscriptions'
import { Merge } from '../../util' import { Merge } from '../../util'
import { analytics } from '../../utils/analytics' import { analytics } from '../../utils/analytics'
import { enqueueRssFeedFetch } from '../../utils/createTask' import { enqueueRssFeedFetch } from '../../utils/createTask'

View File

@ -1,20 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import normalizeUrl from 'normalize-url' import normalizeUrl from 'normalize-url'
import path from 'path' import path from 'path'
import { createPage, getPageByParam, updatePage } from '../../elastic/pages' import { LibraryItemState, LibraryItemType } from '../../entity/library_item'
import { LibraryItemType } from '../../entity/library_item'
import { UploadFile } from '../../entity/upload_file' import { UploadFile } from '../../entity/upload_file'
import { env } from '../../env' import { env } from '../../env'
import { import {
ArticleSavingRequestStatus,
MutationUploadFileRequestArgs, MutationUploadFileRequestArgs,
UploadFileRequestError, UploadFileRequestError,
UploadFileRequestErrorCode, UploadFileRequestErrorCode,
UploadFileRequestSuccess, UploadFileRequestSuccess,
UploadFileStatus, UploadFileStatus,
} from '../../generated/graphql' } from '../../generated/graphql'
import { uploadFileRepository } from '../../repository/upload_file'
import { validateUrl } from '../../services/create_page_save_request' import { validateUrl } from '../../services/create_page_save_request'
import {
createLibraryItem,
findLibraryItemByUrl,
updateLibraryItem,
} from '../../services/library_item'
import { analytics } from '../../utils/analytics' import { analytics } from '../../utils/analytics'
import { authorized, generateSlug } from '../../utils/helpers' import { authorized, generateSlug } from '../../utils/helpers'
import { import {
@ -87,13 +89,15 @@ export const uploadFileRequestResolver = authorized<
return { errorCodes: [UploadFileRequestErrorCode.BadInput] } return { errorCodes: [UploadFileRequestErrorCode.BadInput] }
} }
uploadFileData = await uploadFileRepository.save({ uploadFileData = await authTrx((t) =>
url: input.url, t.getRepository(UploadFile).save({
userId: uid, url: input.url,
fileName, userId: uid,
status: UploadFileStatus.Initialized, fileName,
contentType: input.contentType, status: UploadFileStatus.Initialized,
}) contentType: input.contentType,
})
)
if (uploadFileData.id) { if (uploadFileData.id) {
const uploadFileId = 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 (input.createPageEntry) {
// If we have a file:// URL, don't try to match it // If we have a file:// URL, don't try to match it
// and create a copy of the page, just create a // and create a copy of the page, just create a
// new item. // new item.
const page = isFileUrl(input.url) const item = isFileUrl(input.url)
? await getPageByParam({ ? await findLibraryItemByUrl(input.url, uid)
userId: uid,
url: input.url,
})
: undefined : undefined
if (page) { if (item) {
if ( if (
!(await updatePage( !(await updateLibraryItem(
page.id, item.id,
{ {
savedAt: new Date(), savedAt: new Date(),
archivedAt: null, archivedAt: null,
}, },
ctx uid
)) ))
) { ) {
return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] } return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }
} }
createdPageId = page.id createdItemId = item.id
} else { } else {
const pageId = await createPage( const item = await createLibraryItem(
{ {
url: isFileUrl(input.url) ? publicUrl : input.url, originalUrl: isFileUrl(input.url) ? publicUrl : input.url,
id: input.clientRequestId || '', id: input.clientRequestId || undefined,
userId: uid, user: { id: uid },
title: title, title,
hash: uploadFilePathName, readableContent: '',
content: '', itemType: itemTypeForContentType(input.contentType),
pageType: itemTypeForContentType(input.contentType), uploadFile: { id: uploadFileData.id },
uploadFileId: uploadFileData.id,
slug: generateSlug(uploadFilePathName), slug: generateSlug(uploadFilePathName),
createdAt: new Date(), state: LibraryItemState.Succeeded,
savedAt: new Date(),
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
state: ArticleSavingRequestStatus.Succeeded,
}, },
ctx uid
) )
if (!pageId) { createdItemId = item.id
return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }
}
createdPageId = pageId
} }
} }
return { return {
id: uploadFileData.id, id: uploadFileData.id,
uploadSignedUrl, uploadSignedUrl,
createdPageId: createdPageId, createdPageId: createdItemId,
} }
} else { } else {
return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] } return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }

View File

@ -1,4 +1,3 @@
import { appDataSource } from '../../data_source'
import { UserPersonalization } from '../../entity/user_personalization' import { UserPersonalization } from '../../entity/user_personalization'
import { import {
GetUserPersonalizationError, GetUserPersonalizationError,
@ -9,20 +8,17 @@ import {
SetUserPersonalizationSuccess, SetUserPersonalizationSuccess,
SortOrder, SortOrder,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository, setClaims } from '../../repository'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
export const setUserPersonalizationResolver = authorized< export const setUserPersonalizationResolver = authorized<
SetUserPersonalizationSuccess, SetUserPersonalizationSuccess,
SetUserPersonalizationError, SetUserPersonalizationError,
MutationSetUserPersonalizationArgs MutationSetUserPersonalizationArgs
>(async (_, { input }, { claims: { uid }, log }) => { >(async (_, { input }, { authTrx, claims: { uid }, log }) => {
log.info('setUserPersonalizationResolver', { uid, input }) log.info('setUserPersonalizationResolver', { uid, input })
const result = await appDataSource.transaction(async (entityManager) => { const result = await authTrx(async (t) => {
await setClaims(entityManager, uid) return t.getRepository(UserPersonalization).upsert(
return entityManager.getRepository(UserPersonalization).upsert(
{ {
user: { id: uid }, user: { id: uid },
...input, ...input,
@ -37,9 +33,11 @@ export const setUserPersonalizationResolver = authorized<
} }
} }
const updatedUserPersonalization = await getRepository( const updatedUserPersonalization = await authTrx((t) =>
UserPersonalization t
).findOneBy({ id: result.identifiers[0].id as string }) .getRepository(UserPersonalization)
.findOneBy({ id: result.identifiers[0].id as string })
)
// Cast SortOrder from string to enum // Cast SortOrder from string to enum
const librarySortOrder = updatedUserPersonalization?.librarySortOrder as const librarySortOrder = updatedUserPersonalization?.librarySortOrder as
@ -58,12 +56,12 @@ export const setUserPersonalizationResolver = authorized<
export const getUserPersonalizationResolver = authorized< export const getUserPersonalizationResolver = authorized<
GetUserPersonalizationResult, GetUserPersonalizationResult,
GetUserPersonalizationError GetUserPersonalizationError
>(async (_parent, _args, { uid }) => { >(async (_parent, _args, { authTrx, uid }) => {
const userPersonalization = await getRepository( const userPersonalization = await authTrx((t) =>
UserPersonalization t.getRepository(UserPersonalization).findOneBy({
).findOneBy({ user: { id: uid },
user: { id: uid }, })
}) )
// Cast SortOrder from string to enum // Cast SortOrder from string to enum
const librarySortOrder = userPersonalization?.librarySortOrder as const librarySortOrder = userPersonalization?.librarySortOrder as

View File

@ -1,4 +1,3 @@
import { User } from '../../entity/user'
import { Webhook } from '../../entity/webhook' import { Webhook } from '../../entity/webhook'
import { env } from '../../env' import { env } from '../../env'
import { import {
@ -20,25 +19,18 @@ import {
WebhooksSuccess, WebhooksSuccess,
WebhookSuccess, WebhookSuccess,
} from '../../generated/graphql' } from '../../generated/graphql'
import { getRepository } from '../../repository' import { authTrx } from '../../repository'
import { analytics } from '../../utils/analytics' import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers' import { authorized } from '../../utils/helpers'
export const webhooksResolver = authorized<WebhooksSuccess, WebhooksError>( export const webhooksResolver = authorized<WebhooksSuccess, WebhooksError>(
async (_obj, _params, { claims: { uid }, log }) => { async (_obj, _params, { uid, log }) => {
log.info('webhooksResolver')
try { try {
const user = await getRepository(User).findOneBy({ id: uid }) const webhooks = await authTrx((t) =>
if (!user) { t.getRepository(Webhook).findBy({
return { user: { id: uid },
errorCodes: [WebhooksErrorCode.Unauthorized], })
} )
}
const webhooks = await getRepository(Webhook).findBy({
user: { id: uid },
})
return { return {
webhooks: webhooks.map((webhook) => webhookDataToResponse(webhook)), webhooks: webhooks.map((webhook) => webhookDataToResponse(webhook)),
@ -57,21 +49,14 @@ export const webhookResolver = authorized<
WebhookSuccess, WebhookSuccess,
WebhookError, WebhookError,
QueryWebhookArgs QueryWebhookArgs
>(async (_, { id }, { claims: { uid }, log }) => { >(async (_, { id }, { authTrx, log }) => {
log.info('webhookResolver')
try { try {
const user = await getRepository(User).findOneBy({ id: uid }) const webhook = await authTrx((t) =>
if (!user) { t.getRepository(Webhook).findOne({
return { where: { id },
errorCodes: [WebhookErrorCode.Unauthorized], relations: ['user'],
} })
} )
const webhook = await getRepository(Webhook).findOne({
where: { id },
relations: ['user'],
})
if (!webhook) { if (!webhook) {
return { return {
@ -79,12 +64,6 @@ export const webhookResolver = authorized<
} }
} }
if (webhook.user.id !== uid) {
return {
errorCodes: [WebhookErrorCode.Unauthorized],
}
}
return { return {
webhook: webhookDataToResponse(webhook), webhook: webhookDataToResponse(webhook),
} }
@ -101,42 +80,26 @@ export const deleteWebhookResolver = authorized<
DeleteWebhookSuccess, DeleteWebhookSuccess,
DeleteWebhookError, DeleteWebhookError,
MutationDeleteWebhookArgs MutationDeleteWebhookArgs
>(async (_, { id }, { claims: { uid }, log }) => { >(async (_, { id }, { authTrx, uid, log }) => {
log.info('deleteWebhookResolver')
try { try {
const user = await getRepository(User).findOneBy({ id: uid }) const deletedWebhook = await authTrx(async (t) => {
if (!user) { const webhook = await t.getRepository(Webhook).findOne({
return { where: { id },
errorCodes: [DeleteWebhookErrorCode.Unauthorized], relations: ['user'],
} })
}
const webhook = await getRepository(Webhook).findOne({ if (!webhook) {
where: { id }, throw new Error('Webhook not found')
relations: ['user'], }
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({ analytics.track({
userId: uid, userId: uid,
event: 'webhook_delete', event: 'webhook_delete',
properties: { properties: {
webhookId: webhook.id, webhookId: id,
env: env.server.apiEnv, env: env.server.apiEnv,
}, },
}) })
@ -145,8 +108,7 @@ export const deleteWebhookResolver = authorized<
webhook: webhookDataToResponse(deletedWebhook), webhook: webhookDataToResponse(deletedWebhook),
} }
} catch (error) { } catch (error) {
log.error(error) log.error('Error deleting webhook', error)
return { return {
errorCodes: [DeleteWebhookErrorCode.BadRequest], errorCodes: [DeleteWebhookErrorCode.BadRequest],
} }
@ -157,17 +119,10 @@ export const setWebhookResolver = authorized<
SetWebhookSuccess, SetWebhookSuccess,
SetWebhookError, SetWebhookError,
MutationSetWebhookArgs MutationSetWebhookArgs
>(async (_, { input }, { claims: { uid }, log }) => { >(async (_, { input }, { authTrx, claims: { uid }, log }) => {
log.info('setWebhookResolver') log.info('setWebhookResolver')
try { try {
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [SetWebhookErrorCode.Unauthorized],
}
}
const webhookToSave: Partial<Webhook> = { const webhookToSave: Partial<Webhook> = {
url: input.url, url: input.url,
eventTypes: input.eventTypes as string[], eventTypes: input.eventTypes as string[],
@ -178,27 +133,26 @@ export const setWebhookResolver = authorized<
if (input.id) { if (input.id) {
// Update // Update
const existingWebhook = await getRepository(Webhook).findOne({ const existingWebhook = await authTrx((t) =>
where: { id: input.id }, t.getRepository(Webhook).findOne({
relations: ['user'], where: { id: input.id || '' },
}) relations: ['user'],
})
)
if (!existingWebhook) { if (!existingWebhook) {
return { return {
errorCodes: [SetWebhookErrorCode.NotFound], errorCodes: [SetWebhookErrorCode.NotFound],
} }
} }
if (existingWebhook.user.id !== uid) {
return {
errorCodes: [SetWebhookErrorCode.Unauthorized],
}
}
webhookToSave.id = input.id webhookToSave.id = input.id
} }
const webhook = await getRepository(Webhook).save({ const webhook = await authTrx((t) =>
user, t.getRepository(Webhook).save({
...webhookToSave, user: { id: uid },
}) ...webhookToSave,
})
)
analytics.track({ analytics.track({
userId: uid, userId: uid,

View File

@ -5,7 +5,7 @@ import * as jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa' import jwksClient from 'jwks-rsa'
import { env, homePageURL } from '../../env' import { env, homePageURL } from '../../env'
import { LoginErrorCode } from '../../generated/graphql' import { LoginErrorCode } from '../../generated/graphql'
import { userRepository } from '../../repository' import { userRepository } from '../../repository/user'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
import { createSsoToken, ssoRedirectURL } from '../../utils/sso' import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
import { DecodeTokenResult } from './auth_types' import { DecodeTokenResult } from './auth_types'

View File

@ -3,7 +3,7 @@ import { OAuth2Client } from 'googleapis-common'
import url from 'url' import url from 'url'
import { env, homePageURL } from '../../env' import { env, homePageURL } from '../../env'
import { LoginErrorCode } from '../../generated/graphql' import { LoginErrorCode } from '../../generated/graphql'
import { userRepository } from '../../repository' import { userRepository } from '../../repository/user'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
import { createSsoToken, ssoRedirectURL } from '../../utils/sso' import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
import { DecodeTokenResult } from './auth_types' import { DecodeTokenResult } from './auth_types'

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
import { StatusType } from '../../../entity/user' import { StatusType } from '../../../entity/user'
import { userRepository } from '../../../repository'
import { getUserByEmail } from '../../../services/create_user' import { getUserByEmail } from '../../../services/create_user'
import { sendConfirmationEmail } from '../../../services/send_emails' import { sendConfirmationEmail } from '../../../services/send_emails'
import { comparePassword } from '../../../utils/auth' import { comparePassword } from '../../../utils/auth'

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
import { userRepository } from '../../../repository' import { userRepository } from '../../../repository/user'
import { createUser } from '../../../services/create_user' import { createUser } from '../../../services/create_user'
import { hashPassword } from '../../../utils/auth' import { hashPassword } from '../../../utils/auth'
import { logger } from '../../../utils/logger' import { logger } from '../../../utils/logger'

View File

@ -2,13 +2,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import express from 'express' import express from 'express'
import { appDataSource } from '../../data_source'
import { getPageByParam, updatePage } from '../../elastic/pages' import { getPageByParam, updatePage } from '../../elastic/pages'
import { Page } from '../../elastic/types' import { Page } from '../../elastic/types'
import { ArticleSavingRequestStatus } from '../../generated/graphql' import { ArticleSavingRequestStatus } from '../../generated/graphql'
import { createPubSubClient, readPushSubscription } from '../../pubsub' import { createPubSubClient, readPushSubscription } from '../../pubsub'
import { setClaims } from '../../repository' import { authTrx } from '../../repository'
import { setFileUploadComplete } from '../../services/save_file' import { setFileUploadComplete } from '../../services/upload_file'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
interface UpdateContentMessage { interface UpdateContentMessage {
@ -73,10 +72,9 @@ export function contentServiceRouter() {
pageToUpdate.state = ArticleSavingRequestStatus.Succeeded pageToUpdate.state = ArticleSavingRequestStatus.Succeeded
try { try {
const uploadFileData = await appDataSource.transaction(async (tx) => { const uploadFileData = await authTrx(async (t) =>
await setClaims(tx, page.userId) setFileUploadComplete(fileId)
return setFileUploadComplete(fileId, tx) )
})
logger.info('updated uploadFileData', uploadFileData) logger.info('updated uploadFileData', uploadFileData)
} catch (error) { } catch (error) {
logger.info('error marking file upload as completed', 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 { authTrx } from '../repository'
import { groupRepository } from '../repository/group' import { groupRepository } from '../repository/group'
import { userDataToUser } from '../utils/helpers' import { userDataToUser } from '../utils/helpers'
import { createLabel, getLabelByName } from './labels' import { getLabelsAndCreateIfNotExist } from './labels'
import { createRule } from './rules' import { createRule } from './rules'
export const createGroup = async (input: { export const createGroup = async (input: {
@ -135,12 +135,11 @@ export const joinGroup = async (
// Check if exceeded max members considering concurrent requests // Check if exceeded max members considering concurrent requests
await t.query( await t.query(
` `insert into omnivore.group_membership (user_id, group_id, invite_id)
insert into omnivore.group_membership (user_id, group_id, invite_id) select $1, $2, $3
select $1, $2, $3 from omnivore.group_membership
from omnivore.group_membership where group_id = $2
where group_id = $2 having count(*) < $4`,
having count(*) < $4`,
[user.id, invite.group.id, invite.id, invite.maxMembers] [user.id, invite.group.id, invite.id, invite.maxMembers]
) )
@ -231,11 +230,10 @@ export const createLabelAndRuleForGroup = async (
userId: string, userId: string,
groupName: string groupName: string
) => { ) => {
let label = await getLabelByName(userId, groupName) const labels = await getLabelsAndCreateIfNotExist(
if (!label) { [{ name: groupName }],
// create a new label for the group userId
label = await createLabel(userId, { name: groupName }) )
}
// create a rule to add the label to all pages in the group // create a rule to add the label to all pages in the group
const addLabelPromise = createRule(userId, { const addLabelPromise = createRule(userId, {
@ -243,7 +241,7 @@ export const createLabelAndRuleForGroup = async (
actions: [ actions: [
{ {
type: RuleActionType.AddLabel, type: RuleActionType.AddLabel,
params: [label.id], params: [labels[0].id],
}, },
], ],
// always add the label to pages in the group // always add the label to pages in the group

View File

@ -1,10 +1,10 @@
import { Integration } from '../../entity/integration' import { Integration } from '../../entity/integration'
import { ArticleSavingRequestStatus, Page } from '../../elastic/types' import { LibraryItem, LibraryItemState } from '../../entity/library_item'
export interface RetrievedData { export interface RetrievedData {
url: string url: string
labels?: string[] labels?: string[]
state?: ArticleSavingRequestStatus state?: LibraryItemState
} }
export interface RetrievedResult { export interface RetrievedResult {
data: RetrievedData[] data: RetrievedData[]
@ -27,7 +27,7 @@ export abstract class IntegrationService {
} }
export = async ( export = async (
integration: Integration, integration: Integration,
pages: Page[] items: LibraryItem[]
): Promise<boolean> => { ): Promise<boolean> => {
return Promise.resolve(false) return Promise.resolve(false)
} }

View File

@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { ArticleSavingRequestStatus } from '../../elastic/types' import { LibraryItemState } from '../../entity/library_item'
import { env } from '../../env' import { env } from '../../env'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
import { import {
@ -139,10 +139,10 @@ export class PocketIntegration extends IntegrationService {
} }
const pocketItems = Object.values(pocketData.list) const pocketItems = Object.values(pocketData.list)
const statusToState: Record<string, ArticleSavingRequestStatus> = { const statusToState: Record<string, LibraryItemState> = {
'0': ArticleSavingRequestStatus.Succeeded, '0': LibraryItemState.Succeeded,
'1': ArticleSavingRequestStatus.Archived, '1': LibraryItemState.Archived,
'2': ArticleSavingRequestStatus.Deleted, '2': LibraryItemState.Deleted,
} }
const data = pocketItems.map((item) => ({ const data = pocketItems.map((item) => ({
url: item.given_url, url: item.given_url,

View File

@ -1,8 +1,9 @@
import axios from 'axios' import axios from 'axios'
import { HighlightType, Page } from '../../elastic/types' import { HighlightType } from '../../entity/highlight'
import { getRepository } from '../../repository'
import { Integration } from '../../entity/integration' import { Integration } from '../../entity/integration'
import { LibraryItem } from '../../entity/library_item'
import { env } from '../../env' import { env } from '../../env'
import { authTrx } from '../../repository'
import { wait } from '../../utils/helpers' import { wait } from '../../utils/helpers'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
import { getHighlightUrl } from '../highlights' import { getHighlightUrl } from '../highlights'
@ -59,11 +60,11 @@ export class ReadwiseIntegration extends IntegrationService {
} }
export = async ( export = async (
integration: Integration, integration: Integration,
pages: Page[] items: LibraryItem[]
): Promise<boolean> => { ): Promise<boolean> => {
let result = true 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 there are no highlights, we will skip the sync
if (highlights.length > 0) { if (highlights.length > 0) {
result = await this.syncWithReadwise(integration.token, highlights) result = await this.syncWithReadwise(integration.token, highlights)
@ -72,37 +73,41 @@ export class ReadwiseIntegration extends IntegrationService {
// update integration syncedAt if successful // update integration syncedAt if successful
if (result) { if (result) {
logger.info('updating integration syncedAt') logger.info('updating integration syncedAt')
await getRepository(Integration).update(integration.id, { await authTrx((t) =>
syncedAt: new Date(), t.getRepository(Integration).update(integration.id, {
}) syncedAt: new Date(),
})
)
} }
return result return result
} }
pageToReadwiseHighlight = (page: Page): ReadwiseHighlight[] => { pageToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => {
const { highlights } = page if (!item.highlights) return []
if (!highlights) return [] const category = item.siteName === 'Twitter' ? 'tweets' : 'articles'
const category = page.siteName === 'Twitter' ? 'tweets' : 'articles' return item.highlights
return highlights
.map((highlight) => { .map((highlight) => {
// filter out highlights that are not of type highlight or have no quote // 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 undefined
} }
return { return {
text: highlight.quote, text: highlight.quote,
title: page.title, title: item.title,
author: page.author || undefined, author: item.author || undefined,
highlight_url: getHighlightUrl(page.slug, highlight.id), highlight_url: getHighlightUrl(item.slug, highlight.id),
highlighted_at: new Date(highlight.createdAt).toISOString(), highlighted_at: new Date(highlight.createdAt).toISOString(),
category, category,
image_url: page.image || undefined, image_url: item.thumbnail || undefined,
// location: highlight.highlightPositionAnchorIndex || undefined, // location: highlight.highlightPositionAnchorIndex || undefined,
location_type: 'order', location_type: 'order',
note: highlight.annotation || undefined, note: highlight.annotation || undefined,
source_type: 'omnivore', source_type: 'omnivore',
source_url: page.url, source_url: item.originalUrl,
} }
}) })
.filter((highlight) => highlight !== undefined) as ReadwiseHighlight[] .filter((highlight) => highlight !== undefined) as ReadwiseHighlight[]

View File

@ -7,7 +7,7 @@ import {
LibraryItemType, LibraryItemType,
} from '../entity/library_item' } from '../entity/library_item'
import { createPubSubClient, EntityType } from '../pubsub' import { createPubSubClient, EntityType } from '../pubsub'
import { authTrx, setClaims } from '../repository' import { authTrx } from '../repository'
import { libraryItemRepository } from '../repository/library_item' import { libraryItemRepository } from '../repository/library_item'
import { import {
DateFilter, DateFilter,
@ -261,30 +261,30 @@ export const findLibraryItemById = async (
id: string, id: string,
userId: string userId: string
): Promise<LibraryItem | null> => { ): Promise<LibraryItem | null> => {
return authTrx(async (tx) => { return authTrx(async (tx) =>
return tx tx
.createQueryBuilder(LibraryItem, 'library_item') .createQueryBuilder(LibraryItem, 'library_item')
.leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights') .leftJoinAndSelect('library_item.highlights', 'highlights')
.where('library_item.user_id = :userId', { userId }) .where('library_item.user_id = :userId', { userId })
.andWhere('library_item.id = :id', { id }) .andWhere('library_item.id = :id', { id })
.getOne() .getOne()
}) )
} }
export const findLibraryItemByUrl = async ( export const findLibraryItemByUrl = async (
url: string, url: string,
userId: string userId: string
): Promise<LibraryItem | null> => { ): Promise<LibraryItem | null> => {
return authTrx(async (tx) => { return authTrx(async (tx) =>
return tx tx
.createQueryBuilder(LibraryItem, 'library_item') .createQueryBuilder(LibraryItem, 'library_item')
.leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights') .leftJoinAndSelect('library_item.highlights', 'highlights')
.where('library_item.user_id = :userId', { userId }) .where('library_item.user_id = :userId', { userId })
.andWhere('library_item.url = :url', { url }) .andWhere('library_item.url = :url', { url })
.getOne() .getOne()
}) )
} }
export const updateLibraryItem = async ( export const updateLibraryItem = async (
@ -293,9 +293,9 @@ export const updateLibraryItem = async (
userId: string, userId: string,
pubsub = createPubSubClient() pubsub = createPubSubClient()
): Promise<LibraryItem> => { ): Promise<LibraryItem> => {
const updatedLibraryItem = await authTrx(async (tx) => { const updatedLibraryItem = await authTrx(async (tx) =>
return tx.withRepository(libraryItemRepository).save({ id, ...libraryItem }) tx.withRepository(libraryItemRepository).save({ id, ...libraryItem })
}) )
await pubsub.entityUpdated<DeepPartial<LibraryItem>>( await pubsub.entityUpdated<DeepPartial<LibraryItem>>(
EntityType.PAGE, EntityType.PAGE,
@ -311,11 +311,9 @@ export const createLibraryItem = async (
userId: string, userId: string,
pubsub = createPubSubClient() pubsub = createPubSubClient()
): Promise<LibraryItem> => { ): Promise<LibraryItem> => {
const newLibraryItem = await authTrx(async (tx) => { const newLibraryItem = await authTrx(async (tx) =>
await setClaims(tx, userId) tx.withRepository(libraryItemRepository).save(libraryItem)
)
return tx.withRepository(libraryItemRepository).save(libraryItem)
})
await pubsub.entityCreated<LibraryItem>( await pubsub.entityCreated<LibraryItem>(
EntityType.PAGE, EntityType.PAGE,
@ -330,12 +328,12 @@ export const findLibraryItemsByPrefix = async (
prefix: string, prefix: string,
limit = 5 limit = 5
): Promise<LibraryItem[]> => { ): Promise<LibraryItem[]> => {
return authTrx(async (tx) => { return authTrx(async (tx) =>
return tx tx
.createQueryBuilder(LibraryItem, 'library_item') .createQueryBuilder(LibraryItem, 'library_item')
.where('library_item.title ILIKE :prefix', { prefix: `${prefix}%` }) .where('library_item.title ILIKE :prefix', { prefix: `${prefix}%` })
.orWhere('library_item.site_name ILIKE :prefix', { prefix: `${prefix}%` }) .orWhere('library_item.site_name ILIKE :prefix', { prefix: `${prefix}%` })
.limit(limit) .limit(limit)
.getMany() .getMany()
}) )
} }

View File

@ -1,12 +1,12 @@
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { NewsletterEmail } from '../entity/newsletter_email' import { NewsletterEmail } from '../entity/newsletter_email'
import { User } from '../entity/user'
import { env } from '../env' import { env } from '../env'
import { import {
CreateNewsletterEmailErrorCode, CreateNewsletterEmailErrorCode,
SubscriptionStatus, SubscriptionStatus,
} from '../generated/graphql' } from '../generated/graphql'
import { getRepository } from '../repository' import { authTrx } from '../repository'
import { userRepository } from '../repository/user'
import addressparser = require('nodemailer/lib/addressparser') import addressparser = require('nodemailer/lib/addressparser')
const parsedAddress = (emailAddress: string): string | undefined => { const parsedAddress = (emailAddress: string): string | undefined => {
@ -20,7 +20,7 @@ const parsedAddress = (emailAddress: string): string | undefined => {
export const createNewsletterEmail = async ( export const createNewsletterEmail = async (
userId: string userId: string
): Promise<NewsletterEmail> => { ): Promise<NewsletterEmail> => {
const user = await getRepository(User).findOne({ const user = await userRepository.findOne({
where: { id: userId }, where: { id: userId },
relations: ['profile'], relations: ['profile'],
}) })
@ -32,33 +32,40 @@ export const createNewsletterEmail = async (
// generate a random email address with username prefix // generate a random email address with username prefix
const emailAddress = createRandomEmailAddress(user.profile.username, 8) const emailAddress = createRandomEmailAddress(user.profile.username, 8)
return getRepository(NewsletterEmail).save({ return authTrx((t) =>
address: emailAddress, t.getRepository(NewsletterEmail).save({
user: user, address: emailAddress,
}) user: user,
})
)
} }
export const getNewsletterEmails = async ( export const getNewsletterEmails = async (
userId: string userId: string
): Promise<NewsletterEmail[]> => { ): Promise<NewsletterEmail[]> => {
return getRepository(NewsletterEmail) return authTrx((t) =>
.createQueryBuilder('newsletter_email') t
.leftJoinAndSelect('newsletter_email.user', 'user') .getRepository(NewsletterEmail)
.leftJoinAndSelect( .createQueryBuilder('newsletter_email')
'newsletter_email.subscriptions', .leftJoinAndSelect('newsletter_email.user', 'user')
'subscriptions', .leftJoinAndSelect(
'subscriptions.status = :status', 'newsletter_email.subscriptions',
{ 'subscriptions',
status: SubscriptionStatus.Active, 'subscriptions.status = :status',
} {
) status: SubscriptionStatus.Active,
.where('newsletter_email.user_id = :userId', { userId }) }
.orderBy('newsletter_email.createdAt', 'DESC') )
.getMany() .where('newsletter_email.user_id = :userId', { userId })
.orderBy('newsletter_email.createdAt', 'DESC')
.getMany()
)
} }
export const deleteNewsletterEmail = async (id: string): Promise<boolean> => { 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 return !!result.affected
} }
@ -68,13 +75,16 @@ export const updateConfirmationCode = async (
confirmationCode: string confirmationCode: string
): Promise<boolean> => { ): Promise<boolean> => {
const address = parsedAddress(emailAddress) const address = parsedAddress(emailAddress)
const result = await getRepository(NewsletterEmail) const result = await authTrx((t) =>
.createQueryBuilder() t
.where('address ILIKE :address', { address }) .getRepository(NewsletterEmail)
.update({ .createQueryBuilder()
confirmationCode: confirmationCode, .where('address ILIKE :address', { address })
}) .update({
.execute() confirmationCode: confirmationCode,
})
.execute()
)
return !!result.affected return !!result.affected
} }
@ -83,11 +93,14 @@ export const getNewsletterEmail = async (
emailAddress: string emailAddress: string
): Promise<NewsletterEmail | null> => { ): Promise<NewsletterEmail | null> => {
const address = parsedAddress(emailAddress) const address = parsedAddress(emailAddress)
return getRepository(NewsletterEmail) return authTrx((t) =>
.createQueryBuilder('newsletter_email') t
.innerJoinAndSelect('newsletter_email.user', 'user') .getRepository(NewsletterEmail)
.where('address ILIKE :address', { address }) .createQueryBuilder('newsletter_email')
.getOne() .innerJoinAndSelect('newsletter_email.user', 'user')
.where('address ILIKE :address', { address })
.getOne()
)
} }
const createRandomEmailAddress = (userName: string, length: number): string => { 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 { AbuseReport } from '../entity/reports/abuse_report'
import { ContentDisplayReport } from '../entity/reports/content_display_report' import { ContentDisplayReport } from '../entity/reports/content_display_report'
import { ReportItemInput, ReportType } from '../generated/graphql' import { ReportItemInput, ReportType } from '../generated/graphql'
import { getRepository } from '../repository' import { authTrx } from '../repository'
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
import { findLibraryItemById } from './library_item' import { findLibraryItemById } from './library_item'
@ -10,23 +9,25 @@ export const saveContentDisplayReport = async (
uid: string, uid: string,
input: ReportItemInput input: ReportItemInput
): Promise<boolean> => { ): Promise<boolean> => {
const page = await findLibraryItemById(input.pageId) const item = await findLibraryItemById(input.pageId, uid)
if (!item) {
if (!page) { logger.info('unable to submit report, item not found', input)
logger.info('unable to submit report, page not found', input)
return false return false
} }
// We capture the article content and original html now, in case it // We capture the article content and original html now, in case it
// reparsed or updated later, this gives us a view of exactly // reparsed or updated later, this gives us a view of exactly
// what the user saw. // what the user saw.
const result = await repo.save({ const result = await authTrx((tx) =>
user: { id: uid }, tx.getRepository(ContentDisplayReport).save({
content: page.content, user: { id: uid },
originalHtml: page.originalHtml || undefined, content: item.readableContent,
originalUrl: page.url, originalHtml: item.originalContent || undefined,
reportComment: input.reportComment, originalUrl: item.originalUrl,
}) reportComment: input.reportComment,
libraryItemId: item.id,
})
)
return !!result return !!result
} }
@ -35,12 +36,9 @@ export const saveAbuseReport = async (
uid: string, uid: string,
input: ReportItemInput input: ReportItemInput
): Promise<boolean> => { ): Promise<boolean> => {
const repo = getRepository(AbuseReport) const item = await findLibraryItemById(input.pageId, uid)
if (!item) {
const page = await getPageById(input.pageId) logger.info('unable to submit report, item not found', input)
if (!page) {
logger.info('unable to submit report, page not found', input)
return false return false
} }
@ -52,14 +50,16 @@ export const saveAbuseReport = async (
// We capture the article content and original html now, in case it // We capture the article content and original html now, in case it
// reparsed or updated later, this gives us a view of exactly // reparsed or updated later, this gives us a view of exactly
// what the user saw. // what the user saw.
const result = await repo.save({ const result = await authTrx((tx) =>
reportedBy: uid, tx.getRepository(AbuseReport).save({
sharedBy: input.sharedBy, reportedBy: uid,
elasticPageId: input.pageId, sharedBy: input.sharedBy || undefined,
itemUrl: input.itemUrl, itemUrl: input.itemUrl,
reportTypes: [ReportType.Abusive], reportTypes: [ReportType.Abusive],
reportComment: input.reportComment, reportComment: input.reportComment,
}) libraryItemId: item.id,
})
)
return !!result return !!result
} }

View File

@ -3,7 +3,9 @@ import {
LibraryItemState, LibraryItemState,
LibraryItemType, LibraryItemType,
} from '../entity/library_item' } 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 { enqueueThumbnailTask } from '../utils/createTask'
import { import {
cleanUrl, cleanUrl,
@ -20,7 +22,6 @@ import {
parsePreparedContent, parsePreparedContent,
parseUrlMetadata, parseUrlMetadata,
} from '../utils/parser' } from '../utils/parser'
import { getInternalLabelWithColor } from './labels'
import { createLibraryItem } from './library_item' import { createLibraryItem } from './library_item'
import { updateReceivedEmail } from './received_emails' import { updateReceivedEmail } from './received_emails'
@ -66,11 +67,12 @@ export const saveEmail = async (
siteIcon = await fetchFavicon(url) siteIcon = await fetchFavicon(url)
} }
const existingLibraryItem = await libraryItemRepository.findOneBy({ const existingLibraryItem = await authTrx((t) =>
user: { id: input.userId }, t.withRepository(libraryItemRepository).findOneBy({
originalUrl: cleanedUrl, originalUrl: cleanedUrl,
state: LibraryItemState.Succeeded, state: LibraryItemState.Succeeded,
}) })
)
if (existingLibraryItem) { if (existingLibraryItem) {
const updatedLibraryItem = await libraryItemRepository.save({ const updatedLibraryItem = await libraryItemRepository.save({
...existingLibraryItem, ...existingLibraryItem,
@ -84,55 +86,50 @@ export const saveEmail = async (
const newsletterLabel = getInternalLabelWithColor('newsletter') const newsletterLabel = getInternalLabelWithColor('newsletter')
// start a transaction to create the library item and update the received email // 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 }, user: { id: input.userId },
slug, newsletterEmail: { id: input.newsletterEmailId },
readableContent: content, icon: siteIcon,
originalContent: input.originalContent, lastFetchedAt: new Date(),
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 },
},
],
}, },
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) await updateReceivedEmail(input.receivedEmailId, 'article')
return newLibraryItem
})
// create a task to update thumbnail and pre-cache all images // create a task to update thumbnail and pre-cache all images
try { try {

View File

@ -1,4 +1,3 @@
import { UploadFile } from '../entity/upload_file'
import { User } from '../entity/user' import { User } from '../entity/user'
import { homePageURL } from '../env' import { homePageURL } from '../env'
import { import {
@ -7,24 +6,19 @@ import {
SaveFileInput, SaveFileInput,
SaveResult, SaveResult,
} from '../generated/graphql' } from '../generated/graphql'
import { entityManager, getRepository } from '../repository'
import { WithDataSourcesContext } from '../resolvers/types'
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
import { getStorageFileDetails } from '../utils/uploads' import { getStorageFileDetails } from '../utils/uploads'
import { getLabelsAndCreateIfNotExist } from './labels' import { getLabelsAndCreateIfNotExist } from './labels'
import { setFileUploadComplete } from './upload_file' import { updateLibraryItem } from './library_item'
import { findUploadFileById, setFileUploadComplete } from './upload_file'
export const saveFile = async ( export const saveFile = async (
ctx: WithDataSourcesContext, input: SaveFileInput,
user: User, user: User
input: SaveFileInput
): Promise<SaveResult> => { ): Promise<SaveResult> => {
logger.info('saving file with input', input) logger.info('saving file with input', input)
const pageId = input.clientRequestId const pageId = input.clientRequestId
const uploadFile = await getRepository(UploadFile).findOneBy({ const uploadFile = await findUploadFileById(input.uploadFileId)
id: input.uploadFileId,
user: { id: ctx.uid },
})
if (!uploadFile) { if (!uploadFile) {
return { return {
errorCodes: [SaveErrorCode.Unauthorized], errorCodes: [SaveErrorCode.Unauthorized],
@ -49,13 +43,13 @@ export const saveFile = async (
? await getLabelsAndCreateIfNotExist(input.labels, user.id) ? await getLabelsAndCreateIfNotExist(input.labels, user.id)
: undefined : undefined
if (input.state || input.labels) { if (input.state || input.labels) {
const updated = await updatePage( const updated = await updateLibraryItem(
pageId, pageId,
{ {
archivedAt, archivedAt,
labels, labels,
}, },
ctx user.id
) )
if (!updated) { if (!updated) {
logger.info('error updating page', pageId) logger.info('error updating page', pageId)

View File

@ -16,8 +16,7 @@ import {
SavePageInput, SavePageInput,
SaveResult, SaveResult,
} from '../generated/graphql' } from '../generated/graphql'
import { libraryItemRepository } from '../repository' import { authTrx } from '../repository'
import { WithDataSourcesContext } from '../resolvers/types'
import { enqueueThumbnailTask } from '../utils/createTask' import { enqueueThumbnailTask } from '../utils/createTask'
import { import {
cleanUrl, cleanUrl,
@ -30,8 +29,9 @@ import {
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
import { parsePreparedContent } from '../utils/parser' import { parsePreparedContent } from '../utils/parser'
import { createPageSaveRequest } from './create_page_save_request' import { createPageSaveRequest } from './create_page_save_request'
import { saveHighlight } from './highlights'
import { getLabelsAndCreateIfNotExist } from './labels' 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. // where we can use APIs to fetch their underlying content.
const FORCE_PUPPETEER_URLS = [ const FORCE_PUPPETEER_URLS = [
@ -60,9 +60,8 @@ const shouldParseInBackend = (input: SavePageInput): boolean => {
} }
export const savePage = async ( export const savePage = async (
ctx: WithDataSourcesContext, input: SavePageInput,
user: User, user: User
input: SavePageInput
): Promise<SaveResult> => { ): Promise<SaveResult> => {
const parseResult = await parsePreparedContent( const parseResult = await parsePreparedContent(
input.url, input.url,
@ -77,31 +76,23 @@ export const savePage = async (
) )
const [newSlug, croppedPathname] = createSlug(input.url, input.title) const [newSlug, croppedPathname] = createSlug(input.url, input.title)
let slug = newSlug let slug = newSlug
let pageId = input.clientRequestId let clientRequestId = input.clientRequestId
const itemToSave = parsedContentToLibraryItem({ const itemToSave = parsedContentToLibraryItem({
url: input.url, url: input.url,
title: input.title, title: input.title,
userId: user.id, userId: user.id,
pageId, itemId: clientRequestId,
slug, slug,
croppedPathname, croppedPathname,
parsedContent: parseResult.parsedContent, parsedContent: parseResult.parsedContent,
itemType: parseResult.pageType as unknown as LibraryItemType, itemType: parseResult.pageType as unknown as LibraryItemType,
originalHtml: parseResult.domContent, originalHtml: parseResult.domContent,
canonicalUrl: parseResult.canonicalUrl, canonicalUrl: parseResult.canonicalUrl,
rssFeedUrl: input.rssFeedUrl,
saveTime: input.savedAt ? new Date(input.savedAt) : undefined, saveTime: input.savedAt ? new Date(input.savedAt) : undefined,
publishedAt: input.publishedAt ? new Date(input.publishedAt) : 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' const isImported = input.source === 'csv-importer'
// always parse in backend if the url is in the force puppeteer list // always parse in backend if the url is in the force puppeteer list
@ -110,10 +101,9 @@ export const savePage = async (
await createPageSaveRequest({ await createPageSaveRequest({
userId: user.id, userId: user.id,
url: itemToSave.originalUrl, url: itemToSave.originalUrl,
pubsub: ctx.pubsub, articleSavingRequestId: clientRequestId,
articleSavingRequestId: input.clientRequestId, state: input.state || undefined,
archivedAt, labels: input.labels || undefined,
labels,
}) })
} catch (e) { } catch (e) {
return { return {
@ -122,11 +112,21 @@ export const savePage = async (
} }
} }
} else { } 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 // check if the page already exists
const existingLibraryItem = await libraryItemRepository.findOne({ const existingLibraryItem = await authTrx((t) =>
where: { user: { id: user.id }, originalUrl: itemToSave.originalUrl }, t.getRepository(LibraryItem).findOne({
relations: ['subscriptions'], where: { user: { id: user.id }, originalUrl: itemToSave.originalUrl },
}) relations: ['subscriptions'],
})
)
if (existingLibraryItem) { if (existingLibraryItem) {
// we don't want to update an rss feed page if rss-feeder is tring to re-save it // we don't want to update an rss feed page if rss-feeder is tring to re-save it
if ( if (
@ -134,14 +134,14 @@ export const savePage = async (
existingLibraryItem.subscription.url === input.rssFeedUrl existingLibraryItem.subscription.url === input.rssFeedUrl
) { ) {
return { return {
clientRequestId: pageId, clientRequestId,
url: `${homePageURL()}/${user.profile.username}/${slug}`, url: `${homePageURL()}/${user.profile.username}/${slug}`,
} }
} }
pageId = existingLibraryItem.id clientRequestId = existingLibraryItem.id
slug = existingLibraryItem.slug slug = existingLibraryItem.slug
if (!(await libraryItemRepository.save(itemToSave))) { if (!(await updateLibraryItem(clientRequestId, itemToSave, user.id))) {
return { return {
errorCodes: [SaveErrorCode.Unknown], errorCodes: [SaveErrorCode.Unknown],
message: 'Failed to update existing page', message: 'Failed to update existing page',
@ -149,14 +149,8 @@ export const savePage = async (
} }
} else { } else {
// do not publish a pubsub event if the page is imported // do not publish a pubsub event if the page is imported
const newPageId = await createLibraryItem(itemToSave) const newItem = await createLibraryItem(itemToSave, user.id)
if (!newPageId) { clientRequestId = newItem.id
return {
errorCodes: [SaveErrorCode.Unknown],
message: 'Failed to create new page',
}
}
pageId = newPageId
} }
} }
@ -175,13 +169,12 @@ export const savePage = async (
const highlight = { const highlight = {
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date(), createdAt: new Date(),
userId: ctx.uid, userId: user.id,
elasticPageId: pageId,
...parseResult.highlightData, ...parseResult.highlightData,
type: HighlightType.Highlight, type: HighlightType.Highlight,
} }
if (!(await addHighlightToPage(pageId, highlight, ctx))) { if (!(await saveHighlight(highlight, user.id))) {
return { return {
errorCodes: [SaveErrorCode.EmbeddedHighlightFailed], errorCodes: [SaveErrorCode.EmbeddedHighlightFailed],
message: 'Failed to save highlight', message: 'Failed to save highlight',
@ -190,7 +183,7 @@ export const savePage = async (
} }
return { return {
clientRequestId: pageId, clientRequestId,
url: `${homePageURL()}/${user.profile.username}/${slug}`, url: `${homePageURL()}/${user.profile.username}/${slug}`,
} }
} }
@ -200,7 +193,7 @@ export const parsedContentToLibraryItem = ({
url, url,
userId, userId,
originalHtml, originalHtml,
pageId, itemId,
parsedContent, parsedContent,
slug, slug,
croppedPathname, croppedPathname,
@ -211,8 +204,8 @@ export const parsedContentToLibraryItem = ({
uploadFileHash, uploadFileHash,
uploadFileId, uploadFileId,
saveTime, saveTime,
rssFeedUrl,
publishedAt, publishedAt,
state,
}: { }: {
url: string url: string
userId: string userId: string
@ -221,18 +214,18 @@ export const parsedContentToLibraryItem = ({
itemType: LibraryItemType itemType: LibraryItemType
parsedContent: Readability.ParseResult | null parsedContent: Readability.ParseResult | null
originalHtml?: string | null originalHtml?: string | null
pageId?: string | null itemId?: string | null
title?: string | null title?: string | null
preparedDocument?: PreparedDocumentInput | null preparedDocument?: PreparedDocumentInput | null
canonicalUrl?: string | null canonicalUrl?: string | null
uploadFileHash?: string | null uploadFileHash?: string | null
uploadFileId?: string | null uploadFileId?: string | null
saveTime?: Date saveTime?: Date
rssFeedUrl?: string | null
publishedAt?: Date | null publishedAt?: Date | null
state?: ArticleSavingRequestStatus | null
}): DeepPartial<LibraryItem> & { originalUrl: string } => { }): DeepPartial<LibraryItem> & { originalUrl: string } => {
return { return {
id: pageId ?? undefined, id: itemId || undefined,
slug, slug,
user: { id: userId }, user: { id: userId },
originalContent: originalHtml, originalContent: originalHtml,
@ -257,7 +250,9 @@ export const parsedContentToLibraryItem = ({
uploadFile: { id: uploadFileId ?? undefined }, uploadFile: { id: uploadFileId ?? undefined },
readingProgressTopPercent: 0, readingProgressTopPercent: 0,
readingProgressHighestReadAnchor: 0, readingProgressHighestReadAnchor: 0,
state: LibraryItemState.Succeeded, state: state
? (state as unknown as LibraryItemState)
: LibraryItemState.Succeeded,
createdAt: validatedDate(saveTime), createdAt: validatedDate(saveTime),
savedAt: validatedDate(saveTime), savedAt: validatedDate(saveTime),
siteName: parsedContent?.siteName, siteName: parsedContent?.siteName,

View File

@ -1,12 +1,10 @@
import { ArticleSavingRequestStatus } from '../elastic/types'
import { User } from '../entity/user' import { User } from '../entity/user'
import { homePageURL } from '../env' import { homePageURL } from '../env'
import { SaveErrorCode, SaveResult, SaveUrlInput } from '../generated/graphql' import { SaveErrorCode, SaveResult, SaveUrlInput } from '../generated/graphql'
import { PubsubClient } from '../pubsub' import { PubsubClient } from '../pubsub'
import { getRepository } from '../repository' import { userRepository } from '../repository/user'
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
import { createPageSaveRequest } from './create_page_save_request' import { createPageSaveRequest } from './create_page_save_request'
import { getLabelsAndCreateIfNotExist } from './labels'
interface SaveContext { interface SaveContext {
pubsub: PubsubClient pubsub: PubsubClient
@ -19,22 +17,13 @@ export const saveUrl = async (
input: SaveUrlInput input: SaveUrlInput
): Promise<SaveResult> => { ): Promise<SaveResult> => {
try { 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({ const pageSaveRequest = await createPageSaveRequest({
...input, ...input,
userId: ctx.uid, userId: ctx.uid,
pubsub: ctx.pubsub, pubsub: ctx.pubsub,
articleSavingRequestId: input.clientRequestId, articleSavingRequestId: input.clientRequestId,
archivedAt, state: input.state || undefined,
labels, labels: input.labels || undefined,
user,
locale: input.locale || undefined, locale: input.locale || undefined,
timezone: input.timezone || undefined, timezone: input.timezone || undefined,
savedAt: input.savedAt ? new Date(input.savedAt) : undefined, savedAt: input.savedAt ? new Date(input.savedAt) : undefined,
@ -61,7 +50,7 @@ export const saveUrlFromEmail = async (
url: string, url: string,
clientRequestId: string clientRequestId: string
): Promise<boolean> => { ): Promise<boolean> => {
const user = await getRepository(User).findOneBy({ const user = await userRepository.findOneBy({
id: ctx.uid, id: ctx.uid,
}) })
if (!user) { if (!user) {

View File

@ -2,7 +2,7 @@ import { UploadFile } from '../entity/upload_file'
import { authTrx } from '../repository' import { authTrx } from '../repository'
export const findUploadFileById = async (id: string) => { 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) => { export const setFileUploadComplete = async (id: string) => {