Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export function getShape(schema: z.ZodType): Record<string, z.ZodType> {
if (current instanceof z.ZodOptional) {
current = current.unwrap();
}
if (current instanceof z.ZodIntersection) {
return {
...getShape(current.def.left as z.ZodType),
...getShape(current.def.right as z.ZodType),
};
}
if (current instanceof z.ZodUnion) {
return current.options.reduce((acc, option) => {
return { ...acc, ...getShape(option as z.ZodType) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,32 @@ describe('getShapeAt', () => {
const atPath = getShapeAt(schema, 'path');
expect(atPath).toEqual({});
});

it('should return the shape at the given property in a union', () => {
const schema = z.union([
z.object({ a: z.object({ b: z.string() }) }),
z.object({ c: z.object({ d: z.number() }) }),
]);
const atA = getShapeAt(schema, 'a');
expect(atA).toHaveProperty('b');
expectZodSchemaEqual(atA.b, z.string());
const atC = getShapeAt(schema, 'c');
expect(atC).toHaveProperty('d');
expectZodSchemaEqual(atC.d, z.number());
});

it('should return the shape for a nested union', () => {
const schema = z.object({
body: z.union([
z.union([z.object({ a: z.string() }), z.object({ b: z.number() })]),
z.union([z.object({ c: z.boolean() }), z.object({ d: z.string() })]),
]),
});
const atBody = getShapeAt(schema, 'body');
expect(Object.keys(atBody)).toEqual(['a', 'b', 'c', 'd']);
expectZodSchemaEqual(atBody.a, z.string());
expectZodSchemaEqual(atBody.b, z.number());
expectZodSchemaEqual(atBody.c, z.boolean());
expectZodSchemaEqual(atBody.d, z.string());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
*/

import { z } from '@kbn/zod/v4';
import { getSchemaAtPath } from './get_schema_at_path';
import { getShape } from './get_shape';
import { getZodObjectProperty } from './get_zod_object_property';

export function getShapeAt(schema: z.ZodType, property: string): Record<string, z.ZodType> {
const schemaAtProperty = getZodObjectProperty(schema, property);
const { schema: schemaAtProperty } = getSchemaAtPath(schema, property);
if (schemaAtProperty === null) {
return {};
}
// SPECIAL handling for bulk request body. It is an array of objects, in workflows we wrap it in "operations" property.
if (property === 'body' && schemaAtProperty instanceof z.ZodArray) {
return { operations: schemaAtProperty.describe('Bulk request body') };
}
return getShape(schemaAtProperty as z.ZodObject);
return getShape(schemaAtProperty);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { createClient } from '@hey-api/openapi-ts';
import { execSync } from 'child_process';
import fs from 'fs';
import type { OpenAPIV3 } from 'openapi-types';
import Path from 'path';
Expand All @@ -16,6 +17,7 @@ import {
ES_CONTRACTS_OUTPUT_FILE_PATH,
ES_GENERATED_OUTPUT_FOLDER_PATH,
ES_SPEC_OPENAPI_PATH,
ES_SPEC_OUTPUT_PATH,
ES_SPEC_SCHEMA_PATH,
OPENAPI_TS_OUTPUT_FILENAME,
OPENAPI_TS_OUTPUT_FOLDER_PATH,
Expand Down Expand Up @@ -103,12 +105,13 @@ function generateContracts() {
}

function generateEsConnectorsIndexFile(contracts: ContractMeta[]) {
const esSpecCommitHash = getShortEsSpecCommitHash();
return `${getLicenseHeader()}

/*
* AUTO-GENERATED FILE - DO NOT EDIT
*
* This file contains Elasticsearch connector definitions generated from elasticsearch-specification repository.
* This file contains Elasticsearch connector definitions generated from elasticsearch-specification repository (https://github.com/elastic/elasticsearch-specification/commit/${esSpecCommitHash}).
* Generated at: ${new Date().toISOString()}
* Source: elasticsearch-specification repository (${contracts.length} APIs)
*
Expand Down Expand Up @@ -157,6 +160,18 @@ ${generateContractBlock(contract)}
`;
}

function getShortEsSpecCommitHash(): string {
try {
return execSync('git rev-parse HEAD', { cwd: ES_SPEC_OUTPUT_PATH })
.toString()
.trim()
.substring(0, 7);
} catch (error) {
console.error('❌ Failed to get Elasticsearch specification commit hash:', error);
return 'unknown';
}
}

async function generateZodSchemas() {
try {
const startedAt = performance.now();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { allowShortQuerySyntax } from '../shared/oas_allow_short_query_syntax';
import { removeDiscriminatorsWithoutMapping } from '../shared/oas_remove_discriminators_without_mapping';
import { createRemoveServerDefaults } from '../shared/oas_remove_server_defaults';

console.log('Reading OpenAPI spec from ', ES_SPEC_OPENAPI_PATH);
const openApiSpec = JSON.parse(fs.readFileSync(ES_SPEC_OPENAPI_PATH, 'utf8')) as OpenAPIV3.Document;
console.log('Preprocessing OpenAPI spec...');
const preprocessedOpenApiSpec = [
removeDiscriminatorsWithoutMapping,
allowShortQuerySyntax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@
/* eslint-disable import/no-default-export */

import type { UserConfig } from '@hey-api/openapi-ts';
import fs from 'fs';
import type { OpenAPIV3 } from 'openapi-types';
import yaml from 'yaml';
import {
KIBANA_SPEC_OPENAPI_PATH,
OPENAPI_TS_OUTPUT_FILENAME,
OPENAPI_TS_OUTPUT_FOLDER_PATH,
} from './constants';
import { removeDiscriminatorsWithInvalidMapping } from '../shared/oas_remove_discriminators_with_invalid_mapping';
import { removeDiscriminatorsWithoutMapping } from '../shared/oas_remove_discriminators_without_mapping';

console.log('Reading OpenAPI spec from ', KIBANA_SPEC_OPENAPI_PATH);
const openApiSpec = yaml.parse(
fs.readFileSync(KIBANA_SPEC_OPENAPI_PATH, 'utf8')
) as OpenAPIV3.Document;

console.log('Preprocessing OpenAPI spec...');
const preprocessedOpenApiSpec = [
removeDiscriminatorsWithoutMapping,
removeDiscriminatorsWithInvalidMapping,
].reduce((acc, fn) => fn(acc), openApiSpec);

const config: UserConfig = {
input: KIBANA_SPEC_OPENAPI_PATH,
// @ts-expect-error - for some reason openapi-ts doesn't accept OpenAPIV3.Document
input: preprocessedOpenApiSpec,
output: {
path: OPENAPI_TS_OUTPUT_FOLDER_PATH,
fileName: OPENAPI_TS_OUTPUT_FILENAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,61 @@ describe('generateParameterTypes', () => {
expect(parameterTypes.urlParams).toEqual(['queryParam']);
expect(parameterTypes.bodyParams).toEqual(['bodyParam']);
});
it('should generate parameter types from an operation with oneOf in the request body', () => {
const operationWithOneOf: OpenAPIV3.OperationObject = {
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: '#/components/schemas/requestBodySchema1',
},
{
$ref: '#/components/schemas/requestBodySchema2',
},
],
},
},
},
},
responses: {
'200': {
description: 'Success',
},
},
operationId: 'operationWithOneOf',
};
const openApiDocument: OpenAPIV3.Document = {
openapi: '3.0.0',
info: {
title: 'Test API',
version: '1.0.0',
},
paths: {
'{pathParam}/test': {
get: operationWithOneOf,
},
},
components: {
schemas: {
requestBodySchema1: {
type: 'object',
properties: {
bodyParam1: { type: 'string' },
},
},
requestBodySchema2: {
type: 'object',
properties: {
bodyParam2: { type: 'string' },
},
},
},
},
};
const parameterTypes = generateParameterTypes([operationWithOneOf], openApiDocument);
expect(parameterTypes).toBeDefined();
expect(parameterTypes.bodyParams).toEqual(['bodyParam1', 'bodyParam2']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@
*/

import type { OpenAPIV3 } from 'openapi-types';
import type { ParameterTypes } from './types';
import { getOrResolveObject } from '../../common/utils';

export function generateParameterTypes(
operations: OpenAPIV3.OperationObject[],
openApiDocument: OpenAPIV3.Document
): {
headerParams: string[];
pathParams: string[];
urlParams: string[];
bodyParams: string[];
} {
): ParameterTypes {
const allParameters = operations
.flatMap((operation) => operation.parameters)
.filter(
Expand All @@ -36,6 +32,8 @@ export function generateParameterTypes(
const urlParams = new Set(
allParameters.filter((param) => param.in === 'query').map((param) => param.name)
);

// Extract request body schemas and process them with the new recursive function
const requestBodiesSchemas = operations
.map((operation) => operation.requestBody)
.filter(
Expand All @@ -46,30 +44,88 @@ export function generateParameterTypes(
getOrResolveObject<OpenAPIV3.RequestBodyObject>(requestBody, openApiDocument)
)
.filter((requestBody): requestBody is OpenAPIV3.RequestBodyObject => requestBody !== null)
.map((requestBody) =>
getOrResolveObject<OpenAPIV3.SchemaObject>(
requestBody.content['application/json']?.schema,
openApiDocument
)
)
.filter((schema): schema is OpenAPIV3.SchemaObject => schema !== null);
.map((requestBody) => requestBody.content?.['application/json']?.schema)
.filter(
(schema): schema is OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject => schema !== undefined
);

// Use the new recursive function to extract all properties
const bodyParams = new Set(
requestBodiesSchemas
.map((schema) => getOrResolveObject(schema, openApiDocument))
.filter(
(schema): schema is OpenAPIV3.NonArraySchemaObject =>
schema !== null &&
typeof schema === 'object' &&
'properties' in schema &&
schema.properties !== undefined
)
.map((schema) => Object.keys(schema.properties ?? {}))
.map((schema) => extractPropertiesFromSchema(schema, openApiDocument))
.flat()
);

return {
headerParams: Array.from(headerParams),
pathParams: Array.from(pathParams),
urlParams: Array.from(urlParams),
bodyParams: Array.from(bodyParams),
};
}

export function generateParameterTypesForOperation(
operation: OpenAPIV3.OperationObject,
openApiDocument: OpenAPIV3.Document
): ParameterTypes {
const parameterTypes = generateParameterTypes([operation], openApiDocument);
return parameterTypes;
}

/**
* Recursively extracts all property names from a schema, handling composition schemas
* like oneOf, allOf, anyOf as well as regular object schemas
*/
function extractPropertiesFromSchema(
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
openApiDocument: OpenAPIV3.Document,
visited: WeakSet<object> = new WeakSet()
): string[] {
// Resolve references first
const resolvedSchema = getOrResolveObject<OpenAPIV3.SchemaObject>(schema, openApiDocument);
if (!resolvedSchema || typeof resolvedSchema !== 'object') {
return [];
}

// Prevent infinite recursion for circular references using object identity
if (visited.has(resolvedSchema)) {
return [];
}
visited.add(resolvedSchema);

const properties: Set<string> = new Set();

// Handle direct properties (object schema)
if ('properties' in resolvedSchema && resolvedSchema.properties) {
Object.keys(resolvedSchema.properties).forEach((key) => properties.add(key));
}

// Handle oneOf - union of all possible schemas
if ('oneOf' in resolvedSchema && Array.isArray(resolvedSchema.oneOf)) {
resolvedSchema.oneOf.forEach((subSchema) => {
extractPropertiesFromSchema(subSchema, openApiDocument, visited).forEach((prop) =>
properties.add(prop)
);
});
}

// Handle allOf - intersection of all schemas
if ('allOf' in resolvedSchema && Array.isArray(resolvedSchema.allOf)) {
resolvedSchema.allOf.forEach((subSchema) => {
extractPropertiesFromSchema(subSchema, openApiDocument, visited).forEach((prop) =>
properties.add(prop)
);
});
}

// Handle anyOf - similar to oneOf
if ('anyOf' in resolvedSchema && Array.isArray(resolvedSchema.anyOf)) {
resolvedSchema.anyOf.forEach((subSchema) => {
extractPropertiesFromSchema(subSchema, openApiDocument, visited).forEach((prop) =>
properties.add(prop)
);
});
}

return Array.from(properties);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export function alignDefaultWithEnum(document: OpenAPIV3.Document) {
),
],
};
console.log(key, JSON.stringify(extendedValue, null, 2));
return [key, extendedValue];
})
);
Expand Down
Loading