[Response Ops][Cases] Refactoring tech debt after sub cases removal (#124181)

* Starting aggs refactor

* Removing so client from params adding aggs

* Refactoring some comments

* Addressing feedback

* asArray returns empty array

* Fixing type error
This commit is contained in:
Jonathan Buttner 2022-02-04 13:49:41 -05:00 committed by GitHub
parent d9aa72c7f8
commit d9d3230b94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 390 additions and 373 deletions

View file

@ -19,6 +19,10 @@ import { sortOrderSchema } from './common_schemas';
* - reverse_nested
* - terms
*
* Not fully supported:
* - filter
* - filters
*
* Not implemented:
* - adjacency_matrix
* - auto_date_histogram
@ -27,7 +31,6 @@ import { sortOrderSchema } from './common_schemas';
* - date_histogram
* - date_range
* - diversified_sampler
* - filters
* - geo_distance
* - geohash_grid
* - geotile_grid
@ -44,9 +47,26 @@ import { sortOrderSchema } from './common_schemas';
* - variable_width_histogram
*/
// 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
const termSchema = s.object({
term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])),
});
// 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
const boolSchema = s.object({
bool: s.object({
must_not: s.oneOf([termSchema]),
}),
});
export const bucketAggsSchemas: Record<string, ObjectType> = {
filter: s.object({
term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])),
filter: termSchema,
filters: s.object({
filters: s.recordOf(s.string(), s.oneOf([termSchema, boolSchema])),
}),
histogram: s.object({
field: s.maybe(s.string()),

View file

@ -53,7 +53,6 @@ async function createCommentableCase({
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
}): Promise<CommentableCase> {
const caseInfo = await caseService.getCase({
unsecuredSavedObjectsClient,
id,
});

View file

@ -60,7 +60,6 @@ export async function deleteAll(
try {
const comments = await caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: caseID,
});

View file

@ -250,7 +250,7 @@ export async function getAll(
{ caseID }: GetAllArgs,
clientArgs: CasesClientArgs
): Promise<AllCommentsResponse> {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
const { caseService, logger, authorization } = clientArgs;
try {
const { filter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter(
@ -258,7 +258,6 @@ export async function getAll(
);
const comments = await caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: caseID,
options: {
filter,

View file

@ -50,7 +50,6 @@ async function createCommentableCase({
lensEmbeddableFactory,
}: CombinedCaseParams) {
const caseInfo = await caseService.getCase({
unsecuredSavedObjectsClient,
id: caseID,
});

View file

@ -73,7 +73,6 @@ export const create = async (
});
const newCase = await caseService.postNewCase({
unsecuredSavedObjectsClient,
attributes: transformNewCase({
user,
newCase: query,

View file

@ -30,7 +30,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
authorization,
} = clientArgs;
try {
const cases = await caseService.getCases({ unsecuredSavedObjectsClient, caseIds: ids });
const cases = await caseService.getCases({ caseIds: ids });
const entities = new Map<string, OwnerEntity>();
for (const theCase of cases.saved_objects) {
@ -52,7 +52,6 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
const deleteCasesMapper = async (id: string) =>
caseService.deleteCase({
unsecuredSavedObjectsClient,
id,
});
@ -63,7 +62,6 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
const getCommentsMapper = async (id: string) =>
caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id,
});

View file

@ -15,13 +15,12 @@ import {
CasesFindRequest,
CasesFindRequestRt,
throwErrors,
caseStatuses,
CasesFindResponseRt,
excess,
} from '../../../common/api';
import { createCaseError } from '../../common/error';
import { transformCases } from '../../common/utils';
import { asArray, transformCases } from '../../common/utils';
import { constructQueryOptions } from '../utils';
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
import { Operations } from '../../authorization';
@ -36,7 +35,7 @@ export const find = async (
params: CasesFindRequest,
clientArgs: CasesClientArgs
): Promise<CasesFindResponse> => {
const { unsecuredSavedObjectsClient, caseService, authorization, logger } = clientArgs;
const { caseService, authorization, logger } = clientArgs;
try {
const queryParams = pipe(
@ -55,45 +54,38 @@ export const find = async (
owner: queryParams.owner,
};
const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter });
const cases = await caseService.findCasesGroupedByID({
unsecuredSavedObjectsClient,
caseOptions: {
...queryParams,
...caseQueries,
searchFields:
queryParams.searchFields != null
? Array.isArray(queryParams.searchFields)
? queryParams.searchFields
: [queryParams.searchFields]
: queryParams.searchFields,
fields: includeFieldsRequiredForAuthentication(queryParams.fields),
},
const statusStatsOptions = constructQueryOptions({
...queryArgs,
status: undefined,
authorizationFilter,
});
const caseQueryOptions = constructQueryOptions({ ...queryArgs, authorizationFilter });
ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]);
// casesStatuses are bounded by us. No need to limit concurrent calls.
const [openCases, inProgressCases, closedCases] = await Promise.all([
...caseStatuses.map((status) => {
const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter });
return caseService.findCaseStatusStats({
unsecuredSavedObjectsClient,
caseOptions: statusQuery,
ensureSavedObjectsAreAuthorized,
});
const [cases, statusStats] = await Promise.all([
caseService.findCasesGroupedByID({
caseOptions: {
...queryParams,
...caseQueryOptions,
searchFields: asArray(queryParams.searchFields),
fields: includeFieldsRequiredForAuthentication(queryParams.fields),
},
}),
caseService.getCaseStatusStats({
searchOptions: statusStatsOptions,
}),
]);
ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]);
return CasesFindResponseRt.encode(
transformCases({
casesMap: cases.casesMap,
page: cases.page,
perPage: cases.perPage,
total: cases.total,
countOpenCases: openCases,
countInProgressCases: inProgressCases,
countClosedCases: closedCases,
countOpenCases: statusStats.open,
countInProgressCases: statusStats['in-progress'],
countClosedCases: statusStats.closed,
})
);
} catch (error) {

View file

@ -59,7 +59,7 @@ export const getCasesByAlertID = async (
{ alertID, options }: CasesByAlertIDParams,
clientArgs: CasesClientArgs
): Promise<CasesByAlertId> => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
const { caseService, logger, authorization } = clientArgs;
try {
const queryParams = pipe(
@ -79,7 +79,6 @@ export const getCasesByAlertID = async (
// This will likely only return one comment saved object, the response aggregation will contain
// the keys we need to retrieve the cases
const commentsWithAlert = await caseService.getCaseIdsByAlertId({
unsecuredSavedObjectsClient,
alertId: alertID,
filter,
});
@ -100,7 +99,6 @@ export const getCasesByAlertID = async (
}
const casesInfo = await caseService.getCases({
unsecuredSavedObjectsClient,
caseIds,
});
@ -157,11 +155,10 @@ export const get = async (
{ id, includeComments }: GetParams,
clientArgs: CasesClientArgs
): Promise<CaseResponse> => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
const { caseService, logger, authorization } = clientArgs;
try {
const theCase: SavedObject<CaseAttributes> = await caseService.getCase({
unsecuredSavedObjectsClient,
id,
});
@ -179,7 +176,6 @@ export const get = async (
}
const theComments = await caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id,
options: {
sortField: 'created_at',
@ -209,14 +205,13 @@ export const resolve = async (
{ id, includeComments }: GetParams,
clientArgs: CasesClientArgs
): Promise<CaseResolveResponse> => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
const { caseService, logger, authorization } = clientArgs;
try {
const {
saved_object: resolvedSavedObject,
...resolveData
}: SavedObjectsResolveResponse<CaseAttributes> = await caseService.getResolveCase({
unsecuredSavedObjectsClient,
id,
});
@ -240,7 +235,6 @@ export const resolve = async (
}
const theComments = await caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: resolvedSavedObject.id,
options: {
sortField: 'created_at',

View file

@ -142,12 +142,10 @@ export const push = async (
/* Start of update case with push information */
const [myCase, myCaseConfigure, comments] = await Promise.all([
caseService.getCase({
unsecuredSavedObjectsClient,
id: caseId,
}),
caseConfigureService.find({ unsecuredSavedObjectsClient }),
caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: caseId,
options: {
fields: [],
@ -177,7 +175,6 @@ export const push = async (
const [updatedCase, updatedComments] = await Promise.all([
caseService.patchCase({
originalCase: myCase,
unsecuredSavedObjectsClient,
caseId,
updatedAttributes: {
...(shouldMarkAsClosed

View file

@ -97,17 +97,14 @@ function getID(
async function getAlertComments({
casesToSync,
caseService,
unsecuredSavedObjectsClient,
}: {
casesToSync: UpdateRequestWithOriginalCase[];
caseService: CasesService;
unsecuredSavedObjectsClient: SavedObjectsClientContract;
}): Promise<SavedObjectsFindResponse<CommentAttributes>> {
const idsOfCasesToSync = casesToSync.map(({ updateReq }) => updateReq.id);
// getAllCaseComments will by default get all the comments, unless page or perPage fields are set
return caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: idsOfCasesToSync,
options: {
filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert),
@ -166,7 +163,6 @@ async function updateAlerts({
const totalAlerts = await getAlertComments({
casesToSync,
caseService,
unsecuredSavedObjectsClient,
});
// create an array of requests that indicate the id, index, and status to update an alert
@ -253,7 +249,6 @@ export const update = async (
try {
const myCases = await caseService.getCases({
unsecuredSavedObjectsClient,
caseIds: query.cases.map((q) => q.id),
});
@ -320,7 +315,6 @@ export const update = async (
const { username, full_name, email } = user;
const updatedDt = new Date().toISOString();
const updatedCases = await caseService.patchCases({
unsecuredSavedObjectsClient,
cases: updateCases.map(({ updateReq, originalCase }) => {
// intentionally removing owner from the case so that we don't accidentally allow it to be updated
const { id: caseId, version, owner, ...updateCaseAttributes } = updateReq;

View file

@ -11,7 +11,6 @@ import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/
import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client';
import { CasesClientInternal, createCasesClientInternal } from './client_internal';
import { ConfigureSubClient, createConfigurationSubClient } from './configure/client';
import { createStatsSubClient, StatsSubClient } from './stats/client';
import { createMetricsSubClient, MetricsSubClient } from './metrics/client';
/**
@ -23,7 +22,6 @@ export class CasesClient {
private readonly _attachments: AttachmentsSubClient;
private readonly _userActions: UserActionsSubClient;
private readonly _configure: ConfigureSubClient;
private readonly _stats: StatsSubClient;
private readonly _metrics: MetricsSubClient;
constructor(args: CasesClientArgs) {
@ -32,7 +30,6 @@ export class CasesClient {
this._attachments = createAttachmentsSubClient(args, this, this._casesClientInternal);
this._userActions = createUserActionsSubClient(args);
this._configure = createConfigurationSubClient(args, this._casesClientInternal);
this._stats = createStatsSubClient(args);
this._metrics = createMetricsSubClient(args, this);
}
@ -64,13 +61,6 @@ export class CasesClient {
return this._configure;
}
/**
* Retrieves an interface for retrieving statistics related to the cases entities.
*/
public get stats() {
return this._stats;
}
/**
* Retrieves an interface for retrieving metrics related to the cases entities.
*/

View file

@ -91,17 +91,25 @@ export class CasesClientFactory {
logger: this.logger,
});
const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc);
const unsecuredSavedObjectsClient = savedObjectsService.getScopedClient(request, {
includedHiddenTypes: SAVED_OBJECT_TYPES,
// this tells the security plugin to not perform SO authorization and audit logging since we are handling
// that manually using our Authorization class and audit logger.
excludedWrappers: ['security'],
});
const attachmentService = new AttachmentService(this.logger);
const caseService = new CasesService({
log: this.logger,
authentication: this.options?.securityPluginStart?.authc,
unsecuredSavedObjectsClient,
attachmentService,
});
const userInfo = caseService.getUser({ request });
return createCasesClient({
alertsService: new AlertService(scopedClusterClient, this.logger),
unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, {
includedHiddenTypes: SAVED_OBJECT_TYPES,
// this tells the security plugin to not perform SO authorization and audit logging since we are handling
// that manually using our Authorization class and audit logger.
excludedWrappers: ['security'],
}),
unsecuredSavedObjectsClient,
// We only want these fields from the userInfo object
user: { username: userInfo.username, email: userInfo.email, full_name: userInfo.full_name },
caseService,

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { CaseMetricsResponse } from '../../../common/api';
import { CaseMetricsResponse, CasesStatusRequest, CasesStatusResponse } from '../../../common/api';
import { CasesClient } from '../client';
import { CasesClientArgs } from '../types';
import { getStatusTotalsByType } from './get_cases_metrics';
import { getCaseMetrics, CaseMetricsParams } from './get_case_metrics';
@ -17,6 +18,10 @@ import { getCaseMetrics, CaseMetricsParams } from './get_case_metrics';
*/
export interface MetricsSubClient {
getCaseMetrics(params: CaseMetricsParams): Promise<CaseMetricsResponse>;
/**
* Retrieves the total number of open, closed, and in-progress cases.
*/
getStatusTotalsByType(params: CasesStatusRequest): Promise<CasesStatusResponse>;
}
/**
@ -30,6 +35,8 @@ export const createMetricsSubClient = (
): MetricsSubClient => {
const casesSubClient: MetricsSubClient = {
getCaseMetrics: (params: CaseMetricsParams) => getCaseMetrics(params, casesClient, clientArgs),
getStatusTotalsByType: (params: CasesStatusRequest) =>
getStatusTotalsByType(params, clientArgs),
};
return Object.freeze(casesSubClient);

View file

@ -105,10 +105,9 @@ const checkAndThrowIfInvalidFeatures = (
};
const checkAuthorization = async (params: CaseMetricsParams, clientArgs: CasesClientArgs) => {
const { caseService, unsecuredSavedObjectsClient, authorization } = clientArgs;
const { caseService, authorization } = clientArgs;
const caseInfo = await caseService.getCase({
unsecuredSavedObjectsClient,
id: params.caseId,
});

View file

@ -0,0 +1,59 @@
/*
* 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 Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import {
CasesStatusRequest,
CasesStatusResponse,
excess,
CasesStatusRequestRt,
throwErrors,
CasesStatusResponseRt,
} from '../../../common/api';
import { CasesClientArgs } from '../types';
import { Operations } from '../../authorization';
import { constructQueryOptions } from '../utils';
import { createCaseError } from '../../common/error';
export async function getStatusTotalsByType(
params: CasesStatusRequest,
clientArgs: CasesClientArgs
): Promise<CasesStatusResponse> {
const { caseService, logger, authorization } = clientArgs;
try {
const queryParams = pipe(
excess(CasesStatusRequestRt).decode(params),
fold(throwErrors(Boom.badRequest), identity)
);
const { filter: authorizationFilter } = await authorization.getAuthorizationFilter(
Operations.getCaseStatuses
);
const options = constructQueryOptions({
owner: queryParams.owner,
authorizationFilter,
});
const statusStats = await caseService.getCaseStatusStats({
searchOptions: options,
});
return CasesStatusResponseRt.encode({
count_open_cases: statusStats.open,
count_in_progress_cases: statusStats['in-progress'],
count_closed_cases: statusStats.closed,
});
} catch (error) {
throw createCaseError({ message: `Failed to get status stats: ${error}`, error, logger });
}
}

View file

@ -13,7 +13,6 @@ import { CasesSubClient } from './cases/client';
import { ConfigureSubClient } from './configure/client';
import { CasesClientFactory } from './factory';
import { MetricsSubClient } from './metrics/client';
import { StatsSubClient } from './stats/client';
import { UserActionsSubClient } from './user_actions/client';
type CasesSubClientMock = jest.Mocked<CasesSubClient>;
@ -38,6 +37,7 @@ type MetricsSubClientMock = jest.Mocked<MetricsSubClient>;
const createMetricsSubClientMock = (): MetricsSubClientMock => {
return {
getCaseMetrics: jest.fn(),
getStatusTotalsByType: jest.fn(),
};
};
@ -75,14 +75,6 @@ const createConfigureSubClientMock = (): ConfigureSubClientMock => {
};
};
type StatsSubClientMock = jest.Mocked<StatsSubClient>;
const createStatsSubClientMock = (): StatsSubClientMock => {
return {
getStatusTotalsByType: jest.fn(),
};
};
export interface CasesClientMock extends CasesClient {
cases: CasesSubClientMock;
attachments: AttachmentsSubClientMock;
@ -95,7 +87,6 @@ export const createCasesClientMock = (): CasesClientMock => {
attachments: createAttachmentsSubClientMock(),
userActions: createUserActionsSubClientMock(),
configure: createConfigureSubClientMock(),
stats: createStatsSubClientMock(),
metrics: createMetricsSubClientMock(),
};
return client as unknown as CasesClientMock;

View file

@ -1,88 +0,0 @@
/*
* 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 Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { CasesClientArgs } from '..';
import {
CasesStatusRequest,
CasesStatusResponse,
CasesStatusResponseRt,
caseStatuses,
throwErrors,
excess,
CasesStatusRequestRt,
} from '../../../common/api';
import { Operations } from '../../authorization';
import { createCaseError } from '../../common/error';
import { constructQueryOptions } from '../utils';
/**
* Statistics API contract.
*/
export interface StatsSubClient {
/**
* Retrieves the total number of open, closed, and in-progress cases.
*/
getStatusTotalsByType(params: CasesStatusRequest): Promise<CasesStatusResponse>;
}
/**
* Creates the interface for retrieving the number of open, closed, and in progress cases.
*
* @ignore
*/
export function createStatsSubClient(clientArgs: CasesClientArgs): StatsSubClient {
return Object.freeze({
getStatusTotalsByType: (params: CasesStatusRequest) =>
getStatusTotalsByType(params, clientArgs),
});
}
async function getStatusTotalsByType(
params: CasesStatusRequest,
clientArgs: CasesClientArgs
): Promise<CasesStatusResponse> {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
const queryParams = pipe(
excess(CasesStatusRequestRt).decode(params),
fold(throwErrors(Boom.badRequest), identity)
);
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } =
await authorization.getAuthorizationFilter(Operations.getCaseStatuses);
// casesStatuses are bounded by us. No need to limit concurrent calls.
const [openCases, inProgressCases, closedCases] = await Promise.all([
...caseStatuses.map((status) => {
const statusQuery = constructQueryOptions({
owner: queryParams.owner,
status,
authorizationFilter,
});
return caseService.findCaseStatusStats({
unsecuredSavedObjectsClient,
caseOptions: statusQuery,
ensureSavedObjectsAreAuthorized,
});
}),
]);
return CasesStatusResponseRt.encode({
count_open_cases: openCases,
count_in_progress_cases: inProgressCases,
count_closed_cases: closedCases,
});
} catch (error) {
throw createCaseError({ message: `Failed to get status stats: ${error}`, error, logger });
}
}

View file

@ -118,7 +118,6 @@ export class CommentableCase {
try {
const updatedCase = await this.caseService.patchCase({
originalCase: this.caseInfo,
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
caseId: this.caseInfo.id,
updatedAttributes: {
updated_at: date,
@ -282,7 +281,6 @@ export class CommentableCase {
public async encode(): Promise<CaseResponse> {
try {
const comments = await this.caseService.getAllCaseComments({
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
id: this.caseInfo.id,
options: {
fields: [],

View file

@ -28,6 +28,7 @@ import {
flattenCommentSavedObject,
extractLensReferencesFromCommentString,
getOrUpdateLensReferences,
asArray,
} from './utils';
interface CommentReference {
@ -940,4 +941,26 @@ describe('common utils', () => {
expect(expectedReferences).toEqual(expect.arrayContaining(updatedReferences));
});
});
describe('asArray', () => {
it('returns an empty array when the field is undefined', () => {
expect(asArray(undefined)).toEqual([]);
});
it('returns an empty array when the field is null', () => {
expect(asArray(null)).toEqual([]);
});
it('leaves the string array as is when it is already an array', () => {
expect(asArray(['value'])).toEqual(['value']);
});
it('returns an array of one item when passed a string', () => {
expect(asArray('value')).toEqual(['value']);
});
it('returns an array of one item when passed a number', () => {
expect(asArray(100)).toEqual([100]);
});
});
});

View file

@ -360,3 +360,11 @@ export const getOrUpdateLensReferences = (
return currentNonLensReferences.concat(newCommentLensReferences);
};
export const asArray = <T>(field?: T | T[] | null): T[] => {
if (field === undefined || field === null) {
return [];
}
return Array.isArray(field) ? field : [field];
};

View file

@ -21,7 +21,7 @@ export function initGetCasesStatusApi({ router, logger }: RouteDeps) {
try {
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.stats.getStatusTotalsByType(request.query as CasesStatusRequest),
body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest),
});
} catch (error) {
logger.error(`Failed to get status stats in route: ${error}`);

View file

@ -9,6 +9,7 @@ import {
Logger,
SavedObject,
SavedObjectReference,
SavedObjectsClientContract,
SavedObjectsUpdateOptions,
} from 'kibana/server';
@ -62,6 +63,11 @@ interface BulkUpdateAttachmentArgs extends ClientArgs {
comments: UpdateArgs[];
}
interface CommentStats {
nonAlerts: number;
alerts: number;
}
export class AttachmentService {
constructor(private readonly log: Logger) {}
@ -279,4 +285,104 @@ export class AttachmentService {
throw error;
}
}
public async getCaseCommentStats({
unsecuredSavedObjectsClient,
caseIds,
}: {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
caseIds: string[];
}): Promise<Map<string, CommentStats>> {
if (caseIds.length <= 0) {
return new Map();
}
interface AggsResult {
references: {
caseIds: {
buckets: Array<{
key: string;
doc_count: number;
reverse: {
comments: {
buckets: {
alerts: {
doc_count: number;
};
nonAlerts: {
doc_count: number;
};
};
};
};
}>;
};
};
}
const res = await unsecuredSavedObjectsClient.find<unknown, AggsResult>({
hasReference: caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })),
hasReferenceOperator: 'OR',
type: CASE_COMMENT_SAVED_OBJECT,
perPage: 0,
aggs: AttachmentService.buildCommentStatsAggs(caseIds),
});
return (
res.aggregations?.references.caseIds.buckets.reduce((acc, idBucket) => {
acc.set(idBucket.key, {
nonAlerts: idBucket.reverse.comments.buckets.nonAlerts.doc_count,
alerts: idBucket.reverse.comments.buckets.alerts.doc_count,
});
return acc;
}, new Map<string, CommentStats>()) ?? new Map()
);
}
private static buildCommentStatsAggs(
ids: string[]
): Record<string, estypes.AggregationsAggregationContainer> {
return {
references: {
nested: {
path: `${CASE_COMMENT_SAVED_OBJECT}.references`,
},
aggregations: {
caseIds: {
terms: {
field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`,
size: ids.length,
},
aggregations: {
reverse: {
reverse_nested: {},
aggregations: {
comments: {
filters: {
filters: {
alerts: {
term: {
[`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`]: CommentType.alert,
},
},
nonAlerts: {
bool: {
must_not: {
term: {
[`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`]: CommentType.alert,
},
},
},
},
},
},
},
},
},
},
},
},
},
};
}
}

View file

@ -40,6 +40,7 @@ import {
createSOFindResponse,
} from '../test_utils';
import { ESCaseAttributes } from './types';
import { AttachmentService } from '../attachments';
const createUpdateSOResponse = ({
connector,
@ -117,12 +118,17 @@ const createCasePatchParams = ({
describe('CasesService', () => {
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const mockLogger = loggerMock.create();
const attachmentService = new AttachmentService(mockLogger);
let service: CasesService;
beforeEach(() => {
jest.resetAllMocks();
service = new CasesService(mockLogger);
service = new CasesService({
log: mockLogger,
unsecuredSavedObjectsClient,
attachmentService,
});
});
describe('transforms the external model to the Elasticsearch model', () => {
@ -134,7 +140,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -181,7 +186,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -213,7 +217,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -249,7 +252,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(createJiraConnector()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -278,7 +280,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -307,7 +308,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()),
originalCase: {
references: [{ id: 'a', name: 'awesome', type: 'hello' }],
@ -344,7 +344,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePatchParams({ externalService: createExternalService() }),
originalCase: {
references: [
@ -378,7 +377,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -408,7 +406,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -428,7 +425,6 @@ describe('CasesService', () => {
await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(getNoneCaseConnector()),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -452,7 +448,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(createJiraConnector()),
id: '1',
});
@ -468,7 +463,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(createJiraConnector(), createExternalService()),
id: '1',
});
@ -560,7 +554,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(createJiraConnector(), createExternalService()),
id: '1',
});
@ -589,7 +582,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(
createJiraConnector({ setFieldsToNull: true }),
createExternalService()
@ -608,7 +600,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(getNoneCaseConnector()),
id: '1',
});
@ -624,7 +615,6 @@ describe('CasesService', () => {
);
await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(getNoneCaseConnector()),
id: '1',
});
@ -655,7 +645,6 @@ describe('CasesService', () => {
);
const res = await service.patchCases({
unsecuredSavedObjectsClient,
cases: [
{
caseId: '1',
@ -710,7 +699,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -735,7 +723,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -755,7 +742,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -771,7 +757,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -803,7 +788,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -834,7 +818,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -858,7 +841,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -895,7 +877,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -922,7 +903,6 @@ describe('CasesService', () => {
const res = await service.patchCase({
caseId: '1',
unsecuredSavedObjectsClient,
updatedAttributes: createCaseUpdateParams(),
originalCase: {} as SavedObject<CaseAttributes>,
});
@ -958,7 +938,6 @@ describe('CasesService', () => {
);
const res = await service.postNewCase({
unsecuredSavedObjectsClient,
attributes: createCasePostParams(getNoneCaseConnector()),
id: '1',
});
@ -979,7 +958,7 @@ describe('CasesService', () => {
]);
unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn));
const res = await service.findCases({ unsecuredSavedObjectsClient });
const res = await service.findCases();
expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`);
expect(
res.saved_objects[0].attributes.external_service?.connector_id
@ -996,7 +975,7 @@ describe('CasesService', () => {
]);
unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn));
const res = await service.findCases({ unsecuredSavedObjectsClient });
const res = await service.findCases();
const { saved_objects: ignored, ...findResponseFields } = res;
expect(findResponseFields).toMatchInlineSnapshot(`
Object {
@ -1025,7 +1004,7 @@ describe('CasesService', () => {
})
);
const res = await service.getCases({ unsecuredSavedObjectsClient, caseIds: ['a'] });
const res = await service.getCases({ caseIds: ['a'] });
expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`);
expect(
@ -1050,7 +1029,7 @@ describe('CasesService', () => {
)
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`);
expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`);
@ -1062,7 +1041,7 @@ describe('CasesService', () => {
createCaseSavedObjectResponse({ externalService: createExternalService() })
)
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.connector).toMatchInlineSnapshot(`
Object {
@ -1078,7 +1057,7 @@ describe('CasesService', () => {
unsecuredSavedObjectsClient.get.mockReturnValue(
Promise.resolve(createCaseSavedObjectResponse())
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"none"`);
});
@ -1087,7 +1066,7 @@ describe('CasesService', () => {
unsecuredSavedObjectsClient.get.mockReturnValue(
Promise.resolve(createCaseSavedObjectResponse())
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.external_service).toMatchInlineSnapshot(`
Object {
@ -1118,7 +1097,7 @@ describe('CasesService', () => {
],
} as unknown as SavedObject<ESCaseAttributes>)
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.connector).toMatchInlineSnapshot(`
Object {
@ -1138,7 +1117,7 @@ describe('CasesService', () => {
attributes: { external_service: null },
} as SavedObject<ESCaseAttributes>)
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
const res = await service.getCase({ id: 'a' });
expect(res.attributes.connector).toMatchInlineSnapshot(`
Object {

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import pMap from 'p-map';
import {
KibanaRequest,
Logger,
@ -26,7 +25,6 @@ import { SecurityPluginSetup } from '../../../../security/server';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
MAX_CONCURRENT_SEARCHES,
MAX_DOCS_PER_PAGE,
} from '../../../common/constants';
import {
@ -34,17 +32,16 @@ import {
CaseResponse,
CasesFindRequest,
CommentAttributes,
CommentType,
User,
CaseAttributes,
CaseStatuses,
caseStatuses,
} from '../../../common/api';
import { SavedObjectFindOptionsKueryNode } from '../../common/types';
import { defaultSortField, flattenCaseSavedObject, groupTotalAlertsByID } from '../../common/utils';
import { defaultSortField, flattenCaseSavedObject } from '../../common/utils';
import { defaultPage, defaultPerPage } from '../../routes/api';
import { ClientArgs } from '..';
import { combineFilters } from '../../client/utils';
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
import { EnsureSOAuthCallback } from '../../authorization';
import {
transformSavedObjectToExternalModel,
transformAttributesToESModel,
@ -54,8 +51,9 @@ import {
transformFindResponseToExternalModel,
} from './transform';
import { ESCaseAttributes } from './types';
import { AttachmentService } from '../attachments';
interface GetCaseIdsByAlertIdArgs extends ClientArgs {
interface GetCaseIdsByAlertIdArgs {
alertId: string;
filter?: KueryNode;
}
@ -65,31 +63,25 @@ interface PushedArgs {
pushed_by: User;
}
interface GetCaseArgs extends ClientArgs {
interface GetCaseArgs {
id: string;
}
interface GetCasesArgs extends ClientArgs {
interface GetCasesArgs {
caseIds: string[];
}
interface FindCommentsArgs {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
id: string | string[];
options?: SavedObjectFindOptionsKueryNode;
}
interface FindCaseCommentsArgs {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
id: string | string[];
options?: SavedObjectFindOptionsKueryNode;
}
interface FindCasesArgs extends ClientArgs {
options?: SavedObjectFindOptionsKueryNode;
}
interface PostCaseArgs extends ClientArgs {
interface PostCaseArgs {
attributes: CaseAttributes;
id: string;
}
@ -100,9 +92,9 @@ interface PatchCase {
originalCase: SavedObject<CaseAttributes>;
version?: string;
}
type PatchCaseArgs = PatchCase & ClientArgs;
type PatchCaseArgs = PatchCase;
interface PatchCasesArgs extends ClientArgs {
interface PatchCasesArgs {
cases: PatchCase[];
}
@ -110,11 +102,6 @@ interface GetUserArgs {
request: KibanaRequest;
}
interface CaseCommentStats {
commentTotals: Map<string, number>;
alertTotals: Map<string, number>;
}
interface CasesMapWithPageInfo {
casesMap: Map<string, CaseResponse>;
page: number;
@ -135,10 +122,27 @@ interface GetReportersArgs {
}
export class CasesService {
constructor(
private readonly log: Logger,
private readonly authentication?: SecurityPluginSetup['authc']
) {}
private readonly log: Logger;
private readonly authentication?: SecurityPluginSetup['authc'];
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
private readonly attachmentService: AttachmentService;
constructor({
log,
authentication,
unsecuredSavedObjectsClient,
attachmentService,
}: {
log: Logger;
authentication?: SecurityPluginSetup['authc'];
unsecuredSavedObjectsClient: SavedObjectsClientContract;
attachmentService: AttachmentService;
}) {
this.log = log;
this.authentication = authentication;
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
this.attachmentService = attachmentService;
}
private buildCaseIdsAggs = (
size: number = 100
@ -159,7 +163,6 @@ export class CasesService {
});
public async getCaseIdsByAlertId({
unsecuredSavedObjectsClient,
alertId,
filter,
}: GetCaseIdsByAlertIdArgs): Promise<
@ -172,7 +175,7 @@ export class CasesService {
filter,
]);
const response = await unsecuredSavedObjectsClient.find<
const response = await this.unsecuredSavedObjectsClient.find<
CommentAttributes,
GetCaseIdsByAlertIdAggs
>({
@ -204,35 +207,32 @@ export class CasesService {
* Returns a map of all cases.
*/
public async findCasesGroupedByID({
unsecuredSavedObjectsClient,
caseOptions,
}: {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
caseOptions: FindCaseOptions;
}): Promise<CasesMapWithPageInfo> {
const cases = await this.findCases({
unsecuredSavedObjectsClient,
options: caseOptions,
});
const cases = await this.findCases(caseOptions);
const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => {
accMap.set(caseInfo.id, caseInfo);
return accMap;
}, new Map<string, SavedObjectsFindResult<CaseAttributes>>());
const totalCommentsForCases = await this.getCaseCommentStats({
unsecuredSavedObjectsClient,
ids: Array.from(casesMap.keys()),
const commentTotals = await this.attachmentService.getCaseCommentStats({
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
caseIds: Array.from(casesMap.keys()),
});
const casesWithComments = new Map<string, CaseResponse>();
for (const [id, caseInfo] of casesMap.entries()) {
const { alerts, nonAlerts } = commentTotals.get(id) ?? { alerts: 0, nonAlerts: 0 };
casesWithComments.set(
id,
flattenCaseSavedObject({
savedObject: caseInfo,
totalComment: totalCommentsForCases.commentTotals.get(id) ?? 0,
totalAlerts: totalCommentsForCases.alertTotals.get(id) ?? 0,
totalComment: nonAlerts,
totalAlerts: alerts,
})
);
}
@ -245,107 +245,69 @@ export class CasesService {
};
}
/**
* Retrieves the number of cases that exist with a given status (open, closed, etc).
*/
public async findCaseStatusStats({
unsecuredSavedObjectsClient,
caseOptions,
ensureSavedObjectsAreAuthorized,
public async getCaseStatusStats({
searchOptions,
}: {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
caseOptions: SavedObjectFindOptionsKueryNode;
ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback;
}): Promise<number> {
const cases = await this.findCases({
unsecuredSavedObjectsClient,
options: {
...caseOptions,
page: 1,
perPage: MAX_DOCS_PER_PAGE,
searchOptions: SavedObjectFindOptionsKueryNode;
}): Promise<{
[status in CaseStatuses]: number;
}> {
const cases = await this.unsecuredSavedObjectsClient.find<
ESCaseAttributes,
{
statuses: {
buckets: Array<{
key: string;
doc_count: number;
}>;
};
}
>({
...searchOptions,
type: CASE_SAVED_OBJECT,
perPage: 0,
aggs: {
statuses: {
terms: {
field: `${CASE_SAVED_OBJECT}.attributes.status`,
size: caseStatuses.length,
order: { _key: 'asc' },
},
},
},
});
// make sure that the retrieved cases were correctly filtered by owner
ensureSavedObjectsAreAuthorized(
cases.saved_objects.map((caseInfo) => ({ id: caseInfo.id, owner: caseInfo.attributes.owner }))
);
return cases.saved_objects.length;
const statusBuckets = CasesService.getStatusBuckets(cases.aggregations?.statuses.buckets);
return {
open: statusBuckets?.get('open') ?? 0,
'in-progress': statusBuckets?.get('in-progress') ?? 0,
closed: statusBuckets?.get('closed') ?? 0,
};
}
/**
* Returns the number of total comments and alerts for a case
*/
public async getCaseCommentStats({
unsecuredSavedObjectsClient,
ids,
}: {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
ids: string[];
}): Promise<CaseCommentStats> {
if (ids.length <= 0) {
return {
commentTotals: new Map<string, number>(),
alertTotals: new Map<string, number>(),
};
}
const getCommentsMapper = async (id: string) =>
this.getAllCaseComments({
unsecuredSavedObjectsClient,
id,
options: { page: 1, perPage: 1 },
});
// Ensuring we don't do too many concurrent get running.
const allComments = await pMap(ids, getCommentsMapper, {
concurrency: MAX_CONCURRENT_SEARCHES,
});
const alerts = await this.getAllCaseComments({
unsecuredSavedObjectsClient,
id: ids,
options: {
filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert),
},
});
const getID = (comments: SavedObjectsFindResponse<unknown>) => {
return comments.saved_objects.length > 0
? comments.saved_objects[0].references.find((ref) => ref.type === CASE_SAVED_OBJECT)?.id
: undefined;
};
const groupedComments = allComments.reduce((acc, comments) => {
const id = getID(comments);
if (id) {
acc.set(id, comments.total);
}
private static getStatusBuckets(
buckets: Array<{ key: string; doc_count: number }> | undefined
): Map<string, number> | undefined {
return buckets?.reduce((acc, bucket) => {
acc.set(bucket.key, bucket.doc_count);
return acc;
}, new Map<string, number>());
const groupedAlerts = groupTotalAlertsByID({ comments: alerts });
return { commentTotals: groupedComments, alertTotals: groupedAlerts };
}
public async deleteCase({ unsecuredSavedObjectsClient, id: caseId }: GetCaseArgs) {
public async deleteCase({ id: caseId }: GetCaseArgs) {
try {
this.log.debug(`Attempting to DELETE case ${caseId}`);
return await unsecuredSavedObjectsClient.delete(CASE_SAVED_OBJECT, caseId);
return await this.unsecuredSavedObjectsClient.delete(CASE_SAVED_OBJECT, caseId);
} catch (error) {
this.log.error(`Error on DELETE case ${caseId}: ${error}`);
throw error;
}
}
public async getCase({
unsecuredSavedObjectsClient,
id: caseId,
}: GetCaseArgs): Promise<SavedObject<CaseAttributes>> {
public async getCase({ id: caseId }: GetCaseArgs): Promise<SavedObject<CaseAttributes>> {
try {
this.log.debug(`Attempting to GET case ${caseId}`);
const caseSavedObject = await unsecuredSavedObjectsClient.get<ESCaseAttributes>(
const caseSavedObject = await this.unsecuredSavedObjectsClient.get<ESCaseAttributes>(
CASE_SAVED_OBJECT,
caseId
);
@ -357,12 +319,11 @@ export class CasesService {
}
public async getResolveCase({
unsecuredSavedObjectsClient,
id: caseId,
}: GetCaseArgs): Promise<SavedObjectsResolveResponse<CaseAttributes>> {
try {
this.log.debug(`Attempting to resolve case ${caseId}`);
const resolveCaseResult = await unsecuredSavedObjectsClient.resolve<ESCaseAttributes>(
const resolveCaseResult = await this.unsecuredSavedObjectsClient.resolve<ESCaseAttributes>(
CASE_SAVED_OBJECT,
caseId
);
@ -377,12 +338,11 @@ export class CasesService {
}
public async getCases({
unsecuredSavedObjectsClient,
caseIds,
}: GetCasesArgs): Promise<SavedObjectsBulkResponse<CaseAttributes>> {
try {
this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`);
const cases = await unsecuredSavedObjectsClient.bulkGet<ESCaseAttributes>(
const cases = await this.unsecuredSavedObjectsClient.bulkGet<ESCaseAttributes>(
caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId }))
);
return transformBulkResponseToExternalModel(cases);
@ -392,13 +352,12 @@ export class CasesService {
}
}
public async findCases({
unsecuredSavedObjectsClient,
options,
}: FindCasesArgs): Promise<SavedObjectsFindResponse<CaseAttributes>> {
public async findCases(
options?: SavedObjectFindOptionsKueryNode
): Promise<SavedObjectsFindResponse<CaseAttributes>> {
try {
this.log.debug(`Attempting to find cases`);
const cases = await unsecuredSavedObjectsClient.find<ESCaseAttributes>({
const cases = await this.unsecuredSavedObjectsClient.find<ESCaseAttributes>({
sortField: defaultSortField,
...options,
type: CASE_SAVED_OBJECT,
@ -421,21 +380,20 @@ export class CasesService {
}
private async getAllComments({
unsecuredSavedObjectsClient,
id,
options,
}: FindCommentsArgs): Promise<SavedObjectsFindResponse<CommentAttributes>> {
try {
this.log.debug(`Attempting to GET all comments internal for id ${JSON.stringify(id)}`);
if (options?.page !== undefined || options?.perPage !== undefined) {
return unsecuredSavedObjectsClient.find<CommentAttributes>({
return this.unsecuredSavedObjectsClient.find<CommentAttributes>({
type: CASE_COMMENT_SAVED_OBJECT,
sortField: defaultSortField,
...options,
});
}
return unsecuredSavedObjectsClient.find<CommentAttributes>({
return this.unsecuredSavedObjectsClient.find<CommentAttributes>({
type: CASE_COMMENT_SAVED_OBJECT,
page: 1,
perPage: MAX_DOCS_PER_PAGE,
@ -453,7 +411,6 @@ export class CasesService {
* to override this pass in the either the page or perPage options.
*/
public async getAllCaseComments({
unsecuredSavedObjectsClient,
id,
options,
}: FindCaseCommentsArgs): Promise<SavedObjectsFindResponse<CommentAttributes>> {
@ -470,7 +427,6 @@ export class CasesService {
this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`);
return await this.getAllComments({
unsecuredSavedObjectsClient,
id,
options: {
hasReferenceOperator: 'OR',
@ -485,14 +441,11 @@ export class CasesService {
}
}
public async getReporters({
unsecuredSavedObjectsClient,
filter,
}: GetReportersArgs): Promise<User[]> {
public async getReporters({ filter }: GetReportersArgs): Promise<User[]> {
try {
this.log.debug(`Attempting to GET all reporters`);
const results = await unsecuredSavedObjectsClient.find<
const results = await this.unsecuredSavedObjectsClient.find<
ESCaseAttributes,
{
reporters: {
@ -549,11 +502,11 @@ export class CasesService {
}
}
public async getTags({ unsecuredSavedObjectsClient, filter }: GetTagsArgs): Promise<string[]> {
public async getTags({ filter }: GetTagsArgs): Promise<string[]> {
try {
this.log.debug(`Attempting to GET all cases`);
const results = await unsecuredSavedObjectsClient.find<
const results = await this.unsecuredSavedObjectsClient.find<
ESCaseAttributes,
{ tags: { buckets: Array<{ key: string }> } }
>({
@ -604,15 +557,11 @@ export class CasesService {
}
}
public async postNewCase({
unsecuredSavedObjectsClient,
attributes,
id,
}: PostCaseArgs): Promise<SavedObject<CaseAttributes>> {
public async postNewCase({ attributes, id }: PostCaseArgs): Promise<SavedObject<CaseAttributes>> {
try {
this.log.debug(`Attempting to POST a new case`);
const transformedAttributes = transformAttributesToESModel(attributes);
const createdCase = await unsecuredSavedObjectsClient.create<ESCaseAttributes>(
const createdCase = await this.unsecuredSavedObjectsClient.create<ESCaseAttributes>(
CASE_SAVED_OBJECT,
transformedAttributes.attributes,
{ id, references: transformedAttributes.referenceHandler.build() }
@ -625,7 +574,6 @@ export class CasesService {
}
public async patchCase({
unsecuredSavedObjectsClient,
caseId,
updatedAttributes,
originalCase,
@ -635,7 +583,7 @@ export class CasesService {
this.log.debug(`Attempting to UPDATE case ${caseId}`);
const transformedAttributes = transformAttributesToESModel(updatedAttributes);
const updatedCase = await unsecuredSavedObjectsClient.update<ESCaseAttributes>(
const updatedCase = await this.unsecuredSavedObjectsClient.update<ESCaseAttributes>(
CASE_SAVED_OBJECT,
caseId,
transformedAttributes.attributes,
@ -653,7 +601,6 @@ export class CasesService {
}
public async patchCases({
unsecuredSavedObjectsClient,
cases,
}: PatchCasesArgs): Promise<SavedObjectsBulkUpdateResponse<CaseAttributes>> {
try {
@ -670,7 +617,7 @@ export class CasesService {
};
});
const updatedCases = await unsecuredSavedObjectsClient.bulkUpdate<ESCaseAttributes>(
const updatedCases = await this.unsecuredSavedObjectsClient.bulkUpdate<ESCaseAttributes>(
bulkUpdate
);
return transformUpdateResponsesToExternalModels(updatedCases);

View file

@ -37,9 +37,8 @@ export const createCaseServiceMock = (): CaseServiceMock => {
postNewCase: jest.fn(),
patchCase: jest.fn(),
patchCases: jest.fn(),
getCaseCommentStats: jest.fn(),
findCaseStatusStats: jest.fn(),
findCasesGroupedByID: jest.fn(),
getCaseStatusStats: jest.fn(),
};
// the cast here is required because jest.Mocked tries to include private members and would throw an error
@ -108,6 +107,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => {
getAllAlertsAttachToCase: jest.fn(),
countAlertsAttachedToCase: jest.fn(),
executeCaseActionsAggregations: jest.fn(),
getCaseCommentStats: jest.fn(),
};
// the cast here is required because jest.Mocked tries to include private members and would throw an error