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
19 changes: 19 additions & 0 deletions packages/openapi-to-graphql/src/oas_3_tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,25 @@ export function resolveAllOf<TSource, TContext, TArgs>(
}
})
}

// Collapse nullable if applicable
if (typeof collapsedMemberSchema.nullable !== 'undefined') {
if (typeof collapsedSchema.nullable === 'undefined') {
collapsedSchema.nullable = collapsedMemberSchema.nullable

// Check for incompatible nullable property
} else if (collapsedSchema.nullable !== collapsedMemberSchema.nullable) {
handleWarning({
mitigationType: MitigationTypes.UNRESOLVABLE_SCHEMA,
message:
`Resolving 'allOf' field in schema '${JSON.stringify(
collapsedSchema
)}' ` + `results in incompatible nullable property.`,
data,
log: preprocessingLog
})
}
}
})
}

Expand Down
2 changes: 2 additions & 0 deletions packages/openapi-to-graphql/src/preprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ export function createDataDef<TSource, TContext, TArgs>(
preferredName,
schema: null,
required: [],
nullable: null,
links: null,
subDefinitions: null,
graphQLTypeName: null,
Expand Down Expand Up @@ -794,6 +795,7 @@ export function createDataDef<TSource, TContext, TArgs>(
*/
schema,
required: [],
nullable: collapsedSchema.nullable,
targetGraphQLType, // May change due to allOf and oneOf resolution
subDefinitions: undefined,
links: saneLinks,
Expand Down
32 changes: 28 additions & 4 deletions packages/openapi-to-graphql/src/schema_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,10 +662,34 @@ function createFields<TSource, TContext, TArgs extends object>({
iteration: iteration + 1,
isInputObjectType,
fetch
})
}) as GraphQLObjectType | GraphQLInputObjectType


/* On requiredness / nullability

In GraphQL, fields are either wrapped in a `GraphQLNonNull` (type ends with `!`) or not.
In input types, a field with `GraphQLNonNull` type [must be present and must not be `null`](https://spec.graphql.org/October2021/#sec-Non-Null.Input-Coercion)
In output types, a field with `GraphQLNonNull` type [cannot return a `null` value](https://spec.graphql.org/October2021/#sec-Value-Completion)

The OpenAPI spec is [not clear on the meaning of "required"](https://swagger.io/docs/specification/data-models/data-types/#required)
but the JSON schema specification that OpenAPI 3.0 uses [is clearer](https://json-schema.org/specification-links#draft-5)
> An object instance is valid against this keyword if its property set contains all elements in this keyword's array value.
Essentially, we can interpret "required" as "must be present in the object."

The OpenAPI spec also [provides a "nullable" property](https://swagger.io/docs/specification/data-models/data-types/#null)
> [nullable: true will] specify that the value may be null
Essentially, we can interpret "nullable" as "may be `null` (if present)."

In OpenAPI, the same component schema can be used as both input and output types. So we must interpret "required" and "nullable" differently.
For input types, we must interpret "required" as `GraphQLNonNull` and ignore "nullable" (since `GraphQLNonNull` implies non-nullable).
For output types, we must interpret "nullable" as `GraphQLNonNull` and ignore "required" (since presence in output is determined by the query).
*/

// If the data is not present, assume nullable (GraphQL fields are null by default)

const requiredProperty =
typeof def.required === 'object' && def.required.includes(fieldName)
const nonNullProperty = isInputObjectType ?
def.schema.required?.includes(fieldName) :
fieldTypeDefinition.nullable === false

// Finally, add the object type to the fields (using sanitized field name)
if (objectType) {
Expand Down Expand Up @@ -706,7 +730,7 @@ function createFields<TSource, TContext, TArgs extends object>({
)

fields[sanePropName] = {
type: requiredProperty
type: nonNullProperty
? new GraphQLNonNull(objectType as GraphQLOutputType)
: (objectType as GraphQLOutputType),

Expand Down
7 changes: 7 additions & 0 deletions packages/openapi-to-graphql/src/types/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export type DataDefinition = {
*/
required: string[]

/**
* Similar to the nullable property in object schemas but because of certain
* keywords to combine schemas, e.g. "allOf", this resolves the nullable
* property in all member schemas
*/
nullable?: boolean

// The type GraphQL type this dataDefintion will be created into
targetGraphQLType: TargetGraphQLType

Expand Down
14 changes: 7 additions & 7 deletions packages/openapi-to-graphql/test/example_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ test('Link parameters as constants and variables', () => {
},
everythingLink: {
body:
'http://localhost:3002/api/scanner_get_200_hello_application/json_close'
'http://localhost:3002/api/scanner_get_200_hello_application/json_16'
}
}
}
Expand Down Expand Up @@ -493,10 +493,10 @@ test('Nested links with constants and variables', () => {
body: '123',
everythingLink: {
body:
'http://localhost:3002/api/copier_get_200_123_application/json_close',
'http://localhost:3002/api/copier_get_200_123_application/json_14',
everythingLink: {
body:
'http://localhost:3002/api/copier_get_200_http://localhost:3002/api/copier_get_200_123_application/json_close_application/json_close'
'http://localhost:3002/api/copier_get_200_http://localhost:3002/api/copier_get_200_123_application/json_14_application/json_75'
}
}
}
Expand All @@ -506,7 +506,7 @@ test('Nested links with constants and variables', () => {
},
everythingLink: {
body:
'http://localhost:3002/api/scanner_get_200_val_application/json_close'
'http://localhost:3002/api/scanner_get_200_val_application/json_14'
}
}
}
Expand All @@ -531,7 +531,7 @@ test('Link parameters as constants and variables with request payload', () => {
body: 'req.body: body, req.query.query: query, req.path.path: path',
everythingLink2: {
body:
'http://localhost:3002/api/scanner/path_post_200_body_query_path_application/json_req.body: body, req.query.query: query, req.path.path: path_query_path_close'
'http://localhost:3002/api/scanner/path_post_200_body_query_path_application/json_req.body: body, req.query.query: query, req.path.path: path_query_path_70'
}
}
}
Expand Down Expand Up @@ -642,8 +642,8 @@ test('Get response containing 64-bit integer (using GraphQLBigInt)', () => {
expect(result).toEqual({
data: {
productReviews: [
{ timestamp: BigInt('1502787600000000') },
{ timestamp: BigInt('1502787400000000') }
{ timestamp: Number('1502787600000000') },
{ timestamp: Number('1502787400000000') }
]
}
})
Expand Down
3 changes: 3 additions & 0 deletions packages/openapi-to-graphql/test/file_upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ test('Upload completes without any error', async () => {
const graphqlServer = createServer(async (req, res) => {
try {
const operation = await processRequest(req, res)
if (Array.isArray(operation)) {
throw new Error('Expected a single operation')
}
const result = await graphql({ schema: createdSchema, source: operation.query, variableValues: operation.variables as GraphQLArgs['variableValues'] })
res.end(JSON.stringify(result))
} catch (e) {
Expand Down
26 changes: 17 additions & 9 deletions packages/openapi-to-graphql/test/fixtures/example_oas.json
Original file line number Diff line number Diff line change
Expand Up @@ -1316,13 +1316,14 @@
},
"coordinates": {
"type": "object",
"required": ["lat", "long"],
"properties": {
"lat": {
"type": "number"
"type": "number",
"nullable": false
},
"long": {
"type": "number"
"type": "number",
"nullable": false
}
}
},
Expand All @@ -1332,10 +1333,16 @@
{
"properties": {
"family": {
"$ref": "#/components/schemas/familyString"
"allOf": [
{
"$ref": "#/components/schemas/familyString"
},
{
"nullable": false
}
]
}
},
"required": ["family"]
}
},
{
"properties": {
Expand Down Expand Up @@ -1388,7 +1395,8 @@
"patent-id": {
"type": "string",
"description": "The id of the patent",
"format": "specialIdFormat"
"format": "specialIdFormat",
"nullable": false
},
"inventor-id": {
"type": "string",
Expand Down Expand Up @@ -1600,13 +1608,13 @@
"EverythingLink": {
"operationId": "getCopier",
"parameters": {
"query": "{$url}_{$method}_{$statusCode}_{$request.query.query}_{$request.header.accept}_{$response.header.connection}"
"query": "{$url}_{$method}_{$statusCode}_{$request.query.query}_{$request.header.accept}_{$response.header.content-length}"
}
},
"EverythingLink2": {
"operationId": "getCopier",
"parameters": {
"query": "{$url}_{$method}_{$statusCode}_{$request.body}_{$request.query.query}_{$request.path.path}_{$request.header.accept}_{$response.body#/body}_{$response.query.query}_{$response.path.path}_{$response.header.connection}"
"query": "{$url}_{$method}_{$statusCode}_{$request.body}_{$request.query.query}_{$request.path.path}_{$request.header.accept}_{$response.body#/body}_{$response.query.query}_{$response.path.path}_{$response.header.content-length}"
}
},
"Reviews": {
Expand Down
5 changes: 3 additions & 2 deletions packages/openapi-to-graphql/test/fixtures/example_oas6.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@
"type": "number"
},
"previous_owner": {
"description": "Previouw owner of the pet",
"description": "Previous owner of the pet",
"type": "string"
},
"history": {
Expand All @@ -446,7 +446,8 @@
"data": {
"type": "string"
}
}
},
"nullable": false
},
"history2": {
"description": "History of the pet",
Expand Down
Loading