diff --git a/packages/api/src/entity/library_item.ts b/packages/api/src/entity/library_item.ts index f5c15d42e..f127f52b5 100644 --- a/packages/api/src/entity/library_item.ts +++ b/packages/api/src/entity/library_item.ts @@ -198,4 +198,10 @@ export class LibraryItem { @Column('text') folder!: string + + @Column('text') + labelNames?: string[] + + @Column('text') + highlightAnnotations?: string[] } diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 34c7abdd6..363f97f13 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -76,6 +76,7 @@ import { createOrUpdateLibraryItem, findLibraryItemsByPrefix, searchLibraryItems, + softDeleteLibraryItem, sortParamsToSort, updateLibraryItem, updateLibraryItemReadingProgress, @@ -532,15 +533,7 @@ export const setBookmarkArticleResolver = authorized< } // delete the item and its metadata - const deletedLibraryItem = await updateLibraryItem( - articleID, - { - state: LibraryItemState.Deleted, - deletedAt: new Date(), - }, - uid, - pubsub - ) + const deletedLibraryItem = await softDeleteLibraryItem(articleID, uid, pubsub) analytics.track({ userId: uid, diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts index 33206093d..1bf770a6d 100644 --- a/packages/api/src/services/labels.ts +++ b/packages/api/src/services/labels.ts @@ -58,6 +58,32 @@ export const findOrCreateLabels = async ( ) } +export const createAndAddLabelsToLibraryItem = async ( + libraryItemId: string, + userId: string, + labels?: CreateLabelInput[] | null, + rssFeedUrl?: string | null, + source?: LabelSource +) => { + if (rssFeedUrl) { + // add rss label to labels + labels = (labels || []).concat({ name: 'RSS' }) + source = 'system' + } + + // save labels in item + if (labels && labels.length > 0) { + const newLabels = await findOrCreateLabels(labels, userId) + + await addLabelsToLibraryItem( + newLabels.map((l) => l.id), + libraryItemId, + userId, + source + ) + } +} + export const createAndSaveLabelsInLibraryItem = async ( libraryItemId: string, userId: string, diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 20b47afb7..37422d920 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -8,6 +8,7 @@ import { } from 'typeorm' import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' import { ReadingProgressDataSource } from '../datasources/reading_progress_data_source' +import { EntityLabel } from '../entity/entity_label' import { Highlight } from '../entity/highlight' import { Label } from '../entity/label' import { LibraryItem, LibraryItemState } from '../entity/library_item' @@ -18,6 +19,7 @@ import { authTrx, getColumns, queryBuilderToRawSql } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { Merge } from '../util' import { setRecentlySavedItemInRedis } from '../utils/helpers' +import { logger } from '../utils/logger' import { parseSearchQuery } from '../utils/search' import { addLabelsToLibraryItem } from './labels' @@ -708,6 +710,32 @@ export const restoreLibraryItem = async ( ) } +export const softDeleteLibraryItem = async ( + id: string, + userId: string, + pubsub = createPubSubClient() +): Promise => { + const deletedLibraryItem = await authTrx( + async (tx) => { + const itemRepo = tx.withRepository(libraryItemRepository) + + // mark item as deleted + await itemRepo.update(id, { + state: LibraryItemState.Deleted, + deletedAt: new Date(), + }) + + return itemRepo.findOneByOrFail({ id }) + }, + undefined, + userId + ) + + await pubsub.entityDeleted(EntityType.PAGE, id, userId) + + return deletedLibraryItem +} + export const updateLibraryItem = async ( id: string, libraryItem: QueryDeepPartialEntity, @@ -719,14 +747,11 @@ export const updateLibraryItem = async ( async (tx) => { const itemRepo = tx.withRepository(libraryItemRepository) - // reset deletedAt and archivedAt + // reset archivedAt switch (libraryItem.state) { case LibraryItemState.Archived: libraryItem.archivedAt = new Date() break - case LibraryItemState.Deleted: - libraryItem.deletedAt = new Date() - break case LibraryItemState.Processing: case LibraryItemState.Succeeded: libraryItem.archivedAt = null @@ -847,15 +872,39 @@ export const createOrUpdateLibraryItem = async ( ) if (existingLibraryItem) { + const id = existingLibraryItem.id + + try { + // delete labels and highlights if the item was deleted + if (existingLibraryItem.state === LibraryItemState.Deleted) { + logger.info('Deleting labels and highlights for item', { + id, + }) + await tx.getRepository(Highlight).delete({ + libraryItem: { id: existingLibraryItem.id }, + }) + + await tx.getRepository(EntityLabel).delete({ + libraryItemId: existingLibraryItem.id, + }) + + libraryItem.labelNames = [] + libraryItem.highlightAnnotations = [] + } + } catch (error) { + // continue to save the item even if we failed to delete labels and highlights + logger.error('Failed to delete labels and highlights', error) + } + // update existing library item const newItem = await repo.save({ ...libraryItem, - id: existingLibraryItem.id, + id, slug: existingLibraryItem.slug, // keep the original slug }) // delete the new item if it's different from the existing one - if (libraryItem.id && libraryItem.id !== existingLibraryItem.id) { + if (libraryItem.id && libraryItem.id !== id) { await repo.delete(libraryItem.id) } diff --git a/packages/api/src/services/save_page.ts b/packages/api/src/services/save_page.ts index e4f065e94..f79b28b52 100644 --- a/packages/api/src/services/save_page.ts +++ b/packages/api/src/services/save_page.ts @@ -24,7 +24,7 @@ import { parsePreparedContent } from '../utils/parser' import { contentReaderForLibraryItem } from '../utils/uploads' import { createPageSaveRequest } from './create_page_save_request' import { createHighlight } from './highlights' -import { createAndSaveLabelsInLibraryItem } from './labels' +import { createAndAddLabelsToLibraryItem } from './labels' import { createOrUpdateLibraryItem } from './library_item' // where we can use APIs to fetch their underlying content. @@ -132,7 +132,8 @@ export const savePage = async ( ) clientRequestId = newItem.id - await createAndSaveLabelsInLibraryItem( + // merge labels + await createAndAddLabelsToLibraryItem( clientRequestId, user.id, input.labels, @@ -157,6 +158,7 @@ export const savePage = async ( libraryItem: { id: clientRequestId }, } + // merge highlights try { await createHighlight(highlight, clientRequestId, user.id) } catch (error) { @@ -214,7 +216,7 @@ export const parsedContentToLibraryItem = ({ rssFeedUrl?: string | null folder?: string | null }): DeepPartial & { originalUrl: string } => { - logger.info('save_page: state', { url, state, itemId }) + logger.info('save_page', { url, state, itemId }) return { id: itemId || undefined, slug, @@ -254,5 +256,6 @@ export const parsedContentToLibraryItem = ({ folder: folder || 'inbox', archivedAt: state === ArticleSavingRequestStatus.Archived ? new Date() : null, + deletedAt: state === ArticleSavingRequestStatus.Deleted ? new Date() : null, } } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index c93a388b3..27bbf7818 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -23,16 +23,17 @@ import { getRepository } from '../../src/repository' import { createGroup, deleteGroup } from '../../src/services/groups' import { createLabel, deleteLabels } from '../../src/services/labels' import { - createOrUpdateLibraryItem, createLibraryItems, + createOrUpdateLibraryItem, + CreateOrUpdateLibraryItemArgs, deleteLibraryItemById, deleteLibraryItemByUrl, deleteLibraryItems, deleteLibraryItemsByUserId, findLibraryItemById, findLibraryItemByUrl, + softDeleteLibraryItem, updateLibraryItem, - CreateOrUpdateLibraryItemArgs, } from '../../src/services/library_item' import { deleteUser } from '../../src/services/user' import * as createTask from '../../src/utils/createTask' @@ -712,7 +713,7 @@ describe('Article API', () => { await deleteLibraryItemById(itemId, user.id) }) - it('marks an article as deleted', async () => { + it('soft deletes the item', async () => { await graphqlRequest(setBookmarkQuery(itemId, false), authToken).expect( 200 ) @@ -2062,11 +2063,8 @@ describe('Article API', () => { // Delete some items for (let i = 0; i < 3; i++) { - await updateLibraryItem( - items[i].id, - { state: LibraryItemState.Deleted, deletedAt: new Date() }, - user.id - ) + await softDeleteLibraryItem(items[i].id, user.id) + deletedItems.push(items[i]) } })