From 57fb9e52fdbcb10a95d08db38b9a50de2232285c Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 5 Jan 2023 17:12:15 -0500 Subject: [PATCH] Aggregations working --- .../aggregations/aggs_types/bucket_aggs.ts | 32 ++- .../server/client/user_actions/client.ts | 3 +- .../server/client/user_actions/connectors.ts | 240 ++++++++++++++++ .../cases/server/client/user_actions/get.ts | 152 +--------- .../api/user_actions/get_all_user_actions.ts | 2 + x-pack/plugins/cases/server/services/mocks.ts | 3 + .../server/services/user_actions/index.ts | 267 ++++++++++++++---- .../server/services/user_actions/types.ts | 11 +- 8 files changed, 507 insertions(+), 203 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/user_actions/connectors.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts index 7a6d94c31f291..20ebeb234b479 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts @@ -61,6 +61,36 @@ const existsSchema = s.object({ }) ), }); + +const rangeSchema = s.object({ + range: s.recordOf( + s.string(), + s.object({ + lt: s.maybe(s.string()), + lte: s.maybe(s.string()), + gt: s.maybe(s.string()), + gte: s.maybe(s.string()), + }) + ), +}); + +const termValueSchema = s.object({ + term: s.recordOf(s.string(), s.object({ value: s.string() })), +}); + +const nestedSchema = s.object({ + nested: s.object({ + path: s.string(), + query: s.object({ + bool: s.object({ + filter: s.arrayOf(termValueSchema), + }), + }), + }), +}); + +const arraySchema = s.arrayOf(s.oneOf([nestedSchema, rangeSchema])); + // TODO: it would be great if we could recursively build the schema since the aggregation have be nested // For more details see how the types are defined in the elasticsearch javascript client: // https://github.com/elastic/elasticsearch-js/blob/4ad5daeaf401ce8ebb28b940075e0a67e56ff9ce/src/api/typesWithBodyKey.ts#L5295 @@ -70,7 +100,7 @@ const boolSchema = s.object({ must_not: s.oneOf([termSchema, existsSchema]), }), s.object({ - filter: s.oneOf([termSchema, existsSchema]), + filter: s.oneOf([termSchema, existsSchema, arraySchema]), }), ]), }); diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts index d33485cf0ce0b..8290de10190a6 100644 --- a/x-pack/plugins/cases/server/client/user_actions/client.ts +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -8,7 +8,8 @@ import type { GetCaseConnectorsResponse } from '../../../common/api'; import type { ICaseUserActionsResponse } from '../typedoc_interfaces'; import type { CasesClientArgs } from '../types'; -import { get, getConnectors } from './get'; +import { get } from './get'; +import { getConnectors } from './connectors'; import type { GetConnectorsRequest, UserActionGet } from './types'; /** diff --git a/x-pack/plugins/cases/server/client/user_actions/connectors.ts b/x-pack/plugins/cases/server/client/user_actions/connectors.ts new file mode 100644 index 0000000000000..e6691e6ed86b5 --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/connectors.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual } from 'lodash'; + +import type { SavedObject } from '@kbn/core/server'; +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { + CaseUserActionResponse, + GetCaseConnectorsResponse, + CaseConnector, +} from '../../../common/api'; +import { GetCaseConnectorsResponseRt } from '../../../common/api'; +import { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions'; +import { createCaseError } from '../../common/error'; +import type { CasesClientArgs } from '..'; +import type { Authorization, OwnerEntity } from '../../authorization'; +import { Operations } from '../../authorization'; +import type { GetConnectorsRequest } from './types'; +import type { CaseConnectorActivity, PushInfo } from '../../services/user_actions/types'; +import type { CaseUserActionService } from '../../services'; + +export const getConnectors = async ( + { caseId }: GetConnectorsRequest, + clientArgs: CasesClientArgs +): Promise => { + const { + services: { userActionService }, + logger, + authorization, + actionsClient, + } = clientArgs; + + try { + const [connectors, latestUserAction] = await Promise.all([ + userActionService.getCaseConnectorInformation(caseId), + userActionService.getMostRecentUserAction(caseId), + ]); + + await checkConnectorsAuthorization({ authorization, connectors, latestUserAction }); + + const enrichedConnectors = await enrichConnectors({ + caseId, + actionsClient, + connectors, + latestUserAction, + userActionService, + }); + + const results: GetCaseConnectorsResponse = []; + + for (const enrichedConnector of enrichedConnectors) { + results.push({ + ...enrichedConnector.connector, + name: enrichedConnector.name, + needsToBePushed: hasDataToPush(enrichedConnector), + }); + } + + console.log('connectors response', JSON.stringify(results, null, 2)); + + return GetCaseConnectorsResponseRt.encode(results); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve the case connectors case id: ${caseId}: ${error}`, + error, + logger, + }); + } +}; + +const checkConnectorsAuthorization = async ({ + connectors, + latestUserAction, + authorization, +}: { + connectors: CaseConnectorActivity[]; + latestUserAction?: SavedObject; + authorization: PublicMethodsOf; +}) => { + const entities: OwnerEntity[] = latestUserAction + ? [{ owner: latestUserAction.attributes.owner, id: latestUserAction.id }] + : []; + + for (const connector of connectors) { + entities.push({ + owner: connector.fields.attributes.owner, + id: connector.connectorId, + }); + + if (connector.push) { + entities.push({ + owner: connector.push.attributes.owner, + id: connector.connectorId, + }); + } + } + + await authorization.ensureAuthorized({ + entities, + operation: Operations.getUserActions, + }); +}; + +interface EnrichedConnector { + connector: CaseConnector; + name: string; + pushInfo?: EnrichedPushInfo; + latestUserActionDate?: Date; +} + +const enrichConnectors = async ({ + caseId, + connectors, + latestUserAction, + actionsClient, + userActionService, +}: { + caseId: string; + connectors: CaseConnectorActivity[]; + latestUserAction?: SavedObject; + actionsClient: PublicMethodsOf; + userActionService: CaseUserActionService; +}): Promise => { + const pushInfo = await getPushInfo({ caseId, activity: connectors, userActionService }); + + const enrichedConnectors: EnrichedConnector[] = []; + + for (const aggregationConnector of connectors) { + const connectorDetails = await actionsClient.get({ id: aggregationConnector.connectorId }); + const connector = getConnectorInfoFromSavedObject(aggregationConnector.fields); + + const latestUserActionCreatedAt = getDate(latestUserAction?.attributes.created_at); + + if (connector != null) { + enrichedConnectors.push({ + connector, + name: connectorDetails.name, + pushInfo: pushInfo.get(aggregationConnector.connectorId), + latestUserActionDate: latestUserActionCreatedAt, + }); + } + } + + return enrichedConnectors; +}; + +const getPushInfo = async ({ + caseId, + activity, + userActionService, +}: { + caseId: string; + activity: CaseConnectorActivity[]; + userActionService: CaseUserActionService; +}): Promise> => { + const pushRequest: PushInfo[] = []; + + for (const connectorInfo of activity) { + const pushCreatedAt = getDate(connectorInfo.push?.attributes.created_at); + + if (connectorInfo.push != null && pushCreatedAt != null) { + pushRequest.push({ connectorId: connectorInfo.connectorId, date: pushCreatedAt }); + } + } + + if (pushRequest.length <= 0) { + return new Map(); + } + + const priorToPushFields = await userActionService.getConnectorFieldsBeforePushes( + caseId, + pushRequest + ); + + const enrichedPushInfo = new Map(); + for (const request of pushRequest) { + const connectorFieldsSO = priorToPushFields.get(request.connectorId); + const connectorFields = getConnectorInfoFromSavedObject(connectorFieldsSO); + + if (connectorFields != null) { + enrichedPushInfo.set(request.connectorId, { + pushDate: request.date, + connectorFieldsBeforePush: connectorFields, + }); + } + } + + return enrichedPushInfo; +}; + +const getDate = (timestamp: string | undefined): Date | undefined => { + if (timestamp == null) { + return; + } + + const date = new Date(timestamp); + + if (isDateValid(date)) { + return date; + } +}; + +const isDateValid = (date: Date): boolean => { + return !isNaN(date.getTime()); +}; + +interface EnrichedPushInfo { + pushDate: Date; + connectorFieldsBeforePush: CaseConnector; +} + +const getConnectorInfoFromSavedObject = ( + savedObject: SavedObject | undefined +): CaseConnector | undefined => { + if ( + savedObject != null && + (isConnectorUserAction(savedObject.attributes) || + isCreateCaseUserAction(savedObject.attributes)) + ) { + return savedObject.attributes.payload.connector; + } +}; + +const hasDataToPush = (enrichedConnectorInfo: EnrichedConnector): boolean => { + return ( + !isEqual( + enrichedConnectorInfo.connector, + enrichedConnectorInfo.pushInfo?.connectorFieldsBeforePush + ) || + (enrichedConnectorInfo.pushInfo != null && + enrichedConnectorInfo.latestUserActionDate != null && + enrichedConnectorInfo.pushInfo.pushDate < enrichedConnectorInfo.latestUserActionDate) + ); +}; diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 37d709a66e62f..63711419ad3b9 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -5,157 +5,13 @@ * 2.0. */ -import type { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { - CaseUserActionsResponse, - CaseUserActionResponse, - GetCaseConnectorsResponse, - CaseConnector, -} from '../../../common/api'; -import { CaseUserActionsResponseRt, GetCaseConnectorsResponseRt } from '../../../common/api'; -import { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions'; +import type { SavedObjectsFindResponse } from '@kbn/core/server'; +import type { CaseUserActionsResponse, CaseUserActionResponse } from '../../../common/api'; +import { CaseUserActionsResponseRt } from '../../../common/api'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; -import type { Authorization, OwnerEntity } from '../../authorization'; import { Operations } from '../../authorization'; -import type { GetConnectorsRequest, UserActionGet } from './types'; -import type { CaseConnectors } from '../../services/user_actions/types'; - -export const getConnectors = async ( - { caseId }: GetConnectorsRequest, - clientArgs: CasesClientArgs -): Promise => { - const { - services: { userActionService }, - logger, - authorization, - actionsClient, - } = clientArgs; - - try { - const [connectors, latestUserAction] = await Promise.all([ - userActionService.getCaseConnectorInformation(caseId), - userActionService.getMostRecentUserAction(caseId), - ]); - - await checkConnectorsAuthorization({ authorization, connectors, latestUserAction }); - - const results: GetCaseConnectorsResponse = []; - for (const [id, connector] of connectors.entries()) { - const fieldsCreatedAt = new Date(connector.fields.attributes.created_at); - if (connector.push) { - const pushCreatedAt = new Date(connector.push.attributes.created_at); - if (!isNaN(fieldsCreatedAt.getTime()) && !isNaN(pushCreatedAt.getTime())) { - } - } else { - results.push({ - fields: connector.fields.attributes.payload, - id: connector.id, - name: '', - needsToBePushed: true, - type: '', - }); - } - } - - return GetCaseConnectorsResponseRt.encode({}); - } catch (error) { - throw createCaseError({ - message: `Failed to retrieve user actions case id: ${caseId}: ${error}`, - error, - logger, - }); - } -}; - -const checkConnectorsAuthorization = async ({ - connectors, - latestUserAction, - authorization, -}: { - connectors: CaseConnectors; - latestUserAction?: SavedObject; - authorization: PublicMethodsOf; -}) => { - const entities: OwnerEntity[] = latestUserAction - ? [{ owner: latestUserAction.attributes.owner, id: latestUserAction.id }] - : []; - - for (const connector of connectors) { - entities.push({ - owner: connector.fields.savedObject.attributes.owner, - id: connector.fields.connectorId, - }); - - if (connector.push) { - entities.push({ - owner: connector.push.savedObject.attributes.owner, - id: connector.push.connectorId, - }); - } - } - - await authorization.ensureAuthorized({ - entities, - operation: Operations.getUserActions, - }); -}; - -const enrichConnectors = async ({ - connectors, - latestUserAction, - actionsClient, -}: { - connectors: CaseConnectors; - latestUserAction?: SavedObject; - actionsClient: PublicMethodsOf; -}) => { - const enrichedConnectors: Array<{ - connector: CaseConnector; - fieldsUpdatedDate: Date; - pushDate?: Date; - name: string; - }> = []; - - for (const aggregationConnector of connectors) { - const connectorDetails = await actionsClient.get({ id: aggregationConnector.connectorId }); - const fieldsUpdatedDate = new Date(aggregationConnector.fields.attributes.created_at); - const connector = getConnectorInfoFromSavedObject(aggregationConnector.fields); - - let pushCreatedAt: Date | undefined; - if (aggregationConnector.push) { - pushCreatedAt = new Date(aggregationConnector.push.attributes.created_at); - } - - if (isDateValid(fieldsUpdatedDate) && connector) { - enrichedConnectors.push({ - connector, - fieldsUpdatedDate, - ...(pushCreatedAt != null && { pushDate: pushCreatedAt }), - name: connectorDetails.name, - }); - } - } - - return enrichedConnectors; -}; - -const isDateValid = (date: Date): boolean => { - return !isNaN(date.getTime()); -}; - -const getConnectorInfoFromSavedObject = ( - savedObject: SavedObject -): CaseConnector | undefined => { - if ( - isConnectorUserAction(savedObject.attributes) || - isCreateCaseUserAction(savedObject.attributes) - ) { - return savedObject.attributes.payload.connector; - } -}; +import type { UserActionGet } from './types'; export const get = async ( { caseId }: UserActionGet, diff --git a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index 257db8ab70f00..23ac2f9a6b2bd 100644 --- a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -29,6 +29,8 @@ export const getUserActionsRoute = createCasesRoute({ const casesClient = await caseContext.getCasesClient(); const caseId = request.params.case_id; + await casesClient.userActions.getConnectors({ caseId }); + return response.ok({ body: await casesClient.userActions.getAll({ caseId }), }); diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 1e2ff0999856b..f743a2523c736 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -94,6 +94,9 @@ type FakeUserActionService = PublicMethodsOf & { export const createUserActionServiceMock = (): CaseUserActionServiceMock => { const service: FakeUserActionService = { creator: createUserActionPersisterServiceMock(), + getConnectorFieldsBeforePushes: jest.fn(), + getMostRecentUserAction: jest.fn(), + getCaseConnectorInformation: jest.fn(), getAll: jest.fn(), findStatusChanges: jest.fn(), getUniqueConnectors: jest.fn(), diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index ce73d3a4ce6d9..5e16cd7d58640 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -42,7 +42,7 @@ import { } from '../../common/constants'; import { findConnectorIdReference } from '../transform'; import { buildFilter, combineFilters } from '../../client/utils'; -import type { CaseConnectors, ServiceContext } from './types'; +import type { CaseConnectorActivity, CaseConnectorFields, PushInfo, ServiceContext } from './types'; import { defaultSortField, isCommentRequestTypeExternalReferenceSO } from '../../common/utils'; import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry'; import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references'; @@ -62,23 +62,39 @@ interface MostRecentResults { }; } -interface ConnectorInfoAggsResult { +interface ConnectorActivityAggsResult { references: { connectors: { ids: { buckets: Array<{ key: string; - connectorFields: { - connector: MostRecentResults; - createCase: MostRecentResults; + reverse: { + connectorActivity: { + buckets: { + changeConnector: MostRecentResults; + createCase: MostRecentResults; + pushInfo: MostRecentResults; + }; + }; }; - pushInfo: MostRecentResults; }>; }; }; }; } +interface ConnectorFieldsBeforePushAggsResult { + references: { + connectors: { + reverse: { + ids: { + buckets: Record; + }; + }; + }; + }; +} + export class CaseUserActionService { private readonly _creator: UserActionPersister; @@ -90,10 +106,174 @@ export class CaseUserActionService { return this._creator; } + public async getConnectorFieldsBeforePushes( + caseId: string, + pushes: PushInfo[] + ): Promise { + try { + this.context.log.debug( + `Attempting to retrieve the connector fields before the last push for case id: ${caseId}` + ); + + const connectorsFilter = buildFilter({ + filters: [ActionTypes.connector, ActionTypes.create_case], + field: 'type', + operator: 'or', + type: CASE_USER_ACTION_SAVED_OBJECT, + }); + + const response = await this.context.unsecuredSavedObjectsClient.find< + CaseUserActionAttributesWithoutConnectorId, + ConnectorFieldsBeforePushAggsResult + >({ + type: CASE_USER_ACTION_SAVED_OBJECT, + hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: CaseUserActionService.buildConnectorFieldsBeforePushAggs(pushes), + filter: connectorsFilter, + }); + + console.log('getConnectorFieldsBeforePushes', JSON.stringify(response, null, 2)); + + return this.createCaseConnectorFieldsBeforePushes(response.aggregations); + } catch (error) { + this.context.log.error( + `Error while retrieving the connector fields before the last push: ${caseId}: ${error}` + ); + throw error; + } + } + + private static buildConnectorFieldsBeforePushAggs( + pushes: PushInfo[] + ): Record { + const filters: estypes.AggregationsBuckets = {}; + + /** + * Group the user actions by the unique connector ids and bound the time range + * for that connector's push event + */ + for (const push of pushes) { + filters[push.connectorId] = { + bool: { + filter: [ + { + range: { + [`${CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { + lt: push.date.toISOString(), + }, + }, + }, + { + nested: { + path: `${CASE_USER_ACTION_SAVED_OBJECT}.references`, + query: { + bool: { + filter: [ + { + term: { + [`${CASE_USER_ACTION_SAVED_OBJECT}.references.id`]: { + value: push.connectorId, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }; + } + + console.log('fields before push filters', JSON.stringify(filters, null, 2)); + + return { + references: { + nested: { + path: `${CASE_USER_ACTION_SAVED_OBJECT}.references`, + }, + aggregations: { + connectors: { + filter: { + term: { + [`${CASE_USER_ACTION_SAVED_OBJECT}.references.type`]: 'action', + }, + }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + ids: { + filters: { + filters, + }, + aggregations: { + mostRecent: { + top_hits: { + sort: [ + { + [`${CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { + order: 'desc', + }, + }, + ], + size: 1, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + } + + private createCaseConnectorFieldsBeforePushes( + aggsResults?: ConnectorFieldsBeforePushAggsResult + ): CaseConnectorFields { + const connectorFields: CaseConnectorFields = new Map(); + + if (!aggsResults) { + return connectorFields; + } + + for (const connectorId of Object.keys(aggsResults.references.connectors.reverse.ids.buckets)) { + const fields = aggsResults.references.connectors.reverse.ids.buckets[connectorId]; + + if (fields.mostRecent.hits.hits.length > 0) { + const rawFieldsDoc = fields.mostRecent.hits.hits[0]; + const doc = + this.context.savedObjectsSerializer.rawToSavedObject( + rawFieldsDoc + ); + + const fieldsDoc = transformToExternalModel( + doc, + this.context.persistableStateAttachmentTypeRegistry + ); + + connectorFields.set(connectorId, fieldsDoc); + } + } + + return connectorFields; + } + public async getMostRecentUserAction( caseId: string ): Promise | undefined> { try { + this.context.log.debug( + `Attempting to retrieve the most recent user action for case id: ${caseId}` + ); + const id = caseId; const type = CASE_SAVED_OBJECT; @@ -134,23 +314,16 @@ export class CaseUserActionService { this.context.persistableStateAttachmentTypeRegistry ); } catch (error) { - this.context.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); + this.context.log.error( + `Error while retrieving the most recent user action for case id: ${caseId}: ${error}` + ); throw error; } } - public async getCaseConnectorInformation(caseId: string): Promise { + public async getCaseConnectorInformation(caseId: string): Promise { try { - this.context.log.debug('Attempting to find connector information'); - - /* -1. Get all unique IDs of the connectors used in a case with an aggregation. Let's say the query return [1, 2, 3] -2. Get all user actions in descending order where (connectorID === 1 or connectorID === 2 or connectorID === 3) && ((action === "update" && type === "connector") || (action === "push" type === "push")). - The first three results contain the fields of each connector. For simplicity, I left the case where the fields are in the create case UA. -3. Get all user actions in descending order filtering out the ones that we do not support pushing. For example, updating a connector, changing the status, etc. - Set the page to 1. The result is the latest UA. -4. For each UA connector record from step 2 check if connectorUpdateUA.createdAt < latestUA.createAt. If true then the connector needs to be pushed. - */ + this.context.log.debug(`Attempting to find connector information for case id: ${caseId}`); const connectorsFilter = buildFilter({ filters: [ActionTypes.connector, ActionTypes.create_case, ActionTypes.pushed], @@ -161,7 +334,7 @@ export class CaseUserActionService { const response = await this.context.unsecuredSavedObjectsClient.find< CaseUserActionAttributesWithoutConnectorId, - ConnectorInfoAggsResult + ConnectorActivityAggsResult >({ type: CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, @@ -176,29 +349,35 @@ export class CaseUserActionService { return this.createCaseConnectorInformation(response.aggregations); } catch (error) { - this.context.log.error(`Error finding status changes: ${error}`); + this.context.log.error( + `Error while retrieving the connector information for case id: ${caseId} ${error}` + ); throw error; } } - private createCaseConnectorInformation(aggsResults?: ConnectorInfoAggsResult): CaseConnectors { - const caseConnectorInfo: CaseConnectors = []; + private createCaseConnectorInformation( + aggsResults?: ConnectorActivityAggsResult + ): CaseConnectorActivity[] { + const caseConnectorInfo: CaseConnectorActivity[] = []; if (!aggsResults) { return caseConnectorInfo; } for (const connectorInfo of aggsResults.references.connectors.ids.buckets) { + const changeConnector = connectorInfo.reverse.connectorActivity.buckets.changeConnector; + const createCase = connectorInfo.reverse.connectorActivity.buckets.createCase; let rawFieldsDoc: SavedObjectsRawDoc | undefined; - if (connectorInfo.connectorFields.connector.mostRecent.hits.hits.length > 0) { - rawFieldsDoc = connectorInfo.connectorFields.connector.mostRecent.hits.hits[0]; - } else if (connectorInfo.connectorFields.createCase.mostRecent.hits.hits.length > 0) { + if (changeConnector.mostRecent.hits.hits.length > 0) { + rawFieldsDoc = changeConnector.mostRecent.hits.hits[0]; + } else if (createCase.mostRecent.hits.hits.length > 0) { /** * If there is ever a connector update user action that takes precedence over the information stored * in the create case user action because it indicates that the connector's fields were changed */ - rawFieldsDoc = connectorInfo.connectorFields.createCase.mostRecent.hits.hits[0]; + rawFieldsDoc = createCase.mostRecent.hits.hits[0]; } let fieldsDoc: SavedObject | undefined; @@ -214,9 +393,11 @@ export class CaseUserActionService { ); } + const pushInfo = connectorInfo.reverse.connectorActivity.buckets.pushInfo; let pushDoc: SavedObject | undefined; - if (connectorInfo.pushInfo.mostRecent.hits.hits.length > 0) { - const rawPushDoc = connectorInfo.pushInfo.mostRecent.hits.hits[0]; + + if (pushInfo.mostRecent.hits.hits.length > 0) { + const rawPushDoc = pushInfo.mostRecent.hits.hits[0]; const doc = this.context.savedObjectsSerializer.rawToSavedObject( @@ -269,10 +450,10 @@ export class CaseUserActionService { reverse: { reverse_nested: {}, aggregations: { - connectorFields: { + connectorActivity: { filters: { filters: { - connector: { + changeConnector: { term: { [`${CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: ActionTypes.connector, @@ -284,30 +465,14 @@ export class CaseUserActionService { ActionTypes.create_case, }, }, - }, - }, - aggregations: { - mostRecent: { - top_hits: { - sort: [ - { - [`${CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { - order: 'desc', - }, - }, - ], - size: 1, + pushInfo: { + term: { + [`${CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: + ActionTypes.pushed, + }, }, }, }, - }, - pushInfo: { - filter: { - term: { - [`${CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: - ActionTypes.pushed, - }, - }, aggregations: { mostRecent: { top_hits: { diff --git a/x-pack/plugins/cases/server/services/user_actions/types.ts b/x-pack/plugins/cases/server/services/user_actions/types.ts index b92a51e2d1bef..377a7217cdd02 100644 --- a/x-pack/plugins/cases/server/services/user_actions/types.ts +++ b/x-pack/plugins/cases/server/services/user_actions/types.ts @@ -143,8 +143,15 @@ export interface ServiceContext { auditLogger: AuditLogger; } -export type CaseConnectors = Array<{ +export interface CaseConnectorActivity { connectorId: string; fields: SavedObject; push?: SavedObject; -}>; +} + +export type CaseConnectorFields = Map>; + +export interface PushInfo { + date: Date; + connectorId: string; +}