Merge branch 'master' of https://github.com/backstage/backstage into marley/7641-consume-exported-ado-types

This commit is contained in:
Marley Powell
2021-11-09 07:59:38 +00:00
37 changed files with 1063 additions and 178 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+47
View File
@@ -0,0 +1,47 @@
## API Report File for "@backstage/plugin-azure-devops-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// Warning: (ae-missing-release-tag) "BuildResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum BuildResult {
Canceled = 32,
Failed = 8,
None = 0,
PartiallySucceeded = 4,
Succeeded = 2,
}
// Warning: (ae-missing-release-tag) "BuildStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum BuildStatus {
All = 47,
Cancelling = 4,
Completed = 2,
InProgress = 1,
None = 0,
NotStarted = 32,
Postponed = 8,
}
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
startTime?: Date;
finishTime?: Date;
source: string;
uniqueName?: string;
};
// (No @packageDocumentation comment for this package)
```
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@backstage/plugin-azure-devops-common",
"version": "0.0.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/azure-devops-common"
},
"keywords": [
"backstage"
],
"scripts": {
"build": "backstage-cli build",
"lint": "backstage-cli lint",
"test": "backstage-cli test --passWithNoTests",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.8.1"
},
"files": [
"dist"
]
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum BuildResult {
/**
* No result
*/
None = 0,
/**
* The build completed successfully.
*/
Succeeded = 2,
/**
* The build completed compilation successfully but had other errors.
*/
PartiallySucceeded = 4,
/**
* The build completed unsuccessfully.
*/
Failed = 8,
/**
* The build was canceled before starting.
*/
Canceled = 32,
}
export enum BuildStatus {
/**
* No status.
*/
None = 0,
/**
* The build is currently in progress.
*/
InProgress = 1,
/**
* The build has completed.
*/
Completed = 2,
/**
* The build is cancelling
*/
Cancelling = 4,
/**
* The build is inactive in the queue.
*/
Postponed = 8,
/**
* The build has not yet started.
*/
NotStarted = 32,
/**
* All status.
*/
All = 47,
}
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
startTime?: Date;
finishTime?: Date;
source: string;
uniqueName?: string;
};
@@ -0,0 +1,94 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
BuildResult,
BuildStatus,
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import { BuildTable } from './BuildTable';
import { MemoryRouter } from 'react-router';
import React from 'react';
export default {
title: 'Plugins/Azure Devops/Build Table',
component: BuildTable,
};
const buildStatuses: Array<[BuildStatus, BuildResult]> = [
[BuildStatus.InProgress, BuildResult.None], // In Progress
[BuildStatus.Completed, BuildResult.Succeeded], // Succeeded
[BuildStatus.Completed, BuildResult.Failed], // Failed
[BuildStatus.Completed, BuildResult.PartiallySucceeded], // Partially Succeeded
[BuildStatus.Completed, BuildResult.Canceled], // Cancelled
[BuildStatus.Completed, BuildResult.None], // Unknown
[BuildStatus.Cancelling, BuildResult.None], // Cancelling
[BuildStatus.Postponed, BuildResult.None], // Postponed
[BuildStatus.NotStarted, BuildResult.None], // Not Started
[BuildStatus.None, BuildResult.None], // Unknown
];
const generateTestData = (rows = 10): RepoBuild[] => {
const repoBuilds: RepoBuild[] = [];
for (let i = 0; i < rows; i++) {
const [status, result] = buildStatuses[i] ?? [
BuildStatus.Completed,
BuildResult.Succeeded,
];
repoBuilds.push({
id: rows - i + 12534,
title: `backstage ci - 1.0.0-preview-${rows - i}`,
status,
result,
queueTime: new Date(Date.now() - i * 60000),
source: 'refs/heads/main',
link: '',
});
}
return repoBuilds;
};
export const Default = () => (
<MemoryRouter>
<BuildTable items={generateTestData()} loading={false} error={undefined} />
</MemoryRouter>
);
export const Empty = () => (
<MemoryRouter>
<BuildTable items={[]} loading={false} error={undefined} />
</MemoryRouter>
);
export const Loading = () => (
<MemoryRouter>
<BuildTable items={[]} loading error={undefined} />
</MemoryRouter>
);
export const ErrorMessage = () => (
<MemoryRouter>
<BuildTable
items={[]}
loading={false}
error={new Error('Failed to load builds!')}
/>
</MemoryRouter>
);
@@ -149,9 +149,7 @@ const columns: TableColumn[] = [
field: 'queueTime',
width: 'auto',
render: (row: Partial<RepoBuild>) =>
DateTime.fromISO(
row.queueTime ? row.queueTime.toString() : new Date().toString(),
).toRelative(),
DateTime.fromJSDate(row.queueTime ?? new Date()).toRelative(),
},
];
+8 -5
View File
@@ -870,11 +870,14 @@ export type EntityAncestryResponse = {
// Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type EntityFilter = {
anyOf: {
allOf: EntitiesSearchFilter[];
}[];
};
export type EntityFilter =
| {
allOf: EntityFilter[];
}
| {
anyOf: EntityFilter[];
}
| EntitiesSearchFilter;
// Warning: (ae-missing-release-tag) "EntityPagination" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+4 -3
View File
@@ -22,9 +22,10 @@ import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
* Any (at least one) of the outer sets must match, within which all of the
* individual filters must match.
*/
export type EntityFilter = {
anyOf: { allOf: EntitiesSearchFilter[] }[];
};
export type EntityFilter =
| { allOf: EntityFilter[] }
| { anyOf: EntityFilter[] }
| EntitiesSearchFilter;
/**
* A pagination rule for entities.
@@ -46,7 +46,11 @@ import {
DbPageInfo,
Transaction,
} from './types';
import { EntityPagination } from '../../catalog/types';
import { EntityPagination, EntitiesSearchFilter } from '../../catalog/types';
type LegacyEntityFilter = {
anyOf: { allOf: EntitiesSearchFilter[] }[];
};
// The number of items that are sent per batch to the database layer, when
// doing .batchInsert calls to knex. This needs to be low enough to not cause
@@ -217,13 +221,28 @@ export class CommonDatabase implements Database {
let entitiesQuery = tx<DbEntitiesRow>('entities');
for (const singleFilter of request?.filter?.anyOf ?? []) {
if (
request?.filter &&
(request.filter.hasOwnProperty('key') ||
request.filter.hasOwnProperty('allOf'))
) {
throw new Error(
'Filters for the legacy CommonDatabase must obey the { anyOf: [{ allOf: [] }] } format.',
);
}
for (const singleFilter of (request?.filter as LegacyEntityFilter)?.anyOf ??
[]) {
entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
for (const {
key,
matchValueIn,
matchValueExists,
} of singleFilter.allOf) {
for (const filter of singleFilter.allOf) {
if (
filter.hasOwnProperty('anyOf') ||
filter.hasOwnProperty('allOf')
) {
throw new Error(
'Nested filters are not supported in the legacy CommonDatabase',
);
}
const { key, matchValueIn, matchValueExists } = filter;
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
@@ -23,6 +23,7 @@ import {
DbFinalEntitiesRow,
DbRefreshStateReferencesRow,
DbRefreshStateRow,
DbSearchRow,
} from '../database/tables';
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
@@ -73,6 +74,52 @@ describe('NextEntitiesCatalog', () => {
}
}
async function addEntityToSearch(knex: Knex, entity: Entity) {
const id = uuid();
const entityRef = stringifyEntityRef(entity);
const entityJson = JSON.stringify(entity);
await knex<DbRefreshStateRow>('refresh_state').insert({
entity_id: id,
entity_ref: entityRef,
unprocessed_entity: entityJson,
errors: '[]',
next_update_at: '2031-01-01 23:00:00',
last_discovery_at: '2021-04-01 13:37:00',
});
await knex<DbFinalEntitiesRow>('final_entities').insert({
entity_id: id,
final_entity: entityJson,
hash: 'h',
stitch_ticket: '',
});
await insertSearchRow(knex, id, null, entity);
}
async function insertSearchRow(
knex: Knex,
id: string,
previousKey: string | null,
previousValue: Object,
) {
return Promise.all(
Object.entries(previousValue).map(async ([key, value]) => {
const currentKey = `${previousKey ? `${previousKey}.` : ``}${key}`;
if (typeof value === 'object') {
await insertSearchRow(knex, id, currentKey, value);
} else {
await knex<DbSearchRow>('search').insert({
entity_id: id,
key: currentKey,
value: value,
});
}
}),
);
}
describe('entityAncestry', () => {
it.each(databases.eachSupportedId())(
'should return the ancestry with one parent, %p',
@@ -209,4 +256,106 @@ describe('NextEntitiesCatalog', () => {
60_000,
);
});
describe('entities', () => {
it.each(databases.eachSupportedId())(
'should return correct entity for simple filter',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'one' },
spec: {},
};
const entity2: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'two' },
spec: {
test: 'test value',
},
};
await addEntityToSearch(knex, entity1);
await addEntityToSearch(knex, entity2);
const catalog = new NextEntitiesCatalog(knex);
const testFilter = {
key: 'spec.test',
matchValueExists: true,
};
const request = { filter: testFilter };
const { entities } = await catalog.entities(request);
expect(entities.length).toBe(1);
expect(entities[0]).toEqual(entity2);
},
);
it.each(databases.eachSupportedId())(
'should return correct entity for nested filter',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'one', org: 'a', desc: 'description' },
spec: {},
};
const entity2: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'two', org: 'b', desc: 'description' },
spec: {},
};
const entity3: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'three', org: 'b', color: 'red' },
spec: {},
};
const entity4: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'four', org: 'b', color: 'blue' },
spec: {},
};
await addEntityToSearch(knex, entity1);
await addEntityToSearch(knex, entity2);
await addEntityToSearch(knex, entity3);
await addEntityToSearch(knex, entity4);
const catalog = new NextEntitiesCatalog(knex);
const testFilter1 = {
key: 'metadata.org',
matchValueExists: true,
matchValueIn: ['b'],
};
const testFilter2 = {
key: 'metadata.desc',
matchValueExists: true,
};
const testFilter3 = {
key: 'metadata.color',
matchValueExists: true,
matchValueIn: ['blue'],
};
const request = {
filter: {
allOf: [
testFilter1,
{
anyOf: [testFilter2, testFilter3],
},
],
},
};
const { entities } = await catalog.entities(request);
expect(entities.length).toBe(2);
expect(entities).toContainEqual(entity2);
expect(entities).toContainEqual(entity4);
},
);
});
});
@@ -23,6 +23,8 @@ import {
EntitiesResponse,
EntityAncestryResponse,
EntityPagination,
EntityFilter,
EntitiesSearchFilter,
} from '../catalog/types';
import {
DbFinalEntitiesRow,
@@ -73,6 +75,90 @@ function stringifyPagination(input: { limit: number; offset: number }) {
return base64;
}
function addCondition(
queryBuilder: Knex.QueryBuilder,
db: Knex,
{ key, matchValueIn, matchValueExists }: EntitiesSearchFilter,
) {
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
const matchQuery = db<DbSearchRow>('search')
.select('entity_id')
.where(function keyFilter() {
this.andWhere({ key: key.toLowerCase() });
if (matchValueExists !== false && matchValueIn) {
if (matchValueIn.length === 1) {
this.andWhere({ value: matchValueIn[0].toLowerCase() });
} else if (matchValueIn.length > 1) {
this.andWhere(
'value',
'in',
matchValueIn.map(v => v.toLowerCase()),
);
}
}
});
// Explicitly evaluate matchValueExists as a boolean since it may be undefined
queryBuilder.andWhere(
'entity_id',
matchValueExists === false ? 'not in' : 'in',
matchQuery,
);
}
function isEntitiesSearchFilter(
filter: EntitiesSearchFilter | EntityFilter,
): filter is EntitiesSearchFilter {
return filter.hasOwnProperty('key');
}
function isAndEntityFilter(
filter: { allOf: EntityFilter[] } | EntityFilter,
): filter is { allOf: EntityFilter[] } {
return filter.hasOwnProperty('allOf');
}
function isOrEntityFilter(
filter: { anyOf: EntityFilter[] } | EntityFilter,
): filter is { anyOf: EntityFilter[] } {
return filter.hasOwnProperty('anyOf');
}
function parseFilter(
filter: EntityFilter,
query: Knex.QueryBuilder,
db: Knex,
): Knex.QueryBuilder {
if (isEntitiesSearchFilter(filter)) {
return query.where(function filterFunction() {
addCondition(this, db, filter);
});
}
if (isOrEntityFilter(filter)) {
let cumulativeQuery = query;
for (const subFilter of filter.anyOf ?? []) {
cumulativeQuery = cumulativeQuery.orWhere(subQuery =>
parseFilter(subFilter, subQuery, db),
);
}
return cumulativeQuery;
}
if (isAndEntityFilter(filter)) {
let cumulativeQuery = query;
for (const subFilter of filter.allOf ?? []) {
cumulativeQuery = cumulativeQuery.andWhere(subQuery =>
parseFilter(subFilter, subQuery, db),
);
}
return cumulativeQuery;
}
return query;
}
export class NextEntitiesCatalog implements EntitiesCatalog {
constructor(private readonly database: Knex) {}
@@ -80,41 +166,8 @@ export class NextEntitiesCatalog implements EntitiesCatalog {
const db = this.database;
let entitiesQuery = db<DbFinalEntitiesRow>('final_entities');
for (const singleFilter of request?.filter?.anyOf ?? []) {
entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
for (const {
key,
matchValueIn,
matchValueExists,
} of singleFilter.allOf) {
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
const matchQuery = db<DbSearchRow>('search')
.select('entity_id')
.where(function keyFilter() {
this.andWhere({ key: key.toLowerCase() });
if (matchValueExists !== false && matchValueIn) {
if (matchValueIn.length === 1) {
this.andWhere({ value: matchValueIn[0].toLowerCase() });
} else if (matchValueIn.length > 1) {
this.andWhere(
'value',
'in',
matchValueIn.map(v => v.toLowerCase()),
);
}
}
});
// Explicitly evaluate matchValueExists as a boolean since it may be undefined
this.andWhere(
'entity_id',
matchValueExists === false ? 'not in' : 'in',
matchQuery,
);
}
});
if (request?.filter) {
entitiesQuery = parseFilter(request.filter, entitiesQuery, db);
}
// TODO: move final_entities to use entity_ref
+12 -2
View File
@@ -15,6 +15,7 @@ import { Context } from 'react';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { IconButton } from '@material-ui/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { LinkProps } from '@backstage/core-components';
import { Observable } from '@backstage/types';
import { PropsWithChildren } from 'react';
@@ -736,6 +737,17 @@ export function getEntitySourceLocation(
// @public
export function isOwnerOf(owner: Entity, owned: Entity): boolean;
// @public
export function loadCatalogOwnerRefs(
catalogApi: CatalogApi,
identityOwnerRefs: string[],
): Promise<string[]>;
// @public
export function loadIdentityOwnerRefs(
identityApi: IdentityApi,
): Promise<string[]>;
// Warning: (ae-missing-release-tag) "MockEntityListContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -823,8 +835,6 @@ export function useEntityListProvider<
EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,
>(): EntityListContextProps<EntityFilters>;
// Warning: (ae-missing-release-tag) "useEntityOwnership" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function useEntityOwnership(): {
loading: boolean;
+5 -1
View File
@@ -37,4 +37,8 @@ export { useOwnUser } from './useOwnUser';
export { useRelatedEntities } from './useRelatedEntities';
export { useStarredEntities } from './useStarredEntities';
export { useStarredEntity } from './useStarredEntity';
export { useEntityOwnership } from './useEntityOwnership';
export {
loadCatalogOwnerRefs,
useEntityOwnership,
loadIdentityOwnerRefs,
} from './useEntityOwnership';
@@ -49,9 +49,16 @@ function extendUserId(id: string): string {
}
}
// Takes the relevant parts of the Backstage identity, and translates them into
// a list of entity refs on string form that represent the user's ownership
// connections.
/**
* Takes the relevant parts of the Backstage identity, and translates them into
* a list of entity refs on string form that represent the user's ownership
* connections.
*
* @public
*
* @param identityApi - The IdentityApi implementation
* @returns IdentityOwner refs as a string array
*/
export async function loadIdentityOwnerRefs(
identityApi: IdentityApi,
): Promise<string[]> {
@@ -81,9 +88,17 @@ export async function loadIdentityOwnerRefs(
return result;
}
// Takes the relevant parts of the User entity corresponding to the Backstage
// identity, and translates them into a list of entity refs on string form that
// represent the user's ownership connections.
/**
* Takes the relevant parts of the User entity corresponding to the Backstage
* identity, and translates them into a list of entity refs on string form that
* represent the user's ownership connections.
*
* @public
*
* @param catalogApi - The Catalog API implementation
* @param identityOwnerRefs - List of identity owner refs as strings
* @returns OwnerRefs as a string array
*/
export async function loadCatalogOwnerRefs(
catalogApi: CatalogApi,
identityOwnerRefs: string[],
@@ -113,6 +128,10 @@ export async function loadCatalogOwnerRefs(
* owner of a given entity. When the hook is initially mounted, the loading
* flag will be true and the results returned from the function will always be
* false.
*
* @public
*
* @returns a function that checks if the signed in user owns an entity
*/
export function useEntityOwnership(): {
loading: boolean;
+1
View File
@@ -33,6 +33,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@material-ui/styles": "^4.11.0",
"highlight.js": "^10.6.0",
"luxon": "^2.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
@@ -46,6 +46,8 @@ import { codeCoverageApiRef } from '../../api';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { DateTime } from 'luxon';
type Coverage = 'line' | 'branch';
const useStyles = makeStyles<BackstageTheme>(theme => ({
@@ -69,6 +71,13 @@ const getTrendIcon = (trend: number, classes: ClassNameMap) => {
}
};
// convert timestamp to human friendly form
function formatDateToHuman(timeStamp: string | number) {
return DateTime.fromMillis(Number(timeStamp)).toLocaleString(
DateTime.DATETIME_MED,
);
}
export const CoverageHistoryChart = () => {
const { entity } = useEntity();
const codeCoverageApi = useApi(codeCoverageApiRef);
@@ -149,10 +158,10 @@ export const CoverageHistoryChart = () => {
margin={{ right: 48, top: 32 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<XAxis dataKey="timestamp" tickFormatter={formatDateToHuman} />
<YAxis dataKey="line.percentage" />
<YAxis dataKey="branch.percentage" />
<Tooltip />
<Tooltip labelFormatter={formatDateToHuman} />
<Legend />
<Line
type="monotone"
@@ -232,7 +232,7 @@ export const FileExplorer = () => {
title: 'Coverage',
type: 'numeric',
field: 'coverage',
render: (row: CoverageTableRow) => `${row.coverage}%`,
render: (row: CoverageTableRow) => `${row.coverage.toFixed(2)}%`,
},
{
title: 'Missing lines',
@@ -22,7 +22,7 @@
"dependencies": {
"@backstage/config": "^0.1.8",
"@backstage/search-common": "^0.2.0",
"@elastic/elasticsearch": "^7.13.0",
"@elastic/elasticsearch": "7.13.0",
"@acuris/aws-es-connection": "^2.2.0",
"aws-sdk": "^2.948.0",
"elastic-builder": "^2.16.0",