cont
This commit is contained in:
@ -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
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
SetRuleErrorCode,
|
||||
SetRuleSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { getRepository } from '../../repository'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
|
||||
export const setRuleResolver = authorized<
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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] }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user