[Console] Add logic for query params (#160515)

## Summary

Fixes https://github.com/elastic/kibana/issues/160528 
Follow up to https://github.com/elastic/kibana/pull/159241 

This PR adds logic for query parameters to the new script generating
Console autocomplete definitions from ES specification. The logic is
explained in details in the
[file](https://github.com/elastic/kibana/pull/160515/files#diff-b6853462c38db4e237dbb3cdec9d9f6659aa3fdc5f96a6193f2c4bac1439db43)
but here is a short version:
- Currently, the autocomplete engine only works with url params of 2
types: boolean (`__flag__`) and a list of options (for example `['all',
'open', 'hidden']`). The script will convert all of the url params from
the specification into this format: a boolean or a list. If there are no
set options for a parameter, but a default value is known, then it will
be converted into a list with 1 option, for example `['random']` so that
the autocomplete engine will display it as a single suggestion. We also
need to convert any numbers to strings, because they won't be displayed
otherwise.
- Endpoints in the specification have a property `request` which in turn
has 2 properties describing url params: `attachedBehaviours` and
`query`. Both object contain an array of `property`'s each describing a
url param. `property` is configured with either a built in type
(`string`, `number`, `boolean`) or defined type, for example
`ExpandWildcards`. By finding the type `ExpandWildcards` in the
specification, we can convert this type to a list of options `['open',
'all', 'none', 'hidden']`.

### How to test
Similar to https://github.com/elastic/kibana/pull/159241, you need to
re-generenate the definitions and see if local changes to definitions
files make sense.
1. Checkout the ES specification
[repo](https://github.com/elastic/elasticsearch-specification)
2. Run the command `node scripts/generate_console_definitions.js
--source <ES_SPECIFICATION_REPO> --emptyDest`
3. Check the changes in the folder
`KIBANA_REPO/src/plugins/console/server/lib/spec_definitions/json/generated`

#### Intended changes to the definitions files
- Most of endpoints have 4 default url params that previously were not
in the definitions files but added to all endpoints in this
[file](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/lib/autocomplete/url_params.js).
These params are configured in the interface `CommonQueryParameters` in
the specification (see this
[file](https://github.com/elastic/elasticsearch-specification/blob/main/specification/_spec_utils/behaviors.ts)).
<details>
The interface in the specification

```js
export interface CommonQueryParameters {
  error_trace?: boolean
  filter_path?: string | string[]
  human?: boolean
  pretty?: boolean
}
```
The converted url params

```json
"error_trace": "__flag__",
"filter_path": [],
"human": "__flag__",
"pretty": "__flag__",
```
</details>

- Previously existing `url_components` property in the definitions is
deleted and this change will be addressed separately. (not sure it is
currently working but added a task to the meta issue)
- Previously numbers were configured as `0` or `0.0` but that is not
currently displayed in suggestions. Instead, the new script converts
numbers to strings and if any default value is present, it will be
displayed as a suggestion.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2023-07-05 12:38:55 +02:00 committed by GitHub
parent 54dc40ff69
commit ee6ca657ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1130 additions and 68 deletions

View file

@ -1,3 +1,10 @@
# @kbn/generate-console-definitions
# Generate console definitions
This package is a script to generate definitions used in Console to display autocomplete suggestions. The script is
a new implementation of `kbn-spec-to-console` package: The old script uses [JSON specs](https://github.com/elastic/elasticsearch/tree/main/rest-api-spec) from the Elasticsearch repo as the source, whereas this script uses the Elasticsearch specification [repo](https://github.com/elastic/elasticsearch-specification) as the source.
## Instructions
1. Checkout the Elasticsearch specification [repo](https://github.com/elastic/elasticsearch-specification).
2. Run the command `node scripts/generate_console_definitions.js --source <ES_SPECIFICATION_REPO> --emptyDest`
This command will use the folder `<ES_SPECIFICATION_REPO>` as the source and the constant [`AUTOCOMPLETE_DEFINITIONS_FOLDER`](https://github.com/elastic/kibana/blob/main/src/plugins/console/common/constants/autocomplete_definitions.ts) as the destination. Based on the value of the constant, the autocomplete definitions will be generated in the folder `<KIBANA_REPO>/src/plugins/server/lib/spec_definitions/json/generated`. Using the flag `--emptyDest` will remove any existing files in the destination folder.
3. It's possible to generate the definitions into a different folder. For that pass an option to the command `--dest <DEFINITIONS_FOLDER>` and also update the constant [`AUTOCOMPLETE_DEFINITIONS_FOLDER`](https://github.com/elastic/kibana/blob/main/src/plugins/console/common/constants/autocomplete_definitions.ts) so that the Console server will load the definitions from this folder.
Empty package generated by @kbn/generate

View file

@ -9,51 +9,16 @@
import fs from 'fs';
import Path, { join } from 'path';
import { ToolingLog } from '@kbn/tooling-log';
import { generateQueryParams } from './generate_query_params';
import type {
AutocompleteBodyParams,
AutocompleteDefinition,
AutocompleteUrlParams,
SpecificationTypes,
} from './types';
import { findTypeDefinition } from './utils';
interface EndpointRequest {
name: string;
namespace: string;
}
interface Endpoint {
name: string;
urls: Array<{
methods: string[];
path: string;
}>;
docUrl: string;
request: null | EndpointRequest;
}
interface SchemaType {
name: {
name: string;
namespace: string;
};
}
interface Schema {
endpoints: Endpoint[];
types: SchemaType[];
}
interface UrlParams {
[key: string]: number | string;
}
interface BodyParams {
[key: string]: number | string;
}
interface Definition {
documentation?: string;
methods: string[];
patterns: string[];
url_params?: UrlParams;
data_autocomplete_rules?: BodyParams;
}
const generateMethods = (endpoint: Endpoint): string[] => {
const generateMethods = (endpoint: SpecificationTypes.Endpoint): string[] => {
// this array consists of arrays of strings
const methodsArray = endpoint.urls.map((url) => url.methods);
// flatten to return array of strings
@ -62,7 +27,7 @@ const generateMethods = (endpoint: Endpoint): string[] => {
return [...new Set(flattenMethodsArray)];
};
const generatePatterns = (endpoint: Endpoint): string[] => {
const generatePatterns = (endpoint: SpecificationTypes.Endpoint): string[] => {
return endpoint.urls.map(({ path }) => {
let pattern = path;
// remove leading / if present
@ -73,42 +38,37 @@ const generatePatterns = (endpoint: Endpoint): string[] => {
});
};
const generateDocumentation = (endpoint: Endpoint): string => {
const generateDocumentation = (endpoint: SpecificationTypes.Endpoint): string => {
return endpoint.docUrl;
};
const generateParams = (
endpoint: Endpoint,
schema: Schema
): { urlParams: UrlParams; bodyParams: BodyParams } | undefined => {
endpoint: SpecificationTypes.Endpoint,
schema: SpecificationTypes.Model
): { urlParams: AutocompleteUrlParams; bodyParams: AutocompleteBodyParams } | undefined => {
const { request } = endpoint;
if (!request) {
return;
}
const requestType = schema.types.find(
({ name: { name, namespace } }) => name === request.name && namespace === request.namespace
);
const requestType = findTypeDefinition(schema, request);
if (!requestType) {
return;
}
const urlParams = generateUrlParams(requestType);
const urlParams = generateQueryParams(requestType as SpecificationTypes.Request, schema);
const bodyParams = generateBodyParams(requestType);
return { urlParams, bodyParams };
};
const generateUrlParams = (requestType: SchemaType): UrlParams => {
return {};
};
const generateBodyParams = (requestType: SchemaType): BodyParams => {
const generateBodyParams = (
requestType: SpecificationTypes.TypeDefinition
): AutocompleteBodyParams => {
return {};
};
const addParams = (
definition: Definition,
params: { urlParams: UrlParams; bodyParams: BodyParams }
): Definition => {
definition: AutocompleteDefinition,
params: { urlParams: AutocompleteUrlParams; bodyParams: AutocompleteBodyParams }
): AutocompleteDefinition => {
const { urlParams, bodyParams } = params;
if (urlParams && Object.keys(urlParams).length > 0) {
definition.url_params = urlParams;
@ -119,15 +79,19 @@ const addParams = (
return definition;
};
const generateDefinition = (endpoint: Endpoint, schema: Schema): Definition => {
const generateDefinition = (
endpoint: SpecificationTypes.Endpoint,
schema: SpecificationTypes.Model
): AutocompleteDefinition => {
const methods = generateMethods(endpoint);
const patterns = generatePatterns(endpoint);
const documentation = generateDocumentation(endpoint);
let definition: Definition = { methods, patterns, documentation };
let definition: AutocompleteDefinition = {};
const params = generateParams(endpoint, schema);
if (params) {
definition = addParams(definition, params);
}
definition = { ...definition, methods, patterns, documentation };
return definition;
};
@ -143,7 +107,7 @@ export function generateConsoleDefinitions({
}) {
const pathToSchemaFile = Path.resolve(specsRepo, 'output/schema/schema.json');
log.info('loading the ES specification schema file');
const schema = JSON.parse(fs.readFileSync(pathToSchemaFile, 'utf8')) as Schema;
const schema = JSON.parse(fs.readFileSync(pathToSchemaFile, 'utf8')) as SpecificationTypes.Model;
const { endpoints } = schema;
log.info(`iterating over endpoints array: ${endpoints.length} endpoints`);
@ -151,7 +115,7 @@ export function generateConsoleDefinitions({
const { name } = endpoint;
log.info(name);
const definition = generateDefinition(endpoint, schema);
const fileContent: { [name: string]: Definition } = {
const fileContent: { [name: string]: AutocompleteDefinition } = {
[name]: definition,
};
fs.writeFileSync(

View file

@ -0,0 +1,375 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SpecificationTypes } from './types';
import { generateQueryParams } from './generate_query_params';
import { UrlParamValue } from './types/autocomplete_definition_types';
describe('generateQueryParams', () => {
const mockRequestType: SpecificationTypes.Request = {
body: { kind: 'no_body' },
kind: 'request',
name: {
name: 'TestRequest',
namespace: 'test.namespace',
},
path: [],
query: [],
specLocation: '',
};
const getMockProperty = ({
propertyName,
typeName,
serverDefault,
type,
}: {
propertyName: string;
typeName?: SpecificationTypes.TypeName;
serverDefault?: SpecificationTypes.Property['serverDefault'];
type?: SpecificationTypes.ValueOf;
}): SpecificationTypes.Property => {
return {
description: 'Description',
name: propertyName,
required: false,
serverDefault: serverDefault ?? undefined,
type: type ?? {
kind: 'instance_of',
type: typeName ?? {
name: 'string',
namespace: '_builtins',
},
},
};
};
const mockSchema: SpecificationTypes.Model = {
endpoints: [],
types: [],
};
it('iterates over attachedBehaviours', () => {
const behaviour1: SpecificationTypes.Interface = {
kind: 'interface',
name: {
name: 'behaviour1',
namespace: 'test.namespace',
},
properties: [getMockProperty({ propertyName: 'property1' })],
specLocation: '',
};
const behaviour2: SpecificationTypes.Interface = {
kind: 'interface',
name: {
name: 'behaviour2',
namespace: 'test.namespace',
},
properties: [
getMockProperty({ propertyName: 'property2' }),
getMockProperty({ propertyName: 'property3' }),
],
specLocation: '',
};
const schema: SpecificationTypes.Model = {
...mockSchema,
types: [behaviour1, behaviour2],
};
const requestType: SpecificationTypes.Request = {
...mockRequestType,
attachedBehaviors: ['behaviour1', 'behaviour2'],
};
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
property1: '',
property2: '',
property3: '',
});
});
it('iterates over query properties', () => {
const requestType = {
...mockRequestType,
query: [
getMockProperty({ propertyName: 'property1' }),
getMockProperty({ propertyName: 'property2' }),
],
};
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
property1: '',
property2: '',
});
});
it('converts builtin types', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
typeName: { name: 'string', namespace: '_builtins' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
typeName: { name: 'number', namespace: '_builtins' },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
typeName: { name: 'boolean', namespace: '_builtins' },
});
const requestType = {
...mockRequestType,
query: [stringProperty, numberProperty, booleanProperty],
};
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
stringProperty: '',
numberProperty: '',
booleanProperty: '__flag__',
});
});
it('adds serverDefault value if any', () => {
const propertyWithDefault = getMockProperty({
propertyName: 'propertyWithDefault',
serverDefault: 'default',
});
const requestType = { ...mockRequestType, query: [propertyWithDefault] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
propertyWithDefault: ['default'],
});
});
it('converts an enum property', () => {
const enumProperty = getMockProperty({
propertyName: 'enumProperty',
typeName: { name: 'EnumType', namespace: 'test.namespace' },
});
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{
name: 'enum2',
},
],
name: {
name: 'EnumType',
namespace: 'test.namespace',
},
specLocation: '',
};
const requestType = { ...mockRequestType, query: [enumProperty] };
const schema = { ...mockSchema, types: [enumType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
enumProperty: ['enum1', 'enum2'],
});
});
it('converts a type alias', () => {
const typeAliasProperty = getMockProperty({
propertyName: 'typeAliasProperty',
typeName: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
});
const typeAliasType: SpecificationTypes.TypeAlias = {
kind: 'type_alias',
name: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
specLocation: '',
type: {
kind: 'instance_of',
type: {
name: 'integer',
namespace: '_types',
},
},
};
const requestType = { ...mockRequestType, query: [typeAliasProperty] };
const schema: SpecificationTypes.Model = { ...mockSchema, types: [typeAliasType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
typeAliasProperty: '',
});
});
it('converts a literal_value to a string', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
type: { kind: 'literal_value', value: 'stringValue' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
type: { kind: 'literal_value', value: 14 },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
type: { kind: 'literal_value', value: true },
});
const requestType = {
...mockRequestType,
query: [stringProperty, numberProperty, booleanProperty],
};
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
stringProperty: ['stringValue'],
numberProperty: ['14'],
booleanProperty: ['true'],
});
});
describe('converts a union_of', () => {
it('flattens the array if one of the items is converted to an array', () => {
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{ name: 'enum2' },
],
name: { name: 'EnumType', namespace: 'test.namespace' },
specLocation: '',
};
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'EnumType',
namespace: 'test.namespace',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const schema: SpecificationTypes.Model = { ...mockSchema, types: [enumType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
unionProperty: ['enum1', 'enum2'],
});
});
it('removes empty string from the array', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unionProperty: [],
});
});
it('if one item is a boolean and others are empty, converts to a flag', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'number',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'boolean',
namespace: '_builtins',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unionProperty: '__flag__',
});
});
it('if one item is an unknown type, converts it to an empty string', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'literal_value',
value: 'test',
},
{
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
// check that no `undefined` values are added
const value = urlParams.unionProperty as UrlParamValue[];
expect(value.length).toEqual(1);
});
});
it('converts an unknown type to an empty string', () => {
const unknownTypeProperty = getMockProperty({
propertyName: 'unknownTypeProperty',
type: {
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
});
const requestType = { ...mockRequestType, query: [unknownTypeProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unknownTypeProperty: '',
});
});
});

View file

@ -0,0 +1,219 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Types that are important for query params conversion:
* TypeDefinition = Interface | Request | Response | Enum | TypeAlias
* ValueOf = InstanceOf | ArrayOf | UnionOf | DictionaryOf | UserDefinedValue | LiteralValue;
*
* Conversion steps:
* 1. The schema has a property `endpoints` which is "Endpoint[]"
* 2. Each "Endpoint" has a property `request` which is "TypeName"
* 3. Using "TypeName" we find the "TypeDefinition" in the property `types` of the schema
* 4. the "TypeDefinition" is cast to "Request"
* - "Request" has a property `query` which is "Property[]"
* - "Request" has a property `attachedBehaviours` which is "string[]"
* With "string" we find a "TypeDefinition" that is "Interface"
* This "Interface" has a property `properties` which is "Property[]"
* 5. Each "Property" (from both `query` and `attachedBehaviours`) now can be converted
* 6. Each "Property" has a property `type` that is "ValueOf"
* 7. If "ValueOf" can be one of "InstanceOf", "ArrayOf", "UnionOf", "DictionaryOf", "UserDefinedValue", "LiteralValue"
* - "InstanceOf": it has a property `type` which is a "TypeName"
* - if "TypeName" has a `namespace` = "_builtins" then it's a primitive type like "string" -> convert according to set rules for primitives
* - if "TypeName" has a `namespace` = "_types" then it's a defined type that can be found in the schema
* - the found "TypeDefinition" can be either "Enum" or "TypeAlias" (not "Interface", "Request" or "Response")
* - if it's "TypeAlias", it has a property `type` which is "ValueOf" -> handle it as "ValueOf" (recursion)
* - if it's "Enum", it has a property `members` which is "EnumMember[]" -> convert each "EnumMember" (only need `name` property)
* - "ArrayOf": it has a property `value` which is "ValueOf" -> convert as "ValueOf"
* - "UnionOf": it has a property `items` which is "ValueOf[]" -> convert each as "ValueOf"
* - "DictionaryOf": not used for query params
* - "UserDefinedValue": not used for query params
* - "LiteralValue": it has `value` that is `string`, `number` or `boolean`
*
* Autocomplete definitions currently work with 2 url param types:
* - "__flag__" for a boolean (suggesting value 'true' and 'false')
* - list of options in an array, for example ['30s', '-1', '0'], suggesting all 3 values in a list
* If there is only a default value, we need to wrap it in an array, so that this value is displayed in a suggestion (similar to the list).
* Numbers need to be converted to strings, otherwise they are not displayed as suggestions.
*
*/
import { UrlParamValue } from './types/autocomplete_definition_types';
import type { AutocompleteUrlParams, SpecificationTypes } from './types';
import { findTypeDefinition } from './utils';
const booleanFlagString = '__flag__';
const trueValueString = String(true);
const falseValueString = String(false);
export const generateQueryParams = (
requestType: SpecificationTypes.Request,
schema: SpecificationTypes.Model
): AutocompleteUrlParams => {
let urlParams = {} as AutocompleteUrlParams;
const { types } = schema;
const { attachedBehaviors, query } = requestType;
// if there are any attached behaviors, iterate over each and find its type
if (attachedBehaviors) {
for (const attachedBehavior of attachedBehaviors) {
const foundBehavior = types.find((type) => type.name.name === attachedBehavior);
if (foundBehavior) {
// attached behaviours are interfaces
const behaviorType = foundBehavior as SpecificationTypes.Interface;
// if there are any properties in the behavior, iterate over each and add it to url params
const { properties } = behaviorType;
urlParams = convertProperties(properties, urlParams, schema);
}
}
}
// iterate over properties in query and add it to url params
urlParams = convertProperties(query, urlParams, schema);
return urlParams;
};
const convertProperties = (
properties: SpecificationTypes.Property[],
urlParams: AutocompleteUrlParams,
schema: SpecificationTypes.Model
): AutocompleteUrlParams => {
for (const property of properties) {
const { name, serverDefault, type } = property;
// property has `type` which is `ValueOf`
const convertedValue = convertValueOf(type, serverDefault, schema);
urlParams[name] = convertedValue ?? '';
}
return urlParams;
};
const convertValueOf = (
valueOf: SpecificationTypes.ValueOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { kind } = valueOf;
if (kind === 'instance_of') {
return convertInstanceOf(valueOf, serverDefault, schema);
} else if (kind === 'array_of') {
return convertArrayOf(valueOf, serverDefault, schema);
} else if (kind === 'union_of') {
return convertUnionOf(valueOf, serverDefault, schema);
} else if (kind === 'literal_value') {
return convertLiteralValue(valueOf);
}
// for query params we can ignore 'dictionary_of' and 'user_defined_value'
return '';
};
const convertInstanceOf = (
type: SpecificationTypes.InstanceOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { type: typeName } = type;
const { name: propertyName, namespace } = typeName;
if (namespace === '_builtins') {
/**
* - `string`
* - `boolean`
* - `number`
* - `null` // ignore for query params
* - `void` // ignore for query params
* - `binary` // ignore for query params
*/
if (propertyName === 'boolean') {
// boolean is converted to a flag param
return booleanFlagString;
} else {
// if default value, convert to string and put in an array
return serverDefault ? [serverDefault.toString()] : '';
}
} else {
// if it's a defined type, try to convert it
const definedType = findTypeDefinition(schema, typeName);
if (definedType) {
// TypeDefinition can only be Enum or TypeAlias
if (definedType.kind === 'enum') {
return convertEnum(definedType as SpecificationTypes.Enum);
} else if (definedType.kind === 'type_alias') {
const aliasValueOf = definedType.type;
return convertValueOf(aliasValueOf, serverDefault, schema);
}
}
}
return '';
};
const convertArrayOf = (
type: SpecificationTypes.ArrayOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { value } = type;
// simply convert the value of an array item
return convertValueOf(value, serverDefault, schema);
};
const convertUnionOf = (
type: SpecificationTypes.UnionOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { items } = type;
const itemValues = new Set();
for (const item of items) {
// each item is ValueOf
const convertedValue = convertValueOf(item, serverDefault, schema);
// flatten array if needed
if (convertedValue instanceof Array) {
convertedValue.forEach((v) => itemValues.add(v));
} else itemValues.add(convertedValue);
}
// if an empty string is in values, delete it
if (itemValues.has('')) {
itemValues.delete('');
}
// if there is a flag in the values, convert it to "true" + "false"
if (itemValues.size > 1 && itemValues.has(booleanFlagString)) {
itemValues.delete(booleanFlagString);
itemValues.add(trueValueString);
itemValues.add(falseValueString);
}
// if only 2 values ("true","false"), convert back to a flag
// that can happen if the values before were ("true", "__flag__") or ("false", "__flag__")
if (
itemValues.size === 2 &&
itemValues.has(trueValueString) &&
itemValues.has(falseValueString)
) {
itemValues.clear();
itemValues.add(booleanFlagString);
}
// if only 1 element that is a flag, don't put it in an array
if (itemValues.size === 1 && itemValues.has(booleanFlagString)) {
return itemValues.values().next().value;
}
return [...itemValues] as UrlParamValue;
};
const convertLiteralValue = (type: SpecificationTypes.LiteralValue): UrlParamValue | undefined => {
// convert the value to a string
return [type.value.toString()];
};
const convertEnum = (enumDefinition: SpecificationTypes.Enum): UrlParamValue => {
const { members } = enumDefinition;
// only need the `name` property
return members.map((member) => member.name);
};

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type UrlParamValue = number | string | number[] | string[] | boolean;
export interface AutocompleteUrlParams {
[key: string]: UrlParamValue;
}
export interface AutocompleteBodyParams {
[key: string]: number | string;
}
export interface AutocompleteDefinition {
documentation?: string;
methods?: string[];
patterns?: string[];
url_params?: AutocompleteUrlParams;
data_autocomplete_rules?: AutocompleteBodyParams;
}

View file

@ -0,0 +1,15 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type {
AutocompleteDefinition,
AutocompleteUrlParams,
AutocompleteBodyParams,
} from './autocomplete_definition_types';
export * as SpecificationTypes from './specification_types';

View file

@ -0,0 +1,442 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* --------------- THIS FILE IS COPIED FROM ES SPECIFICATION REPO -------------------
*
*/
/**
* The name of a type, composed of a simple name and a namespace. Hierarchical namespace elements are separated by
* a dot, e.g 'cat.cat_aliases'.
*
* Builtin namespaces:
* - "generic" for type names that are generic parameter values from the enclosing type.
* - "internal" for primitive and builtin types (e.g. Id, IndexName, etc)
* Builtin types:
* - boolean,
* - string,
* - number: a 64bits floating point number. Additional types will be added for integers.
* - null: the null value. Since JS distinguishes undefined and null, some APIs make use of this value.
* - object: used to represent "any". We may forbid it at some point. UserDefinedValue should be used for user data.
*/
export interface TypeName {
namespace: string;
name: string;
}
// ------------------------------------------------------------------------------------------------
// Value types
// Note: "required" is part of Property. This means we can have optional properties but we can't have null entries in
// containers (array and dictionary), which doesn't seem to be needed.
//
// The 'kind' property is used to tag and disambiguate union type members, and allow type-safe pattern matching in TS:
// see https://blog.logrocket.com/pattern-matching-and-type-safety-in-typescript-1da1231a2e34/
// and https://medium.com/@fillopeter/pattern-matching-with-typescript-done-right-94049ddd671c
/**
* Type of a value. Used both for property types and nested type definitions.
*/
export type ValueOf =
| InstanceOf
| ArrayOf
| UnionOf
| DictionaryOf
| UserDefinedValue
| LiteralValue;
/**
* A single value
*/
export interface InstanceOf {
kind: 'instance_of';
type: TypeName;
/** generic parameters: either concrete types or open parameters from the enclosing type */
generics?: ValueOf[];
}
/**
* An array
*/
export interface ArrayOf {
kind: 'array_of';
value: ValueOf;
}
/**
* One of several possible types which don't necessarily have a common superinterface
*/
export interface UnionOf {
kind: 'union_of';
items: ValueOf[];
}
/**
* A dictionary (or map). The key is a string or a number (or a union thereof), possibly through an alias.
*
* If `singleKey` is true, then this dictionary can only have a single key. This is a common pattern in ES APIs,
* used to associate a value to a field name or some other identifier.
*/
export interface DictionaryOf {
kind: 'dictionary_of';
key: ValueOf;
value: ValueOf;
singleKey: boolean;
}
/**
* A user defined value. To be used when bubbling a generic parameter up to the top-level interface is
* inconvenient or impossible (e.g. for lists of user-defined values of possibly different types).
*
* Clients will allow providing a serializer/deserializer when reading/writing properties of this type,
* and should also accept raw json.
*
* Think twice before using this as it defeats the purpose of a strongly typed API, and deserialization
* will also require to buffer raw JSON data which may have performance implications.
*/
export interface UserDefinedValue {
kind: 'user_defined_value';
}
/**
* A literal value. This is used for tagged unions, where each type member of a union has a 'type'
* attribute that defines its kind. This metamodel heavily uses this approach with its 'kind' attributes.
*
* It may later be used to set a property to a constant value, which is why it accepts not only strings but also
* other primitive types.
*/
export interface LiteralValue {
kind: 'literal_value';
value: string | number | boolean;
}
/**
* An interface or request interface property.
*/
export interface Property {
name: string;
type: ValueOf;
required: boolean;
description?: string;
docUrl?: string;
docId?: string;
since?: string;
serverDefault?: boolean | string | number | string[] | number[];
deprecation?: Deprecation;
availability?: Availabilities;
stability?: Stability;
/**
* If specified takes precedence over `name` when generating code. `name` is always the value
* to be sent over the wire
*/
codegenName?: string;
/** An optional set of aliases for `name` */
aliases?: string[];
/** If the enclosing interface is a variants container, is this a property of the container and not a variant? */
containerProperty?: boolean;
/** If this property has a quirk that needs special attention, give a short explanation about it */
esQuirk?: string;
}
// ------------------------------------------------------------------------------------------------
// Type definitions
export type TypeDefinition = Interface | Request | Response | Enum | TypeAlias;
// ------------------------------------------------------------------------------------------------
/**
* Common attributes for all type definitions
*/
export interface BaseType {
name: TypeName;
description?: string;
/** Link to public documentation */
docUrl?: string;
docId?: string;
deprecation?: Deprecation;
/** If this endpoint has a quirk that needs special attention, give a short explanation about it */
esQuirk?: string;
kind: string;
/** Variant name for externally tagged variants */
variantName?: string;
/**
* Additional identifiers for use by code generators. Usage depends on the actual type:
* - on unions (modeled as alias(union_of)), these are identifiers for the union members
* - for additional properties, this is the name of the dict that holds these properties
* - for additional property, this is the name of the key and value fields that hold the
* additional property
*/
codegenNames?: string[];
/**
* Location of an item. The path is relative to the "specification" directory, e.g "_types/common.ts#L1-L2"
*/
specLocation: string;
}
export type Variants = ExternalTag | InternalTag | Container;
export interface VariantBase {
/**
* Is this variant type open to extensions? Default to false. Used for variants that can
* be extended with plugins. If true, target clients should allow for additional variants
* with a variant tag outside the ones defined in the spec and arbitrary data as the value.
*/
nonExhaustive?: boolean;
}
export interface ExternalTag extends VariantBase {
kind: 'external_tag';
}
export interface InternalTag extends VariantBase {
kind: 'internal_tag';
/* Name of the property that holds the variant tag */
tag: string;
/* Default value for the variant tag if it's missing */
defaultTag?: string;
}
export interface Container extends VariantBase {
kind: 'container';
}
/**
* Inherits clause (aka extends or implements) for an interface or request
*/
export interface Inherits {
type: TypeName;
generics?: ValueOf[];
}
/**
* An interface type
*/
export interface Interface extends BaseType {
kind: 'interface';
/**
* Open generic parameters. The name is that of the parameter, the namespace is an arbitrary value that allows
* this fully qualified type name to be used when this open generic parameter is used in property's type.
*/
generics?: TypeName[];
inherits?: Inherits;
implements?: Inherits[];
/**
* Behaviors directly implemented by this interface
*/
behaviors?: Inherits[];
/**
* Behaviors attached to this interface, coming from the interface itself (see `behaviors`)
* or from inherits and implements ancestors
*/
attachedBehaviors?: string[];
properties: Property[];
/**
* The property that can be used as a shortcut for the entire data structure in the JSON.
*/
shortcutProperty?: string;
/** Identify containers */
variants?: Container;
}
/**
* A request type
*/
export interface Request extends BaseType {
// Note: does not extend Interface as properties are split across path, query and body
kind: 'request';
generics?: TypeName[];
/** The parent defines additional body properties that are added to the body, that has to be a PropertyBody */
inherits?: Inherits;
implements?: Inherits[];
/** URL path properties */
path: Property[];
/** Query string properties */
query: Property[];
// FIXME: we need an annotation that lists query params replaced by a body property so that we can skip them.
// Examples on _search: sort -> sort, _source -> (_source, _source_include, _source_exclude)
// Or can we say that implicitly a body property replaces all path params starting with its name?
// Is there a priority rule between path and body parameters?
//
// We can also pull path parameter descriptions on body properties they replace
/**
* Body type. Most often a list of properties (that can extend those of the inherited interface, see above), except for a
* few specific cases that use other types such as bulk (array) or create (generic parameter). Or NoBody for requests
* that don't have a body.
*/
body: Body;
behaviors?: Inherits[];
attachedBehaviors?: string[];
}
/**
* A response type
*/
export interface Response extends BaseType {
kind: 'response';
generics?: TypeName[];
body: Body;
behaviors?: Inherits[];
attachedBehaviors?: string[];
exceptions?: ResponseException[];
}
export interface ResponseException {
description?: string;
body: Body;
statusCodes: number[];
}
export type Body = ValueBody | PropertiesBody | NoBody;
export interface ValueBody {
kind: 'value';
value: ValueOf;
codegenName?: string;
}
export interface PropertiesBody {
kind: 'properties';
properties: Property[];
}
export interface NoBody {
kind: 'no_body';
}
/**
* An enumeration member.
*
* When enumeration members can become ambiguous when translated to an identifier, the `name` property will be a good
* identifier name, and `stringValue` will be the string value to use on the wire.
* See DateMathTimeUnit for an example of this, which have members for "m" (minute) and "M" (month).
*/
export interface EnumMember {
/** The identifier to use for this enum */
name: string;
/** An optional set of aliases for `name` */
aliases?: string[];
/**
* If specified takes precedence over `name` when generating code. `name` is always the value
* to be sent over the wire
*/
codegenName?: string;
description?: string;
deprecation?: Deprecation;
since?: string;
}
/**
* An enumeration
*/
export interface Enum extends BaseType {
kind: 'enum';
/**
* If the enum is open, it means that other than the specified values it can accept an arbitrary value.
* If this property is not present, it means that the enum is not open (in other words, is closed).
*/
isOpen?: boolean;
members: EnumMember[];
}
/**
* An alias for an existing type.
*/
export interface TypeAlias extends BaseType {
kind: 'type_alias';
type: ValueOf;
/** generic parameters: either concrete types or open parameters from the enclosing type */
generics?: TypeName[];
/** Only applicable to `union_of` aliases: identify typed_key unions (external) and variant inventories (internal) */
variants?: InternalTag | ExternalTag;
}
// ------------------------------------------------------------------------------------------------
export enum Stability {
stable = 'stable',
beta = 'beta',
experimental = 'experimental',
}
export enum Visibility {
public = 'public',
feature_flag = 'feature_flag',
private = 'private',
}
export interface Deprecation {
version: string;
description: string;
}
export interface Availabilities {
stack?: Availability;
serverless?: Availability;
}
export interface Availability {
since?: string;
featureFlag?: string;
stability?: Stability;
visibility?: Visibility;
}
export interface Endpoint {
name: string;
description: string;
docUrl: string;
docId?: string;
deprecation?: Deprecation;
availability: Availabilities;
/**
* If the request value is `null` it means that there is not yet a
* request type definition for this endpoint.
*/
request: TypeName | null;
requestBodyRequired: boolean; // Not sure this is useful
/**
* If the response value is `null` it means that there is not yet a
* response type definition for this endpoint.
*/
response: TypeName | null;
urls: UrlTemplate[];
/**
* The version when this endpoint reached its current stability level.
* Missing data means "forever", i.e. before any of the target client versions produced from this spec.
*/
since?: string;
stability?: Stability;
visibility?: Visibility;
featureFlag?: string;
requestMediaType?: string[];
responseMediaType?: string[];
privileges?: {
index?: string[];
cluster?: string[];
};
}
export interface UrlTemplate {
path: string;
methods: string[];
deprecation?: Deprecation;
}
export interface Model {
types: TypeDefinition[];
endpoints: Endpoint[];
}

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SpecificationTypes } from './types';
export const findTypeDefinition = (
schema: SpecificationTypes.Model,
typeName: SpecificationTypes.TypeName
): SpecificationTypes.TypeDefinition | undefined => {
return schema.types.find(
(type) => type.name.name === typeName.name && type.name.namespace === typeName.namespace
);
};