cont
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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<
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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] }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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[]
|
||||||
|
|||||||
@ -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()
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user