11import type { OpenAPIV3 } from "openapi-types" ;
22import { traverse } from './utils/OpenApiTraverser' ;
33import isEqual from 'lodash.isequal' ;
4- import { compressMultipleUnderscores , isPrimitiveType , resolveObj , isReferenceObject , isEmptyObjectSchema , is_simple_ref } from './utils/helper' ;
4+ import { compressMultipleUnderscores , isPrimitiveType , resolveObj , isReferenceObject , isEmptyObjectSchema , is_simple_ref , toSnakeCase } from './utils/helper' ;
55import logger from "./utils/logger" ;
66
77
88const DEFAULT_MAP_KEY = 'field' // default key for simplified additionalProperties
99const DEFAULT_MAP_VALUE = 'value' // default value for simplified additionalProperties
10+ const QUERY_CONTAINER_SCHEMA_NAME = 'QueryContainer' // schema name that requires special inline handling.
1011
1112export class SchemaModifier {
1213 root : OpenAPIV3 . Document ;
@@ -21,7 +22,6 @@ export class SchemaModifier {
2122 this . convertNullTypeToNullValue ( schema )
2223 this . deduplicateOneOfWithArrayType ( schema )
2324 this . collapseSingleItemComposite ( schema ) ;
24- this . removeArrayOfMapWrapper ( schema )
2525 } ,
2626 onSchema : ( schema , schemaName ) => {
2727 if ( ! schema || isReferenceObject ( schema ) ) return ;
@@ -33,14 +33,13 @@ export class SchemaModifier {
3333 this . deduplicateOneOfWithArrayType ( schema )
3434 this . collapseSingleItemComposite ( schema ) ;
3535 this . collapseOneOfObjectPropContainsTitleSchema ( schema )
36- this . removeArrayOfMapWrapper ( schema )
3736 this . convertOneOfToMinMaxProperties ( schema )
3837 } ,
3938 } ) ;
4039 const visit = new Set ( ) ;
4140 traverse ( this . root , {
42- onSchemaProperty : ( schema ) => {
43- this . simplifySingleMapSchema ( schema , visit ) ;
41+ onSchemaProperty : ( schema , propertyName , parentSchemaName ) => {
42+ this . simplifySingleMapSchema ( schema , visit , parentSchemaName ) ;
4443 this . handleAdditionalPropertiesUndefined ( schema )
4544
4645 } ,
@@ -280,53 +279,120 @@ export class SchemaModifier {
280279 }
281280
282281 /**
283- * Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into standard schema by reconstructing
284- * the additional property definitions.
285- * Example:
286- * Input:
282+ * Extracts type name from $ref or title
283+ **/
284+ private getTypeName ( schema : OpenAPIV3 . SchemaObject | OpenAPIV3 . ReferenceObject ) : string | null {
285+ if ( '$ref' in schema ) {
286+ const parts = schema . $ref . split ( '/' ) ;
287+ return parts [ parts . length - 1 ] ;
288+ }
289+ if ( 'title' in schema && schema . title ) {
290+ return schema . title ;
291+ }
292+ return null ;
293+ }
294+
295+ /**
296+ * Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into a $ref to a new map schema.
297+ *
298+ * Input (items of DecayFunction):
287299 * {
288300 * type: "object",
301+ * propertyNames: { title: "field", type: "string" },
289302 * additionalProperties: {
290- * - ref: "#/components/schemas/Model"
303+ * title: "placement",
304+ * $ref: "#/components/schemas/DecayPlacement"
291305 * },
292306 * minProperties: 1,
293307 * maxProperties: 1,
294- * };
295- * Model:
296- * properties: {
297- * properties1: string
298- * properties2: string
299- * }
300- *
308+ * }
301309 *
302- *Output:
303- * {
304- * ref: "#/components/schemas/Example
305- * }
306- *
307- * Model:
308- * properties: {
309- * field: string
310- * properties1: string
311- * properties2: string
312- * }
310+ * Output:
311+ * - Replaces with: { $ref: "#/components/schemas/DecayPlacementSingleMap" }
312+ * - Creates new schema DecayPlacementSingleMap:
313+ * {
314+ * type: "object",
315+ * properties: {
316+ * field: { type: "string" },
317+ * decay_placement: {
318+ * title: "placement",
319+ * $ref: "#/components/schemas/DecayPlacement"
320+ * }
321+ * },
322+ * required: ["field", "decay_placement"]
323+ * }
313324 *
314325 **/
315- simplifySingleMapSchema ( schema : OpenAPIV3 . SchemaObject , visit : Set < any > ) : void {
326+ simplifySingleMapSchema ( schema : OpenAPIV3 . SchemaObject , visit : Set < any > , parentSchemaName ?: string ) : void {
316327 if ( schema . type === 'object' && typeof schema . additionalProperties === 'object' &&
317328 ! Array . isArray ( schema . additionalProperties ) && schema . minProperties === 1 && schema . maxProperties === 1 ) {
318329
319- const reconstructAdditionalPropertySchema = this . reconstructAdditionalPropertySchema ( schema . additionalProperties , visit ) ;
330+ // Check if this is a QueryContainer property
331+ // If so, use the old inline behavior to avoid breaking QueryContainer structure
332+ // TODO: Remove this special case in next major release and use SingleMap wrapper for all schemas
333+ if ( parentSchemaName === QUERY_CONTAINER_SCHEMA_NAME ) {
334+ logger . info ( `Using inline modification for ${ QUERY_CONTAINER_SCHEMA_NAME } property (legacy behavior)` + JSON . stringify ( schema ) ) ;
335+ // Old behavior: inline modification
336+ const reconstructAdditionalPropertySchema = this . reconstructAdditionalPropertySchema ( schema . additionalProperties , visit ) ;
337+ Object . assign ( schema , reconstructAdditionalPropertySchema ) ;
338+ } else {
339+ // New behavior: create intermediate SingleMap schema
340+ const valueSchema = schema . additionalProperties ;
341+ const typeName = this . getTypeName ( valueSchema ) || 'Value' ;
342+
343+ // Extract field property name and schema from propertyNames
344+ let fieldPropertyName = 'field' ;
345+ let fieldPropertySchema : any = { type : 'string' as const } ;
346+
347+ if ( ( schema as any ) . propertyNames ) {
348+ const propertyNames = ( schema as any ) . propertyNames ;
349+
350+ // If propertyNames has a title, use it as the property name
351+ if ( propertyNames . title && typeof propertyNames . title === 'string' ) {
352+ fieldPropertyName = propertyNames . title ;
353+
354+ fieldPropertySchema = { ...propertyNames } ;
355+ delete fieldPropertySchema . title ;
356+ } else {
357+ // Use propertyNames as-is for the field schema
358+ fieldPropertySchema = propertyNames ;
359+ }
360+ }
361+
362+ // Create new map schema name
363+ const mapSchemaName = `${ typeName } SingleMap` ;
364+
365+ // Create the new map schema
366+ const newMapSchema : OpenAPIV3 . SchemaObject = {
367+ type : 'object' ,
368+ properties : {
369+ [ fieldPropertyName ] : fieldPropertySchema ,
370+ [ toSnakeCase ( typeName ) ] : valueSchema
371+ } ,
372+ required : [ fieldPropertyName , toSnakeCase ( typeName ) ]
373+ } ;
374+
375+ // Add to components.schemas if not already exists
376+ if ( ! this . root . components ) {
377+ this . root . components = { } ;
378+ }
379+ if ( ! this . root . components . schemas ) {
380+ this . root . components . schemas = { } ;
381+ }
382+ if ( ! this . root . components . schemas [ mapSchemaName ] ) {
383+ this . root . components . schemas [ mapSchemaName ] = newMapSchema ;
384+ }
320385
321- Object . assign ( schema , reconstructAdditionalPropertySchema )
386+ // Replace current schema with $ref to the new map schema
387+ ( schema as any ) . $ref = `#/components/schemas/${ mapSchemaName } ` ;
388+ }
322389
390+ // Cleanup properties for both paths (keep vendor extensions like x-*)
391+ delete schema . type ;
323392 delete schema . additionalProperties ;
324393 delete schema . minProperties ;
325394 delete schema . maxProperties ;
326- delete schema . type
327- if ( 'propertyNames' in schema ) {
328- delete schema . propertyNames ;
329- }
395+ delete ( schema as any ) . propertyNames
330396 }
331397 }
332398
@@ -449,44 +515,6 @@ export class SchemaModifier {
449515 logger . info ( `Converted additionalProperties to named property '${ propertyName } ' with type: object` ) ;
450516 }
451517
452- /**
453- * Removes the array wrapper if the schema is an array of maps (additionalProperties).
454- * Converts array of objects with only additionalProperties into just the additionalProperties schema.
455- *
456- * Example:
457- * Input:
458- * {
459- * type: "array",
460- * items: {
461- * type: "object",
462- * additionalProperties: {
463- * $ref: "#/components/schemas/Value"
464- * }
465- * }
466- * }
467- *
468- * Output:
469- * {
470- * type: "object",
471- * additionalProperties: {
472- * $ref: "#/components/schemas/Value"
473- * }
474- * }
475- **/
476- removeArrayOfMapWrapper ( schema : OpenAPIV3 . SchemaObject ) : void {
477- if ( schema . type === 'array' && schema . items && typeof schema . items === 'object' && ! ( '$ref' in schema . items ) ) {
478- const items = schema . items as OpenAPIV3 . SchemaObject ;
479-
480- if ( items . type === 'object' && items . additionalProperties && ! items . properties ) {
481- ( schema as any ) . type = 'object' ;
482- schema . additionalProperties = items . additionalProperties ;
483- delete ( schema as any ) . items ;
484-
485- logger . info ( `Removed array wrapper from array of maps schema` ) ;
486- }
487- }
488- }
489-
490518 /**
491519 * Converts oneOf pattern with single-property objects into minProperties/maxProperties pattern.
492520 * For AggregationContainer
0 commit comments