Skip to content

Commit

Permalink
Union one of issue (#5415)
Browse files Browse the repository at this point in the history
* add INPUT_OBJECT location to oneOf directive

* add test with one of and scalar enum

* Fix

---------

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
devsergiy and ardatan authored May 10, 2023
1 parent 612b441 commit 0db5f91
Show file tree
Hide file tree
Showing 18 changed files with 303 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-cats-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@omnigraph/json-schema': patch
---

Handle non object type resolution correctly in union types
12 changes: 10 additions & 2 deletions packages/loaders/json-schema/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ export const ResolveRootDirective = new GraphQLDirective({
locations: [DirectiveLocation.FIELD_DEFINITION],
});

function rootResolver(root: any) {
return root;
}

export function processResolveRootAnnotations(field: GraphQLField<any, any>) {
field.resolve = root => root;
field.resolve = rootResolver;
}

export const ResolveRootFieldDirective = new GraphQLDirective({
Expand Down Expand Up @@ -703,7 +707,11 @@ export const EnumDirective = new GraphQLDirective({

export const OneOfDirective = new GraphQLDirective({
name: 'oneOf',
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
locations: [
DirectiveLocation.OBJECT,
DirectiveLocation.INTERFACE,
DirectiveLocation.INPUT_OBJECT,
],
});

export const ExampleDirective = new GraphQLDirective({
Expand Down
39 changes: 30 additions & 9 deletions packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphQLObjectType, GraphQLTypeResolver } from 'graphql';
import { createGraphQLError } from '@graphql-tools/utils';
import { GraphQLObjectType, GraphQLResolveInfo, GraphQLTypeResolver } from 'graphql';
import { createGraphQLError, getDirective } from '@graphql-tools/utils';

export function getTypeResolverFromOutputTCs({
possibleTypes,
Expand All @@ -12,7 +12,7 @@ export function getTypeResolverFromOutputTCs({
discriminatorMapping?: Record<string, string>;
statusCodeTypeNameMap?: Record<string, string>;
}): GraphQLTypeResolver<any, any> {
return function resolveType(data: any) {
return function resolveType(data: any, _ctx: any, info: GraphQLResolveInfo) {
if (data.__typename) {
return data.__typename;
} else if (discriminatorField != null && data[discriminatorField]) {
Expand All @@ -27,13 +27,34 @@ export function getTypeResolverFromOutputTCs({
}
}

const dataTypeOf = typeof data;

if (dataTypeOf !== 'object') {
for (const possibleType of possibleTypes) {
const fieldMap = possibleType.getFields();
const fields = Object.values(fieldMap);
if (fields.length === 1) {
const field = fields[0];
const directiveObjs = getDirective(info.schema, field, 'resolveRoot');
if (directiveObjs?.length) {
const fieldType = field.type;
if ('parseValue' in fieldType) {
try {
fieldType.parseValue(data);
return possibleType.name;
} catch (e) {}
}
}
}
}
}

// const validationErrors: Record<string, ErrorObject[]> = {};
const dataKeys =
typeof data === 'object'
? Object.keys(data)
// Remove metadata fields used to pass data
.filter(property => !property.toString().startsWith('$'))
: null;
const dataKeys = dataTypeOf
? Object.keys(data)
// Remove metadata fields used to pass data
.filter(property => !property.toString().startsWith('$'))
: null;
for (const possibleType of possibleTypes) {
const typeName = possibleType.name;
if (dataKeys != null) {
Expand Down
9 changes: 7 additions & 2 deletions packages/loaders/json-schema/src/getUnionTypeComposers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'graphql-compose';
import { Logger } from '@graphql-mesh/types';
import { JSONSchemaObject } from '@json-schema-tools/meta-schema';
import { StatusCodeTypeNameDirective } from './directives.js';
import { ResolveRootDirective, StatusCodeTypeNameDirective } from './directives.js';
import { TypeComposers } from './getComposerFromJSONSchema.js';

export interface GetUnionTypeComposersOpts {
Expand All @@ -25,11 +25,16 @@ export interface GetUnionTypeComposersOpts {

export function getContainerTC(schemaComposer: SchemaComposer, output: ComposeInputType) {
const containerTypeName = `${output.getTypeName()}_container`;
schemaComposer.addDirective(ResolveRootDirective);
return schemaComposer.getOrCreateOTC(containerTypeName, otc =>
otc.addFields({
[output.getTypeName()]: {
type: output as any,
resolve: root => root,
directives: [
{
name: 'resolveRoot',
},
],
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`Basket should generate the correct schema 1`] = `
mutation: Mutation
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @statusCodeTypeName(typeName: String, statusCode: ID) repeatable on UNION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`Calendly should generate the correct schema 1`] = `
mutation: Mutation
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @enum(value: String) on ENUM_VALUE
Expand All @@ -20,6 +20,8 @@ directive @regexp(pattern: String) on SCALAR
directive @length(min: Int, max: Int) on SCALAR
directive @resolveRoot on FIELD_DEFINITION
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
directive @httpOperation(path: String, operationSpecificHeaders: ObjMap, httpMethod: HTTPMethod, isBinary: Boolean, requestBaseBody: ObjMap, queryParamArgMap: ObjMap, queryStringOptionsByParam: ObjMap) on FIELD_DEFINITION
Expand Down Expand Up @@ -1469,7 +1471,7 @@ input post_organizations_uuid_invitations_request_Input @example(value: "{\\"val
union revoke_users_organization_invitation_response @statusCodeTypeName(statusCode: 204, typeName: "Void_container") @statusCodeTypeName(statusCode: 400, typeName: "Error_Response") @statusCodeTypeName(statusCode: 401, typeName: "UNAUTHENTICATED_response") @statusCodeTypeName(statusCode: 403, typeName: "PERMISSION_DENIED_response") @statusCodeTypeName(statusCode: 404, typeName: "NOT_FOUND_response") @statusCodeTypeName(statusCode: 500, typeName: "UNKNOWN_response") = Void_container | Error_Response | UNAUTHENTICATED_response | PERMISSION_DENIED_response | NOT_FOUND_response | UNKNOWN_response
type Void_container {
Void: Void
Void: Void @resolveRoot
}
"Represents empty values"
Expand Down Expand Up @@ -1557,7 +1559,7 @@ enum _1_const @typescript(type: "1") @example(value: "1") {
union post_data_compliance_deletion_invitees_response @statusCodeTypeName(statusCode: 202, typeName: "JSON_container") @statusCodeTypeName(statusCode: 400, typeName: "INVALID_ARGUMENT_response") @statusCodeTypeName(statusCode: 401, typeName: "UNAUTHENTICATED_response") @statusCodeTypeName(statusCode: 403, typeName: "Error_Response") @statusCodeTypeName(statusCode: 404, typeName: "NOT_FOUND_response") @statusCodeTypeName(statusCode: 500, typeName: "UNKNOWN_response") = JSON_container | INVALID_ARGUMENT_response | UNAUTHENTICATED_response | Error_Response | NOT_FOUND_response | UNKNOWN_response
type JSON_container {
JSON: JSON
JSON: JSON @resolveRoot
}
input post_data_compliance_deletion_invitees_request_Input @example(value: "{\\"value\\":{\\"emails\\":[\\"test@example.com\\"]}}") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`Cloudflare should generate the correct schema: cloudflare 1`] = `
mutation: Mutation
}

directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT

directive @resolveRoot on FIELD_DEFINITION

Expand Down Expand Up @@ -3699,7 +3699,7 @@ type cloudflare_images_image_details_4xx_response {
union cloudflare_images_base_image_response @statusCodeTypeName(statusCode: 200, typeName: "String_container") @statusCodeTypeName(statusCode: "4xx", typeName: "cloudflare_images_base_image_4xx_response") = String_container | cloudflare_images_base_image_4xx_response

type String_container {
String: String
String: String @resolveRoot
}

type cloudflare_images_base_image_4xx_response {
Expand Down Expand Up @@ -16581,7 +16581,7 @@ type query_filters_get_a_filter_oneOf_0_allOf_1_result_oneOf_0 {
}

type JSON_container {
JSON: JSON
JSON: JSON @resolveRoot
}

type filters_get_a_filter_4xx_response {
Expand Down Expand Up @@ -27371,7 +27371,7 @@ type magic_network_monitoring_rules_update_rule_4xx_response {
union magic_network_monitoring_rules_update_advertisement_for_rule_response @statusCodeTypeName(statusCode: 200, typeName: "Boolean_container") @statusCodeTypeName(statusCode: "4xx", typeName: "magic_network_monitoring_rules_update_advertisement_for_rule_4xx_response") = Boolean_container | magic_network_monitoring_rules_update_advertisement_for_rule_4xx_response

type Boolean_container {
Boolean: Boolean
Boolean: Boolean @resolveRoot
}

type magic_network_monitoring_rules_update_advertisement_for_rule_4xx_response {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`Discriminator Mapping should generate correct schema: discriminator-map
query: Query
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exports[`example_api should generate the schema correctly 1`] = `
directive @enum(value: String) on ENUM_VALUE
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @resolveRootField(field: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`OpenAPI loader: Handle anyOf and oneOf should generate the schema corre
query: Query
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @resolveRoot on FIELD_DEFINITION
Expand Down Expand Up @@ -65,13 +65,13 @@ type differentAttributeObject {
union oneOf2_200_response = commonAttributeObject | Int_container
type Int_container {
Int: Int
Int: Int @resolveRoot
}
union oneOf3_200_response = String_container | Int_container
type String_container {
String: String
String: String @resolveRoot
}
union oneOf5_200_response = commonAttributeObject | differentAttributeObject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`example_api6 should generate the schema correctly 1`] = `
mutation: Mutation
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`openAPI loader: government_social_work should generate the schema corre
mutation: Mutation
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @enum(value: String) on ENUM_VALUE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`OpanAPI: nested objects should generate the schema correctly 1`] = `
query: Query
}
directive @oneOf on OBJECT | INTERFACE
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @statusCodeTypeName(typeName: String, statusCode: ID) repeatable on UNION
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`oneOf without discriminator should generate correct schema: discriminator-mapping 1`] = `
"schema {
query: Query
mutation: Mutation
}
directive @oneOf on OBJECT | INTERFACE | INPUT_OBJECT
directive @enum(value: String) on ENUM_VALUE
directive @typescript(type: String) on SCALAR | ENUM
directive @example(value: ObjMap) repeatable on FIELD_DEFINITION | OBJECT | INPUT_OBJECT | ENUM | SCALAR
directive @resolveRoot on FIELD_DEFINITION
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
directive @httpOperation(path: String, operationSpecificHeaders: ObjMap, httpMethod: HTTPMethod, isBinary: Boolean, requestBaseBody: ObjMap, queryParamArgMap: ObjMap, queryStringOptionsByParam: ObjMap) on FIELD_DEFINITION
type Query @globalOptions(sourceName: "test") {
dummy: String
}
type Mutation {
test_endpoint(input: TestType_Input): TestType @httpOperation(path: "/test", operationSpecificHeaders: "{\\"Content-Type\\":\\"application/json\\",\\"accept\\":\\"application/json\\"}", httpMethod: POST)
}
union TestType = A_const_container | mutation_test_endpoint_oneOf_1
type A_const_container {
A_const: A_const @resolveRoot
}
enum A_const @typescript(type: "\\"A\\"") @example(value: "\\"A\\"") {
A @enum(value: "\\"A\\"")
}
type mutation_test_endpoint_oneOf_1 {
B: String
}
input TestType_Input @oneOf {
A_const: A_const
mutation_test_endpoint_oneOf_1_Input: mutation_test_endpoint_oneOf_1_Input
}
input mutation_test_endpoint_oneOf_1_Input {
B: String
}
scalar ObjMap
enum HTTPMethod {
GET
HEAD
POST
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
}"
`;
Loading

0 comments on commit 0db5f91

Please sign in to comment.