diff --git a/packages/api/src/repository/index.ts b/packages/api/src/repository/index.ts index 943dc0dd8..61a204cec 100644 --- a/packages/api/src/repository/index.ts +++ b/packages/api/src/repository/index.ts @@ -17,6 +17,10 @@ export const getColumns = (repository: Repository): (keyof T)[] => { ) as (keyof T)[] } +export const getColumnsDbName = (repository: Repository): string[] => { + return repository.metadata.columns.map((col) => col.databaseName) +} + export const setClaims = async ( manager: EntityManager, uid = '00000000-0000-0000-0000-000000000000', diff --git a/packages/api/src/repository/library_item.ts b/packages/api/src/repository/library_item.ts index ebddb4b10..22e1e64f8 100644 --- a/packages/api/src/repository/library_item.ts +++ b/packages/api/src/repository/library_item.ts @@ -1,5 +1,16 @@ +import { DeepPartial } from 'typeorm' +import { getColumnsDbName } from '.' import { appDataSource } from '../data_source' import { LibraryItem } from '../entity/library_item' +import { keysToCamelCase, wordsCount } from '../utils/helpers' +import { logger } from '../utils/logger' + +const convertToLibraryItem = (item: DeepPartial) => { + return { + ...item, + wordCount: item.wordCount ?? wordsCount(item.readableContent || ''), + } +} export const libraryItemRepository = appDataSource .getRepository(LibraryItem) @@ -20,6 +31,33 @@ export const libraryItemRepository = appDataSource return this.countBy({ createdAt }) }, + async upsertLibraryItem(item: DeepPartial) { + // overwrites columns except id and slug + const overwrites = getColumnsDbName(this).filter( + (column) => !['id', 'slug'].includes(column) + ) + const hashedUrl = 'md5(original_url)' + + const [query, params] = this.createQueryBuilder() + .insert() + .into(LibraryItem) + .values(convertToLibraryItem(item)) + .orUpdate(overwrites, ['user_id', hashedUrl], { + skipUpdateIfNoValuesChanged: true, + }) + .returning('*') + .getQueryAndParameters() + + // this is a workaround for the typeorm bug which quotes the md5 function + const newQuery = query.replace(`"${hashedUrl}"`, hashedUrl) + const results = (await this.query(newQuery, params)) as never[] + + // convert to camel case + const newItem = keysToCamelCase(results[0]) as LibraryItem + + return newItem + }, + createByPopularRead(name: string, userId: string) { return this.query( ` diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 1d0e95a64..34c7abdd6 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -6,7 +6,6 @@ import { Readability } from '@omnivore/readability' import graphqlFields from 'graphql-fields' import { IsNull } from 'typeorm' -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' import { LibraryItem, LibraryItemState } from '../../entity/library_item' import { env } from '../../env' import { @@ -74,9 +73,7 @@ import { import { batchDelete, batchUpdateLibraryItems, - createLibraryItem, - findLibraryItemById, - findLibraryItemByUrl, + createOrUpdateLibraryItem, findLibraryItemsByPrefix, searchLibraryItems, sortParamsToSort, @@ -261,7 +258,7 @@ export const createArticleResolver = authorized< FORCE_PUPPETEER_URLS.some((regex) => regex.test(url)) ) { await createPageSaveRequest({ - userId: uid, + user: userData, url, state: state || undefined, labels: inputLabels || undefined, @@ -286,7 +283,7 @@ export const createArticleResolver = authorized< // We have a URL but no document, so we try to send this to puppeteer // and return a dummy response. await createPageSaveRequest({ - userId: uid, + user: userData, url, state: state || undefined, labels: inputLabels || undefined, @@ -340,29 +337,12 @@ export const createArticleResolver = authorized< } } - let libraryItemToReturn: LibraryItem - - const existingLibraryItem = await findLibraryItemByUrl( - libraryItemToSave.originalUrl, - uid + // create new item in database + const libraryItemToReturn = await createOrUpdateLibraryItem( + libraryItemToSave, + uid, + pubsub ) - articleSavingRequestId = existingLibraryItem?.id || articleSavingRequestId - if (articleSavingRequestId) { - // update existing item's state from processing to succeeded - libraryItemToReturn = await updateLibraryItem( - articleSavingRequestId, - libraryItemToSave as QueryDeepPartialEntity, - uid, - pubsub - ) - } else { - // create new item in database - libraryItemToReturn = await createLibraryItem( - libraryItemToSave, - uid, - pubsub - ) - } await createAndSaveLabelsInLibraryItem( libraryItemToReturn.id, @@ -371,14 +351,6 @@ export const createArticleResolver = authorized< rssFeedUrl ) - log.info( - 'item created in database', - libraryItemToReturn.id, - libraryItemToReturn.originalUrl, - libraryItemToReturn.slug, - libraryItemToReturn.title - ) - return { user, created: true, @@ -607,7 +579,7 @@ export const saveArticleReadingProgressResolver = authorized< force, }, }, - { pubsub, uid, dataSources } + { authTrx, pubsub, uid, dataSources } ) => { if ( readingProgressPercent < 0 || @@ -623,7 +595,14 @@ export const saveArticleReadingProgressResolver = authorized< // We don't need to update the values of reading progress here // because the function resolver will handle that for us when // it resolves the properties of the Article object - let updatedItem = await findLibraryItemById(id, uid) + let updatedItem = await authTrx((tx) => + tx.getRepository(LibraryItem).findOne({ + where: { + id, + }, + relations: ['user'], + }) + ) if (!updatedItem) { return { errorCodes: [SaveArticleReadingProgressErrorCode.Unauthorized], @@ -1002,7 +981,7 @@ export const moveToFolderResolver = authorized< if (item.state === LibraryItemState.ContentNotFetched) { try { await createPageSaveRequest({ - userId: uid, + user: item.user, url: item.originalUrl, articleSavingRequestId: id, priority: 'high', @@ -1029,7 +1008,7 @@ export const fetchContentResolver = authorized< FetchContentSuccess, FetchContentError, MutationFetchContentArgs ->(async (_, { id }, { uid, log, pubsub }) => { +>(async (_, { id }, { authTrx, uid, log, pubsub }) => { analytics.track({ userId: uid, event: 'fetch_content', @@ -1038,7 +1017,14 @@ export const fetchContentResolver = authorized< }, }) - const item = await findLibraryItemById(id, uid) + const item = await authTrx((tx) => + tx.getRepository(LibraryItem).findOne({ + where: { + id, + }, + relations: ['user'], + }) + ) if (!item) { return { errorCodes: [FetchContentErrorCode.Unauthorized], @@ -1049,7 +1035,7 @@ export const fetchContentResolver = authorized< if (item.state === LibraryItemState.ContentNotFetched) { try { await createPageSaveRequest({ - userId: uid, + user: item.user, url: item.originalUrl, articleSavingRequestId: id, priority: 'high', diff --git a/packages/api/src/resolvers/article_saving_request/index.ts b/packages/api/src/resolvers/article_saving_request/index.ts index 9f4cf7c4a..b42171f6f 100644 --- a/packages/api/src/resolvers/article_saving_request/index.ts +++ b/packages/api/src/resolvers/article_saving_request/index.ts @@ -42,9 +42,14 @@ export const createArticleSavingRequestResolver = authorized< }, }) + const user = await userRepository.findById(uid) + if (!user) { + return { errorCodes: [CreateArticleSavingRequestErrorCode.Unauthorized] } + } + try { const articleSavingRequest = await createPageSaveRequest({ - userId: uid, + user, url, pubsub, }) diff --git a/packages/api/src/routers/article_router.ts b/packages/api/src/routers/article_router.ts index efceb02ef..6b3bab37f 100644 --- a/packages/api/src/routers/article_router.ts +++ b/packages/api/src/routers/article_router.ts @@ -7,6 +7,7 @@ import * as jwt from 'jsonwebtoken' import { Speech } from '../entity/speech' import { env } from '../env' import { CreateArticleErrorCode } from '../generated/graphql' +import { userRepository } from '../repository/user' import { Claims } from '../resolvers/types' import { createPageSaveRequest } from '../services/create_page_save_request' import { findLibraryItemById } from '../services/library_item' @@ -32,6 +33,9 @@ export function articleRouter() { const { url } = req.body as { url?: string } + if (!url) { + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } const token = req?.cookies?.auth || req?.headers?.authorization const claims = await getClaimsByToken(token) @@ -40,20 +44,12 @@ export function articleRouter() { } const { uid } = claims - - logger.info('Article saving request', { - body: req.body, - labels: { - source: 'SaveEndpoint', - userId: uid, - }, - }) - - if (!url) { - return res.status(400).send({ errorCode: 'BAD_DATA' }) + const user = await userRepository.findById(uid) + if (!user) { + return res.status(400).send('Bad Request') } - const result = await createPageSaveRequest({ userId: uid, url }) + const result = await createPageSaveRequest({ user, url }) if (isSiteBlockedForParse(url)) { return res diff --git a/packages/api/src/routers/page_router.ts b/packages/api/src/routers/page_router.ts index f3c521301..459456292 100644 --- a/packages/api/src/routers/page_router.ts +++ b/packages/api/src/routers/page_router.ts @@ -13,7 +13,7 @@ import { PageType, UploadFileStatus } from '../generated/graphql' import { authTrx } from '../repository' import { Claims } from '../resolvers/types' import { - createLibraryItem, + createOrUpdateLibraryItem, findLibraryItemById, findLibraryItemByUrl, restoreLibraryItem, @@ -101,7 +101,7 @@ export function pageRouter() { if (item) { await restoreLibraryItem(item.id, claims.uid) } else { - await createLibraryItem( + await createOrUpdateLibraryItem( { originalUrl: signedUrl, id: clientRequestId, diff --git a/packages/api/src/routers/svc/email_attachment.ts b/packages/api/src/routers/svc/email_attachment.ts index 6c127135b..0de5481a0 100644 --- a/packages/api/src/routers/svc/email_attachment.ts +++ b/packages/api/src/routers/svc/email_attachment.ts @@ -9,7 +9,7 @@ import { UploadFile } from '../../entity/upload_file' import { env } from '../../env' import { PageType, UploadFileStatus } from '../../generated/graphql' import { authTrx } from '../../repository' -import { createLibraryItem } from '../../services/library_item' +import { createOrUpdateLibraryItem } from '../../services/library_item' import { findNewsletterEmailByAddress } from '../../services/newsletters' import { updateReceivedEmail } from '../../services/received_emails' import { @@ -170,7 +170,7 @@ export function emailAttachmentRouter() { : ContentReaderType.EPUB, } - const item = await createLibraryItem(itemToCreate, user.id) + const item = await createOrUpdateLibraryItem(itemToCreate, user.id) // update received email type await updateReceivedEmail(receivedEmailId, 'article', user.id) diff --git a/packages/api/src/routers/svc/following.ts b/packages/api/src/routers/svc/following.ts index 834d6c531..18e912fda 100644 --- a/packages/api/src/routers/svc/following.ts +++ b/packages/api/src/routers/svc/following.ts @@ -6,7 +6,7 @@ import { PreparedDocumentInput, } from '../../generated/graphql' import { createAndSaveLabelsInLibraryItem } from '../../services/labels' -import { createLibraryItem } from '../../services/library_item' +import { createOrUpdateLibraryItem } from '../../services/library_item' import { parsedContentToLibraryItem } from '../../services/save_page' import { cleanUrl, generateSlug } from '../../utils/helpers' import { createThumbnailUrl } from '../../utils/imageproxy' @@ -123,7 +123,7 @@ export function followingServiceRouter() { state: ArticleSavingRequestStatus.ContentNotFetched, }) - const newItem = await createLibraryItem(itemToSave, userId) + const newItem = await createOrUpdateLibraryItem(itemToSave, userId) logger.info('feed item saved in following') // save RSS label in the item diff --git a/packages/api/src/routers/svc/links.ts b/packages/api/src/routers/svc/links.ts index 05d69225d..f247b6ffa 100644 --- a/packages/api/src/routers/svc/links.ts +++ b/packages/api/src/routers/svc/links.ts @@ -5,6 +5,7 @@ import express from 'express' import { LessThan } from 'typeorm' import { LibraryItemState } from '../../entity/library_item' import { readPushSubscription } from '../../pubsub' +import { userRepository } from '../../repository/user' import { createPageSaveRequest } from '../../services/create_page_save_request' import { deleteLibraryItemsByAdmin } from '../../services/library_item' import { logger } from '../../utils/logger' @@ -65,9 +66,14 @@ export function linkServiceRouter() { } const msg = data as CreateLinkRequestMessage + const user = await userRepository.findById(msg.userId) + if (!user) { + return res.status(400).send('Bad Request') + } + try { const request = await createPageSaveRequest({ - userId: msg.userId, + user, url: msg.url, }) diff --git a/packages/api/src/services/create_page_save_request.ts b/packages/api/src/services/create_page_save_request.ts index bd336f28c..403ccf3f0 100644 --- a/packages/api/src/services/create_page_save_request.ts +++ b/packages/api/src/services/create_page_save_request.ts @@ -1,5 +1,6 @@ import * as privateIpLib from 'private-ip' import { LibraryItemState } from '../entity/library_item' +import { User } from '../entity/user' import { ArticleSavingRequest, ArticleSavingRequestStatus, @@ -8,7 +9,6 @@ import { PageType, } from '../generated/graphql' import { createPubSubClient, PubsubClient } from '../pubsub' -import { userRepository } from '../repository/user' import { enqueueParseRequest } from '../utils/createTask' import { cleanUrl, @@ -16,15 +16,10 @@ import { libraryItemToArticleSavingRequest, } from '../utils/helpers' import { logger } from '../utils/logger' -import { - countByCreatedAt, - createLibraryItem, - findLibraryItemByUrl, - updateLibraryItem, -} from './library_item' +import { countByCreatedAt, createOrUpdateLibraryItem } from './library_item' interface PageSaveRequest { - userId: string + user: User url: string pubsub?: PubsubClient articleSavingRequestId?: string @@ -80,7 +75,7 @@ export const validateUrl = (url: string): URL => { } export const createPageSaveRequest = async ({ - userId, + user, url, pubsub = createPubSubClient(), articleSavingRequestId, @@ -102,52 +97,29 @@ export const createPageSaveRequest = async ({ errorCode: CreateArticleSavingRequestErrorCode.BadData, }) } - // if user is not specified, get it from the database - const user = await userRepository.findById(userId) - if (!user) { - logger.info(`User not found: ${userId}`) - return Promise.reject({ - errorCode: CreateArticleSavingRequestErrorCode.BadData, - }) - } + const userId = user.id url = cleanUrl(url) - // look for existing library item - let libraryItem = await findLibraryItemByUrl(url, userId) - if (!libraryItem) { - logger.info('libraryItem does not exist', { url }) - // create processing item - libraryItem = await createLibraryItem( - { - id: articleSavingRequestId, - user: { id: userId }, - readableContent: SAVING_CONTENT, - itemType: PageType.Unknown, - slug: generateSlug(url), - title: url, - originalUrl: url, - state: LibraryItemState.Processing, - publishedAt, - folder, - subscription, - savedAt, - }, - userId, - pubsub - ) - } - // reset state to processing - if (libraryItem.state !== LibraryItemState.Processing) { - libraryItem = await updateLibraryItem( - libraryItem.id, - { - state: LibraryItemState.Processing, - }, - userId, - pubsub - ) - } + // create processing item + const libraryItem = await createOrUpdateLibraryItem( + { + id: articleSavingRequestId, + user: { id: userId }, + readableContent: SAVING_CONTENT, + itemType: PageType.Unknown, + slug: generateSlug(url), + title: url, + originalUrl: url, + state: LibraryItemState.Processing, + publishedAt, + folder, + subscription, + savedAt, + }, + userId, + pubsub + ) // get priority by checking rate limit if not specified priority = priority || (await getPriorityByRateLimit(userId)) diff --git a/packages/api/src/services/highlights.ts b/packages/api/src/services/highlights.ts index 436a3dd1a..ac6b08f0c 100644 --- a/packages/api/src/services/highlights.ts +++ b/packages/api/src/services/highlights.ts @@ -180,7 +180,7 @@ export const findHighlightById = async ( return authTrx( async (tx) => { const highlightRepo = tx.withRepository(highlightRepository) - return highlightRepo.findOneByOrFail({ + return highlightRepo.findOneBy({ id: highlightId, }) }, diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts index c4f8d4d13..33206093d 100644 --- a/packages/api/src/services/labels.ts +++ b/packages/api/src/services/labels.ts @@ -187,10 +187,12 @@ export const saveLabelsInHighlight = async ( ) const highlight = await findHighlightById(highlightId, userId) - // update labels in library item - await bulkEnqueueUpdateLabels([ - { libraryItemId: highlight.libraryItemId, userId }, - ]) + if (highlight) { + // update labels in library item + await bulkEnqueueUpdateLabels([ + { libraryItemId: highlight.libraryItemId, userId }, + ]) + } } export const findLabelsByIds = async ( diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index a715babef..c567995ba 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -14,15 +14,9 @@ import { LibraryItem, LibraryItemState } from '../entity/library_item' import { BulkActionType, InputMaybe, SortParams } from '../generated/graphql' import { createPubSubClient, EntityType } from '../pubsub' import { redisDataSource } from '../redis_data_source' -import { - authTrx, - getColumns, - isUniqueViolation, - queryBuilderToRawSql, -} from '../repository' +import { authTrx, getColumns, queryBuilderToRawSql } from '../repository' import { libraryItemRepository } from '../repository/library_item' -import { setRecentlySavedItemInRedis, wordsCount } from '../utils/helpers' -import { logger } from '../utils/logger' +import { setRecentlySavedItemInRedis } from '../utils/helpers' import { parseSearchQuery } from '../utils/search' import { addLabelsToLibraryItem } from './labels' @@ -831,74 +825,44 @@ export const createLibraryItems = async ( ) } -export const createLibraryItem = async ( +export const createOrUpdateLibraryItem = async ( libraryItem: DeepPartial, userId: string, pubsub = createPubSubClient(), skipPubSub = false ): Promise => { - if (!libraryItem.originalUrl) { - throw new Error('Original url is required') + const newLibraryItem = await authTrx( + async (tx) => + tx.withRepository(libraryItemRepository).upsertLibraryItem(libraryItem), + undefined, + userId + ) + + // set recently saved item in redis if redis is enabled + if (redisDataSource.redisClient) { + await setRecentlySavedItemInRedis( + redisDataSource.redisClient, + userId, + newLibraryItem.originalUrl + ) } - try { - const newLibraryItem = await authTrx( - async (tx) => - tx.withRepository(libraryItemRepository).save({ - ...libraryItem, - wordCount: - libraryItem.wordCount ?? - wordsCount(libraryItem.readableContent || ''), - }), - undefined, - userId - ) - logger.info('item created', { url: libraryItem.originalUrl }) - - // set recently saved item in redis if redis is enabled - if (redisDataSource.redisClient) { - await setRecentlySavedItemInRedis( - redisDataSource.redisClient, - userId, - newLibraryItem.originalUrl - ) - } - - if (skipPubSub) { - return newLibraryItem - } - - await pubsub.entityCreated>( - EntityType.PAGE, - { - ...newLibraryItem, - // don't send original content and readable content - originalContent: undefined, - readableContent: undefined, - }, - userId - ) - + if (skipPubSub) { return newLibraryItem - } catch (error) { - if (isUniqueViolation(error)) { - logger.info('item already created', { url: libraryItem.originalUrl }) - - const existingItem = await findLibraryItemByUrl( - libraryItem.originalUrl, - userId - ) - - if (!existingItem) { - throw new Error(`Item not found for url: ${libraryItem.originalUrl}`) - } - - return existingItem - } - - logger.error('error creating item', error) - throw error } + + await pubsub.entityCreated>( + EntityType.PAGE, + { + ...newLibraryItem, + // don't send original content and readable content + originalContent: undefined, + readableContent: undefined, + }, + userId + ) + + return newLibraryItem } export const findLibraryItemsByPrefix = async ( diff --git a/packages/api/src/services/recommendation.ts b/packages/api/src/services/recommendation.ts index bf563060a..cd090cdb6 100644 --- a/packages/api/src/services/recommendation.ts +++ b/packages/api/src/services/recommendation.ts @@ -5,7 +5,7 @@ import { Recommendation } from '../entity/recommendation' import { authTrx } from '../repository' import { logger } from '../utils/logger' import { createHighlights } from './highlights' -import { createLibraryItem, findLibraryItemByUrl } from './library_item' +import { createOrUpdateLibraryItem, findLibraryItemByUrl } from './library_item' export const addRecommendation = async ( item: LibraryItem, @@ -39,7 +39,7 @@ export const addRecommendation = async ( publishedAt: item.publishedAt, } - recommendedItem = await createLibraryItem(newItem, userId) + recommendedItem = await createOrUpdateLibraryItem(newItem, userId) const highlights = item.highlights ?.filter((highlight) => highlightIds?.includes(highlight.id)) diff --git a/packages/api/src/services/save_email.ts b/packages/api/src/services/save_email.ts index 8ecd25d53..ef8add223 100644 --- a/packages/api/src/services/save_email.ts +++ b/packages/api/src/services/save_email.ts @@ -17,7 +17,7 @@ import { } from '../utils/parser' import { createAndSaveLabelsInLibraryItem } from './labels' import { - createLibraryItem, + createOrUpdateLibraryItem, findLibraryItemByUrl, restoreLibraryItem, } from './library_item' @@ -80,7 +80,7 @@ export const saveEmail = async ( } // start a transaction to create the library item and update the received email - const newLibraryItem = await createLibraryItem( + const newLibraryItem = await createOrUpdateLibraryItem( { user: { id: input.userId }, slug, diff --git a/packages/api/src/services/save_page.ts b/packages/api/src/services/save_page.ts index 793c9d703..d8e28a43c 100644 --- a/packages/api/src/services/save_page.ts +++ b/packages/api/src/services/save_page.ts @@ -1,6 +1,5 @@ import { Readability } from '@omnivore/readability' import { DeepPartial } from 'typeorm' -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' import { Highlight } from '../entity/highlight' import { LibraryItem, LibraryItemState } from '../entity/library_item' import { User } from '../entity/user' @@ -12,8 +11,6 @@ import { SavePageInput, SaveResult, } from '../generated/graphql' -import { authTrx } from '../repository' -import { libraryItemRepository } from '../repository/library_item' import { enqueueThumbnailJob } from '../utils/createTask' import { cleanUrl, @@ -28,7 +25,7 @@ import { contentReaderForLibraryItem } from '../utils/uploads' import { createPageSaveRequest } from './create_page_save_request' import { createHighlight } from './highlights' import { createAndSaveLabelsInLibraryItem } from './labels' -import { createLibraryItem, updateLibraryItem } from './library_item' +import { createOrUpdateLibraryItem } from './library_item' // where we can use APIs to fetch their underlying content. const FORCE_PUPPETEER_URLS = [ @@ -103,7 +100,7 @@ export const savePage = async ( if (shouldParseInBackend(input)) { try { await createPageSaveRequest({ - userId: user.id, + user, url: itemToSave.originalUrl, articleSavingRequestId: clientRequestId || undefined, state: input.state || undefined, @@ -118,44 +115,15 @@ export const savePage = async ( } } } else { - // check if the item already exists - const existingLibraryItem = await authTrx((t) => - t - .withRepository(libraryItemRepository) - .findByUserIdAndUrl(user.id, input.url) + // do not publish a pubsub event if the item is imported + const newItem = await createOrUpdateLibraryItem( + itemToSave, + user.id, + undefined, + isImported ) - if (existingLibraryItem) { - clientRequestId = existingLibraryItem.id - slug = existingLibraryItem.slug - - // we don't want to update an rss feed item if rss-feeder is tring to re-save it - if (existingLibraryItem.subscription === input.rssFeedUrl) { - return { - clientRequestId, - url: `${homePageURL()}/${user.profile.username}/${slug}`, - } - } - - // update the item except for id and slug - await updateLibraryItem( - clientRequestId, - { - ...itemToSave, - id: undefined, - slug: undefined, - } as QueryDeepPartialEntity, - user.id - ) - } else { - // do not publish a pubsub event if the item is imported - const newItem = await createLibraryItem( - itemToSave, - user.id, - undefined, - isImported - ) - clientRequestId = newItem.id - } + clientRequestId = newItem.id + slug = newItem.slug await createAndSaveLabelsInLibraryItem( clientRequestId, diff --git a/packages/api/src/services/save_url.ts b/packages/api/src/services/save_url.ts index 806db0e53..04089e0ea 100644 --- a/packages/api/src/services/save_url.ts +++ b/packages/api/src/services/save_url.ts @@ -12,7 +12,7 @@ export const saveUrl = async ( try { const pageSaveRequest = await createPageSaveRequest({ ...input, - userId: user.id, + user, articleSavingRequestId: input.clientRequestId, state: input.state || undefined, labels: input.labels || undefined, diff --git a/packages/api/src/services/upload_file.ts b/packages/api/src/services/upload_file.ts index 1df0ab97f..1d284916f 100644 --- a/packages/api/src/services/upload_file.ts +++ b/packages/api/src/services/upload_file.ts @@ -17,7 +17,7 @@ import { generateUploadSignedUrl, } from '../utils/uploads' import { validateUrl } from './create_page_save_request' -import { createLibraryItem } from './library_item' +import { createOrUpdateLibraryItem } from './library_item' const isFileUrl = (url: string): boolean => { const parsedUrl = new URL(url) @@ -122,7 +122,7 @@ export const uploadFile = async ( // If we have a file:// URL, don't try to match it // and create a copy of the item, just create a // new item. - const item = await createLibraryItem( + const item = await createOrUpdateLibraryItem( { id: input.clientRequestId || undefined, originalUrl: isFileUrl(input.url) ? attachmentUrl : input.url, diff --git a/packages/api/test/db.ts b/packages/api/test/db.ts index 01c9f9b6a..e618b47f3 100644 --- a/packages/api/test/db.ts +++ b/packages/api/test/db.ts @@ -12,7 +12,7 @@ import { authTrx, getRepository, setClaims } from '../src/repository' import { highlightRepository } from '../src/repository/highlight' import { userRepository } from '../src/repository/user' import { createUser } from '../src/services/create_user' -import { createLibraryItem } from '../src/services/library_item' +import { createOrUpdateLibraryItem } from '../src/services/library_item' import { createDeviceToken } from '../src/services/user_device_tokens' import { bulkEnqueueUpdateLabels, @@ -120,7 +120,7 @@ export const createTestLibraryItem = async ( slug: 'test-with-omnivore', } - const createdItem = await createLibraryItem(item, userId) + const createdItem = await createOrUpdateLibraryItem(item, userId) if (labels) { await saveLabelsInLibraryItem(labels, createdItem.id, userId) } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index abf05f437..d35ef6f4f 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -23,7 +23,7 @@ import { getRepository } from '../../src/repository' import { createGroup, deleteGroup } from '../../src/services/groups' import { createLabel, deleteLabels } from '../../src/services/labels' import { - createLibraryItem, + createOrUpdateLibraryItem, createLibraryItems, deleteLibraryItemById, deleteLibraryItemByUrl, @@ -408,7 +408,7 @@ describe('Article API', () => { document = '

test

' title = 'new title' - const item = await createLibraryItem( + const item = await createOrUpdateLibraryItem( { readableContent: document, slug: 'test saving an archived article slug', @@ -450,17 +450,22 @@ describe('Article API', () => { readingProgressTopPercent: 100, user, originalUrl: 'https://blog.omnivore.app/test-with-omnivore', - highlights: [ - { - shortId: 'test short id', - patch: 'test patch', - quote: 'test quote', - user, - }, - ], } - const item = await createLibraryItem(itemToCreate, user.id) + const item = await createOrUpdateLibraryItem(itemToCreate, user.id) itemId = item.id + + // save highlights + await createHighlight( + { + shortId: 'test short id', + patch: 'test patch', + quote: 'test quote', + user, + libraryItem: item, + }, + itemId, + user.id + ) }) after(async () => { @@ -698,7 +703,7 @@ describe('Article API', () => { originalUrl: 'https://blog.omnivore.app/setBookmarkArticle', slug: 'test-with-omnivore', } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) itemId = item.id }) @@ -802,7 +807,7 @@ describe('Article API', () => { context('when force is true', () => { before(async () => { itemId = ( - await createLibraryItem( + await createOrUpdateLibraryItem( { user: { id: user.id }, originalUrl: 'https://blog.omnivore.app/setBookmarkArticle', @@ -843,7 +848,7 @@ describe('Article API', () => { let itemId = '' before(async () => { - const item = await createLibraryItem( + const item = await createOrUpdateLibraryItem( { user: { id: user.id }, originalUrl: 'https://blog.omnivore.app/setBookmarkArticle', @@ -928,7 +933,7 @@ describe('Article API', () => { siteName: 'Example', readingProgressBottomPercent: readingProgressArray[i], } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) items.push(item) // Create some test highlights @@ -1973,7 +1978,7 @@ describe('Article API', () => { slug: '', originalUrl: `https://blog.omnivore.app/p/typeahead-search-${i}`, } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) items.push(item) } }) @@ -2047,7 +2052,7 @@ describe('Article API', () => { originalUrl: `https://blog.omnivore.app/p/updates-since-${i}`, user, } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) items.push(item) } @@ -2163,7 +2168,7 @@ describe('Article API', () => { before(async () => { // Create some test items for (let i = 0; i < 5; i++) { - await createLibraryItem( + await createOrUpdateLibraryItem( { user, itemType: i == 0 ? PageType.Article : PageType.File, @@ -2256,7 +2261,7 @@ describe('Article API', () => { before(async () => { // Create some test items for (let i = 0; i < 5; i++) { - const item = await createLibraryItem( + const item = await createOrUpdateLibraryItem( { user, itemType: i == 0 ? PageType.Article : PageType.File, @@ -2318,7 +2323,7 @@ describe('Article API', () => { readableContent: '

test

', originalUrl: `https://blog.omnivore.app/p/setFavoriteArticle`, } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) articleId = item.id }) @@ -2365,7 +2370,7 @@ describe('Article API', () => { deletedAt: new Date(), state: LibraryItemState.Deleted, } - const item = await createLibraryItem(itemToSave, user.id) + const item = await createOrUpdateLibraryItem(itemToSave, user.id) items.push(item) } }) diff --git a/packages/api/test/resolvers/highlight.test.ts b/packages/api/test/resolvers/highlight.test.ts index 1d839d515..a81602cd1 100644 --- a/packages/api/test/resolvers/highlight.test.ts +++ b/packages/api/test/resolvers/highlight.test.ts @@ -295,8 +295,8 @@ describe('Highlights API', () => { expect(res.body.data.mergeHighlight.highlight.id).to.eq(newHighlightId) const highlight = await findHighlightById(newHighlightId, user.id) - expect(highlight.labels).to.have.lengthOf(1) - expect(highlight.labels?.[0]?.name).to.eq(labelName) + expect(highlight?.labels).to.have.lengthOf(1) + expect(highlight?.labels?.[0]?.name).to.eq(labelName) highlightId = newHighlightId })