From c80e75e46fab20535ed9c3eb46abc6ae7d1da0b8 Mon Sep 17 00:00:00 2001 From: Charles Bodman Date: Mon, 9 Feb 2026 22:04:17 -0800 Subject: [PATCH] refactor(get-type): enhance handling of $ref and additionalProperties in schema processing - Updated the getType function to use a while loop for resolving $ref references. - Improved handling of additionalProperties to support recursive types and combined properties. - Added tests for various scenarios including additionalProperties with recursive types and multi-level $ref resolution. refactor(openapi-common): streamline additionalProperties handling in writeObjectProperties - Modified writeObjectProperties to handle additionalProperties more effectively, including cases for true and object types. - Introduced new tests to validate the behavior of writeObjectProperties with complex schemas. --- packages/massimo-cli/lib/get-type.js | 39 ++++---- packages/massimo-cli/lib/openapi-common.js | 12 ++- packages/massimo-cli/test/get-type.test.js | 66 +++++++++++++ .../massimo-cli/test/openapi-common.test.js | 95 +++++++++++++++++++ 4 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 packages/massimo-cli/test/openapi-common.test.js diff --git a/packages/massimo-cli/lib/get-type.js b/packages/massimo-cli/lib/get-type.js index 1257a91..7c05137 100644 --- a/packages/massimo-cli/lib/get-type.js +++ b/packages/massimo-cli/lib/get-type.js @@ -1,7 +1,7 @@ import jsonpointer from 'jsonpointer' export function getType (typeDef, methodType, spec) { - if (typeDef.$ref) { + while (typeDef.$ref) { typeDef = jsonpointer.get(spec, typeDef.$ref.replace('#', '')) } if (typeDef.schema) { @@ -84,37 +84,40 @@ export function getType (typeDef, methodType, spec) { } if (typeDef.type === 'object') { const additionalProps = typeDef?.additionalProperties - const additionalPropsObj = additionalProps?.properties - const additionalPropsType = additionalProps?.type - const additionalPropsRequired = additionalProps?.required const nullable = typeDef.nullable - const objProperties = typeDef.properties || additionalPropsObj + const objProperties = typeDef.properties if (!objProperties || Object.keys(objProperties).length === 0) { // Object without properties - const resultType = additionalPropsType - ? `Record` - : 'object' + let resultType = 'object' + if (additionalProps) { + if (typeof additionalProps === 'object') { + resultType = `Record` + } else if (additionalProps === true) { + resultType = 'Record' + } + } return nullable === true ? `${resultType} | null` : resultType } - let output = additionalPropsObj && additionalPropsType === 'object' ? 'Record { + let output = '{ ' + const props = Object.keys(objProperties).map(prop => { let required = false if (typeDef.required) { required = !!typeDef.required.includes(prop) } - if (additionalPropsRequired) { - required = required || !!additionalPropsRequired.includes(prop) - } return `'${prop}'${required ? '' : '?'}: ${getType(objProperties[prop], methodType, spec)}` }) - if (additionalProps === true) { - props.push('[key: string]: unknown') + + if (additionalProps) { + if (typeof additionalProps === 'object') { + props.push(`[key: string]: ${getType(additionalProps, methodType, spec)}`) + } else if (additionalProps === true) { + props.push('[key: string]: unknown') + } } + output += props.join('; ') - output += additionalPropsObj ? ' }>' : ' }' + output += ' }' if (nullable === true) { output += ' | null' } diff --git a/packages/massimo-cli/lib/openapi-common.js b/packages/massimo-cli/lib/openapi-common.js index f91694e..6c88536 100644 --- a/packages/massimo-cli/lib/openapi-common.js +++ b/packages/massimo-cli/lib/openapi-common.js @@ -264,7 +264,7 @@ export function writeObjectProperties (writer, schema, spec, addedProps, methodT } } - if (schema.$ref) { + while (schema.$ref) { schema = jsonpointer.get(spec, schema.$ref.replace('#', '')) } if (schema.type === 'object') { @@ -272,8 +272,14 @@ export function writeObjectProperties (writer, schema, spec, addedProps, methodT _writeObjectProps(schema.properties) } - if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { - _writeObjectProps(schema.additionalProperties) + if (schema.additionalProperties) { + if (typeof schema.additionalProperties === 'object') { + writer.write(`[key: string]: ${getType(schema.additionalProperties, methodType, spec)};`) + writer.newLine() + } else if (schema.additionalProperties === true) { + writer.write('[key: string]: unknown;') + writer.newLine() + } } } else { throw new TypeNotSupportedError(schema.type) diff --git a/packages/massimo-cli/test/get-type.test.js b/packages/massimo-cli/test/get-type.test.js index d7e5c83..7785a56 100644 --- a/packages/massimo-cli/test/get-type.test.js +++ b/packages/massimo-cli/test/get-type.test.js @@ -353,3 +353,69 @@ test('support type nullable null', async () => { } equal(getType(def), 'null') }) + +test('support additionalProperties with recursive types', async () => { + const schema = { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + enum: ['READ', 'UPDATE'] + } + } + } + const type = getType(schema) + equal(type, "Record>") +}) + +test('support additionalProperties combined with properties', async () => { + const schema = { + type: 'object', + properties: { + foo: { type: 'string' } + }, + additionalProperties: { + type: 'number' + } + } + const type = getType(schema) + equal(type, "{ 'foo'?: string; [key: string]: number }") +}) + +test('support additionalProperties: true combined with properties', async () => { + const schema = { + type: 'object', + properties: { + foo: { type: 'string' } + }, + additionalProperties: true + } + const type = getType(schema) + equal(type, "{ 'foo'?: string; [key: string]: unknown }") +}) + +test('support multi-level $ref resolution', async () => { + const spec = { + components: { + schemas: { + ActualObject: { + type: 'object', + properties: { + id: { type: 'string' } + } + }, + Alias1: { + $ref: '#/components/schemas/ActualObject' + }, + Alias2: { + $ref: '#/components/schemas/Alias1' + } + } + } + } + + const typeDef = { $ref: '#/components/schemas/Alias2' } + const type = getType(typeDef, 'req', spec) + equal(type, "{ 'id'?: string }") +}) diff --git a/packages/massimo-cli/test/openapi-common.test.js b/packages/massimo-cli/test/openapi-common.test.js new file mode 100644 index 0000000..ef9df44 --- /dev/null +++ b/packages/massimo-cli/test/openapi-common.test.js @@ -0,0 +1,95 @@ +import { equal } from 'node:assert' +import { test } from 'node:test' +import CodeBlockWriter from 'code-block-writer' +import { writeObjectProperties } from '../lib/openapi-common.js' + +test('writeObjectProperties should handle complex additionalProperties', async () => { + const schema = { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + enum: ['READ', 'UPDATE'] + } + } + } + const writer = new CodeBlockWriter({ + indentNumberOfSpaces: 2, + useTabs: false, + useSingleQuote: true + }) + const addedProps = new Set() + writeObjectProperties(writer, schema, {}, addedProps, 'req', false) + const output = writer.toString() + equal(output, "[key: string]: Array<'READ' | 'UPDATE'>;\n") +}) + +test('writeObjectProperties should handle additionalProperties: true', async () => { + const schema = { + type: 'object', + additionalProperties: true + } + const writer = new CodeBlockWriter({ + indentNumberOfSpaces: 2, + useTabs: false, + useSingleQuote: true + }) + const addedProps = new Set() + writeObjectProperties(writer, schema, {}, addedProps, 'req', false) + const output = writer.toString() + equal(output, '[key: string]: unknown;\n') +}) + +test('writeObjectProperties should handle both properties and additionalProperties', async () => { + const schema = { + type: 'object', + properties: { + foo: { type: 'string' } + }, + required: ['foo'], + additionalProperties: { + type: 'number' + } + } + const writer = new CodeBlockWriter({ + indentNumberOfSpaces: 2, + useTabs: false, + useSingleQuote: true + }) + const addedProps = new Set() + writeObjectProperties(writer, schema, {}, addedProps, 'req', false) + const output = writer.toString() + equal(output, "'foo': string;\n[key: string]: number;\n") +}) + +test('writeObjectProperties should handle multi-level $ref resolution', async () => { + const spec = { + components: { + schemas: { + ActualObject: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + }, + Alias1: { + $ref: '#/components/schemas/ActualObject' + } + } + } + } + + const schema = { $ref: '#/components/schemas/Alias1' } + const writer = new CodeBlockWriter({ + indentNumberOfSpaces: 2, + useTabs: false, + useSingleQuote: true + }) + const addedProps = new Set() + + writeObjectProperties(writer, schema, spec, addedProps, 'req', false) + const output = writer.toString() + equal(output, "'id': string;\n") +})