Skip to content

Commit

Permalink
Aggregations working
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathan-buttner committed Jan 5, 2023
1 parent 5350d7d commit 57fb9e5
Show file tree
Hide file tree
Showing 8 changed files with 507 additions and 203 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ const existsSchema = s.object({
})
),
});

const rangeSchema = s.object({
range: s.recordOf(
s.string(),
s.object({
lt: s.maybe(s.string()),
lte: s.maybe(s.string()),
gt: s.maybe(s.string()),
gte: s.maybe(s.string()),
})
),
});

const termValueSchema = s.object({
term: s.recordOf(s.string(), s.object({ value: s.string() })),
});

const nestedSchema = s.object({
nested: s.object({
path: s.string(),
query: s.object({
bool: s.object({
filter: s.arrayOf(termValueSchema),
}),
}),
}),
});

const arraySchema = s.arrayOf(s.oneOf([nestedSchema, rangeSchema]));

// TODO: it would be great if we could recursively build the schema since the aggregation have be nested
// For more details see how the types are defined in the elasticsearch javascript client:
// https://github.com/elastic/elasticsearch-js/blob/4ad5daeaf401ce8ebb28b940075e0a67e56ff9ce/src/api/typesWithBodyKey.ts#L5295
Expand All @@ -70,7 +100,7 @@ const boolSchema = s.object({
must_not: s.oneOf([termSchema, existsSchema]),
}),
s.object({
filter: s.oneOf([termSchema, existsSchema]),
filter: s.oneOf([termSchema, existsSchema, arraySchema]),
}),
]),
});
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/server/client/user_actions/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import type { GetCaseConnectorsResponse } from '../../../common/api';
import type { ICaseUserActionsResponse } from '../typedoc_interfaces';
import type { CasesClientArgs } from '../types';
import { get, getConnectors } from './get';
import { get } from './get';
import { getConnectors } from './connectors';
import type { GetConnectorsRequest, UserActionGet } from './types';

/**
Expand Down
240 changes: 240 additions & 0 deletions x-pack/plugins/cases/server/client/user_actions/connectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { isEqual } from 'lodash';

import type { SavedObject } from '@kbn/core/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type {
CaseUserActionResponse,
GetCaseConnectorsResponse,
CaseConnector,
} from '../../../common/api';
import { GetCaseConnectorsResponseRt } from '../../../common/api';
import { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions';
import { createCaseError } from '../../common/error';
import type { CasesClientArgs } from '..';
import type { Authorization, OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
import type { GetConnectorsRequest } from './types';
import type { CaseConnectorActivity, PushInfo } from '../../services/user_actions/types';
import type { CaseUserActionService } from '../../services';

export const getConnectors = async (
{ caseId }: GetConnectorsRequest,
clientArgs: CasesClientArgs
): Promise<GetCaseConnectorsResponse> => {
const {
services: { userActionService },
logger,
authorization,
actionsClient,
} = clientArgs;

try {
const [connectors, latestUserAction] = await Promise.all([
userActionService.getCaseConnectorInformation(caseId),
userActionService.getMostRecentUserAction(caseId),
]);

await checkConnectorsAuthorization({ authorization, connectors, latestUserAction });

const enrichedConnectors = await enrichConnectors({
caseId,
actionsClient,
connectors,
latestUserAction,
userActionService,
});

const results: GetCaseConnectorsResponse = [];

for (const enrichedConnector of enrichedConnectors) {
results.push({
...enrichedConnector.connector,
name: enrichedConnector.name,
needsToBePushed: hasDataToPush(enrichedConnector),
});
}

console.log('connectors response', JSON.stringify(results, null, 2));

return GetCaseConnectorsResponseRt.encode(results);
} catch (error) {
throw createCaseError({
message: `Failed to retrieve the case connectors case id: ${caseId}: ${error}`,
error,
logger,
});
}
};

const checkConnectorsAuthorization = async ({
connectors,
latestUserAction,
authorization,
}: {
connectors: CaseConnectorActivity[];
latestUserAction?: SavedObject<CaseUserActionResponse>;
authorization: PublicMethodsOf<Authorization>;
}) => {
const entities: OwnerEntity[] = latestUserAction
? [{ owner: latestUserAction.attributes.owner, id: latestUserAction.id }]
: [];

for (const connector of connectors) {
entities.push({
owner: connector.fields.attributes.owner,
id: connector.connectorId,
});

if (connector.push) {
entities.push({
owner: connector.push.attributes.owner,
id: connector.connectorId,
});
}
}

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

interface EnrichedConnector {
connector: CaseConnector;
name: string;
pushInfo?: EnrichedPushInfo;
latestUserActionDate?: Date;
}

const enrichConnectors = async ({
caseId,
connectors,
latestUserAction,
actionsClient,
userActionService,
}: {
caseId: string;
connectors: CaseConnectorActivity[];
latestUserAction?: SavedObject<CaseUserActionResponse>;
actionsClient: PublicMethodsOf<ActionsClient>;
userActionService: CaseUserActionService;
}): Promise<EnrichedConnector[]> => {
const pushInfo = await getPushInfo({ caseId, activity: connectors, userActionService });

const enrichedConnectors: EnrichedConnector[] = [];

for (const aggregationConnector of connectors) {
const connectorDetails = await actionsClient.get({ id: aggregationConnector.connectorId });
const connector = getConnectorInfoFromSavedObject(aggregationConnector.fields);

const latestUserActionCreatedAt = getDate(latestUserAction?.attributes.created_at);

if (connector != null) {
enrichedConnectors.push({
connector,
name: connectorDetails.name,
pushInfo: pushInfo.get(aggregationConnector.connectorId),
latestUserActionDate: latestUserActionCreatedAt,
});
}
}

return enrichedConnectors;
};

const getPushInfo = async ({
caseId,
activity,
userActionService,
}: {
caseId: string;
activity: CaseConnectorActivity[];
userActionService: CaseUserActionService;
}): Promise<Map<string, EnrichedPushInfo>> => {
const pushRequest: PushInfo[] = [];

for (const connectorInfo of activity) {
const pushCreatedAt = getDate(connectorInfo.push?.attributes.created_at);

if (connectorInfo.push != null && pushCreatedAt != null) {
pushRequest.push({ connectorId: connectorInfo.connectorId, date: pushCreatedAt });
}
}

if (pushRequest.length <= 0) {
return new Map();
}

const priorToPushFields = await userActionService.getConnectorFieldsBeforePushes(
caseId,
pushRequest
);

const enrichedPushInfo = new Map<string, EnrichedPushInfo>();
for (const request of pushRequest) {
const connectorFieldsSO = priorToPushFields.get(request.connectorId);
const connectorFields = getConnectorInfoFromSavedObject(connectorFieldsSO);

if (connectorFields != null) {
enrichedPushInfo.set(request.connectorId, {
pushDate: request.date,
connectorFieldsBeforePush: connectorFields,
});
}
}

return enrichedPushInfo;
};

const getDate = (timestamp: string | undefined): Date | undefined => {
if (timestamp == null) {
return;
}

const date = new Date(timestamp);

if (isDateValid(date)) {
return date;
}
};

const isDateValid = (date: Date): boolean => {
return !isNaN(date.getTime());
};

interface EnrichedPushInfo {
pushDate: Date;
connectorFieldsBeforePush: CaseConnector;
}

const getConnectorInfoFromSavedObject = (
savedObject: SavedObject<CaseUserActionResponse> | undefined
): CaseConnector | undefined => {
if (
savedObject != null &&
(isConnectorUserAction(savedObject.attributes) ||
isCreateCaseUserAction(savedObject.attributes))
) {
return savedObject.attributes.payload.connector;
}
};

const hasDataToPush = (enrichedConnectorInfo: EnrichedConnector): boolean => {
return (
!isEqual(
enrichedConnectorInfo.connector,
enrichedConnectorInfo.pushInfo?.connectorFieldsBeforePush
) ||
(enrichedConnectorInfo.pushInfo != null &&
enrichedConnectorInfo.latestUserActionDate != null &&
enrichedConnectorInfo.pushInfo.pushDate < enrichedConnectorInfo.latestUserActionDate)
);
};
Loading

0 comments on commit 57fb9e5

Please sign in to comment.