diff --git a/packages/api/src/elastic/pages.ts b/packages/api/src/elastic/pages.ts index 84c68ba64..34093b5f1 100644 --- a/packages/api/src/elastic/pages.ts +++ b/packages/api/src/elastic/pages.ts @@ -22,6 +22,7 @@ import { import { client, INDEX_ALIAS } from './index' import { EntityType } from '../datalayer/pubsub' import { ResponseError } from '@elastic/elasticsearch/lib/errors' +import { BulkActionType } from '../generated/graphql' const appendQuery = (body: SearchBody, query: string): void => { body.query.bool.should.push({ @@ -645,11 +646,27 @@ export const searchAsYouType = async ( } } -export const archiveAllAsync = async ( +export const updatePagesAsync = async ( userId: string, - ctx: PageContext + action: BulkActionType, + args?: PageSearchArgs ): Promise => { - const archivedAt = new Date() + // default action is archive + let must_not = [ + { + exists: { + field: 'archivedAt', + }, + }, + ] + let params: Record = { archivedAt: new Date() } + if (action === BulkActionType.Delete) { + must_not = [] + params = { state: ArticleSavingRequestStatus.Deleted } + } + // get update field + const field = Object.keys(params)[0] + try { const { body } = await client.updateByQuery({ index: INDEX_ALIAS, @@ -673,37 +690,27 @@ export const archiveAllAsync = async ( }, }, ], - must_not: [ - { - exists: { - field: 'archivedAt', - }, - }, - ], + must_not, }, }, script: { - source: 'ctx._source.archivedAt = params.archivedAt', + source: `ctx._source.${field} = params.${field}`, lang: 'painless', - params: { - archivedAt, - }, + params, }, }, }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (body.failures?.length > 0) { - console.log('failed to archive pages in elastic', body.failures) + console.log('failed to update pages in elastic', body.failures) return null } - await ctx.pubsub.entityUpdated(EntityType.PAGE, { archivedAt }, ctx.uid) - - console.log('archived all task started', body.task) + console.log('update pages task started', body.task) return body.task as string } catch (e) { - console.log('failed to archive all in elastic', e) + console.log('failed to update pages in elastic', e) return null } } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index fb11a4081..161a0dca3 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -62,23 +62,6 @@ export type ApiKeysSuccess = { apiKeys: Array; }; -export type ArchiveAllError = { - __typename?: 'ArchiveAllError'; - errorCodes: Array; -}; - -export enum ArchiveAllErrorCode { - BadRequest = 'BAD_REQUEST', - Unauthorized = 'UNAUTHORIZED' -} - -export type ArchiveAllResult = ArchiveAllError | ArchiveAllSuccess; - -export type ArchiveAllSuccess = { - __typename?: 'ArchiveAllSuccess'; - success: Scalars['Boolean']; -}; - export type ArchiveLinkError = { __typename?: 'ArchiveLinkError'; errorCodes: Array; @@ -235,6 +218,27 @@ export type ArticlesSuccess = { pageInfo: PageInfo; }; +export type BulkActionError = { + __typename?: 'BulkActionError'; + errorCodes: Array; +}; + +export enum BulkActionErrorCode { + Unauthorized = 'UNAUTHORIZED' +} + +export type BulkActionResult = BulkActionError | BulkActionSuccess; + +export type BulkActionSuccess = { + __typename?: 'BulkActionSuccess'; + success: Scalars['Boolean']; +}; + +export enum BulkActionType { + Archive = 'ARCHIVE', + Delete = 'DELETE' +} + export enum ContentReader { Pdf = 'PDF', Web = 'WEB' @@ -1181,7 +1185,7 @@ export type MoveLabelSuccess = { export type Mutation = { __typename?: 'Mutation'; addPopularRead: AddPopularReadResult; - archiveAll: ArchiveAllResult; + bulkAction: BulkActionResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; createGroup: CreateGroupResult; @@ -1255,6 +1259,12 @@ export type MutationAddPopularReadArgs = { }; +export type MutationBulkActionArgs = { + action: BulkActionType; + query?: InputMaybe; +}; + + export type MutationCreateArticleArgs = { input: CreateArticleInput; }; @@ -3197,10 +3207,6 @@ export type ResolversTypes = { ApiKeysErrorCode: ApiKeysErrorCode; ApiKeysResult: ResolversTypes['ApiKeysError'] | ResolversTypes['ApiKeysSuccess']; ApiKeysSuccess: ResolverTypeWrapper; - ArchiveAllError: ResolverTypeWrapper; - ArchiveAllErrorCode: ArchiveAllErrorCode; - ArchiveAllResult: ResolversTypes['ArchiveAllError'] | ResolversTypes['ArchiveAllSuccess']; - ArchiveAllSuccess: ResolverTypeWrapper; ArchiveLinkError: ResolverTypeWrapper; ArchiveLinkErrorCode: ArchiveLinkErrorCode; ArchiveLinkInput: ArchiveLinkInput; @@ -3224,6 +3230,11 @@ export type ResolversTypes = { ArticlesResult: ResolversTypes['ArticlesError'] | ResolversTypes['ArticlesSuccess']; ArticlesSuccess: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; + BulkActionError: ResolverTypeWrapper; + BulkActionErrorCode: BulkActionErrorCode; + BulkActionResult: ResolversTypes['BulkActionError'] | ResolversTypes['BulkActionSuccess']; + BulkActionSuccess: ResolverTypeWrapper; + BulkActionType: BulkActionType; ContentReader: ContentReader; CreateArticleError: ResolverTypeWrapper; CreateArticleErrorCode: CreateArticleErrorCode; @@ -3676,9 +3687,6 @@ export type ResolversParentTypes = { ApiKeysError: ApiKeysError; ApiKeysResult: ResolversParentTypes['ApiKeysError'] | ResolversParentTypes['ApiKeysSuccess']; ApiKeysSuccess: ApiKeysSuccess; - ArchiveAllError: ArchiveAllError; - ArchiveAllResult: ResolversParentTypes['ArchiveAllError'] | ResolversParentTypes['ArchiveAllSuccess']; - ArchiveAllSuccess: ArchiveAllSuccess; ArchiveLinkError: ArchiveLinkError; ArchiveLinkInput: ArchiveLinkInput; ArchiveLinkResult: ResolversParentTypes['ArchiveLinkError'] | ResolversParentTypes['ArchiveLinkSuccess']; @@ -3697,6 +3705,9 @@ export type ResolversParentTypes = { ArticlesResult: ResolversParentTypes['ArticlesError'] | ResolversParentTypes['ArticlesSuccess']; ArticlesSuccess: ArticlesSuccess; Boolean: Scalars['Boolean']; + BulkActionError: BulkActionError; + BulkActionResult: ResolversParentTypes['BulkActionError'] | ResolversParentTypes['BulkActionSuccess']; + BulkActionSuccess: BulkActionSuccess; CreateArticleError: CreateArticleError; CreateArticleInput: CreateArticleInput; CreateArticleResult: ResolversParentTypes['CreateArticleError'] | ResolversParentTypes['CreateArticleSuccess']; @@ -4090,20 +4101,6 @@ export type ApiKeysSuccessResolvers; }; -export type ArchiveAllErrorResolvers = { - errorCodes?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - -export type ArchiveAllResultResolvers = { - __resolveType: TypeResolveFn<'ArchiveAllError' | 'ArchiveAllSuccess', ParentType, ContextType>; -}; - -export type ArchiveAllSuccessResolvers = { - success?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type ArchiveLinkErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; message?: Resolver; @@ -4225,6 +4222,20 @@ export type ArticlesSuccessResolvers; }; +export type BulkActionErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type BulkActionResultResolvers = { + __resolveType: TypeResolveFn<'BulkActionError' | 'BulkActionSuccess', ParentType, ContextType>; +}; + +export type BulkActionSuccessResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateArticleErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4910,7 +4921,7 @@ export type MoveLabelSuccessResolvers = { addPopularRead?: Resolver>; - archiveAll?: Resolver; + bulkAction?: Resolver>; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; createGroup?: Resolver>; @@ -5930,9 +5941,6 @@ export type Resolvers = { ApiKeysError?: ApiKeysErrorResolvers; ApiKeysResult?: ApiKeysResultResolvers; ApiKeysSuccess?: ApiKeysSuccessResolvers; - ArchiveAllError?: ArchiveAllErrorResolvers; - ArchiveAllResult?: ArchiveAllResultResolvers; - ArchiveAllSuccess?: ArchiveAllSuccessResolvers; ArchiveLinkError?: ArchiveLinkErrorResolvers; ArchiveLinkResult?: ArchiveLinkResultResolvers; ArchiveLinkSuccess?: ArchiveLinkSuccessResolvers; @@ -5948,6 +5956,9 @@ export type Resolvers = { ArticlesError?: ArticlesErrorResolvers; ArticlesResult?: ArticlesResultResolvers; ArticlesSuccess?: ArticlesSuccessResolvers; + BulkActionError?: BulkActionErrorResolvers; + BulkActionResult?: BulkActionResultResolvers; + BulkActionSuccess?: BulkActionSuccessResolvers; CreateArticleError?: CreateArticleErrorResolvers; CreateArticleResult?: CreateArticleResultResolvers; CreateArticleSavingRequestError?: CreateArticleSavingRequestErrorResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index f565c867a..34968394b 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -41,21 +41,6 @@ type ApiKeysSuccess { apiKeys: [ApiKey!]! } -type ArchiveAllError { - errorCodes: [ArchiveAllErrorCode!]! -} - -enum ArchiveAllErrorCode { - BAD_REQUEST - UNAUTHORIZED -} - -union ArchiveAllResult = ArchiveAllError | ArchiveAllSuccess - -type ArchiveAllSuccess { - success: Boolean! -} - type ArchiveLinkError { errorCodes: [ArchiveLinkErrorCode!]! message: String! @@ -194,6 +179,25 @@ type ArticlesSuccess { pageInfo: PageInfo! } +type BulkActionError { + errorCodes: [BulkActionErrorCode!]! +} + +enum BulkActionErrorCode { + UNAUTHORIZED +} + +union BulkActionResult = BulkActionError | BulkActionSuccess + +type BulkActionSuccess { + success: Boolean! +} + +enum BulkActionType { + ARCHIVE + DELETE +} + enum ContentReader { PDF WEB @@ -1051,7 +1055,7 @@ type MoveLabelSuccess { type Mutation { addPopularRead(name: String!): AddPopularReadResult! - archiveAll: ArchiveAllResult! + bulkAction(action: BulkActionType!, query: String): BulkActionResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! createGroup(input: CreateGroupInput!): CreateGroupResult! diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 1b429b9db..a7cd6c906 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -4,20 +4,21 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { - ArchiveAllError, - ArchiveAllErrorCode, - ArchiveAllSuccess, Article, ArticleError, ArticleErrorCode, ArticlesError, ArticleSuccess, + BulkActionError, + BulkActionErrorCode, + BulkActionSuccess, ContentReader, CreateArticleError, CreateArticleErrorCode, CreateArticleSuccess, FeedArticle, InputMaybe, + MutationBulkActionArgs, MutationCreateArticleArgs, MutationSaveArticleReadingProgressArgs, MutationSetBookmarkArticleArgs, @@ -92,13 +93,13 @@ import { SearchItem as SearchItemData, } from '../../elastic/types' import { - archiveAllAsync, createPage, getPageById, getPageByParam, searchAsYouType, searchPages, updatePage, + updatePagesAsync, } from '../../elastic/pages' import { searchHighlights } from '../../elastic/highlights' import { saveSearchHistory } from '../../services/search_history' @@ -1047,31 +1048,30 @@ export const updatesSinceResolver = authorized< } ) -export const archiveAllResolver = authorized< - ArchiveAllSuccess, - ArchiveAllError ->(async (_parent, _input, ctx) => { - const { - claims: { uid }, - log, - } = ctx - log.info('archiveAllResolver') +export const bulkActionResolver = authorized< + BulkActionSuccess, + BulkActionError, + MutationBulkActionArgs +>(async (_parent, { action }, { claims: { uid }, log }) => { + log.info('bulkActionResolver') if (!uid) { - log.error('archiveAllResolver', { error: 'Unauthorized' }) - return { errorCodes: [ArchiveAllErrorCode.Unauthorized] } + log.error('bulkActionResolver', { error: 'Unauthorized' }) + return { errorCodes: [BulkActionErrorCode.Unauthorized] } } analytics.track({ userId: uid, - event: 'archiveAll', + event: 'BulkAction', properties: { env: env.server.apiEnv, }, }) - // start a task to archive all pages - const taskId = await archiveAllAsync(uid, ctx) + // TODO: get search filters from query + + // start a task to update pages + const taskId = await updatePagesAsync(uid, action) return { success: !!taskId } }) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 2270561db..e1d7e6c9a 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -23,8 +23,8 @@ import { import { addPopularReadResolver, apiKeysResolver, - archiveAllResolver, articleSavingRequestResolver, + bulkActionResolver, createArticleResolver, createArticleSavingRequestResolver, createGroupResolver, @@ -202,7 +202,7 @@ export const functionResolvers = { leaveGroup: leaveGroupResolver, uploadImportFile: uploadImportFileResolver, markEmailAsItem: markEmailAsItemResolver, - archiveAll: archiveAllResolver, + bulkAction: bulkActionResolver, }, Query: { me: getMeUserResolver, @@ -666,5 +666,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('UploadImportFile'), ...resultResolveTypeResolver('RecentEmails'), ...resultResolveTypeResolver('MarkEmailAsItem'), - ...resultResolveTypeResolver('ArchiveAll'), + ...resultResolveTypeResolver('BulkAction'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 3011c2c29..5d4353c0b 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2373,17 +2373,22 @@ const schema = gql` NOT_FOUND } - union ArchiveAllResult = ArchiveAllSuccess | ArchiveAllError + enum BulkActionType { + DELETE + ARCHIVE + } - type ArchiveAllSuccess { + union BulkActionResult = BulkActionSuccess | BulkActionError + + type BulkActionSuccess { success: Boolean! } - type ArchiveAllError { - errorCodes: [ArchiveAllErrorCode!]! + type BulkActionError { + errorCodes: [BulkActionErrorCode!]! } - enum ArchiveAllErrorCode { + enum BulkActionErrorCode { UNAUTHORIZED } @@ -2476,7 +2481,7 @@ const schema = gql` contentType: String! ): UploadImportFileResult! markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult! - archiveAll: ArchiveAllResult! + bulkAction(query: String, action: BulkActionType!): BulkActionResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index e79344705..77fd10fad 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -11,6 +11,7 @@ import 'mocha' import { User } from '../../src/entity/user' import chaiString from 'chai-string' import { + BulkActionType, SyncUpdatedItemEdge, UpdateReason, UploadFileStatus, @@ -1085,14 +1086,14 @@ describe('Article API', () => { }) }) - describe('ArchiveAll API', () => { - const archiveAllQuery = ` + describe('BulkAction API', () => { + const bulkActionQuery = (action: BulkActionType) => ` mutation { - archiveAll { - ... on ArchiveAllSuccess { + bulkAction (action: ${action}) { + ... on BulkActionSuccess { success } - ... on ArchiveAllError { + ... on BulkActionError { errorCodes } } @@ -1132,14 +1133,39 @@ describe('Article API', () => { await deletePagesByParam({ userId: user.id }, ctx) }) - it('archives all pages', async () => { - const res = await graphqlRequest(archiveAllQuery, authToken).expect(200) - expect(res.body.data.archiveAll.success).to.be.true - // Wait for the archive to finish - await setTimeout(async () => { - const pages = await graphqlRequest(searchQuery(), authToken).expect(200) - expect(pages.body.data.search.pageInfo.totalCount).to.eql(0) - }, 1000) + context('when action is Archive', () => { + it('archives all pages', async () => { + const res = await graphqlRequest( + bulkActionQuery(BulkActionType.Archive), + authToken + ).expect(200) + expect(res.body.data.bulkAction.success).to.be.true + // Wait for the archive to finish + await setTimeout(async () => { + const pages = await graphqlRequest(searchQuery(), authToken).expect( + 200 + ) + expect(pages.body.data.search.pageInfo.totalCount).to.eql(0) + }, 1000) + }) + }) + + context('when action is Delete', () => { + it('deletes all pages', async () => { + const res = await graphqlRequest( + bulkActionQuery(BulkActionType.Delete), + authToken + ).expect(200) + expect(res.body.data.bulkAction.success).to.be.true + // Wait for the delete to finish + await setTimeout(async () => { + const pages = await graphqlRequest( + searchQuery('in:all'), + authToken + ).expect(200) + expect(pages.body.data.search.pageInfo.totalCount).to.eql(0) + }, 1000) + }) }) }) })