Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ coverage
# test files
.gen/
test/generated
test-output-*

# debug files
openapi-ts-debug-*
Expand Down
51 changes: 33 additions & 18 deletions packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>(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<unknown>(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'))
);
},
Expand All @@ -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
Expand Down
56 changes: 56 additions & 0 deletions specs/3.1.x/transforms-read-write-all-scoped.yaml
Original file line number Diff line number Diff line change
@@ -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