Change archiveAll to bulkAction which allows deletion too

This commit is contained in:
Hongbo Wu 2023-02-02 21:26:37 +08:00
parent 3b888bbb08
commit bb5c104a42
7 changed files with 171 additions and 118 deletions

View file

@ -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<string | null> => {
const archivedAt = new Date()
// default action is archive
let must_not = [
{
exists: {
field: 'archivedAt',
},
},
]
let params: Record<string, any> = { 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
}
}

View file

@ -62,23 +62,6 @@ 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>;
@ -235,6 +218,27 @@ export type ArticlesSuccess = {
pageInfo: PageInfo;
};
export type BulkActionError = {
__typename?: 'BulkActionError';
errorCodes: Array<BulkActionErrorCode>;
};
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<Scalars['String']>;
};
export type MutationCreateArticleArgs = {
input: CreateArticleInput;
};
@ -3197,10 +3207,6 @@ 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;
@ -3224,6 +3230,11 @@ export type ResolversTypes = {
ArticlesResult: ResolversTypes['ArticlesError'] | ResolversTypes['ArticlesSuccess'];
ArticlesSuccess: ResolverTypeWrapper<ArticlesSuccess>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
BulkActionError: ResolverTypeWrapper<BulkActionError>;
BulkActionErrorCode: BulkActionErrorCode;
BulkActionResult: ResolversTypes['BulkActionError'] | ResolversTypes['BulkActionSuccess'];
BulkActionSuccess: ResolverTypeWrapper<BulkActionSuccess>;
BulkActionType: BulkActionType;
ContentReader: ContentReader;
CreateArticleError: ResolverTypeWrapper<CreateArticleError>;
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<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>;
@ -4225,6 +4222,20 @@ export type ArticlesSuccessResolvers<ContextType = ResolverContext, ParentType e
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type BulkActionErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['BulkActionError'] = ResolversParentTypes['BulkActionError']> = {
errorCodes?: Resolver<Array<ResolversTypes['BulkActionErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type BulkActionResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['BulkActionResult'] = ResolversParentTypes['BulkActionResult']> = {
__resolveType: TypeResolveFn<'BulkActionError' | 'BulkActionSuccess', ParentType, ContextType>;
};
export type BulkActionSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['BulkActionSuccess'] = ResolversParentTypes['BulkActionSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateArticleErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateArticleError'] = ResolversParentTypes['CreateArticleError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateArticleErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -4910,7 +4921,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>;
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'action'>>;
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'>>;
@ -5930,9 +5941,6 @@ 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>;
@ -5948,6 +5956,9 @@ export type Resolvers<ContextType = ResolverContext> = {
ArticlesError?: ArticlesErrorResolvers<ContextType>;
ArticlesResult?: ArticlesResultResolvers<ContextType>;
ArticlesSuccess?: ArticlesSuccessResolvers<ContextType>;
BulkActionError?: BulkActionErrorResolvers<ContextType>;
BulkActionResult?: BulkActionResultResolvers<ContextType>;
BulkActionSuccess?: BulkActionSuccessResolvers<ContextType>;
CreateArticleError?: CreateArticleErrorResolvers<ContextType>;
CreateArticleResult?: CreateArticleResultResolvers<ContextType>;
CreateArticleSavingRequestError?: CreateArticleSavingRequestErrorResolvers<ContextType>;

View file

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

View file

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

View file

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

View file

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

View file

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