diff --git a/packages/loaders/json-schema/src/directives.ts b/packages/loaders/json-schema/src/directives.ts index 8fcae4787ee8..1dd1e3d37ca1 100644 --- a/packages/loaders/json-schema/src/directives.ts +++ b/packages/loaders/json-schema/src/directives.ts @@ -87,27 +87,20 @@ export const DiscriminatorDirective = new GraphQLDirective({ field: { type: GraphQLString, }, - }, -}); - -export const DiscriminatorMappingDirective = new GraphQLDirective({ - name: 'discriminatorMapping', - locations: [DirectiveLocation.INTERFACE, DirectiveLocation.UNION], - args: { - value: { - type: GraphQLString, - }, - schema: { - type: GraphQLString, + mapping: { + type: ObjMapScalar, }, }, }); -export function processDiscriminatorAnnotations( - interfaceType: GraphQLInterfaceType, - fieldName: string, -) { - interfaceType.resolveType = root => root[fieldName]; +export function processDiscriminatorAnnotations({ + interfaceType, + discriminatorFieldName, +}: { + interfaceType: GraphQLInterfaceType; + discriminatorFieldName: string; +}) { + interfaceType.resolveType = root => root[discriminatorFieldName]; } export const ResolveRootDirective = new GraphQLDirective({ @@ -570,7 +563,10 @@ export function processDirectives({ for (const directiveAnnotation of directiveAnnotations) { switch (directiveAnnotation.name) { case 'discriminator': - processDiscriminatorAnnotations(type, directiveAnnotation.args.field); + processDiscriminatorAnnotations({ + interfaceType: type, + discriminatorFieldName: directiveAnnotation.args.field, + }); break; } } @@ -579,6 +575,7 @@ export function processDirectives({ const directiveAnnotations = getDirectives(schema, type); let statusCodeTypeNameIndexMap: Record; let discriminatorField: string; + let discriminatorMapping: Record; for (const directiveAnnotation of directiveAnnotations) { switch (directiveAnnotation.name) { case 'statusCodeTypeName': @@ -588,14 +585,16 @@ export function processDirectives({ break; case 'discriminator': discriminatorField = directiveAnnotation.args.field; + discriminatorMapping = directiveAnnotation.args.mapping; break; } } - type.resolveType = getTypeResolverFromOutputTCs( - type.getTypes(), + type.resolveType = getTypeResolverFromOutputTCs({ + possibleTypes: type.getTypes(), discriminatorField, - statusCodeTypeNameIndexMap, - ); + discriminatorMapping, + statusCodeTypeNameMap: statusCodeTypeNameIndexMap, + }); } if (isEnumType(type)) { const directiveAnnotations = getDirectives(schema, type); diff --git a/packages/loaders/json-schema/src/getComposerFromJSONSchema.ts b/packages/loaders/json-schema/src/getComposerFromJSONSchema.ts index 7f431ea42bdb..a270de4a1a3e 100644 --- a/packages/loaders/json-schema/src/getComposerFromJSONSchema.ts +++ b/packages/loaders/json-schema/src/getComposerFromJSONSchema.ts @@ -54,7 +54,6 @@ import { sanitizeNameForGraphQL } from '@graphql-mesh/utils'; import { DictionaryDirective, DiscriminatorDirective, - DiscriminatorMappingDirective, EnumDirective, ExampleDirective, LengthDirective, @@ -738,26 +737,19 @@ export function getComposerFromJSONSchema( } if (subSchema.discriminator?.propertyName) { schemaComposer.addDirective(DiscriminatorDirective); + const mappingByName: Record = {}; + for (const discriminatorValue in subSchema.discriminator.mapping) { + const ref = subSchema.discriminator.mapping[discriminatorValue]; + const typeName = ref.replace('#/components/schemas/', ''); + mappingByName[discriminatorValue] = typeName; + } directives.push({ name: 'discriminator', args: { field: subSchema.discriminator.propertyName, + mapping: mappingByName, }, }); - - if (subSchema.discriminator.mapping) { - schemaComposer.addDirective(DiscriminatorMappingDirective); - Object.keys(subSchema.discriminator.mapping).forEach(value => { - const mappedSchema = subSchema.discriminator.mapping[value].split('schemas/')[1]; - directives.push({ - name: 'discriminatorMapping', - args: { - value, - schema: mappedSchema, - }, - }); - }); - } } const output = schemaComposer.createUnionTC({ name: getValidTypeName({ diff --git a/packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts b/packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts index 454c46b00048..6d304d2b594a 100644 --- a/packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts +++ b/packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts @@ -1,17 +1,23 @@ import { GraphQLObjectType, GraphQLTypeResolver } from 'graphql'; import { createGraphQLError } from '@graphql-tools/utils'; -export function getTypeResolverFromOutputTCs( - possibleTypes: readonly GraphQLObjectType[], - discriminatorField?: string, - discriminatorMapping?: Record, - statusCodeTypeNameMap?: Record, -): GraphQLTypeResolver { +export function getTypeResolverFromOutputTCs({ + possibleTypes, + discriminatorField, + discriminatorMapping, + statusCodeTypeNameMap, +}: { + possibleTypes: readonly GraphQLObjectType[]; + discriminatorField?: string; + discriminatorMapping?: Record; + statusCodeTypeNameMap?: Record; +}): GraphQLTypeResolver { return function resolveType(data: any) { if (data.__typename) { return data.__typename; } else if (discriminatorField != null && data[discriminatorField]) { - return discriminatorMapping[data[discriminatorField]] || data[discriminatorField]; + const discriminatorValue = data[discriminatorField]; + return discriminatorMapping?.[discriminatorValue] || discriminatorValue; } if (data.$statusCode && statusCodeTypeNameMap) { const typeName = diff --git a/packages/loaders/openapi/package.json b/packages/loaders/openapi/package.json index 290b62d536e9..6943fe3c47d4 100644 --- a/packages/loaders/openapi/package.json +++ b/packages/loaders/openapi/package.json @@ -44,6 +44,7 @@ }, "devDependencies": { "@graphql-tools/utils": "9.2.1", + "@whatwg-node/fetch": "0.8.4", "@whatwg-node/router": "0.3.0", "graphql-yoga": "3.8.0", "json-bigint-patch": "0.0.8" diff --git a/packages/loaders/openapi/tests/__snapshots__/discriminator.test.ts.snap b/packages/loaders/openapi/tests/__snapshots__/discriminator.test.ts.snap new file mode 100644 index 000000000000..949626d0c82b --- /dev/null +++ b/packages/loaders/openapi/tests/__snapshots__/discriminator.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Discriminator Mapping should generate correct schema: discriminator-mapping 1`] = ` +"schema { + query: Query +} + +directive @oneOf on OBJECT | INTERFACE + +directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION + +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") { + pets_by_id(id: String!): Pet @httpOperation(path: "/pets/{args.id}", operationSpecificHeaders: "{\\"accept\\":\\"application/json\\"}", httpMethod: GET) +} + +union Pet @discriminator(field: "petType", mapping: "{\\"Dog\\":\\"DogDifferent\\",\\"Cat\\":\\"Cat\\"}") = Cat | DogDifferent + +type Cat { + petType: String + cat_exclusive: String +} + +type DogDifferent { + petType: String + dog_exclusive: String +} + +scalar ObjMap + +enum HTTPMethod { + GET + HEAD + POST + PUT + DELETE + CONNECT + OPTIONS + TRACE + PATCH +}" +`; diff --git a/packages/loaders/openapi/tests/__snapshots__/schemas.test.ts.snap b/packages/loaders/openapi/tests/__snapshots__/schemas.test.ts.snap index 6cbddc47d716..28a40a85b14c 100644 --- a/packages/loaders/openapi/tests/__snapshots__/schemas.test.ts.snap +++ b/packages/loaders/openapi/tests/__snapshots__/schemas.test.ts.snap @@ -41929,52 +41929,6 @@ enum HTTPMethod { }" `; -exports[`Schemas DiscriminatorMapping should generate the correct schema: DiscriminatorMapping 1`] = ` -"schema { - query: Query -} - -directive @oneOf on OBJECT | INTERFACE - -directive @discriminator(field: String) on INTERFACE | UNION - -directive @discriminatorMapping(value: String, schema: String) on INTERFACE | UNION - -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: "DiscriminatorMapping") { - pets_by_id(id: String!): Pet @httpOperation(path: "/pets/{args.id}", operationSpecificHeaders: "{\\"accept\\":\\"application/json\\"}", httpMethod: GET) -} - -union Pet @discriminator(field: "petType") @discriminatorMapping(value: "Dog", schema: "DogDifferent") @discriminatorMapping(value: "Cat", schema: "Cat") = Cat | DogDifferent - -type Cat { - petType: String - cat_exclusive: String -} - -type DogDifferent { - petType: String - dog_exclusive: String -} - -scalar ObjMap - -enum HTTPMethod { - GET - HEAD - POST - PUT - DELETE - CONNECT - OPTIONS - TRACE - PATCH -}" -`; - exports[`Schemas GitHub should generate the correct schema: GitHub 1`] = ` "schema { query: Query @@ -49695,9 +49649,7 @@ directive @example(value: ObjMap) repeatable on FIELD_DEFINITION | OBJECT | INPU directive @oneOf on OBJECT | INTERFACE -directive @discriminator(field: String) on INTERFACE | UNION - -directive @discriminatorMapping(value: String, schema: String) on INTERFACE | UNION +directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT @@ -49740,7 +49692,7 @@ type TicketMessageGet { Author: PersonGet @link(defaultRootType: "Mutation", defaultField: "TicketMessagesUpdateTicketMessage") } -union PersonGet @discriminator(field: "_resolveType") @discriminatorMapping(value: "user", schema: "UserGet") @discriminatorMapping(value: "company", schema: "CompanyGet") = CompanyGet | UserGet +union PersonGet @discriminator(field: "_resolveType", mapping: "{\\"user\\":\\"UserGet\\",\\"company\\":\\"CompanyGet\\"}") = CompanyGet | UserGet type CompanyGet { _resolveType: company_const! @@ -50388,9 +50340,7 @@ directive @resolveRoot on FIELD_DEFINITION directive @example(value: ObjMap) repeatable on FIELD_DEFINITION | OBJECT | INPUT_OBJECT | ENUM | SCALAR -directive @discriminator(field: String) on INTERFACE | UNION - -directive @discriminatorMapping(value: String, schema: String) on INTERFACE | UNION +directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION directive @dictionary on FIELD_DEFINITION @@ -53410,7 +53360,7 @@ type PageBeanCustomFieldContextDefaultValue { values: [CustomFieldContextDefaultValue] } -union CustomFieldContextDefaultValue @discriminator(field: "type") @discriminatorMapping(value: "option.cascading", schema: "CustomFieldContextDefaultValueCascadingOption") @discriminatorMapping(value: "option.multiple", schema: "CustomFieldContextDefaultValueMultipleOption") @discriminatorMapping(value: "option.single", schema: "CustomFieldContextDefaultValueSingleOption") @discriminatorMapping(value: "single.user.select", schema: "CustomFieldContextSingleUserPickerDefaults") @discriminatorMapping(value: "multi.user.select", schema: "CustomFieldContextDefaultValueMultiUserPicker") @discriminatorMapping(value: "grouppicker.single", schema: "CustomFieldContextDefaultValueSingleGroupPicker") @discriminatorMapping(value: "grouppicker.multiple", schema: "CustomFieldContextDefaultValueMultipleGroupPicker") @discriminatorMapping(value: "datepicker", schema: "CustomFieldContextDefaultValueDate") @discriminatorMapping(value: "datetimepicker", schema: "CustomFieldContextDefaultValueDateTime") @discriminatorMapping(value: "url", schema: "CustomFieldContextDefaultValueURL") @discriminatorMapping(value: "project", schema: "CustomFieldContextDefaultValueProject") @discriminatorMapping(value: "float", schema: "CustomFieldContextDefaultValueFloat") @discriminatorMapping(value: "labels", schema: "CustomFieldContextDefaultValueLabels") @discriminatorMapping(value: "textfield", schema: "CustomFieldContextDefaultValueTextField") @discriminatorMapping(value: "textarea", schema: "CustomFieldContextDefaultValueTextArea") @discriminatorMapping(value: "readonly", schema: "CustomFieldContextDefaultValueReadOnly") @discriminatorMapping(value: "version.single", schema: "CustomFieldContextDefaultValueSingleVersionPicker") @discriminatorMapping(value: "version.multiple", schema: "CustomFieldContextDefaultValueMultipleVersionPicker") @discriminatorMapping(value: "forge.string", schema: "CustomFieldContextDefaultValueForgeStringField") @discriminatorMapping(value: "forge.string.list", schema: "CustomFieldContextDefaultValueForgeMultiStringField") @discriminatorMapping(value: "forge.object", schema: "CustomFieldContextDefaultValueForgeObjectField") @discriminatorMapping(value: "forge.datetime", schema: "CustomFieldContextDefaultValueForgeDateTimeField") @discriminatorMapping(value: "forge.group", schema: "CustomFieldContextDefaultValueForgeGroupField") @discriminatorMapping(value: "forge.group.list", schema: "CustomFieldContextDefaultValueForgeMultiGroupField") @discriminatorMapping(value: "forge.number", schema: "CustomFieldContextDefaultValueForgeNumberField") @discriminatorMapping(value: "forge.user", schema: "CustomFieldContextDefaultValueForgeUserField") @discriminatorMapping(value: "forge.user.list", schema: "CustomFieldContextDefaultValueForgeMultiUserField") = CustomFieldContextDefaultValueCascadingOption | CustomFieldContextDefaultValueMultipleOption | CustomFieldContextDefaultValueSingleOption | CustomFieldContextSingleUserPickerDefaults | CustomFieldContextDefaultValueMultiUserPicker | CustomFieldContextDefaultValueSingleGroupPicker | CustomFieldContextDefaultValueMultipleGroupPicker | CustomFieldContextDefaultValueDate | CustomFieldContextDefaultValueDateTime | CustomFieldContextDefaultValueURL | CustomFieldContextDefaultValueProject | CustomFieldContextDefaultValueFloat | CustomFieldContextDefaultValueLabels | CustomFieldContextDefaultValueTextField | CustomFieldContextDefaultValueTextArea | CustomFieldContextDefaultValueReadOnly | CustomFieldContextDefaultValueSingleVersionPicker | CustomFieldContextDefaultValueMultipleVersionPicker | CustomFieldContextDefaultValueForgeStringField | CustomFieldContextDefaultValueForgeMultiStringField | CustomFieldContextDefaultValueForgeObjectField | CustomFieldContextDefaultValueForgeDateTimeField | CustomFieldContextDefaultValueForgeGroupField | CustomFieldContextDefaultValueForgeMultiGroupField | CustomFieldContextDefaultValueForgeNumberField | CustomFieldContextDefaultValueForgeUserField | CustomFieldContextDefaultValueForgeMultiUserField +union CustomFieldContextDefaultValue @discriminator(field: "type", mapping: "{\\"option.cascading\\":\\"CustomFieldContextDefaultValueCascadingOption\\",\\"option.multiple\\":\\"CustomFieldContextDefaultValueMultipleOption\\",\\"option.single\\":\\"CustomFieldContextDefaultValueSingleOption\\",\\"single.user.select\\":\\"CustomFieldContextSingleUserPickerDefaults\\",\\"multi.user.select\\":\\"CustomFieldContextDefaultValueMultiUserPicker\\",\\"grouppicker.single\\":\\"CustomFieldContextDefaultValueSingleGroupPicker\\",\\"grouppicker.multiple\\":\\"CustomFieldContextDefaultValueMultipleGroupPicker\\",\\"datepicker\\":\\"CustomFieldContextDefaultValueDate\\",\\"datetimepicker\\":\\"CustomFieldContextDefaultValueDateTime\\",\\"url\\":\\"CustomFieldContextDefaultValueURL\\",\\"project\\":\\"CustomFieldContextDefaultValueProject\\",\\"float\\":\\"CustomFieldContextDefaultValueFloat\\",\\"labels\\":\\"CustomFieldContextDefaultValueLabels\\",\\"textfield\\":\\"CustomFieldContextDefaultValueTextField\\",\\"textarea\\":\\"CustomFieldContextDefaultValueTextArea\\",\\"readonly\\":\\"CustomFieldContextDefaultValueReadOnly\\",\\"version.single\\":\\"CustomFieldContextDefaultValueSingleVersionPicker\\",\\"version.multiple\\":\\"CustomFieldContextDefaultValueMultipleVersionPicker\\",\\"forge.string\\":\\"CustomFieldContextDefaultValueForgeStringField\\",\\"forge.string.list\\":\\"CustomFieldContextDefaultValueForgeMultiStringField\\",\\"forge.object\\":\\"CustomFieldContextDefaultValueForgeObjectField\\",\\"forge.datetime\\":\\"CustomFieldContextDefaultValueForgeDateTimeField\\",\\"forge.group\\":\\"CustomFieldContextDefaultValueForgeGroupField\\",\\"forge.group.list\\":\\"CustomFieldContextDefaultValueForgeMultiGroupField\\",\\"forge.number\\":\\"CustomFieldContextDefaultValueForgeNumberField\\",\\"forge.user\\":\\"CustomFieldContextDefaultValueForgeUserField\\",\\"forge.user.list\\":\\"CustomFieldContextDefaultValueForgeMultiUserField\\"}") = CustomFieldContextDefaultValueCascadingOption | CustomFieldContextDefaultValueMultipleOption | CustomFieldContextDefaultValueSingleOption | CustomFieldContextSingleUserPickerDefaults | CustomFieldContextDefaultValueMultiUserPicker | CustomFieldContextDefaultValueSingleGroupPicker | CustomFieldContextDefaultValueMultipleGroupPicker | CustomFieldContextDefaultValueDate | CustomFieldContextDefaultValueDateTime | CustomFieldContextDefaultValueURL | CustomFieldContextDefaultValueProject | CustomFieldContextDefaultValueFloat | CustomFieldContextDefaultValueLabels | CustomFieldContextDefaultValueTextField | CustomFieldContextDefaultValueTextArea | CustomFieldContextDefaultValueReadOnly | CustomFieldContextDefaultValueSingleVersionPicker | CustomFieldContextDefaultValueMultipleVersionPicker | CustomFieldContextDefaultValueForgeStringField | CustomFieldContextDefaultValueForgeMultiStringField | CustomFieldContextDefaultValueForgeObjectField | CustomFieldContextDefaultValueForgeDateTimeField | CustomFieldContextDefaultValueForgeGroupField | CustomFieldContextDefaultValueForgeMultiGroupField | CustomFieldContextDefaultValueForgeNumberField | CustomFieldContextDefaultValueForgeUserField | CustomFieldContextDefaultValueForgeMultiUserField "The default value for a cascading select custom field." type CustomFieldContextDefaultValueCascadingOption { @@ -56867,7 +56817,7 @@ type WorkflowRules { } "The workflow transition rule conditions tree." -union WorkflowCondition @discriminator(field: "nodeType") @discriminatorMapping(value: "simple", schema: "WorkflowSimpleCondition") @discriminatorMapping(value: "compound", schema: "WorkflowCompoundCondition") = WorkflowSimpleCondition | WorkflowCompoundCondition +union WorkflowCondition @discriminator(field: "nodeType", mapping: "{\\"simple\\":\\"WorkflowSimpleCondition\\",\\"compound\\":\\"WorkflowCompoundCondition\\"}") = WorkflowSimpleCondition | WorkflowCompoundCondition "A workflow transition rule condition. This object returns \`nodeType\` as \`simple\`." type WorkflowSimpleCondition { @@ -90579,7 +90529,7 @@ exports[`Schemas Pet should generate the correct schema: Pet 1`] = ` query: Query } -directive @discriminator(field: String) on INTERFACE | UNION +directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT diff --git a/packages/loaders/openapi/tests/discriminator.test.ts b/packages/loaders/openapi/tests/discriminator.test.ts new file mode 100644 index 000000000000..2f6ca4962c70 --- /dev/null +++ b/packages/loaders/openapi/tests/discriminator.test.ts @@ -0,0 +1,70 @@ +import { execute, GraphQLSchema, parse } from 'graphql'; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { Response } from '@whatwg-node/fetch'; +import { loadGraphQLSchemaFromOpenAPI } from '../src/loadGraphQLSchemaFromOpenAPI.js'; + +describe('Discriminator Mapping', () => { + let createdSchema: GraphQLSchema; + beforeAll(async () => { + createdSchema = await loadGraphQLSchemaFromOpenAPI('test', { + source: './fixtures/discriminator-mapping.yml', + cwd: __dirname, + ignoreErrorResponses: true, + async fetch(url) { + if (url === 'pets/1') { + return Response.json({ + petType: 'Dog', + dog_exclusive: 'DOG_EXCLUSIVE', + }); + } + if (url === 'pets/2') { + return Response.json({ + petType: 'Cat', + cat_exclusive: 'CAT_EXCLUSIVE', + }); + } + return new Response(null, { + status: 404, + }); + }, + // It is not possible to provide a union type with File scalar + }); + }); + it('should generate correct schema', () => { + expect(printSchemaWithDirectives(createdSchema)).toMatchSnapshot('discriminator-mapping'); + }); + it('should handle discriminator mapping', async () => { + const query = /* GraphQL */ ` + query { + dog: pets_by_id(id: "1") { + __typename + ... on DogDifferent { + petType + } + } + cat: pets_by_id(id: "2") { + __typename + ... on Cat { + petType + } + } + } + `; + const result = await execute({ + schema: createdSchema, + document: parse(query), + }); + expect(result).toEqual({ + data: { + dog: { + __typename: 'DogDifferent', + petType: 'Dog', + }, + cat: { + __typename: 'Cat', + petType: 'Cat', + }, + }, + }); + }); +}); diff --git a/packages/loaders/openapi/tests/schemas.test.ts b/packages/loaders/openapi/tests/schemas.test.ts index 4a4d7e59c5e7..f08320270dab 100644 --- a/packages/loaders/openapi/tests/schemas.test.ts +++ b/packages/loaders/openapi/tests/schemas.test.ts @@ -22,7 +22,6 @@ const schemas: Record = { BlockFrost: 'blockfrost.json', 'Int64 with Defaults': 'int64-with-defaults.yml', 'Different fields with the same type': 'different-prop-same-type.yaml', - DiscriminatorMapping: 'discriminator-mapping.yml', }; describe('Schemas', () => {