mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1759 from omnivore-app/archive-all-api
bulk action api
This commit is contained in:
commit
1458395630
7 changed files with 284 additions and 0 deletions
|
|
@ -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({
|
||||
|
|
@ -644,3 +645,72 @@ export const searchAsYouType = async (
|
|||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const updatePagesAsync = async (
|
||||
userId: string,
|
||||
action: BulkActionType,
|
||||
args?: PageSearchArgs
|
||||
): Promise<string | null> => {
|
||||
// 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,
|
||||
conflicts: 'proceed',
|
||||
wait_for_completion: false,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
state: [
|
||||
ArticleSavingRequestStatus.Succeeded,
|
||||
ArticleSavingRequestStatus.Failed,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not,
|
||||
},
|
||||
},
|
||||
script: {
|
||||
source: `ctx._source.${field} = params.${field}`,
|
||||
lang: 'painless',
|
||||
params,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (body.failures?.length > 0) {
|
||||
console.log('failed to update pages in elastic', body.failures)
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('update pages task started', body.task)
|
||||
return body.task as string
|
||||
} catch (e) {
|
||||
console.log('failed to update pages in elastic', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,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'
|
||||
|
|
@ -1164,6 +1185,7 @@ export type MoveLabelSuccess = {
|
|||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
addPopularRead: AddPopularReadResult;
|
||||
bulkAction: BulkActionResult;
|
||||
createArticle: CreateArticleResult;
|
||||
createArticleSavingRequest: CreateArticleSavingRequestResult;
|
||||
createGroup: CreateGroupResult;
|
||||
|
|
@ -1237,6 +1259,12 @@ export type MutationAddPopularReadArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationBulkActionArgs = {
|
||||
action: BulkActionType;
|
||||
query?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateArticleArgs = {
|
||||
input: CreateArticleInput;
|
||||
};
|
||||
|
|
@ -3202,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;
|
||||
|
|
@ -3672,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'];
|
||||
|
|
@ -4186,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>;
|
||||
|
|
@ -4871,6 +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'>>;
|
||||
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'>>;
|
||||
|
|
@ -5905,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>;
|
||||
|
|
|
|||
|
|
@ -179,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
|
||||
|
|
@ -1036,6 +1055,7 @@ type MoveLabelSuccess {
|
|||
|
||||
type Mutation {
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
bulkAction(action: BulkActionType!, query: String): BulkActionResult!
|
||||
createArticle(input: CreateArticleInput!): CreateArticleResult!
|
||||
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@ import {
|
|||
ArticleErrorCode,
|
||||
ArticlesError,
|
||||
ArticleSuccess,
|
||||
BulkActionError,
|
||||
BulkActionErrorCode,
|
||||
BulkActionSuccess,
|
||||
ContentReader,
|
||||
CreateArticleError,
|
||||
CreateArticleErrorCode,
|
||||
CreateArticleSuccess,
|
||||
FeedArticle,
|
||||
InputMaybe,
|
||||
MutationBulkActionArgs,
|
||||
MutationCreateArticleArgs,
|
||||
MutationSaveArticleReadingProgressArgs,
|
||||
MutationSetBookmarkArticleArgs,
|
||||
|
|
@ -95,6 +99,7 @@ import {
|
|||
searchAsYouType,
|
||||
searchPages,
|
||||
updatePage,
|
||||
updatePagesAsync,
|
||||
} from '../../elastic/pages'
|
||||
import { searchHighlights } from '../../elastic/highlights'
|
||||
import { saveSearchHistory } from '../../services/search_history'
|
||||
|
|
@ -1043,6 +1048,34 @@ export const updatesSinceResolver = authorized<
|
|||
}
|
||||
)
|
||||
|
||||
export const bulkActionResolver = authorized<
|
||||
BulkActionSuccess,
|
||||
BulkActionError,
|
||||
MutationBulkActionArgs
|
||||
>(async (_parent, { action }, { claims: { uid }, log }) => {
|
||||
log.info('bulkActionResolver')
|
||||
|
||||
if (!uid) {
|
||||
log.error('bulkActionResolver', { error: 'Unauthorized' })
|
||||
return { errorCodes: [BulkActionErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'BulkAction',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: get search filters from query
|
||||
|
||||
// start a task to update pages
|
||||
const taskId = await updatePagesAsync(uid, action)
|
||||
|
||||
return { success: !!taskId }
|
||||
})
|
||||
|
||||
const getUpdateReason = (page: Page, since: Date) => {
|
||||
if (page.state === ArticleSavingRequestStatus.Deleted) {
|
||||
return UpdateReason.Deleted
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
addPopularReadResolver,
|
||||
apiKeysResolver,
|
||||
articleSavingRequestResolver,
|
||||
bulkActionResolver,
|
||||
createArticleResolver,
|
||||
createArticleSavingRequestResolver,
|
||||
createGroupResolver,
|
||||
|
|
@ -201,6 +202,7 @@ export const functionResolvers = {
|
|||
leaveGroup: leaveGroupResolver,
|
||||
uploadImportFile: uploadImportFileResolver,
|
||||
markEmailAsItem: markEmailAsItemResolver,
|
||||
bulkAction: bulkActionResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
|
|
@ -664,4 +666,5 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('UploadImportFile'),
|
||||
...resultResolveTypeResolver('RecentEmails'),
|
||||
...resultResolveTypeResolver('MarkEmailAsItem'),
|
||||
...resultResolveTypeResolver('BulkAction'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2373,6 +2373,25 @@ const schema = gql`
|
|||
NOT_FOUND
|
||||
}
|
||||
|
||||
enum BulkActionType {
|
||||
DELETE
|
||||
ARCHIVE
|
||||
}
|
||||
|
||||
union BulkActionResult = BulkActionSuccess | BulkActionError
|
||||
|
||||
type BulkActionSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type BulkActionError {
|
||||
errorCodes: [BulkActionErrorCode!]!
|
||||
}
|
||||
|
||||
enum BulkActionErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -2462,6 +2481,7 @@ const schema = gql`
|
|||
contentType: String!
|
||||
): UploadImportFileResult!
|
||||
markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult!
|
||||
bulkAction(query: String, action: BulkActionType!): BulkActionResult!
|
||||
}
|
||||
|
||||
# FIXME: remove sort from feedArticles after all cached tabs are closed
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'mocha'
|
|||
import { User } from '../../src/entity/user'
|
||||
import chaiString from 'chai-string'
|
||||
import {
|
||||
BulkActionType,
|
||||
SyncUpdatedItemEdge,
|
||||
UpdateReason,
|
||||
UploadFileStatus,
|
||||
|
|
@ -1084,4 +1085,87 @@ describe('Article API', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BulkAction API', () => {
|
||||
const bulkActionQuery = (action: BulkActionType) => `
|
||||
mutation {
|
||||
bulkAction (action: ${action}) {
|
||||
... on BulkActionSuccess {
|
||||
success
|
||||
}
|
||||
... on BulkActionError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
before(async () => {
|
||||
// Create some test pages
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createPage(
|
||||
{
|
||||
id: '',
|
||||
hash: '',
|
||||
userId: user.id,
|
||||
pageType: i == 0 ? PageType.Article : PageType.File,
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue