Skip to content

Commit abfea1e

Browse files
mladjanmimiljkovic
andcommitted
Add support for processing discriminator property used with allOf property
Co-authored-by: Ilija Miljkovic <[email protected]> Signed-off-by: Mlađan Mihajlović <[email protected]>
1 parent 784b89a commit abfea1e

File tree

7 files changed

+1004
-35
lines changed

7 files changed

+1004
-35
lines changed

packages/openapi-to-graphql/src/preprocessor.ts

+295-10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import debug from 'debug'
2727
import { handleWarning, getCommonPropertyNames, MitigationTypes } from './utils'
2828
import { GraphQLOperationType } from './types/graphql'
2929
import { methodToHttpMethod } from './oas_3_tools'
30+
import { GraphQLObjectType } from 'graphql'
31+
import { getGraphQLType } from './schema_builder'
3032

3133
const preprocessingLog = debug('preprocessing')
3234

@@ -651,7 +653,8 @@ export function createDataDef<TSource, TContext, TArgs>(
651653
isInputObjectType: boolean,
652654
data: PreprocessingData<TSource, TContext, TArgs>,
653655
oas: Oas3,
654-
links?: { [key: string]: LinkObject }
656+
links?: { [key: string]: LinkObject },
657+
resolveDiscriminator: boolean = true
655658
): DataDefinition {
656659
const preferredName = getPreferredName(names)
657660

@@ -866,6 +869,26 @@ export function createDataDef<TSource, TContext, TArgs>(
866869
}
867870
}
868871

872+
/**
873+
* Union types will be extracted either from the discriminator mapping
874+
* or from the enum list defined for discriminator property
875+
*/
876+
if (hasDiscriminator(schema) && resolveDiscriminator) {
877+
const unionDef = createDataDefFromDiscriminator(
878+
saneName,
879+
schema,
880+
isInputObjectType,
881+
def,
882+
data,
883+
oas
884+
)
885+
886+
if (unionDef && typeof unionDef === 'object') {
887+
def.targetGraphQLType = 'json'
888+
return def
889+
}
890+
}
891+
869892
if (targetGraphQLType) {
870893
switch (targetGraphQLType) {
871894
case 'list':
@@ -944,6 +967,12 @@ export function createDataDef<TSource, TContext, TArgs>(
944967
}
945968
}
946969

970+
// Checks if schema object has discriminator field
971+
function hasDiscriminator(schema: SchemaObject): boolean {
972+
const collapsedSchema: SchemaObject = JSON.parse(JSON.stringify(schema))
973+
return collapsedSchema.discriminator?.propertyName ? true : false
974+
}
975+
947976
/**
948977
* Returns the index of the data definition object in the given list that
949978
* contains the same schema and preferred name as the given one. Returns -1 if
@@ -1138,6 +1167,248 @@ function addObjectPropertiesToDataDef<TSource, TContext, TArgs>(
11381167
}
11391168
}
11401169

1170+
/**
1171+
* Iterate through discriminator object mapping or through discriminator
1172+
* enum values and resolve derived schemas
1173+
*/
1174+
function createDataDefFromDiscriminator<TSource, TContext, TArgs>(
1175+
saneName: string,
1176+
schema: SchemaObject,
1177+
isInputObjectType = false,
1178+
def: DataDefinition,
1179+
data: PreprocessingData<TSource, TContext, TArgs>,
1180+
oas: Oas3
1181+
): DataDefinition {
1182+
/**
1183+
* Check if discriminator exists and if it has
1184+
* defined property name
1185+
*/
1186+
if (!schema.discriminator?.propertyName) {
1187+
return null
1188+
}
1189+
1190+
const unionTypes: DataDefinition[] = []
1191+
const schemaToTypeMap: Map<string, string> = new Map()
1192+
1193+
// Get the discriminator property name
1194+
const discriminator = schema.discriminator.propertyName
1195+
1196+
/**
1197+
* Check if there is defined property pointed by discriminator
1198+
* and if that property is in the required properties list
1199+
*/
1200+
if (
1201+
schema.properties &&
1202+
schema.properties[discriminator] &&
1203+
schema.required &&
1204+
schema.required.indexOf(discriminator) > -1
1205+
) {
1206+
let discriminatorProperty = schema.properties[discriminator]
1207+
1208+
// Dereference discriminator property
1209+
if ('$ref' in discriminatorProperty) {
1210+
discriminatorProperty = Oas3Tools.resolveRef(
1211+
discriminatorProperty['$ref'],
1212+
oas
1213+
) as SchemaObject
1214+
}
1215+
1216+
/**
1217+
* Check if there is mapping defined for discriminator property
1218+
* and iterate through the map in order to generate derived types
1219+
*/
1220+
if (schema.discriminator.mapping) {
1221+
for (const key in schema.discriminator.mapping) {
1222+
const unionTypeDef = createUnionSubDefinitionFromDiscriminator(
1223+
schema,
1224+
saneName,
1225+
schema.discriminator.mapping[key],
1226+
isInputObjectType,
1227+
data,
1228+
oas
1229+
)
1230+
1231+
if (unionTypeDef) {
1232+
unionTypes.push(unionTypeDef)
1233+
schemaToTypeMap.set(key, unionTypeDef.preferredName)
1234+
}
1235+
}
1236+
} else if (
1237+
/**
1238+
* If there is no defined mapping, check if discriminator property
1239+
* schema has defined enum, and if enum exists iterate through
1240+
* the enum values and generate derived types
1241+
*/
1242+
discriminatorProperty.enum &&
1243+
discriminatorProperty.enum.length > 0
1244+
) {
1245+
const discriminatorAllowedValues = discriminatorProperty.enum
1246+
discriminatorAllowedValues.forEach((enumValue) => {
1247+
const unionTypeDef = createUnionSubDefinitionFromDiscriminator(
1248+
schema,
1249+
saneName,
1250+
enumValue,
1251+
isInputObjectType,
1252+
data,
1253+
oas
1254+
)
1255+
1256+
if (unionTypeDef) {
1257+
unionTypes.push(unionTypeDef)
1258+
schemaToTypeMap.set(enumValue, unionTypeDef.preferredName)
1259+
}
1260+
})
1261+
}
1262+
}
1263+
1264+
// Union type will be created if unionTypes list is not empty
1265+
if (unionTypes.length > 0) {
1266+
const iteration = 0
1267+
1268+
/**
1269+
* Get GraphQL types for union type members so that
1270+
* these types can be used in resolveType method for
1271+
* this union
1272+
*/
1273+
const types = Object.values(unionTypes).map((memberTypeDefinition) => {
1274+
return getGraphQLType({
1275+
def: memberTypeDefinition,
1276+
data,
1277+
iteration: iteration + 1,
1278+
isInputObjectType
1279+
}) as GraphQLObjectType
1280+
})
1281+
1282+
/**
1283+
* TODO: Refactor this when GraphQL receives a support for input unions.
1284+
* Create DataDefinition object for union with custom resolveType function
1285+
* which resolves union types based on discriminator provided in the Open API
1286+
* schema. The union data definition should be used for generating response
1287+
* type and for inputs parent data definition should be used
1288+
*/
1289+
def.unionDefinition = {
1290+
...def,
1291+
targetGraphQLType: 'union',
1292+
subDefinitions: unionTypes,
1293+
resolveType: (source, context, info) => {
1294+
// Find the appropriate union member type
1295+
return types.find((type) => {
1296+
// Check if source contains not empty discriminator field
1297+
if (source[discriminator]) {
1298+
const typeName = schemaToTypeMap.get(source[discriminator])
1299+
return typeName === type.name
1300+
}
1301+
1302+
return false
1303+
})
1304+
}
1305+
}
1306+
1307+
return def
1308+
}
1309+
1310+
return null
1311+
}
1312+
1313+
function createUnionSubDefinitionFromDiscriminator<TSource, TContext, TArgs>(
1314+
unionSchema: SchemaObject,
1315+
unionSaneName: string,
1316+
subSchemaName: string,
1317+
isInputObjectType: boolean,
1318+
data: PreprocessingData<TSource, TContext, TArgs>,
1319+
oas: Oas3
1320+
): DataDefinition {
1321+
// Find schema for derived type using schemaName
1322+
let schema = oas.components.schemas[subSchemaName]
1323+
1324+
// Resolve reference
1325+
if (schema && '$ref' in schema) {
1326+
schema = Oas3Tools.resolveRef(schema['$ref'], oas) as SchemaObject
1327+
}
1328+
1329+
if (!schema) {
1330+
handleWarning({
1331+
mitigationType: MitigationTypes.MISSING_SCHEMA,
1332+
message: `Resolving schema from discriminator with name ${subSchemaName} in schema '${JSON.stringify(
1333+
unionSchema
1334+
)} failed because such schema was not found.`,
1335+
data,
1336+
log: preprocessingLog
1337+
})
1338+
return null
1339+
}
1340+
1341+
if (!isSchemaDerivedFrom(schema, unionSchema, oas)) {
1342+
return null
1343+
}
1344+
1345+
const collapsedSchema = resolveAllOf(schema, {}, data, oas)
1346+
1347+
if (
1348+
collapsedSchema &&
1349+
Oas3Tools.getSchemaTargetGraphQLType(collapsedSchema, data) === 'object'
1350+
) {
1351+
let subNames = {}
1352+
if (deepEqual(unionSchema, schema)) {
1353+
subNames = {
1354+
fromRef: `${unionSaneName}Member`,
1355+
fromSchema: collapsedSchema.title
1356+
}
1357+
} else {
1358+
subNames = {
1359+
fromRef: subSchemaName,
1360+
fromSchema: collapsedSchema.title
1361+
}
1362+
}
1363+
1364+
return createDataDef(
1365+
subNames,
1366+
schema,
1367+
isInputObjectType,
1368+
data,
1369+
oas,
1370+
{},
1371+
false
1372+
)
1373+
}
1374+
1375+
return null
1376+
}
1377+
1378+
/**
1379+
* Check if child schema is derived from parent schema by recursively
1380+
* looking into schemas references in child's allOf property
1381+
*/
1382+
function isSchemaDerivedFrom(
1383+
childSchema: SchemaObject,
1384+
parentSchema: SchemaObject,
1385+
oas: Oas3
1386+
) {
1387+
if (!childSchema.allOf) {
1388+
return false
1389+
}
1390+
1391+
for (const allOfSchema of childSchema.allOf) {
1392+
let resolvedSchema: SchemaObject = null
1393+
if (allOfSchema && '$ref' in allOfSchema) {
1394+
resolvedSchema = Oas3Tools.resolveRef(
1395+
allOfSchema['$ref'],
1396+
oas
1397+
) as SchemaObject
1398+
} else {
1399+
resolvedSchema = allOfSchema
1400+
}
1401+
1402+
if (deepEqual(resolvedSchema, parentSchema)) {
1403+
return true
1404+
} else if (isSchemaDerivedFrom(resolvedSchema, parentSchema, oas)) {
1405+
return true
1406+
}
1407+
}
1408+
1409+
return false
1410+
}
1411+
11411412
/**
11421413
* Recursively traverse a schema and resolve allOf by appending the data to the
11431414
* parent schema
@@ -1284,23 +1555,26 @@ function getMemberSchemaData<TSource, TContext, TArgs>(
12841555
schema = Oas3Tools.resolveRef(schema['$ref'], oas) as SchemaObject
12851556
}
12861557

1558+
const collapsedSchema = resolveAllOf(schema, {}, data, oas)
1559+
12871560
// Consolidate target GraphQL type
12881561
const memberTargetGraphQLType = Oas3Tools.getSchemaTargetGraphQLType(
1289-
schema,
1562+
collapsedSchema,
12901563
data
12911564
)
1565+
12921566
if (memberTargetGraphQLType) {
12931567
result.allTargetGraphQLTypes.push(memberTargetGraphQLType)
12941568
}
12951569

12961570
// Consolidate properties
1297-
if (schema.properties) {
1298-
result.allProperties.push(schema.properties)
1571+
if (collapsedSchema.properties) {
1572+
result.allProperties.push(collapsedSchema.properties)
12991573
}
13001574

13011575
// Consolidate required
1302-
if (schema.required) {
1303-
result.allRequired = result.allRequired.concat(schema.required)
1576+
if (collapsedSchema.required) {
1577+
result.allRequired = result.allRequired.concat(collapsedSchema.required)
13041578
}
13051579
})
13061580

@@ -1587,21 +1861,32 @@ function createDataDefFromOneOf<TSource, TContext, TArgs>(
15871861
) as SchemaObject
15881862
}
15891863

1864+
const collapsedMemberSchema = resolveAllOf(
1865+
memberSchema,
1866+
{},
1867+
data,
1868+
oas
1869+
)
1870+
15901871
// Member types of GraphQL unions must be object types
15911872
if (
1592-
Oas3Tools.getSchemaTargetGraphQLType(memberSchema, data) ===
1593-
'object'
1873+
Oas3Tools.getSchemaTargetGraphQLType(
1874+
collapsedMemberSchema,
1875+
data
1876+
) === 'object'
15941877
) {
15951878
const subDefinition = createDataDef(
15961879
{
15971880
fromRef,
1598-
fromSchema: memberSchema.title,
1881+
fromSchema: collapsedMemberSchema.title,
15991882
fromPath: `${saneName}Member`
16001883
},
16011884
memberSchema,
16021885
isInputObjectType,
16031886
data,
1604-
oas
1887+
oas,
1888+
{},
1889+
false
16051890
)
16061891
;(def.subDefinitions as DataDefinition[]).push(subDefinition)
16071892
} else {

0 commit comments

Comments
 (0)