diff --git a/packages/core/http/core-http-router-server-internal/src/router.test.ts b/packages/core/http/core-http-router-server-internal/src/router.test.ts index 98656d262c3de..18589d5d39d52 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.test.ts @@ -49,6 +49,7 @@ describe('Router', () => { validate: { body: validation, query: validation, params: validation }, options: { deprecated: true, + discontinued: 'post test discontinued', summary: 'post test summary', description: 'post test description', }, @@ -66,6 +67,7 @@ describe('Router', () => { isVersioned: false, options: { deprecated: true, + discontinued: 'post test discontinued', summary: 'post test summary', description: 'post test description', }, diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts index 1ebb49ac630fe..d56de36ba9a29 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts @@ -33,7 +33,12 @@ describe('Versioned router', () => { it('provides the expected metadata', () => { const versionedRouter = CoreVersionedRouter.from({ router }); - versionedRouter.get({ path: '/test/{id}', access: 'internal', deprecated: true }); + versionedRouter.get({ + path: '/test/{id}', + access: 'internal', + deprecated: true, + discontinued: 'x.y.z', + }); versionedRouter.post({ path: '/test', access: 'internal', @@ -49,6 +54,7 @@ describe('Versioned router', () => { "options": Object { "access": "internal", "deprecated": true, + "discontinued": "x.y.z", }, "path": "/test/{id}", }, diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts index 85eef993adcc0..c47688b60d3cd 100644 --- a/packages/core/http/core-http-server/src/router/route.ts +++ b/packages/core/http/core-http-server/src/router/route.ts @@ -207,6 +207,15 @@ export interface RouteConfigOptions { * @remarks This will be surfaced in OAS documentation. */ deprecated?: boolean; + + /** + * Release version or date that this route will be removed + * Use with `deprecated: true` + * + * @remarks This will be surfaced in OAS documentation. + * @example 9.0.0 + */ + discontinued?: string; } /** diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index 6b465a2be74d0..c552abd251a1f 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -33,7 +33,10 @@ export type VersionedRouteConfig = Omit< RouteConfig, 'validate' | 'options' > & { - options?: Omit, 'access' | 'description' | 'deprecated'>; + options?: Omit< + RouteConfigOptions, + 'access' | 'description' | 'deprecated' | 'discontinued' + >; /** See {@link RouteConfigOptions['access']} */ access: Exclude['access'], undefined>; /** @@ -91,6 +94,14 @@ export type VersionedRouteConfig = Omit< * @default false */ deprecated?: boolean; + + /** + * Release version or date that this route will be removed + * Use with `deprecated: true` + * + * @default undefined + */ + discontinued?: string; }; /** diff --git a/packages/kbn-config-schema/index.ts b/packages/kbn-config-schema/index.ts index 031e1ceb90465..b41f7d65a82c8 100644 --- a/packages/kbn-config-schema/index.ts +++ b/packages/kbn-config-schema/index.ts @@ -435,6 +435,7 @@ export const schema = { export type Schema = typeof schema; import { + META_FIELD_X_OAS_DISCONTINUED, META_FIELD_X_OAS_ANY, META_FIELD_X_OAS_OPTIONAL, META_FIELD_X_OAS_DEPRECATED, @@ -444,6 +445,7 @@ import { } from './src/oas_meta_fields'; export const metaFields = Object.freeze({ + META_FIELD_X_OAS_DISCONTINUED, META_FIELD_X_OAS_ANY, META_FIELD_X_OAS_OPTIONAL, META_FIELD_X_OAS_DEPRECATED, diff --git a/packages/kbn-config-schema/src/oas_meta_fields.ts b/packages/kbn-config-schema/src/oas_meta_fields.ts index 1eac5ea0b7216..b9368f6b80b50 100644 --- a/packages/kbn-config-schema/src/oas_meta_fields.ts +++ b/packages/kbn-config-schema/src/oas_meta_fields.ts @@ -18,3 +18,4 @@ export const META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES = 'x-oas-get-additional-properties' as const; export const META_FIELD_X_OAS_DEPRECATED = 'x-oas-deprecated' as const; export const META_FIELD_X_OAS_ANY = 'x-oas-any-type' as const; +export const META_FIELD_X_OAS_DISCONTINUED = 'x-oas-discontinued' as const; diff --git a/packages/kbn-config-schema/src/types/type.test.ts b/packages/kbn-config-schema/src/types/type.test.ts index ee69e38cf3acc..5bb151c345f74 100644 --- a/packages/kbn-config-schema/src/types/type.test.ts +++ b/packages/kbn-config-schema/src/types/type.test.ts @@ -10,7 +10,7 @@ import { get } from 'lodash'; import { internals } from '../internals'; import { Type, TypeOptions } from './type'; -import { META_FIELD_X_OAS_DEPRECATED } from '../oas_meta_fields'; +import { META_FIELD_X_OAS_DEPRECATED, META_FIELD_X_OAS_DISCONTINUED } from '../oas_meta_fields'; class MyType extends Type { constructor(opts: TypeOptions = {}) { @@ -26,6 +26,19 @@ describe('meta', () => { const meta = type.getSchema().describe(); expect(get(meta, 'flags.description')).toBe('my description'); expect(get(meta, `metas[0].${META_FIELD_X_OAS_DEPRECATED}`)).toBe(true); + expect(get(meta, `metas[1].${META_FIELD_X_OAS_DISCONTINUED}`)).toBeUndefined(); + }); + + it('sets meta with all fields provided', () => { + const type = new MyType({ + meta: { description: 'my description', deprecated: true, 'x-discontinued': '9.0.0' }, + }); + const meta = type.getSchema().describe(); + expect(get(meta, 'flags.description')).toBe('my description'); + + expect(get(meta, `metas[0].${META_FIELD_X_OAS_DEPRECATED}`)).toBe(true); + + expect(get(meta, `metas[1].${META_FIELD_X_OAS_DISCONTINUED}`)).toBe('9.0.0'); }); it('does not set meta when no provided', () => { diff --git a/packages/kbn-config-schema/src/types/type.ts b/packages/kbn-config-schema/src/types/type.ts index 52333cd7d0b79..1b70dd83e11e6 100644 --- a/packages/kbn-config-schema/src/types/type.ts +++ b/packages/kbn-config-schema/src/types/type.ts @@ -16,7 +16,7 @@ import { type WhenOptions, CustomHelpers, } from 'joi'; -import { META_FIELD_X_OAS_DEPRECATED } from '../oas_meta_fields'; +import { META_FIELD_X_OAS_DEPRECATED, META_FIELD_X_OAS_DISCONTINUED } from '../oas_meta_fields'; import { SchemaTypeError, ValidationError } from '../errors'; import { Reference } from '../references'; @@ -33,6 +33,11 @@ export interface TypeMeta { * Whether this field is deprecated. */ deprecated?: boolean; + /** + * Release version or date that this route will be removed + * @example 9.0.0 + */ + 'x-discontinued'?: string; } export interface TypeOptions { @@ -129,6 +134,8 @@ export abstract class Type { if (options.meta.deprecated) { schema = schema.meta({ [META_FIELD_X_OAS_DEPRECATED]: true }); } + if (options.meta.deprecated && options.meta['x-discontinued']) + schema = schema.meta({ [META_FIELD_X_OAS_DISCONTINUED]: options.meta['x-discontinued'] }); } // Attach generic error handler only if it hasn't been attached yet since diff --git a/packages/kbn-router-to-openapispec/openapi-types.d.ts b/packages/kbn-router-to-openapispec/openapi-types.d.ts new file mode 100644 index 0000000000000..90c034a855fdc --- /dev/null +++ b/packages/kbn-router-to-openapispec/openapi-types.d.ts @@ -0,0 +1,20 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +export * from 'openapi-types'; + +declare module 'openapi-types' { + export namespace OpenAPIV3 { + export interface BaseSchemaObject { + // Custom OpenAPI field added by Kibana for a new field at the shema level. + 'x-discontinued'?: string; + } + } +} diff --git a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap index a3c634f582d43..818c0502ad774 100644 --- a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap +++ b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap @@ -164,6 +164,7 @@ Object { "deprecated": true, "description": "deprecated foo", "type": "string", + "x-discontinued": "route discontinued version or date", }, "foo": Object { "type": "string", @@ -224,6 +225,7 @@ OK response oas-test-version-2", "tags": Array [ "versioned", ], + "x-discontinued": "route discontinued version or date", }, }, "/foo/{id}/{path*}": Object { @@ -596,6 +598,7 @@ OK response oas-test-version-2", "deprecated": true, "description": "deprecated foo", "type": "string", + "x-discontinued": "route discontinued version or date", }, "foo": Object { "type": "string", diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts index 1310ea250f972..b3f20da38915b 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts @@ -34,6 +34,7 @@ export const sharedOas = { '/bar': { get: { deprecated: true, + 'x-discontinued': 'route discontinued version or date', operationId: '%2Fbar#0', parameters: [ { diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts index ebee2635e4459..898f234cdc310 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts @@ -64,6 +64,7 @@ export const getVersionedRouterDefaults = (bodySchema?: RuntimeSchema) => ({ summary: 'versioned route', access: 'public', deprecated: true, + discontinued: 'route discontinued version or date', options: { tags: ['ignore-me', 'oas-tag:versioned'], }, @@ -79,7 +80,13 @@ export const getVersionedRouterDefaults = (bodySchema?: RuntimeSchema) => ({ schema.object({ foo: schema.string(), deprecatedFoo: schema.maybe( - schema.string({ meta: { description: 'deprecated foo', deprecated: true } }) + schema.string({ + meta: { + description: 'deprecated foo', + deprecated: true, + 'x-discontinued': 'route discontinued version or date', + }, + }) ), }), }, diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.ts b/packages/kbn-router-to-openapispec/src/generate_oas.ts index 97c18aeec6aeb..8bc3333193624 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { OpenAPIV3 } from 'openapi-types'; import type { CoreVersionedRouter, Router } from '@kbn/core-http-router-server-internal'; +import type { OpenAPIV3 } from 'openapi-types'; import { OasConverter } from './oas_converter'; import { createOperationIdCounter } from './operation_id_counter'; import { processRouter } from './process_router'; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts index 6d1784d57f576..2caf40d04e510 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts @@ -9,8 +9,8 @@ import Joi from 'joi'; import joiToJsonParse from 'joi-to-json'; -import type { OpenAPIV3 } from 'openapi-types'; import { omit } from 'lodash'; +import type { OpenAPIV3 } from 'openapi-types'; import { createCtx, postProcessMutations } from './post_process_mutations'; import type { IContext } from './post_process_mutations'; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts index f8322611d8547..7253492eca4ca 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts @@ -11,7 +11,7 @@ import Joi from 'joi'; import { metaFields } from '@kbn/config-schema'; import type { OpenAPIV3 } from 'openapi-types'; import { parse } from '../../parse'; -import { deleteField, stripBadDefault, processDeprecated } from './utils'; +import { deleteField, stripBadDefault, processDeprecated, processDiscontinued } from './utils'; import { IContext } from '../context'; const { @@ -58,13 +58,14 @@ export const processMap = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void export const processAllTypes = (schema: OpenAPIV3.SchemaObject): void => { processDeprecated(schema); + processDiscontinued(schema); stripBadDefault(schema); }; export const processAnyType = (schema: OpenAPIV3.SchemaObject): void => { // Map schema to an empty object: `{}` for (const key of Object.keys(schema)) { - deleteField(schema as Record, key); + deleteField(schema as unknown as Record, key); } }; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts index 4a8de19287471..5b5a176d8b59e 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { OpenAPIV3 } from 'openapi-types'; import { metaFields } from '@kbn/config-schema'; +import type { OpenAPIV3 } from 'openapi-types'; import { deleteField, stripBadDefault } from './utils'; const { META_FIELD_X_OAS_OPTIONAL } = metaFields; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/utils.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/utils.ts index a3f4a0bba9e38..c2b17f05dacd4 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/utils.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/utils.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { OpenAPIV3 } from 'openapi-types'; import { metaFields } from '@kbn/config-schema'; +import type { OpenAPIV3 } from 'openapi-types'; export const stripBadDefault = (schema: OpenAPIV3.SchemaObject): void => { if (schema.default?.special === 'deep') { @@ -35,6 +35,13 @@ export const processDeprecated = (schema: OpenAPIV3.SchemaObject): void => { } }; +export const processDiscontinued = (schema: OpenAPIV3.SchemaObject): void => { + if (metaFields.META_FIELD_X_OAS_DISCONTINUED in schema) { + schema['x-discontinued'] = schema[metaFields.META_FIELD_X_OAS_DISCONTINUED] as string; + deleteField(schema, metaFields.META_FIELD_X_OAS_DISCONTINUED); + } +}; + /** Just for type convenience */ export const deleteField = (schema: Record, field: string): void => { delete schema[field]; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/zod/lib.ts b/packages/kbn-router-to-openapispec/src/oas_converter/zod/lib.ts index 655119decdd1a..7d247ace892b5 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/zod/lib.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/zod/lib.ts @@ -8,9 +8,10 @@ */ import { z, isZod } from '@kbn/zod'; -import type { OpenAPIV3 } from 'openapi-types'; // eslint-disable-next-line import/no-extraneous-dependencies import zodToJsonSchema from 'zod-to-json-schema'; +import type { OpenAPIV3 } from 'openapi-types'; + import { KnownParameters } from '../../type'; import { validatePathParameters } from '../common'; diff --git a/packages/kbn-router-to-openapispec/src/process_router.test.ts b/packages/kbn-router-to-openapispec/src/process_router.test.ts index 67c9a23dcae27..22e03efdf08fc 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.test.ts @@ -86,7 +86,7 @@ describe('processRouter', () => { getRoutes: () => [ { path: '/foo', - options: {}, + options: { access: 'internal', deprecated: true, discontinued: 'discontinued router' }, handler: jest.fn(), validationSchemas: { request: { body: schema.object({}) } }, }, diff --git a/packages/kbn-router-to-openapispec/src/process_router.ts b/packages/kbn-router-to-openapispec/src/process_router.ts index 6bf41397f65c6..4437e35ea1f3e 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.ts @@ -66,6 +66,7 @@ export const processRouter = ( tags: route.options.tags ? extractTags(route.options.tags) : [], ...(route.options.description ? { description: route.options.description } : {}), ...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}), + ...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}), requestBody: !!validationSchemas?.body ? { content: { diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts index 2ad1b2feb879a..9addfdf22da01 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts @@ -153,6 +153,8 @@ const createTestRoute: () => VersionedRouterRoute = () => ({ method: 'get', options: { access: 'public', + deprecated: true, + discontinued: 'discontinued versioned router', options: { body: { access: ['application/test+json'] } as any }, }, handlers: [ diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts index 6aeed11e6d71f..97b92f92fde57 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts @@ -98,6 +98,7 @@ export const processVersionedRouter = ( tags: route.options.options?.tags ? extractTags(route.options.options.tags) : [], ...(route.options.description ? { description: route.options.description } : {}), ...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}), + ...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}), requestBody: hasBody ? { content: hasVersionFilter diff --git a/packages/kbn-router-to-openapispec/src/type.ts b/packages/kbn-router-to-openapispec/src/type.ts index 75bf966e62a09..09dc247e5a5c9 100644 --- a/packages/kbn-router-to-openapispec/src/type.ts +++ b/packages/kbn-router-to-openapispec/src/type.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { OpenAPIV3 } from 'openapi-types'; - +import type { OpenAPIV3 } from '../openapi-types'; +export type { OpenAPIV3 } from '../openapi-types'; export interface KnownParameters { [paramName: string]: { optional: boolean }; } diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8755ff540ba5d..37ef5ebe6c233 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -46,6 +46,7 @@ export const IGNORE_FILE_GLOBS = [ 'test/package/Vagrantfile', 'x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile', '**/test/**/fixtures/**/*', + 'packages/kbn-router-to-openapispec/openapi-types.d.ts', // Required to match the name in the docs.elastic.dev repo. 'dev_docs/nav-kibana-dev.docnav.json',