allow mark as read and add labels bulk actions

This commit is contained in:
Hongbo Wu
2023-06-08 13:05:30 +08:00
parent 1af845e4e5
commit de3623d3f2
5 changed files with 197 additions and 201 deletions

View File

@ -18,6 +18,7 @@ import {
import { client, INDEX_ALIAS } from './index'
import {
ArticleSavingRequestStatus,
Label,
Page,
PageContext,
PageSearchArgs,
@ -424,103 +425,111 @@ export const getPageById = async (id: string): Promise<Page | undefined> => {
}
}
const buildSearchBody = (userId: string, args: PageSearchArgs) => {
const {
query,
readFilter = ReadFilter.ALL,
typeFilter,
labelFilters,
inFilter = InFilter.ALL,
hasFilters,
dateFilters,
termFilters,
matchFilters,
ids,
noFilters,
siteName,
} = args
const includeLabels = labelFilters?.filter(
(filter) => filter.type === LabelFilterType.INCLUDE
)
const excludeLabels = labelFilters?.filter(
(filter) => filter.type === LabelFilterType.EXCLUDE
)
// start building the query
let builder = esBuilder().query('term', { userId })
// append filters
if (query) {
builder = appendQuery(builder, query)
}
if (typeFilter) {
builder = appendTypeFilter(builder, typeFilter)
}
if (inFilter !== InFilter.ALL) {
builder = appendInFilter(builder, inFilter)
}
if (readFilter !== ReadFilter.ALL) {
builder = appendReadFilter(builder, readFilter)
}
if (hasFilters && hasFilters.length > 0) {
builder = appendHasFilters(builder, hasFilters)
}
if (includeLabels && includeLabels.length > 0) {
builder = appendIncludeLabelFilter(builder, includeLabels)
}
if (excludeLabels && excludeLabels.length > 0) {
builder = appendExcludeLabelFilter(builder, excludeLabels)
}
if (dateFilters && dateFilters.length > 0) {
builder = appendDateFilters(builder, dateFilters)
}
if (termFilters) {
builder = appendTermFilters(builder, termFilters)
}
if (matchFilters) {
builder = appendMatchFilters(builder, matchFilters)
}
if (ids && ids.length > 0) {
builder = appendIdsFilter(builder, ids)
}
if (args.recommendedBy) {
builder = appendRecommendedBy(builder, args.recommendedBy)
}
if (!args.includePending) {
builder = builder.notQuery('term', {
state: ArticleSavingRequestStatus.Processing,
})
}
if (!args.includeDeleted) {
builder = builder.notQuery('term', {
state: ArticleSavingRequestStatus.Deleted,
})
}
if (noFilters) {
builder = appendNoFilters(builder, noFilters)
}
if (siteName) {
builder = appendSiteNameFilter(builder, siteName)
}
return builder
}
export const searchPages = async (
args: PageSearchArgs,
userId: string
): Promise<[Page[], number] | undefined> => {
try {
const {
from = 0,
size = 10,
sort,
query,
readFilter = ReadFilter.ALL,
typeFilter,
labelFilters,
inFilter = InFilter.ALL,
hasFilters,
dateFilters,
termFilters,
matchFilters,
ids,
includeContent,
noFilters,
siteName,
} = args
const { from = 0, size = 10, sort, includeContent } = args
// default order is descending
const sortOrder = sort?.order || SortOrder.DESCENDING
// default sort by saved_at
const sortField = sort?.by || SortBy.SAVED
const includeLabels = labelFilters?.filter(
(filter) => filter.type === LabelFilterType.INCLUDE
)
const excludeLabels = labelFilters?.filter(
(filter) => filter.type === LabelFilterType.EXCLUDE
)
// start building the query
let builder = esBuilder()
.query('term', { userId })
// build the query
const builder = buildSearchBody(userId, args)
const body = builder
.sort(sortField, sortOrder)
.from(from)
.size(size)
.rawOption('_source', {
excludes: includeContent ? [] : ['originalHtml', 'content'],
})
// append filters
if (query) {
builder = appendQuery(builder, query)
}
if (typeFilter) {
builder = appendTypeFilter(builder, typeFilter)
}
if (inFilter !== InFilter.ALL) {
builder = appendInFilter(builder, inFilter)
}
if (readFilter !== ReadFilter.ALL) {
builder = appendReadFilter(builder, readFilter)
}
if (hasFilters && hasFilters.length > 0) {
builder = appendHasFilters(builder, hasFilters)
}
if (includeLabels && includeLabels.length > 0) {
builder = appendIncludeLabelFilter(builder, includeLabels)
}
if (excludeLabels && excludeLabels.length > 0) {
builder = appendExcludeLabelFilter(builder, excludeLabels)
}
if (dateFilters && dateFilters.length > 0) {
builder = appendDateFilters(builder, dateFilters)
}
if (termFilters) {
builder = appendTermFilters(builder, termFilters)
}
if (matchFilters) {
builder = appendMatchFilters(builder, matchFilters)
}
if (ids && ids.length > 0) {
builder = appendIdsFilter(builder, ids)
}
if (args.recommendedBy) {
builder = appendRecommendedBy(builder, args.recommendedBy)
}
if (!args.includePending) {
builder = builder.notQuery('term', {
state: ArticleSavingRequestStatus.Processing,
})
}
if (!args.includeDeleted) {
builder = builder.notQuery('term', {
state: ArticleSavingRequestStatus.Deleted,
})
}
if (noFilters) {
builder = appendNoFilters(builder, noFilters)
}
if (siteName) {
builder = appendSiteNameFilter(builder, siteName)
}
// build the query
const body = builder.build()
.build()
console.debug('searching pages in elastic', JSON.stringify(body))
const response = await client.search<SearchResponse<Page>, BuiltQuery>({
@ -688,61 +697,83 @@ export const searchAsYouType = async (
export const updatePagesAsync = async (
userId: string,
action: BulkActionType
action: BulkActionType,
args: PageSearchArgs,
labels?: Label[]
): Promise<string | null> => {
// default action is archive
let must_not = [
{
exists: {
field: 'archivedAt',
},
},
]
let params: Record<string, unknown> = { archivedAt: new Date() }
if (action === BulkActionType.Delete) {
must_not = []
params = { state: ArticleSavingRequestStatus.Deleted }
// build the script
let script = {
source: '',
params: {},
}
// get update field
const field = Object.keys(params)[0]
switch (action) {
case BulkActionType.Archive:
script = {
source: `ctx._source.archivedAt = params.archivedAt`,
params: {
archivedAt: new Date(),
},
}
break
case BulkActionType.Delete:
script = {
source: `ctx._source.state = params.state`,
params: {
state: ArticleSavingRequestStatus.Deleted,
},
}
break
case BulkActionType.AddLabels:
script = {
source: `ctx._source.labels.addAll(params.labels)`,
params: {
labels,
},
}
break
case BulkActionType.MarkAsRead:
script = {
source: `ctx._source.readAt = params.readAt;
ctx._source.readingProgressPercent = params.readingProgressPercent;
ctx._source.readingProgressTopPercent = params.readingProgressTopPercent`,
params: {
readAt: new Date(),
readingProgressPercent: 100,
readingProgressTopPercent: 100,
},
}
break
default:
throw new Error('Invalid bulk action')
}
// add updatedAt to the script
const updatedScript = {
source: `${script.source}; ctx._source.updatedAt = params.updatedAt`,
lang: 'painless',
params: {
...script.params,
updatedAt: new Date(),
},
}
// build the query
const searchBody = buildSearchBody(userId, args)
.rawOption('script', updatedScript)
.build()
console.debug('updating pages in elastic', JSON.stringify(searchBody))
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,
ArticleSavingRequestStatus.Processing,
],
},
},
],
must_not,
},
},
script: {
source: `ctx._source.${field} = params.${field}`,
lang: 'painless',
params,
},
},
body: searchBody,
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (body.failures?.length > 0) {
if (body.failures && body.failures.length > 0) {
console.log('failed to update pages in elastic', body.failures)
return null
}

View File

@ -222,34 +222,16 @@ export type ArticlesSuccess = {
pageInfo: PageInfo;
};
export type BulkAction = {
itemIds: Array<Scalars['ID']>;
itemType: BulkActionItemType;
labelIds?: InputMaybe<Array<Scalars['ID']>>;
type: BulkActionType;
};
export type BulkActionError = {
__typename?: 'BulkActionError';
errorCodes: Array<BulkActionErrorCode>;
};
export enum BulkActionErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type BulkActionInput = {
actions: Array<BulkAction>;
query?: InputMaybe<Scalars['String']>;
};
export enum BulkActionItemType {
Highlight = 'HIGHLIGHT',
Label = 'LABEL',
Page = 'PAGE',
Subscription = 'SUBSCRIPTION'
}
export type BulkActionResult = BulkActionError | BulkActionSuccess;
export type BulkActionSuccess = {
@ -1319,7 +1301,9 @@ export type MutationAddPopularReadArgs = {
export type MutationBulkActionArgs = {
input: BulkActionInput;
action: BulkActionType;
labelIds?: InputMaybe<Array<Scalars['ID']>>;
query: Scalars['String'];
};
@ -3313,11 +3297,8 @@ export type ResolversTypes = {
ArticlesResult: ResolversTypes['ArticlesError'] | ResolversTypes['ArticlesSuccess'];
ArticlesSuccess: ResolverTypeWrapper<ArticlesSuccess>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
BulkAction: BulkAction;
BulkActionError: ResolverTypeWrapper<BulkActionError>;
BulkActionErrorCode: BulkActionErrorCode;
BulkActionInput: BulkActionInput;
BulkActionItemType: BulkActionItemType;
BulkActionResult: ResolversTypes['BulkActionError'] | ResolversTypes['BulkActionSuccess'];
BulkActionSuccess: ResolverTypeWrapper<BulkActionSuccess>;
BulkActionType: BulkActionType;
@ -3796,9 +3777,7 @@ export type ResolversParentTypes = {
ArticlesResult: ResolversParentTypes['ArticlesError'] | ResolversParentTypes['ArticlesSuccess'];
ArticlesSuccess: ArticlesSuccess;
Boolean: Scalars['Boolean'];
BulkAction: BulkAction;
BulkActionError: BulkActionError;
BulkActionInput: BulkActionInput;
BulkActionResult: ResolversParentTypes['BulkActionError'] | ResolversParentTypes['BulkActionSuccess'];
BulkActionSuccess: BulkActionSuccess;
CreateArticleError: CreateArticleError;
@ -5037,7 +5016,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, 'input'>>;
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'action' | 'query'>>;
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'>>;

View File

@ -183,33 +183,15 @@ type ArticlesSuccess {
pageInfo: PageInfo!
}
input BulkAction {
itemIds: [ID!]!
itemType: BulkActionItemType!
labelIds: [ID!]
type: BulkActionType!
}
type BulkActionError {
errorCodes: [BulkActionErrorCode!]!
}
enum BulkActionErrorCode {
BAD_REQUEST
UNAUTHORIZED
}
input BulkActionInput {
actions: [BulkAction!]!
query: String
}
enum BulkActionItemType {
HIGHLIGHT
LABEL
PAGE
SUBSCRIPTION
}
union BulkActionResult = BulkActionError | BulkActionSuccess
type BulkActionSuccess {
@ -1111,7 +1093,7 @@ type MoveLabelSuccess {
type Mutation {
addPopularRead(name: String!): AddPopularReadResult!
bulkAction(input: BulkActionInput!): BulkActionResult!
bulkAction(action: BulkActionType!, labelIds: [ID!], query: String!): BulkActionResult!
createArticle(input: CreateArticleInput!): CreateArticleResult!
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
createGroup(input: CreateGroupInput!): CreateGroupResult!

View File

@ -31,6 +31,7 @@ import {
BulkActionError,
BulkActionErrorCode,
BulkActionSuccess,
BulkActionType,
ContentReader,
CreateArticleError,
CreateArticleErrorCode,
@ -73,7 +74,7 @@ import {
UpdatesSinceSuccess,
} from '../../generated/graphql'
import { createPageSaveRequest } from '../../services/create_page_save_request'
import { createLabels } from '../../services/labels'
import { createLabels, getLabelsByIds } from '../../services/labels'
import { parsedContentToPage } from '../../services/save_page'
import { traceAs } from '../../tracing'
import { Merge } from '../../util'
@ -1097,26 +1098,43 @@ export const bulkActionResolver = authorized<
BulkActionSuccess,
BulkActionError,
MutationBulkActionArgs
>(async (_parent, { action }, { claims: { uid }, log }) => {
>(async (_parent, { query, action, labelIds }, { 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,
action,
},
})
// TODO: get search filters from query
if (!uid) {
log.log('bulkActionResolver', { error: 'Unauthorized' })
return { errorCodes: [BulkActionErrorCode.Unauthorized] }
}
if (!query) {
log.log('bulkActionResolver', { error: 'no query' })
return { errorCodes: [BulkActionErrorCode.BadRequest] }
}
// get labels if needed
let labels = undefined
if (action === BulkActionType.AddLabels) {
if (!labelIds || labelIds.length === 0) {
return { errorCodes: [BulkActionErrorCode.BadRequest] }
}
labels = await getLabelsByIds(uid, labelIds)
}
// parse query
const searchQuery = parseSearchQuery(query)
// start a task to update pages
const taskId = await updatePagesAsync(uid, action)
const taskId = await updatePagesAsync(uid, action, searchQuery, labels)
return { success: !!taskId }
})

View File

@ -2420,25 +2420,6 @@ const schema = gql`
ADD_LABELS
}
input BulkAction {
itemType: BulkActionItemType!
type: BulkActionType!
itemIds: [ID!]!
labelIds: [ID!]
}
enum BulkActionItemType {
PAGE
HIGHLIGHT
LABEL
SUBSCRIPTION
}
input BulkActionInput {
query: String
actions: [BulkAction!]!
}
union BulkActionResult = BulkActionSuccess | BulkActionError
type BulkActionSuccess {
@ -2451,6 +2432,7 @@ const schema = gql`
enum BulkActionErrorCode {
UNAUTHORIZED
BAD_REQUEST
}
union ImportFromIntegrationResult =
@ -2559,7 +2541,11 @@ const schema = gql`
contentType: String!
): UploadImportFileResult!
markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult!
bulkAction(input: BulkActionInput!): BulkActionResult!
bulkAction(
query: String!
action: BulkActionType!
labelIds: [ID!]
): BulkActionResult!
importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
}