"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.CaseUserActionService = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _domain = require("../../../common/types/domain"); var _api = require("../../../common/api"); var _constants = require("../../../common/constants"); var _utils = require("../../client/utils"); var _utils2 = require("../../common/utils"); var _create = require("./operations/create"); var _find = require("./operations/find"); var _transform = require("./transform"); var _user_actions = require("../../common/types/user_actions"); var _api2 = require("../../../common/types/api"); /* * 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. */ class CaseUserActionService { constructor(context) { (0, _defineProperty2.default)(this, "_creator", void 0); (0, _defineProperty2.default)(this, "_finder", void 0); this.context = context; this._creator = new _create.UserActionPersister(context); this._finder = new _find.UserActionFinder(context); } get creator() { return this._creator; } get finder() { return this._finder; } async getConnectorFieldsBeforeLatestPush(caseId, pushes) { try { this.context.log.debug(`Attempting to retrieve the connector fields before the last push for case id: ${caseId}`); if (pushes.length <= 0) { return new Map(); } const connectorsFilter = (0, _utils.buildFilter)({ filters: [_domain.UserActionTypes.connector, _domain.UserActionTypes.create_case], field: 'type', operator: 'or', type: _constants.CASE_USER_ACTION_SAVED_OBJECT }); const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: CaseUserActionService.buildConnectorFieldsUsedInPushAggs(pushes), filter: connectorsFilter }); return this.createCaseConnectorFieldsUsedInPushes(response.aggregations); } catch (error) { this.context.log.error(`Error while retrieving the connector fields before the last push: ${caseId}: ${error}`); throw error; } } static buildConnectorFieldsUsedInPushAggs(pushes) { const filters = {}; /** * Group the user actions by the unique connector ids and bound the time range * for that connector's push event. We want to search for the fields before the push timestamp. */ for (const push of pushes) { filters[push.connectorId] = { bool: { filter: [{ // Search for connector field user action prior to the push occurrence range: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { lt: push.date.toISOString() } } }, { nested: { path: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references`, query: { bool: { filter: [{ // We only want to search a time frame for a specific connector id term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.id`]: { value: push.connectorId } } }] } } } }] } }; } return { references: { nested: { path: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references` }, aggregations: { connectors: { filter: { // Only search for user actions that have a connector reference aka a reference with type action term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.type`]: 'action' } }, aggregations: { reverse: { reverse_nested: {}, aggregations: { ids: { filters: { filters }, aggregations: { mostRecent: { top_hits: { sort: [{ [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { order: 'desc' } }], size: 1 } } } } } } } } } } }; } createCaseConnectorFieldsUsedInPushes(aggsResults) { const connectorFields = 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 res = (0, _transform.transformToExternalModel)(doc, this.context.persistableStateAttachmentTypeRegistry); const decodeRes = (0, _api.decodeOrThrow)(_user_actions.UserActionTransformedAttributesRt)(res.attributes); const fieldsDoc = Object.assign(res, { attributes: decodeRes }); connectorFields.set(connectorId, fieldsDoc); } } return connectorFields; } async getMostRecentUserAction(caseId) { try { this.context.log.debug(`Attempting to retrieve the most recent user action for case id: ${caseId}`); const id = caseId; const type = _constants.CASE_SAVED_OBJECT; const connectorsFilter = (0, _utils.buildFilter)({ filters: [_domain.UserActionTypes.comment, _domain.UserActionTypes.description, _domain.UserActionTypes.tags, _domain.UserActionTypes.title], field: 'type', operator: 'or', type: _constants.CASE_USER_ACTION_SAVED_OBJECT }); const userActions = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, perPage: 1, sortField: 'created_at', sortOrder: 'desc', filter: connectorsFilter }); if (userActions.saved_objects.length <= 0) { return; } const res = (0, _transform.transformToExternalModel)(userActions.saved_objects[0], this.context.persistableStateAttachmentTypeRegistry); const decodeRes = (0, _api.decodeOrThrow)(_user_actions.UserActionTransformedAttributesRt)(res.attributes); return { ...res, attributes: decodeRes }; } catch (error) { this.context.log.error(`Error while retrieving the most recent user action for case id: ${caseId}: ${error}`); throw error; } } async getCaseConnectorInformation(caseId) { try { this.context.log.debug(`Attempting to find connector information for case id: ${caseId}`); const connectorsFilter = (0, _utils.buildFilter)({ filters: [_domain.UserActionTypes.connector, _domain.UserActionTypes.create_case, _domain.UserActionTypes.pushed], field: 'type', operator: 'or', type: _constants.CASE_USER_ACTION_SAVED_OBJECT }); const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: CaseUserActionService.buildConnectorInfoAggs(), filter: connectorsFilter }); return this.createCaseConnectorInformation(response.aggregations); } catch (error) { this.context.log.error(`Error while retrieving the connector information for case id: ${caseId} ${error}`); throw error; } } createCaseConnectorInformation(aggsResults) { const caseConnectorInfo = []; 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; 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 = createCase.mostRecent.hits.hits[0]; } let fieldsDoc; if (rawFieldsDoc != null) { const doc = this.context.savedObjectsSerializer.rawToSavedObject(rawFieldsDoc); const res = (0, _transform.transformToExternalModel)(doc, this.context.persistableStateAttachmentTypeRegistry); const decodeRes = (0, _api.decodeOrThrow)(_user_actions.UserActionTransformedAttributesRt)(res.attributes); fieldsDoc = { ...res, attributes: decodeRes }; } const pushDocs = this.getPushDocs(connectorInfo.reverse.connectorActivity.buckets.pushInfo); if (fieldsDoc != null) { caseConnectorInfo.push({ connectorId: connectorInfo.key, fields: fieldsDoc, push: pushDocs }); } else { this.context.log.warn(`Unable to find fields for connector id: ${connectorInfo.key}`); } } return caseConnectorInfo; } getPushDocs(pushTimeFrameInfo) { const mostRecentPushDoc = this.getTopHitsDoc(pushTimeFrameInfo.mostRecent); const oldestPushDoc = this.getTopHitsDoc(pushTimeFrameInfo.oldest); if (mostRecentPushDoc && oldestPushDoc) { return { mostRecent: mostRecentPushDoc, oldest: oldestPushDoc }; } } getTopHitsDoc(topHits) { if (topHits.hits.hits.length > 0) { const rawPushDoc = topHits.hits.hits[0]; const doc = this.context.savedObjectsSerializer.rawToSavedObject(rawPushDoc); const res = (0, _transform.transformToExternalModel)(doc, this.context.persistableStateAttachmentTypeRegistry); const decodeRes = (0, _api.decodeOrThrow)(_user_actions.UserActionTransformedAttributesRt)(res.attributes); return { ...res, attributes: decodeRes }; } } static buildConnectorInfoAggs() { return { references: { nested: { path: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references` }, aggregations: { connectors: { filter: { term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.type`]: 'action' } }, aggregations: { ids: { // Bucket by connector id terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.id`, // We're assuming that a case will not have more than 1000 connectors size: 1000 }, aggregations: { reverse: { reverse_nested: {}, aggregations: { connectorActivity: { filters: { filters: { // look for connector fields user actions from "change connector" occurrence changeConnector: { term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: _domain.UserActionTypes.connector } }, // If the case was initialized with a connector, the fields could exist in the create_case // user action createCase: { term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: _domain.UserActionTypes.create_case } }, // Also grab the most recent push occurrence for the connector pushInfo: { term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.type`]: _domain.UserActionTypes.pushed } } } }, aggregations: { mostRecent: { top_hits: { sort: [{ [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { order: 'desc' } }], size: 1 } }, oldest: { top_hits: { sort: [{ [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { order: 'asc' } }], size: 1 } } } } } } } } } } } } }; } async getAll(caseId) { try { const id = caseId; const type = _constants.CASE_SAVED_OBJECT; const userActions = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, perPage: _constants.MAX_DOCS_PER_PAGE, sortField: 'created_at', sortOrder: 'asc' }); const transformedUserActions = (0, _transform.legacyTransformFindResponseToExternalModel)(userActions, this.context.persistableStateAttachmentTypeRegistry); const validatedUserActions = []; for (const so of transformedUserActions.saved_objects) { const validatedAttributes = (0, _api.decodeOrThrow)(_api2.CaseUserActionDeprecatedResponseRt)(so.attributes); validatedUserActions.push(Object.assign(so, { attributes: validatedAttributes })); } return Object.assign(transformedUserActions, { saved_objects: validatedUserActions }); } catch (error) { this.context.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } } async getUserActionIdsForCases(caseIds) { try { this.context.log.debug(`Attempting to retrieve user actions associated with cases: [${caseIds}]`); // We are intentionally not adding the type here because we only want to interact with the id and this function // should not use the attributes const finder = this.context.unsecuredSavedObjectsClient.createPointInTimeFinder({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: caseIds.map(id => ({ id, type: _constants.CASE_SAVED_OBJECT })), sortField: 'created_at', sortOrder: 'asc', /** * We only care about the ids so to reduce the data returned we should limit the fields in the response. Core * doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request * the owner even though we don't need it. */ fields: ['owner'], perPage: _constants.MAX_DOCS_PER_PAGE }); const ids = []; for await (const userActionSavedObject of finder.find()) { ids.push(...userActionSavedObject.saved_objects.map(userAction => userAction.id)); } return ids; } catch (error) { this.context.log.error(`Error retrieving user action ids for cases: [${caseIds}]: ${error}`); throw error; } } async getUniqueConnectors({ caseId, filter }) { try { var _response$aggregation, _response$aggregation2, _response$aggregation3, _response$aggregation4, _response$aggregation5, _response$aggregation6; this.context.log.debug(`Attempting to count connectors for case id ${caseId}`); const connectorsFilter = (0, _utils.buildFilter)({ filters: [_domain.UserActionTypes.connector, _domain.UserActionTypes.create_case], field: 'type', operator: 'or', type: _constants.CASE_USER_ACTION_SAVED_OBJECT }); const combinedFilter = (0, _utils.combineFilters)([connectorsFilter, filter]); const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: this.buildCountConnectorsAggs(), filter: combinedFilter }); return (_response$aggregation = (_response$aggregation2 = response.aggregations) === null || _response$aggregation2 === void 0 ? void 0 : (_response$aggregation3 = _response$aggregation2.references) === null || _response$aggregation3 === void 0 ? void 0 : (_response$aggregation4 = _response$aggregation3.connectors) === null || _response$aggregation4 === void 0 ? void 0 : (_response$aggregation5 = _response$aggregation4.ids) === null || _response$aggregation5 === void 0 ? void 0 : (_response$aggregation6 = _response$aggregation5.buckets) === null || _response$aggregation6 === void 0 ? void 0 : _response$aggregation6.map(({ key }) => ({ id: key }))) !== null && _response$aggregation !== void 0 ? _response$aggregation : []; } catch (error) { this.context.log.error(`Error while counting connectors for case id ${caseId}: ${error}`); throw error; } } buildCountConnectorsAggs( /** * It is high unlikely for a user to have more than * 100 connectors attached to a case */ size = 100) { return { references: { nested: { path: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references` }, aggregations: { connectors: { filter: { term: { [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.type`]: 'action' } }, aggregations: { ids: { terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.id`, size } } } } } } }; } async getMultipleCasesUserActionsTotal({ caseIds }) { var _response$aggregation7; const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: caseIds.map(id => ({ type: _constants.CASE_SAVED_OBJECT, id })), hasReferenceOperator: 'OR', page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: CaseUserActionService.buildMultipleCasesUserActionsTotalAgg(caseIds.length) }); const result = {}; response === null || response === void 0 ? void 0 : (_response$aggregation7 = response.aggregations) === null || _response$aggregation7 === void 0 ? void 0 : _response$aggregation7.references.caseUserActions.buckets.forEach(({ key, doc_count: totalUserActions }) => { result[key] = totalUserActions; }); return result; } static buildMultipleCasesUserActionsTotalAgg(idsLength) { return { references: { nested: { path: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references` }, aggregations: { caseUserActions: { terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.references.id`, size: idsLength } } } } }; } async getCaseUserActionStats({ caseId }) { var _response$aggregation8; const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: CaseUserActionService.buildUserActionStatsAgg() }); const result = { total: response.total, total_comments: 0, total_other_actions: 0 }; (_response$aggregation8 = response.aggregations) === null || _response$aggregation8 === void 0 ? void 0 : _response$aggregation8.totals.buckets.forEach(({ key, doc_count: docCount }) => { if (key === 'user') { result.total_comments = docCount; } }); result.total_other_actions = result.total - result.total_comments; return result; } static buildUserActionStatsAgg() { return { totals: { terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.comment.type`, size: 100 } } }; } async getUsers({ caseId }) { var _response$aggregation9, _response$aggregation10, _response$aggregation11, _response$aggregation12; const response = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, page: 1, perPage: 1, sortField: _utils2.defaultSortField, aggs: CaseUserActionService.buildParticipantsAgg() }); const assignedAndUnassignedUsers = new Set(); const participants = []; const participantsBuckets = (_response$aggregation9 = (_response$aggregation10 = response.aggregations) === null || _response$aggregation10 === void 0 ? void 0 : _response$aggregation10.participants.buckets) !== null && _response$aggregation9 !== void 0 ? _response$aggregation9 : []; const assigneesBuckets = (_response$aggregation11 = (_response$aggregation12 = response.aggregations) === null || _response$aggregation12 === void 0 ? void 0 : _response$aggregation12.assignees.buckets) !== null && _response$aggregation11 !== void 0 ? _response$aggregation11 : []; for (const bucket of participantsBuckets) { const rawDoc = bucket.docs.hits.hits[0]; const user = this.context.savedObjectsSerializer.rawToSavedObject(rawDoc); /** * We are interested only for the created_by * and the owner. For that reason, there is no * need to call transformToExternalModel which * injects the references ids to the document. */ participants.push({ id: user.id, user: user.attributes.created_by, owner: user.attributes.owner }); } /** * The users set includes any * user that got assigned in the * case even if they removed as * assignee at some point in time. */ for (const bucket of assigneesBuckets) { assignedAndUnassignedUsers.add(bucket.key); } return { participants, assignedAndUnassignedUsers }; } static buildParticipantsAgg() { return { participants: { terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.created_by.username`, size: _constants.MAX_DOCS_PER_PAGE, order: { _key: 'asc' }, missing: 'Unknown' }, aggregations: { docs: { top_hits: { size: 1, sort: [{ [`${_constants.CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: { order: 'desc' } }] } } } }, assignees: { terms: { field: `${_constants.CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.assignees.uid`, size: _constants.MAX_DOCS_PER_PAGE, order: { _key: 'asc' } } } }; } } exports.CaseUserActionService = CaseUserActionService;