Skip to content

Commit

Permalink
Refactor connectors
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Apr 14, 2020
1 parent f44d951 commit 0f22570
Show file tree
Hide file tree
Showing 15 changed files with 908 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { connector as getServiceNowConnector } from './servicenow';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { schema } from '@kbn/config-schema';

export const MappingActionType = schema.oneOf([
schema.literal('nothing'),
schema.literal('overwrite'),
schema.literal('append'),
]);

export const MapRecordSchema = schema.object({
source: schema.string(),
target: schema.string(),
actionType: MappingActionType,
});

export const CaseConfigurationSchema = schema.object({
mapping: schema.arrayOf(MapRecordSchema),
});

export const ConnectorPublicConfiguration = {
apiUrl: schema.string(),
casesConfiguration: CaseConfigurationSchema,
};

export const ConnectorPublicConfigurationSchema = schema.object(ConnectorPublicConfiguration);

export const ConnectorSecretConfiguration = {
password: schema.string(),
username: schema.string(),
};

export const ConnectorSecretConfigurationSchema = schema.object(ConnectorSecretConfiguration);

export const UserSchema = schema.object({
fullName: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]),
username: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]),
email: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]),
});

const EntityInformation = {
createdAt: schema.string(),
createdBy: UserSchema,
updatedAt: schema.nullable(schema.string()),
updatedBy: schema.nullable(UserSchema),
};

export const EntityInformationSchema = schema.object(EntityInformation);

export const CommentSchema = schema.object({
commentId: schema.string(),
comment: schema.string(),
version: schema.maybe(schema.string()),
...EntityInformation,
});

export const ExecutorActionSchema = schema.oneOf([
schema.literal('getIncident'),
schema.literal('pushToService'),
schema.literal('handshake'),
]);

export const ExecutorActionParams = {
caseId: schema.string(),
title: schema.string(),
description: schema.maybe(schema.string()),
comments: schema.maybe(schema.arrayOf(CommentSchema)),
externalCaseId: schema.nullable(schema.string()),
...EntityInformation,
};

export const ExecutorActionParamsSchema = schema.object(ExecutorActionParams);

export const ExecutorParamsSchema = schema.object({
action: ExecutorActionSchema,
actionParams: ExecutorActionParamsSchema,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { zipWith } from 'lodash';

import {
ConnectorApi,
ExternalServiceParams,
ExternalServiceCommentResponse,
Comment,
PushToServiceResponse,
ConnectorApiHandlerArgs,
} from '../types';
import { prepareFieldsForTransformation, transformFields, transformComments } from '../utils';

const handshakeHandler = async ({
externalService,
mapping,
params,
}: ConnectorApiHandlerArgs) => {};
const getIncidentHandler = async ({
externalService,
mapping,
params,
}: ConnectorApiHandlerArgs) => {};

const pushToServiceHandler = async ({
externalService,
mapping,
params,
}: ConnectorApiHandlerArgs): Promise<PushToServiceResponse> => {
const { externalCaseId, comments } = params;
const updateIncident = externalCaseId ? true : false;
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
let currentIncident: ExternalServiceParams | undefined;
let res: PushToServiceResponse;

if (externalCaseId) {
currentIncident = await externalService.getIncident(externalCaseId);
}

const fields = prepareFieldsForTransformation({
params,
mapping,
defaultPipes,
});

const incident = transformFields({
params,
fields,
currentIncident,
});

if (updateIncident) {
res = await externalService.updateIncident({ incidentId: externalCaseId, incident });
} else {
res = await externalService.createIncident({ incident });
}

if (
comments &&
Array.isArray(comments) &&
comments.length > 0 &&
mapping.get('comments').actionType !== 'nothing'
) {
const commentsTransformed = transformComments(comments, ['informationAdded']);

const createdComments = await Promise.all(
commentsTransformed.map(comment =>
externalService.createComment({
incidentId: res.id,
comment,
field: mapping.get('comments').target,
})
)
);

const zippedComments: ExternalServiceCommentResponse[] = zipWith(
commentsTransformed,
createdComments,
(a: Comment, b: ExternalServiceCommentResponse) => ({
commentId: a.commentId,
pushedDate: b.pushedDate,
})
);

res.comments = [...zippedComments];
}

return res;
};

export const api: ConnectorApi = {
handshake: handshakeHandler,
pushToService: pushToServiceHandler,
getIncident: getIncidentHandler,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ConnectorConfiguration } from '../types';
import * as i18n from './translations';

export const config: ConnectorConfiguration = {
id: '.servicenow',
name: i18n.NAME,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { createConnector } from '../utils';

import { api } from './api';
import { config } from './config';
import { validate } from './validators';
import { createExternalService } from './service';

export const connector = createConnector({
api,
config,
validate,
createExternalService,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import axios from 'axios';

import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types';
import { addTimeZoneToDate, patch, request } from '../utils';

const API_VERSION = 'v2';
const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`;
const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`;
const COMMENT_URL = `api/now/${API_VERSION}/table/incident`;

// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html
const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`;

const getErrorMessage = (msg: string) => {
return `[Action][ServiceNow]: ${msg}`;
};

export const createExternalService = ({
url,
username,
password,
}: ExternalServiceCredential): ExternalService => {
if (!url || !username || !password) {
throw Error('[Action][ServiceNow]: Wrong configuration.');
}

const incidentUrl = `${url}/${INCIDENT_URL}`;
const commentUrl = `${url}/${COMMENT_URL}`;
const axiosInstance = axios.create({
auth: { username, password },
});

const getIncidentViewURL = (id: string) => {
return `${url}/${VIEW_INCIDENT_URL}${id}`;
};

const getIncident = async (id: string) => {
try {
const res = await request({
axios: axiosInstance,
url: `${incidentUrl}/${id}`,
});

return { ...res.data.result };
} catch (error) {
throw new Error(
getErrorMessage(`Unable to get incident with id ${id}. Error: ${error.message}`)
);
}
};

const createIncident = async ({ incident }: ExternalServiceParams) => {
try {
const res = await request({
axios: axiosInstance,
url: `${incidentUrl}`,
method: 'post',
data: { ...incident },
});

return {
title: res.data.result.number,
id: res.data.result.sys_id,
pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(),
url: getIncidentViewURL(res.data.result.sys_id),
};
} catch (error) {
throw new Error(getErrorMessage(`Unable to create incident. Error: ${error.message}`));
}
};

const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
try {
const res = await patch({
axios: axiosInstance,
url: `${incidentUrl}/${incidentId}`,
data: { ...incident },
});

return {
title: res.data.result.number,
id: res.data.result.sys_id,
pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
url: getIncidentViewURL(res.data.result.sys_id),
};
} catch (error) {
throw new Error(
getErrorMessage(`Unable to update incident with id ${incidentId}. Error: ${error.message}`)
);
}
};

const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
try {
const res = await patch({
axios: axiosInstance,
url: `${commentUrl}/${incidentId}`,
data: { [field]: comment.comment },
});

return {
commentId: comment.commentId,
pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
};
} catch (error) {
throw new Error(
getErrorMessage(
`Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
)
);
}
};

return {
getIncident,
createIncident,
updateIncident,
createComment,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', {
defaultMessage: 'ServiceNow',
});

export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', {
defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty',
});

export const WHITE_LISTED_ERROR = (message: string) =>
i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', {
defaultMessage: 'error configuring servicenow action: {message}',
values: {
message,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
Loading

0 comments on commit 0f22570

Please sign in to comment.