diff --git a/packages/api/src/elastic/pages.ts b/packages/api/src/elastic/pages.ts index c0abb1a54..10a5cf31d 100644 --- a/packages/api/src/elastic/pages.ts +++ b/packages/api/src/elastic/pages.ts @@ -644,3 +644,65 @@ export const searchAsYouType = async ( return [] } } + +export const archiveAll = async ( + userId: string, + ctx: PageContext +): Promise => { + const archivedAt = new Date() + try { + const { body } = await client.updateByQuery({ + index: INDEX_ALIAS, + conflicts: 'proceed', + body: { + query: { + bool: { + filter: [ + { + term: { + userId, + }, + }, + { + terms: { + state: [ + ArticleSavingRequestStatus.Succeeded, + ArticleSavingRequestStatus.Failed, + ], + }, + }, + ], + must_not: [ + { + exists: { + field: 'archivedAt', + }, + }, + ], + }, + }, + script: { + source: 'ctx._source.archivedAt = params.archivedAt', + lang: 'painless', + params: { + archivedAt, + }, + }, + }, + }) + + // 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) + return false + } + + await ctx.pubsub.entityUpdated(EntityType.PAGE, { archivedAt }, ctx.uid) + + console.log('archived pages in elastic', body.updated) + return true + } catch (e) { + console.log('failed to archive pages in elastic', e) + return false + } +} diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index f6cfc874c..fb11a4081 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -62,6 +62,23 @@ 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; @@ -1164,6 +1181,7 @@ export type MoveLabelSuccess = { export type Mutation = { __typename?: 'Mutation'; addPopularRead: AddPopularReadResult; + archiveAll: ArchiveAllResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; createGroup: CreateGroupResult; @@ -3179,6 +3197,10 @@ 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; @@ -3654,6 +3676,9 @@ 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']; @@ -4065,6 +4090,20 @@ 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; @@ -4871,6 +4910,7 @@ export type MoveLabelSuccessResolvers = { addPopularRead?: Resolver>; + archiveAll?: Resolver; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; createGroup?: Resolver>; @@ -5890,6 +5930,9 @@ export type Resolvers = { ApiKeysError?: ApiKeysErrorResolvers; ApiKeysResult?: ApiKeysResultResolvers; ApiKeysSuccess?: ApiKeysSuccessResolvers; + ArchiveAllError?: ArchiveAllErrorResolvers; + ArchiveAllResult?: ArchiveAllResultResolvers; + ArchiveAllSuccess?: ArchiveAllSuccessResolvers; ArchiveLinkError?: ArchiveLinkErrorResolvers; ArchiveLinkResult?: ArchiveLinkResultResolvers; ArchiveLinkSuccess?: ArchiveLinkSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 4154541a5..f565c867a 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -41,6 +41,21 @@ 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! @@ -1036,6 +1051,7 @@ type MoveLabelSuccess { type Mutation { addPopularRead(name: String!): AddPopularReadResult! + archiveAll: ArchiveAllResult! 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 e7ba636a9..f5bac64f0 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -4,6 +4,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { + ArchiveAllError, + ArchiveAllErrorCode, + ArchiveAllSuccess, Article, ArticleError, ArticleErrorCode, @@ -89,6 +92,7 @@ import { SearchItem as SearchItemData, } from '../../elastic/types' import { + archiveAll, createPage, getPageById, getPageByParam, @@ -1043,6 +1047,32 @@ export const updatesSinceResolver = authorized< } ) +export const archiveAllResolver = authorized< + ArchiveAllSuccess, + ArchiveAllError +>(async (_parent, _input, ctx) => { + const { + claims: { uid }, + log, + } = ctx + log.info('archiveAllResolver') + + if (!uid) { + log.error('archiveAllResolver', { error: 'Unauthorized' }) + return { errorCodes: [ArchiveAllErrorCode.Unauthorized] } + } + + analytics.track({ + userId: uid, + event: 'archiveAll', + properties: { + env: env.server.apiEnv, + }, + }) + + return { success: await archiveAll(uid, ctx) } +}) + const getUpdateReason = (page: Page, since: Date) => { if (page.state === ArticleSavingRequestStatus.Deleted) { return UpdateReason.Deleted diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index ea5ff9ecf..2270561db 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -23,6 +23,7 @@ import { import { addPopularReadResolver, apiKeysResolver, + archiveAllResolver, articleSavingRequestResolver, createArticleResolver, createArticleSavingRequestResolver, @@ -201,6 +202,7 @@ export const functionResolvers = { leaveGroup: leaveGroupResolver, uploadImportFile: uploadImportFileResolver, markEmailAsItem: markEmailAsItemResolver, + archiveAll: archiveAllResolver, }, Query: { me: getMeUserResolver, @@ -664,4 +666,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('UploadImportFile'), ...resultResolveTypeResolver('RecentEmails'), ...resultResolveTypeResolver('MarkEmailAsItem'), + ...resultResolveTypeResolver('ArchiveAll'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 64915652d..3011c2c29 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2373,6 +2373,20 @@ const schema = gql` NOT_FOUND } + union ArchiveAllResult = ArchiveAllSuccess | ArchiveAllError + + type ArchiveAllSuccess { + success: Boolean! + } + + type ArchiveAllError { + errorCodes: [ArchiveAllErrorCode!]! + } + + enum ArchiveAllErrorCode { + UNAUTHORIZED + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2462,6 +2476,7 @@ const schema = gql` contentType: String! ): UploadImportFileResult! markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult! + archiveAll: ArchiveAllResult! } # 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 a539101a4..3652551ad 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -1084,4 +1084,60 @@ describe('Article API', () => { ) }) }) + + describe('ArchiveAll API', () => { + const archiveAllQuery = ` + mutation { + archiveAll { + ... on ArchiveAllSuccess { + success + } + ... on ArchiveAllError { + errorCodes + } + } + } + ` + + before(async () => { + // Create some test pages + for (let i = 0; i < 5; i++) { + await createPage( + { + id: '', + hash: '', + userId: user.id, + pageType: PageType.Article, + title: 'test page', + content: '', + slug: '', + createdAt: new Date(), + updatedAt: new Date(), + readingProgressPercent: 0, + readingProgressAnchorIndex: 0, + url: '', + savedAt: new Date(), + state: + i == 0 + ? ArticleSavingRequestStatus.Failed + : ArticleSavingRequestStatus.Succeeded, + }, + ctx + ) + } + }) + + after(async () => { + // Delete all pages + 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 + await refreshIndex() + const pages = await graphqlRequest(searchQuery(), authToken).expect(200) + expect(pages.body.data.search.pageInfo.totalCount).to.eql(0) + }) + }) })