diff --git a/packages/kbn-generate-console-definitions/README.md b/packages/kbn-generate-console-definitions/README.md index 71596b6fdf0bc..75d43fd7c493e 100644 --- a/packages/kbn-generate-console-definitions/README.md +++ b/packages/kbn-generate-console-definitions/README.md @@ -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 --emptyDest` + This command will use the folder `` 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 `/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 ` 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 diff --git a/packages/kbn-generate-console-definitions/src/generate_console_definitions.ts b/packages/kbn-generate-console-definitions/src/generate_console_definitions.ts index f5bb7cd687c7b..1d4918f44c6b1 100644 --- a/packages/kbn-generate-console-definitions/src/generate_console_definitions.ts +++ b/packages/kbn-generate-console-definitions/src/generate_console_definitions.ts @@ -9,51 +9,16 @@ import fs from 'fs'; import Path, { join } from 'path'; import { ToolingLog } from '@kbn/tooling-log'; - -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[] => { +import { generateQueryParams } from './generate_query_params'; +import type { + AutocompleteBodyParams, + AutocompleteDefinition, + AutocompleteUrlParams, + SpecificationTypes, +} from './types'; +import { findTypeDefinition } from './utils'; + +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( diff --git a/packages/kbn-generate-console-definitions/src/generate_query_params.test.ts b/packages/kbn-generate-console-definitions/src/generate_query_params.test.ts new file mode 100644 index 0000000000000..3d658ba60f174 --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/generate_query_params.test.ts @@ -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: '', + }); + }); +}); diff --git a/packages/kbn-generate-console-definitions/src/generate_query_params.ts b/packages/kbn-generate-console-definitions/src/generate_query_params.ts new file mode 100644 index 0000000000000..5310e85d68936 --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/generate_query_params.ts @@ -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); +}; diff --git a/packages/kbn-generate-console-definitions/src/types/autocomplete_definition_types.ts b/packages/kbn-generate-console-definitions/src/types/autocomplete_definition_types.ts new file mode 100644 index 0000000000000..edbb9bd74d9a8 --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/types/autocomplete_definition_types.ts @@ -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; +} diff --git a/packages/kbn-generate-console-definitions/src/types/index.ts b/packages/kbn-generate-console-definitions/src/types/index.ts new file mode 100644 index 0000000000000..a182592350681 --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/types/index.ts @@ -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'; diff --git a/packages/kbn-generate-console-definitions/src/types/specification_types.ts b/packages/kbn-generate-console-definitions/src/types/specification_types.ts new file mode 100644 index 0000000000000..b9e61ded06448 --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/types/specification_types.ts @@ -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[]; +} diff --git a/packages/kbn-generate-console-definitions/src/utils.ts b/packages/kbn-generate-console-definitions/src/utils.ts new file mode 100644 index 0000000000000..29c24e63fa58f --- /dev/null +++ b/packages/kbn-generate-console-definitions/src/utils.ts @@ -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 + ); +};