Skip to content

Commit

Permalink
[Cases] Add bulk attachments internal route (#129092)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
cnasikas and kibanamachine authored Apr 14, 2022
1 parent b84383e commit b5817af
Show file tree
Hide file tree
Showing 19 changed files with 1,091 additions and 118 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,15 @@ export const FindQueryParamsRt = rt.partial({
...SavedObjectFindOptionsRt.props,
});

export const BulkCreateCommentRequestRt = rt.array(CommentRequestRt);

export type FindQueryParams = rt.TypeOf<typeof FindQueryParamsRt>;
export type AttributesTypeActions = rt.TypeOf<typeof AttributesTypeActionsRt>;
export type AttributesTypeAlerts = rt.TypeOf<typeof AttributesTypeAlertsRt>;
export type AttributesTypeUser = rt.TypeOf<typeof AttributesTypeUserRt>;
export type CommentAttributes = rt.TypeOf<typeof CommentAttributesRt>;
export type CommentRequest = rt.TypeOf<typeof CommentRequestRt>;
export type BulkCreateCommentRequest = rt.TypeOf<typeof BulkCreateCommentRequestRt>;
export type CommentResponse = rt.TypeOf<typeof CommentResponseRt>;
export type CommentResponseUserType = rt.TypeOf<typeof CommentResponseTypeUserRt>;
export type CommentResponseAlertsType = rt.TypeOf<typeof CommentResponseTypeAlertsRt>;
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/cases/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const;

export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as const;

/**
* Internal routes
*/

export const CASES_INTERNAL_URL = '/internal/cases' as const;
export const INTERNAL_BULK_CREATE_ATTACHMENTS_URL =
`${CASES_INTERNAL_URL}/{case_id}/attachments/_bulk_create` as const;

/**
* Action routes
*/
Expand Down
88 changes: 88 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/bulk_create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 { SavedObjectsUtils } from '../../../../../../src/core/server';

import {
BulkCreateCommentRequest,
BulkCreateCommentRequestRt,
CaseResponse,
CommentRequest,
throwErrors,
} from '../../../common/api';

import { CaseCommentModel } from '../../common/models';
import { createCaseError } from '../../common/error';
import { CasesClientArgs } from '..';

import { decodeCommentRequest } from '../utils';
import { Operations, OwnerEntity } from '../../authorization';

export interface BulkCreateArgs {
caseId: string;
attachments: BulkCreateCommentRequest;
}

/**
* Create an attachment to a case.
*
* @ignore
*/
export const bulkCreate = async (
args: BulkCreateArgs,
clientArgs: CasesClientArgs
): Promise<CaseResponse> => {
const { attachments, caseId } = args;

pipe(
BulkCreateCommentRequestRt.decode(attachments),
fold(throwErrors(Boom.badRequest), identity)
);

attachments.forEach((attachment) => {
decodeCommentRequest(attachment);
});

const { logger, authorization } = clientArgs;

try {
const [attachmentsWithIds, entities]: [Array<{ id: string } & CommentRequest>, OwnerEntity[]] =
attachments.reduce<[Array<{ id: string } & CommentRequest>, OwnerEntity[]]>(
([a, e], attachment) => {
const savedObjectID = SavedObjectsUtils.generateId();
return [
[...a, { id: savedObjectID, ...attachment }],
[...e, { owner: attachment.owner, id: savedObjectID }],
];
},
[[], []]
);

await authorization.ensureAuthorized({
operation: Operations.createComment,
entities,
});

const model = await CaseCommentModel.create(caseId, clientArgs);
const updatedModel = await model.bulkCreate({
attachments: attachmentsWithIds,
});

return await updatedModel.encodeWithComments();
} catch (error) {
throw createCaseError({
message: `Failed while bulk creating attachment to case id: ${caseId} error: ${error}`,
error,
logger,
});
}
};
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CasesClientInternal } from '../client_internal';
import { IAllCommentsResponse, ICaseResponse, ICommentsResponse } from '../typedoc_interfaces';
import { CasesClientArgs } from '../types';
import { AddArgs, addComment } from './add';
import { bulkCreate, BulkCreateArgs } from './bulk_create';
import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete';
import {
find,
Expand All @@ -33,6 +34,7 @@ export interface AttachmentsSubClient {
* Adds an attachment to a case.
*/
add(params: AddArgs): Promise<ICaseResponse>;
bulkCreate(params: BulkCreateArgs): Promise<ICaseResponse>;
/**
* Deletes all attachments associated with a single case.
*/
Expand Down Expand Up @@ -77,6 +79,7 @@ export const createAttachmentsSubClient = (
): AttachmentsSubClient => {
const attachmentSubClient: AttachmentsSubClient = {
add: (params: AddArgs) => addComment(params, clientArgs),
bulkCreate: (params: BulkCreateArgs) => bulkCreate(params, clientArgs),
deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs),
delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs),
find: (findArgs: FindArgs) => find(findArgs, clientArgs),
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/server/client/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type AttachmentsSubClientMock = jest.Mocked<AttachmentsSubClient>;
const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => {
return {
add: jest.fn(),
bulkCreate: jest.fn(),
deleteAll: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
Expand Down
126 changes: 101 additions & 25 deletions x-pack/plugins/cases/server/common/models/case_with_comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,9 @@ export class CaseCommentModel {
id: string;
}): Promise<CaseCommentModel> {
try {
this.validateCreateCommentRequest(commentReq);
this.validateCreateCommentRequest([commentReq]);

let references = this.buildRefsToCase();

if (commentReq.type === CommentType.user && commentReq?.comment) {
const commentStringReferences = getOrUpdateLensReferences(
this.params.lensEmbeddableFactory,
commentReq.comment
);
references = [...references, ...commentStringReferences];
}
const references = [...this.buildRefsToCase(), ...this.getCommentReferences(commentReq)];

const [comment, commentableCase] = await Promise.all([
this.params.attachmentService.create({
Expand All @@ -215,7 +207,7 @@ export class CaseCommentModel {
]);

await Promise.all([
commentableCase.handleAlertComments(comment, commentReq),
commentableCase.handleAlertComments([commentReq]),
this.createCommentUserAction(comment, commentReq),
]);

Expand All @@ -229,12 +221,15 @@ export class CaseCommentModel {
}
}

private validateCreateCommentRequest(req: CommentRequest) {
if (req.type === CommentType.alert && this.caseInfo.attributes.status === CaseStatuses.closed) {
private validateCreateCommentRequest(req: CommentRequest[]) {
if (
req.some((attachment) => attachment.type === CommentType.alert) &&
this.caseInfo.attributes.status === CaseStatuses.closed
) {
throw Boom.badRequest('Alert cannot be attached to a closed case');
}

if (req.owner !== this.caseInfo.attributes.owner) {
if (req.some((attachment) => attachment.owner !== this.caseInfo.attributes.owner)) {
throw Boom.badRequest('The owner field of the comment must match the case');
}
}
Expand All @@ -249,20 +244,38 @@ export class CaseCommentModel {
];
}

private async handleAlertComments(comment: SavedObject<CommentAttributes>, req: CommentRequest) {
if (
comment.attributes.type === CommentType.alert &&
this.caseInfo.attributes.settings.syncAlerts
) {
await this.updateAlertsStatus(req);
private getCommentReferences(commentReq: CommentRequest) {
let references: SavedObjectReference[] = [];

if (commentReq.type === CommentType.user && commentReq?.comment) {
const commentStringReferences = getOrUpdateLensReferences(
this.params.lensEmbeddableFactory,
commentReq.comment
);
references = [...references, ...commentStringReferences];
}

return references;
}

private async updateAlertsStatus(req: CommentRequest) {
const alertsToUpdate = createAlertUpdateRequest({
comment: req,
status: this.caseInfo.attributes.status,
});
private async handleAlertComments(attachments: CommentRequest[]) {
const alerts = attachments.filter(
(attachment) =>
attachment.type === CommentType.alert && this.caseInfo.attributes.settings.syncAlerts
);

await this.updateAlertsStatus(alerts);
}

private async updateAlertsStatus(alerts: CommentRequest[]) {
const alertsToUpdate = alerts
.map((alert) =>
createAlertUpdateRequest({
comment: alert,
status: this.caseInfo.attributes.status,
})
)
.flat();

await this.params.alertsService.updateAlertsStatus(alertsToUpdate);
}
Expand All @@ -285,6 +298,19 @@ export class CaseCommentModel {
});
}

private async bulkCreateCommentUserAction(attachments: Array<{ id: string } & CommentRequest>) {
await this.params.userActionService.bulkCreateAttachmentCreation({
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
caseId: this.caseInfo.id,
attachments: attachments.map(({ id, ...attachment }) => ({
id,
owner: attachment.owner,
attachment,
})),
user: this.params.user,
});
}

private formatForEncoding(totalComment: number) {
return {
id: this.caseInfo.id,
Expand Down Expand Up @@ -322,4 +348,54 @@ export class CaseCommentModel {
});
}
}
public async bulkCreate({
attachments,
}: {
attachments: Array<{ id: string } & CommentRequest>;
}): Promise<CaseCommentModel> {
try {
this.validateCreateCommentRequest(attachments);

const caseReference = this.buildRefsToCase();

const [newlyCreatedAttachments, commentableCase] = await Promise.all([
this.params.attachmentService.bulkCreate({
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
attachments: attachments.map(({ id, ...attachment }) => {
return {
attributes: transformNewComment({
createdDate: new Date().toISOString(),
...attachment,
...this.params.user,
}),
references: [...caseReference, ...this.getCommentReferences(attachment)],
id,
};
}),
}),
this.updateCaseUserAndDate(new Date().toISOString()),
]);

const savedObjectsWithoutErrors = newlyCreatedAttachments.saved_objects.filter(
(attachment) => attachment.error == null
);

const attachmentsWithoutErrors = attachments.filter((attachment) =>
savedObjectsWithoutErrors.some((so) => so.id === attachment.id)
);

await Promise.all([
commentableCase.handleAlertComments(attachmentsWithoutErrors),
this.bulkCreateCommentUserAction(attachmentsWithoutErrors),
]);

return commentableCase;
} catch (error) {
throw createCaseError({
message: `Failed bulk creating attachments on a commentable case, case id: ${this.caseInfo.id}: ${error}`,
error,
logger: this.params.logger,
});
}
}
}
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getExternalRoutes } from './routes/api/get_external_routes';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { createCasesTelemetry, scheduleCasesTelemetryTask } from './telemetry';
import { getInternalRoutes } from './routes/api/get_internal_routes';

export interface PluginsSetup {
actions: ActionsPluginSetup;
Expand Down Expand Up @@ -130,7 +131,7 @@ export class CasePlugin {

registerRoutes({
router,
routes: getExternalRoutes(),
routes: [...getExternalRoutes(), ...getInternalRoutes()],
logger: this.logger,
kibanaVersion: this.kibanaVersion,
telemetryUsageCounter,
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { bulkCreateAttachmentsRoute } from './internal/bulk_create_attachments';
import { CaseRoute } from './types';

export const getInternalRoutes = () => [bulkCreateAttachmentsRoute] as CaseRoute[];
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { INTERNAL_BULK_CREATE_ATTACHMENTS_URL } from '../../../../common/constants';
import { BulkCreateCommentRequest } from '../../../../common/api';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
import { escapeHatch } from '../utils';

export const bulkCreateAttachmentsRoute = createCasesRoute({
method: 'post',
path: INTERNAL_BULK_CREATE_ATTACHMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
body: schema.arrayOf(escapeHatch),
},
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;
const attachments = request.body as BulkCreateCommentRequest;

return response.ok({
body: await casesClient.attachments.bulkCreate({ caseId, attachments }),
});
} catch (error) {
throw createCaseError({
message: `Failed to bulk create attachments in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
},
});
Loading

0 comments on commit b5817af

Please sign in to comment.