Add archiveAll API

This commit is contained in:
Hongbo Wu 2023-02-01 18:05:16 +08:00
parent 2e3f186352
commit 03ab96b7f8
7 changed files with 225 additions and 0 deletions

View file

@ -644,3 +644,65 @@ export const searchAsYouType = async (
return []
}
}
export const archiveAll = async (
userId: string,
ctx: PageContext
): Promise<boolean> => {
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
}
}

View file

@ -62,6 +62,23 @@ export type ApiKeysSuccess = {
apiKeys: Array<ApiKey>;
};
export type ArchiveAllError = {
__typename?: 'ArchiveAllError';
errorCodes: Array<ArchiveAllErrorCode>;
};
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<ArchiveLinkErrorCode>;
@ -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<ApiKeysSuccess>;
ArchiveAllError: ResolverTypeWrapper<ArchiveAllError>;
ArchiveAllErrorCode: ArchiveAllErrorCode;
ArchiveAllResult: ResolversTypes['ArchiveAllError'] | ResolversTypes['ArchiveAllSuccess'];
ArchiveAllSuccess: ResolverTypeWrapper<ArchiveAllSuccess>;
ArchiveLinkError: ResolverTypeWrapper<ArchiveLinkError>;
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<ContextType = ResolverContext, ParentType ex
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ArchiveAllErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ArchiveAllError'] = ResolversParentTypes['ArchiveAllError']> = {
errorCodes?: Resolver<Array<ResolversTypes['ArchiveAllErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ArchiveAllResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ArchiveAllResult'] = ResolversParentTypes['ArchiveAllResult']> = {
__resolveType: TypeResolveFn<'ArchiveAllError' | 'ArchiveAllSuccess', ParentType, ContextType>;
};
export type ArchiveAllSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ArchiveAllSuccess'] = ResolversParentTypes['ArchiveAllSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ArchiveLinkErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ArchiveLinkError'] = ResolversParentTypes['ArchiveLinkError']> = {
errorCodes?: Resolver<Array<ResolversTypes['ArchiveLinkErrorCode']>, ParentType, ContextType>;
message?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@ -4871,6 +4910,7 @@ export type MoveLabelSuccessResolvers<ContextType = ResolverContext, ParentType
export type MutationResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = {
addPopularRead?: Resolver<ResolversTypes['AddPopularReadResult'], ParentType, ContextType, RequireFields<MutationAddPopularReadArgs, 'name'>>;
archiveAll?: Resolver<ResolversTypes['ArchiveAllResult'], ParentType, ContextType>;
createArticle?: Resolver<ResolversTypes['CreateArticleResult'], ParentType, ContextType, RequireFields<MutationCreateArticleArgs, 'input'>>;
createArticleSavingRequest?: Resolver<ResolversTypes['CreateArticleSavingRequestResult'], ParentType, ContextType, RequireFields<MutationCreateArticleSavingRequestArgs, 'input'>>;
createGroup?: Resolver<ResolversTypes['CreateGroupResult'], ParentType, ContextType, RequireFields<MutationCreateGroupArgs, 'input'>>;
@ -5890,6 +5930,9 @@ export type Resolvers<ContextType = ResolverContext> = {
ApiKeysError?: ApiKeysErrorResolvers<ContextType>;
ApiKeysResult?: ApiKeysResultResolvers<ContextType>;
ApiKeysSuccess?: ApiKeysSuccessResolvers<ContextType>;
ArchiveAllError?: ArchiveAllErrorResolvers<ContextType>;
ArchiveAllResult?: ArchiveAllResultResolvers<ContextType>;
ArchiveAllSuccess?: ArchiveAllSuccessResolvers<ContextType>;
ArchiveLinkError?: ArchiveLinkErrorResolvers<ContextType>;
ArchiveLinkResult?: ArchiveLinkResultResolvers<ContextType>;
ArchiveLinkSuccess?: ArchiveLinkSuccessResolvers<ContextType>;

View file

@ -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!

View file

@ -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

View file

@ -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'),
}

View file

@ -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

View file

@ -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)
})
})
})