[ML] Transforms: API schemas and integration tests (#75164)

- Adds schema definitions to transform API endpoints and adds API integration tests.
- The type definitions based on the schema definitions can be used on the client side too.
- Adds apidoc documentation.
This commit is contained in:
Walter Rafelsberger 2020-09-14 16:31:23 +02:00 committed by GitHub
parent cbcd1ebd32
commit dd1822047c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 3110 additions and 1374 deletions

View file

@ -0,0 +1,9 @@
/*
* 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 { TransformMessage } from '../types/messages';
export type GetTransformsAuditMessagesResponseSchema = TransformMessage[];

View file

@ -0,0 +1,48 @@
/*
* 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, TypeOf } from '@kbn/config-schema';
import { TRANSFORM_STATE } from '../constants';
export const transformIdsSchema = schema.arrayOf(
schema.object({
id: schema.string(),
})
);
export type TransformIdsSchema = TypeOf<typeof transformIdsSchema>;
export const transformStateSchema = schema.oneOf([
schema.literal(TRANSFORM_STATE.ABORTING),
schema.literal(TRANSFORM_STATE.FAILED),
schema.literal(TRANSFORM_STATE.INDEXING),
schema.literal(TRANSFORM_STATE.STARTED),
schema.literal(TRANSFORM_STATE.STOPPED),
schema.literal(TRANSFORM_STATE.STOPPING),
]);
export const indexPatternTitleSchema = schema.object({
/** Title of the index pattern for which to return stats. */
indexPatternTitle: schema.string(),
});
export type IndexPatternTitleSchema = TypeOf<typeof indexPatternTitleSchema>;
export const transformIdParamSchema = schema.object({
transformId: schema.string(),
});
export type TransformIdParamSchema = TypeOf<typeof transformIdParamSchema>;
export interface ResponseStatus {
success: boolean;
error?: any;
}
export interface CommonResponseStatusSchema {
[key: string]: ResponseStatus;
}

View file

@ -0,0 +1,37 @@
/*
* 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, TypeOf } from '@kbn/config-schema';
import { transformStateSchema, ResponseStatus } from './common';
export const deleteTransformsRequestSchema = schema.object({
/**
* Delete Transform & Destination Index
*/
transformsInfo: schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
),
deleteDestIndex: schema.maybe(schema.boolean()),
deleteDestIndexPattern: schema.maybe(schema.boolean()),
forceDelete: schema.maybe(schema.boolean()),
});
export type DeleteTransformsRequestSchema = TypeOf<typeof deleteTransformsRequestSchema>;
export interface DeleteTransformStatus {
transformDeleted: ResponseStatus;
destIndexDeleted?: ResponseStatus;
destIndexPatternDeleted?: ResponseStatus;
destinationIndex?: string | undefined;
}
export interface DeleteTransformsResponseSchema {
[key: string]: DeleteTransformStatus;
}

View file

@ -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 { schema, TypeOf } from '@kbn/config-schema';
export const fieldHistogramsRequestSchema = schema.object({
/** Query to match documents in the index. */
query: schema.any(),
/** The fields to return histogram data. */
fields: schema.arrayOf(schema.any()),
/** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
});
export type FieldHistogramsRequestSchema = TypeOf<typeof fieldHistogramsRequestSchema>;
export type FieldHistogramsResponseSchema = any[];

View file

@ -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 { TypeOf } from '@kbn/config-schema';
import { transformIdsSchema, CommonResponseStatusSchema } from './common';
export const startTransformsRequestSchema = transformIdsSchema;
export type StartTransformsRequestSchema = TypeOf<typeof startTransformsRequestSchema>;
export type StartTransformsResponseSchema = CommonResponseStatusSchema;

View file

@ -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 { schema, TypeOf } from '@kbn/config-schema';
import { transformStateSchema, CommonResponseStatusSchema } from './common';
export const stopTransformsRequestSchema = schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
);
export type StopTransformsRequestSchema = TypeOf<typeof stopTransformsRequestSchema>;
export type StopTransformsResponseSchema = CommonResponseStatusSchema;

View file

@ -0,0 +1,127 @@
/*
* 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, TypeOf } from '@kbn/config-schema';
import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common';
import type { Dictionary } from '../types/common';
import type { PivotAggDict } from '../types/pivot_aggs';
import type { PivotGroupByDict } from '../types/pivot_group_by';
import type { TransformId, TransformPivotConfig } from '../types/transform';
import { transformStateSchema } from './common';
// GET transforms
export const getTransformsRequestSchema = schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
);
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsRequestSchema>;
export interface GetTransformsResponseSchema {
count: number;
transforms: TransformPivotConfig[];
}
// schemas shared by parts of the preview, create and update endpoint
export const destSchema = schema.object({
index: schema.string(),
pipeline: schema.maybe(schema.string()),
});
export const pivotSchema = schema.object({
group_by: schema.any(),
aggregations: schema.any(),
});
export const settingsSchema = schema.object({
max_page_search_size: schema.maybe(schema.number()),
// The default value is null, which disables throttling.
docs_per_second: schema.maybe(schema.nullable(schema.number())),
});
export const sourceSchema = schema.object({
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
});
export const syncSchema = schema.object({
time: schema.object({
delay: schema.maybe(schema.string()),
field: schema.string(),
}),
});
// PUT transforms/{transformId}
export const putTransformsRequestSchema = schema.object({
description: schema.maybe(schema.string()),
dest: destSchema,
frequency: schema.maybe(schema.string()),
pivot: pivotSchema,
settings: schema.maybe(settingsSchema),
source: sourceSchema,
sync: schema.maybe(syncSchema),
});
export interface PutTransformsRequestSchema extends TypeOf<typeof putTransformsRequestSchema> {
pivot: {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
};
}
interface TransformCreated {
transform: TransformId;
}
interface TransformCreatedError {
id: TransformId;
error: any;
}
export interface PutTransformsResponseSchema {
transformsCreated: TransformCreated[];
errors: TransformCreatedError[];
}
// POST transforms/_preview
export const postTransformsPreviewRequestSchema = schema.object({
pivot: pivotSchema,
source: sourceSchema,
});
export interface PostTransformsPreviewRequestSchema
extends TypeOf<typeof postTransformsPreviewRequestSchema> {
pivot: {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
};
}
interface EsMappingType {
type: ES_FIELD_TYPES;
}
export type PreviewItem = Dictionary<any>;
export type PreviewData = PreviewItem[];
export type PreviewMappingsProperties = Dictionary<EsMappingType>;
export interface PostTransformsPreviewResponseSchema {
generated_dest_index: {
mappings: {
_meta: {
_transform: {
transform: string;
version: { create: string };
creation_date_in_millis: number;
};
created_by: string;
};
properties: PreviewMappingsProperties;
};
settings: { index: { number_of_shards: string; auto_expand_replicas: string } };
aliases: Record<string, any>;
};
preview: PreviewData;
}

View file

@ -0,0 +1,21 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { TransformStats } from '../types/transform_stats';
import { getTransformsRequestSchema } from './transforms';
export const getTransformsStatsRequestSchema = getTransformsRequestSchema;
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
export interface GetTransformsStatsResponseSchema {
node_failures?: object;
count: number;
transforms: TransformStats[];
}

View file

@ -0,0 +1,114 @@
/*
* 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 type { SearchResponse7 } from '../../../ml/common';
import type { EsIndex } from '../types/es_index';
// To be able to use the type guards on the client side, we need to make sure we don't import
// the code of '@kbn/config-schema' but just its types, otherwise the client side code will
// fail to build.
import type { FieldHistogramsResponseSchema } from './field_histograms';
import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages';
import type { DeleteTransformsResponseSchema } from './delete_transforms';
import type { StartTransformsResponseSchema } from './start_transforms';
import type { StopTransformsResponseSchema } from './stop_transforms';
import type {
GetTransformsResponseSchema,
PostTransformsPreviewResponseSchema,
PutTransformsResponseSchema,
} from './transforms';
import type { GetTransformsStatsResponseSchema } from './transforms_stats';
import type { PostTransformsUpdateResponseSchema } from './update_transforms';
const isBasicObject = (arg: any) => {
return typeof arg === 'object' && arg !== null;
};
const isGenericResponseSchema = <T>(arg: any): arg is T => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'count') &&
{}.hasOwnProperty.call(arg, 'transforms') &&
Array.isArray(arg.transforms)
);
};
export const isGetTransformsResponseSchema = (arg: any): arg is GetTransformsResponseSchema => {
return isGenericResponseSchema<GetTransformsResponseSchema>(arg);
};
export const isGetTransformsStatsResponseSchema = (
arg: any
): arg is GetTransformsStatsResponseSchema => {
return isGenericResponseSchema<GetTransformsStatsResponseSchema>(arg);
};
export const isDeleteTransformsResponseSchema = (
arg: any
): arg is DeleteTransformsResponseSchema => {
return (
isBasicObject(arg) &&
Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'transformDeleted')))
);
};
export const isEsIndices = (arg: any): arg is EsIndex[] => {
return Array.isArray(arg);
};
export const isEsSearchResponse = (arg: any): arg is SearchResponse7 => {
return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'hits');
};
export const isFieldHistogramsResponseSchema = (arg: any): arg is FieldHistogramsResponseSchema => {
return Array.isArray(arg);
};
export const isGetTransformsAuditMessagesResponseSchema = (
arg: any
): arg is GetTransformsAuditMessagesResponseSchema => {
return Array.isArray(arg);
};
export const isPostTransformsPreviewResponseSchema = (
arg: any
): arg is PostTransformsPreviewResponseSchema => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'generated_dest_index') &&
{}.hasOwnProperty.call(arg, 'preview') &&
typeof arg.generated_dest_index !== undefined &&
Array.isArray(arg.preview)
);
};
export const isPostTransformsUpdateResponseSchema = (
arg: any
): arg is PostTransformsUpdateResponseSchema => {
return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'id') && typeof arg.id === 'string';
};
export const isPutTransformsResponseSchema = (arg: any): arg is PutTransformsResponseSchema => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'transformsCreated') &&
{}.hasOwnProperty.call(arg, 'errors') &&
Array.isArray(arg.transformsCreated) &&
Array.isArray(arg.errors)
);
};
const isGenericSuccessResponseSchema = (arg: any) =>
isBasicObject(arg) && Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'success')));
export const isStartTransformsResponseSchema = (arg: any): arg is StartTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};
export const isStopTransformsResponseSchema = (arg: any): arg is StopTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};

View file

@ -0,0 +1,24 @@
/*
* 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, TypeOf } from '@kbn/config-schema';
import { TransformPivotConfig } from '../types/transform';
import { destSchema, settingsSchema, sourceSchema, syncSchema } from './transforms';
// POST _transform/{transform_id}/_update
export const postTransformsUpdateRequestSchema = schema.object({
description: schema.maybe(schema.string()),
dest: schema.maybe(destSchema),
frequency: schema.maybe(schema.string()),
settings: schema.maybe(settingsSchema),
source: schema.maybe(sourceSchema),
sync: schema.maybe(syncSchema),
});
export type PostTransformsUpdateRequestSchema = TypeOf<typeof postTransformsUpdateRequestSchema>;
export type PostTransformsUpdateResponseSchema = TransformPivotConfig;

View file

@ -75,3 +75,24 @@ export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
];
export const APP_INDEX_PRIVILEGES = ['monitor'];
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243
export const TRANSFORM_STATE = {
ABORTING: 'aborting',
FAILED: 'failed',
INDEXING: 'indexing',
STARTED: 'started',
STOPPED: 'stopped',
STOPPING: 'stopping',
} as const;
const transformStates = Object.values(TRANSFORM_STATE);
export type TransformState = typeof transformStates[number];
export const TRANSFORM_MODE = {
BATCH: 'batch',
CONTINUOUS: 'continuous',
} as const;
const transformModes = Object.values(TRANSFORM_MODE);
export type TransformMode = typeof transformModes[number];

View file

@ -1,58 +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;
* you may not use this file except in compliance with the Elastic License.
*/
export interface MissingPrivileges {
[key: string]: string[] | undefined;
}
export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}
export type TransformId = string;
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243
export enum TRANSFORM_STATE {
ABORTING = 'aborting',
FAILED = 'failed',
INDEXING = 'indexing',
STARTED = 'started',
STOPPED = 'stopped',
STOPPING = 'stopping',
}
export interface TransformEndpointRequest {
id: TransformId;
state?: TRANSFORM_STATE;
}
export interface ResultData {
success: boolean;
error?: any;
}
export interface TransformEndpointResult {
[key: string]: ResultData;
}
export interface DeleteTransformEndpointRequest {
transformsInfo: TransformEndpointRequest[];
deleteDestIndex?: boolean;
deleteDestIndexPattern?: boolean;
forceDelete?: boolean;
}
export interface DeleteTransformStatus {
transformDeleted: ResultData;
destIndexDeleted?: ResultData;
destIndexPatternDeleted?: ResultData;
destinationIndex?: string | undefined;
}
export interface DeleteTransformEndpointResult {
[key: string]: DeleteTransformStatus;
}

View file

@ -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 type { SearchResponse7 } from '../../ml/common';

View file

@ -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 type AggName = string;

View file

@ -0,0 +1,9 @@
/*
* 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 interface EsIndex {
name: string;
}

View file

@ -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 type EsFieldName = string;

View file

@ -0,0 +1,31 @@
/*
* 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 { AggName } from './aggregations';
import { EsFieldName } from './fields';
export const PIVOT_SUPPORTED_AGGS = {
AVG: 'avg',
CARDINALITY: 'cardinality',
MAX: 'max',
MIN: 'min',
PERCENTILES: 'percentiles',
SUM: 'sum',
VALUE_COUNT: 'value_count',
FILTER: 'filter',
} as const;
export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS];
export type PivotAgg = {
[key in PivotSupportedAggs]?: {
field: EsFieldName;
};
};
export type PivotAggDict = {
[key in AggName]: PivotAgg;
};

View file

@ -0,0 +1,33 @@
/*
* 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 { Dictionary } from './common';
import { EsFieldName } from './fields';
export type GenericAgg = object;
export interface TermsAgg {
terms: {
field: EsFieldName;
};
}
export interface HistogramAgg {
histogram: {
field: EsFieldName;
interval: string;
};
}
export interface DateHistogramAgg {
date_histogram: {
field: EsFieldName;
calendar_interval: string;
};
}
export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg;
export type PivotGroupByDict = Dictionary<PivotGroupBy>;

View file

@ -0,0 +1,14 @@
/*
* 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 interface MissingPrivileges {
[key: string]: string[] | undefined;
}
export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}

View file

@ -0,0 +1,17 @@
/*
* 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 type { PutTransformsRequestSchema } from '../api_schemas/transforms';
export type IndexName = string;
export type IndexPattern = string;
export type TransformId = string;
export interface TransformPivotConfig extends PutTransformsRequestSchema {
id: TransformId;
create_time?: number;
version?: string;
}

View file

@ -0,0 +1,62 @@
/*
* 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 { TransformState, TRANSFORM_STATE } from '../constants';
import { TransformId } from './transform';
export interface TransformStats {
id: TransformId;
checkpointing: {
last: {
checkpoint: number;
timestamp_millis?: number;
};
next?: {
checkpoint: number;
checkpoint_progress?: {
total_docs: number;
docs_remaining: number;
percent_complete: number;
};
};
operations_behind: number;
};
node?: {
id: string;
name: string;
ephemeral_id: string;
transport_address: string;
attributes: Record<string, any>;
};
stats: {
documents_indexed: number;
documents_processed: number;
index_failures: number;
index_time_in_ms: number;
index_total: number;
pages_processed: number;
search_failures: number;
search_time_in_ms: number;
search_total: number;
trigger_count: number;
processing_time_in_ms: number;
processing_total: number;
exponential_avg_checkpoint_duration_ms: number;
exponential_avg_documents_indexed: number;
exponential_avg_documents_processed: number;
};
reason?: string;
state: TransformState;
}
export function isTransformStats(arg: any): arg is TransformStats {
return (
typeof arg === 'object' &&
arg !== null &&
{}.hasOwnProperty.call(arg, 'state') &&
Object.values(TRANSFORM_STATE).includes(arg.state)
);
}