diff --git a/.gitignore b/.gitignore index e2ab6676e..f9f88da03 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ coverage # test files .gen/ test/generated +test-output-* # debug files openapi-ts-debug-* diff --git a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts index 080151b13..ec6820e41 100644 --- a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts +++ b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts @@ -392,44 +392,46 @@ export const splitSchemas = ({ for (const [pointer, nodeInfo] of graph.nodes) { const name = pointerToSchema(pointer); - // Only split top-level schemas, with either read-only or write-only scopes (or both). + // Only split top-level schemas with read-only or write-only scopes. + // Includes schemas with all readOnly/writeOnly properties. if ( !name || - !(nodeInfo.scopes?.has('read') || nodeInfo.scopes?.has('write')) || - !nodeInfo.scopes?.has('normal') + !(nodeInfo.scopes?.has('read') || nodeInfo.scopes?.has('write')) ) { continue; } // read variant const readSchema = deepClone(nodeInfo.node); - pruneSchemaByScope(graph, readSchema, 'writeOnly'); - const readBase = applyNaming(name, config.responses); - const readName = - readBase === name - ? readBase - : getUniqueComponentName({ - base: readBase, - components: existingNames, - }); - existingNames.add(readName); - split.schemas[readName] = readSchema; - const readPointer = `${schemasPointerNamespace}${readName}`; + const readShouldBeRemoved = pruneSchemaByScope( + graph, + readSchema, + 'writeOnly', + ); // write variant const writeSchema = deepClone(nodeInfo.node); - pruneSchemaByScope(graph, writeSchema, 'readOnly'); + const writeShouldBeRemoved = pruneSchemaByScope( + graph, + writeSchema, + 'readOnly', + ); + + // If either variant should be removed (empty after pruning), skip splitting + if (readShouldBeRemoved || writeShouldBeRemoved) { + continue; + } // Check if this schema (or any of its descendants) references any schema that // will need read/write variants. This is determined by checking transitive - // dependencies for schemas with both 'normal' and ('read' or 'write') scopes. + // dependencies for schemas with read or write scopes (regardless of 'normal' scope presence). const transitiveDeps = graph.transitiveDependencies.get(pointer) || new Set(); const referencesReadWriteSchemas = Array.from(transitiveDeps).some( (depPointer) => { const depNodeInfo = graph.nodes.get(depPointer); return ( - depNodeInfo?.scopes?.has('normal') && + depNodeInfo?.scopes && (depNodeInfo.scopes.has('read') || depNodeInfo.scopes.has('write')) ); }, @@ -445,6 +447,19 @@ export const splitSchemas = ({ ) { continue; } + + const readBase = applyNaming(name, config.responses); + const readName = + readBase === name + ? readBase + : getUniqueComponentName({ + base: readBase, + components: existingNames, + }); + existingNames.add(readName); + split.schemas[readName] = readSchema; + const readPointer = `${schemasPointerNamespace}${readName}`; + const writeBase = applyNaming(name, config.requests); const writeName = writeBase === name && writeBase !== readName diff --git a/specs/3.1.x/transforms-read-write-all-scoped.yaml b/specs/3.1.x/transforms-read-write-all-scoped.yaml new file mode 100644 index 000000000..1bd9beec1 --- /dev/null +++ b/specs/3.1.x/transforms-read-write-all-scoped.yaml @@ -0,0 +1,56 @@ +openapi: 3.1.1 +info: + title: Test for all readOnly/writeOnly properties + version: 1.0.0 +paths: + /token: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtain' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtain' + description: OK +components: + schemas: + TokenObtain: + type: object + properties: + email: + type: string + writeOnly: true + password: + type: string + writeOnly: true + expires_at: + type: integer + readOnly: true + user_id: + type: string + format: uuid + readOnly: true + refresh_token: + type: string + readOnly: true + access_token: + type: string + readOnly: true + token_type: + type: string + readOnly: true + default: Bearer + required: + - access_token + - email + - expires_at + - password + - refresh_token + - token_type + - user_id