Skip to content

Commit d417143

Browse files
tsirlucasardatan
andauthored
enhance(json-schema): handle discriminator mapping (#5206)
* add DiscriminatorMappingDirective * add test case for discriminator mapping * Go --------- Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
1 parent 63399df commit d417143

File tree

8 files changed

+212
-22
lines changed

8 files changed

+212
-22
lines changed

packages/loaders/json-schema/src/directives.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,20 @@ export const DiscriminatorDirective = new GraphQLDirective({
8787
field: {
8888
type: GraphQLString,
8989
},
90+
mapping: {
91+
type: ObjMapScalar,
92+
},
9093
},
9194
});
9295

93-
export function processDiscriminatorAnnotations(
94-
interfaceType: GraphQLInterfaceType,
95-
fieldName: string,
96-
) {
97-
interfaceType.resolveType = root => root[fieldName];
96+
export function processDiscriminatorAnnotations({
97+
interfaceType,
98+
discriminatorFieldName,
99+
}: {
100+
interfaceType: GraphQLInterfaceType;
101+
discriminatorFieldName: string;
102+
}) {
103+
interfaceType.resolveType = root => root[discriminatorFieldName];
98104
}
99105

100106
export const ResolveRootDirective = new GraphQLDirective({
@@ -557,7 +563,10 @@ export function processDirectives({
557563
for (const directiveAnnotation of directiveAnnotations) {
558564
switch (directiveAnnotation.name) {
559565
case 'discriminator':
560-
processDiscriminatorAnnotations(type, directiveAnnotation.args.field);
566+
processDiscriminatorAnnotations({
567+
interfaceType: type,
568+
discriminatorFieldName: directiveAnnotation.args.field,
569+
});
561570
break;
562571
}
563572
}
@@ -566,6 +575,7 @@ export function processDirectives({
566575
const directiveAnnotations = getDirectives(schema, type);
567576
let statusCodeTypeNameIndexMap: Record<number, string>;
568577
let discriminatorField: string;
578+
let discriminatorMapping: Record<string, string>;
569579
for (const directiveAnnotation of directiveAnnotations) {
570580
switch (directiveAnnotation.name) {
571581
case 'statusCodeTypeName':
@@ -575,14 +585,16 @@ export function processDirectives({
575585
break;
576586
case 'discriminator':
577587
discriminatorField = directiveAnnotation.args.field;
588+
discriminatorMapping = directiveAnnotation.args.mapping;
578589
break;
579590
}
580591
}
581-
type.resolveType = getTypeResolverFromOutputTCs(
582-
type.getTypes(),
592+
type.resolveType = getTypeResolverFromOutputTCs({
593+
possibleTypes: type.getTypes(),
583594
discriminatorField,
584-
statusCodeTypeNameIndexMap,
585-
);
595+
discriminatorMapping,
596+
statusCodeTypeNameMap: statusCodeTypeNameIndexMap,
597+
});
586598
}
587599
if (isEnumType(type)) {
588600
const directiveAnnotations = getDirectives(schema, type);

packages/loaders/json-schema/src/getComposerFromJSONSchema.ts

+7
Original file line numberDiff line numberDiff line change
@@ -737,10 +737,17 @@ export function getComposerFromJSONSchema(
737737
}
738738
if (subSchema.discriminator?.propertyName) {
739739
schemaComposer.addDirective(DiscriminatorDirective);
740+
const mappingByName: Record<string, string> = {};
741+
for (const discriminatorValue in subSchema.discriminator.mapping) {
742+
const ref = subSchema.discriminator.mapping[discriminatorValue];
743+
const typeName = ref.replace('#/components/schemas/', '');
744+
mappingByName[discriminatorValue] = typeName;
745+
}
740746
directives.push({
741747
name: 'discriminator',
742748
args: {
743749
field: subSchema.discriminator.propertyName,
750+
mapping: mappingByName,
744751
},
745752
});
746753
}

packages/loaders/json-schema/src/getTypeResolverFromOutputTCs.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { GraphQLObjectType, GraphQLTypeResolver } from 'graphql';
22
import { createGraphQLError } from '@graphql-tools/utils';
33

4-
export function getTypeResolverFromOutputTCs(
5-
possibleTypes: readonly GraphQLObjectType[],
6-
discriminatorField?: string,
7-
statusCodeTypeNameMap?: Record<string, string>,
8-
): GraphQLTypeResolver<any, any> {
4+
export function getTypeResolverFromOutputTCs({
5+
possibleTypes,
6+
discriminatorField,
7+
discriminatorMapping,
8+
statusCodeTypeNameMap,
9+
}: {
10+
possibleTypes: readonly GraphQLObjectType[];
11+
discriminatorField?: string;
12+
discriminatorMapping?: Record<string, string>;
13+
statusCodeTypeNameMap?: Record<string, string>;
14+
}): GraphQLTypeResolver<any, any> {
915
return function resolveType(data: any) {
1016
if (data.__typename) {
1117
return data.__typename;
1218
} else if (discriminatorField != null && data[discriminatorField]) {
13-
return data[discriminatorField];
19+
const discriminatorValue = data[discriminatorField];
20+
return discriminatorMapping?.[discriminatorValue] || discriminatorValue;
1421
}
1522
if (data.$statusCode && statusCodeTypeNameMap) {
1623
const typeName =
@@ -19,6 +26,7 @@ export function getTypeResolverFromOutputTCs(
1926
return typeName;
2027
}
2128
}
29+
2230
// const validationErrors: Record<string, ErrorObject[]> = {};
2331
const dataKeys =
2432
typeof data === 'object'

packages/loaders/openapi/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
},
4545
"devDependencies": {
4646
"@graphql-tools/utils": "9.2.1",
47+
"@whatwg-node/fetch": "0.8.4",
4748
"@whatwg-node/router": "0.3.0",
4849
"graphql-yoga": "3.8.0",
4950
"json-bigint-patch": "0.0.8"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Discriminator Mapping should generate correct schema: discriminator-mapping 1`] = `
4+
"schema {
5+
query: Query
6+
}
7+
8+
directive @oneOf on OBJECT | INTERFACE
9+
10+
directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION
11+
12+
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
13+
14+
directive @httpOperation(path: String, operationSpecificHeaders: ObjMap, httpMethod: HTTPMethod, isBinary: Boolean, requestBaseBody: ObjMap, queryParamArgMap: ObjMap, queryStringOptionsByParam: ObjMap) on FIELD_DEFINITION
15+
16+
type Query @globalOptions(sourceName: "test") {
17+
pets_by_id(id: String!): Pet @httpOperation(path: "/pets/{args.id}", operationSpecificHeaders: "{\\"accept\\":\\"application/json\\"}", httpMethod: GET)
18+
}
19+
20+
union Pet @discriminator(field: "petType", mapping: "{\\"Dog\\":\\"DogDifferent\\",\\"Cat\\":\\"Cat\\"}") = Cat | DogDifferent
21+
22+
type Cat {
23+
petType: String
24+
cat_exclusive: String
25+
}
26+
27+
type DogDifferent {
28+
petType: String
29+
dog_exclusive: String
30+
}
31+
32+
scalar ObjMap
33+
34+
enum HTTPMethod {
35+
GET
36+
HEAD
37+
POST
38+
PUT
39+
DELETE
40+
CONNECT
41+
OPTIONS
42+
TRACE
43+
PATCH
44+
}"
45+
`;

packages/loaders/openapi/tests/__snapshots__/schemas.test.ts.snap

+6-6
Original file line numberDiff line numberDiff line change
@@ -49649,7 +49649,7 @@ directive @example(value: ObjMap) repeatable on FIELD_DEFINITION | OBJECT | INPU
4964949649

4965049650
directive @oneOf on OBJECT | INTERFACE
4965149651

49652-
directive @discriminator(field: String) on INTERFACE | UNION
49652+
directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION
4965349653

4965449654
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
4965549655

@@ -49692,7 +49692,7 @@ type TicketMessageGet {
4969249692
Author: PersonGet @link(defaultRootType: "Mutation", defaultField: "TicketMessagesUpdateTicketMessage")
4969349693
}
4969449694

49695-
union PersonGet @discriminator(field: "_resolveType") = CompanyGet | UserGet
49695+
union PersonGet @discriminator(field: "_resolveType", mapping: "{\\"user\\":\\"UserGet\\",\\"company\\":\\"CompanyGet\\"}") = CompanyGet | UserGet
4969649696

4969749697
type CompanyGet {
4969849698
_resolveType: company_const!
@@ -50340,7 +50340,7 @@ directive @resolveRoot on FIELD_DEFINITION
5034050340

5034150341
directive @example(value: ObjMap) repeatable on FIELD_DEFINITION | OBJECT | INPUT_OBJECT | ENUM | SCALAR
5034250342

50343-
directive @discriminator(field: String) on INTERFACE | UNION
50343+
directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION
5034450344

5034550345
directive @dictionary on FIELD_DEFINITION
5034650346

@@ -53360,7 +53360,7 @@ type PageBeanCustomFieldContextDefaultValue {
5336053360
values: [CustomFieldContextDefaultValue]
5336153361
}
5336253362

53363-
union CustomFieldContextDefaultValue @discriminator(field: "type") = 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
53363+
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
5336453364

5336553365
"The default value for a cascading select custom field."
5336653366
type CustomFieldContextDefaultValueCascadingOption {
@@ -56817,7 +56817,7 @@ type WorkflowRules {
5681756817
}
5681856818

5681956819
"The workflow transition rule conditions tree."
56820-
union WorkflowCondition @discriminator(field: "nodeType") = WorkflowSimpleCondition | WorkflowCompoundCondition
56820+
union WorkflowCondition @discriminator(field: "nodeType", mapping: "{\\"simple\\":\\"WorkflowSimpleCondition\\",\\"compound\\":\\"WorkflowCompoundCondition\\"}") = WorkflowSimpleCondition | WorkflowCompoundCondition
5682156821

5682256822
"A workflow transition rule condition. This object returns \`nodeType\` as \`simple\`."
5682356823
type WorkflowSimpleCondition {
@@ -90529,7 +90529,7 @@ exports[`Schemas Pet should generate the correct schema: Pet 1`] = `
9052990529
query: Query
9053090530
}
9053190531

90532-
directive @discriminator(field: String) on INTERFACE | UNION
90532+
directive @discriminator(field: String, mapping: ObjMap) on INTERFACE | UNION
9053390533

9053490534
directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT
9053590535

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { execute, GraphQLSchema, parse } from 'graphql';
2+
import { printSchemaWithDirectives } from '@graphql-tools/utils';
3+
import { Response } from '@whatwg-node/fetch';
4+
import { loadGraphQLSchemaFromOpenAPI } from '../src/loadGraphQLSchemaFromOpenAPI.js';
5+
6+
describe('Discriminator Mapping', () => {
7+
let createdSchema: GraphQLSchema;
8+
beforeAll(async () => {
9+
createdSchema = await loadGraphQLSchemaFromOpenAPI('test', {
10+
source: './fixtures/discriminator-mapping.yml',
11+
cwd: __dirname,
12+
ignoreErrorResponses: true,
13+
async fetch(url) {
14+
if (url === 'pets/1') {
15+
return Response.json({
16+
petType: 'Dog',
17+
dog_exclusive: 'DOG_EXCLUSIVE',
18+
});
19+
}
20+
if (url === 'pets/2') {
21+
return Response.json({
22+
petType: 'Cat',
23+
cat_exclusive: 'CAT_EXCLUSIVE',
24+
});
25+
}
26+
return new Response(null, {
27+
status: 404,
28+
});
29+
},
30+
// It is not possible to provide a union type with File scalar
31+
});
32+
});
33+
it('should generate correct schema', () => {
34+
expect(printSchemaWithDirectives(createdSchema)).toMatchSnapshot('discriminator-mapping');
35+
});
36+
it('should handle discriminator mapping', async () => {
37+
const query = /* GraphQL */ `
38+
query {
39+
dog: pets_by_id(id: "1") {
40+
__typename
41+
... on DogDifferent {
42+
petType
43+
}
44+
}
45+
cat: pets_by_id(id: "2") {
46+
__typename
47+
... on Cat {
48+
petType
49+
}
50+
}
51+
}
52+
`;
53+
const result = await execute({
54+
schema: createdSchema,
55+
document: parse(query),
56+
});
57+
expect(result).toEqual({
58+
data: {
59+
dog: {
60+
__typename: 'DogDifferent',
61+
petType: 'Dog',
62+
},
63+
cat: {
64+
__typename: 'Cat',
65+
petType: 'Cat',
66+
},
67+
},
68+
});
69+
});
70+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Swagger Petstore
5+
license:
6+
name: MIT
7+
paths:
8+
/pets/{id}:
9+
get:
10+
parameters:
11+
- name: id
12+
required: true
13+
in: path
14+
schema:
15+
type: string
16+
responses:
17+
200:
18+
content:
19+
application/json:
20+
schema:
21+
$ref: '#/components/schemas/Pet'
22+
23+
components:
24+
schemas:
25+
Pet:
26+
oneOf:
27+
- $ref: '#/components/schemas/Cat'
28+
- $ref: '#/components/schemas/DogDifferent'
29+
discriminator:
30+
propertyName: petType
31+
mapping:
32+
Dog: '#/components/schemas/DogDifferent'
33+
Cat: '#/components/schemas/Cat'
34+
Cat:
35+
type: object
36+
properties:
37+
petType:
38+
type: string
39+
cat_exclusive:
40+
type: string
41+
DogDifferent:
42+
type: object
43+
properties:
44+
petType:
45+
type: string
46+
dog_exclusive:
47+
type: string

0 commit comments

Comments
 (0)