Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Allow deep merging objects in allOf operations #5643

Merged
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
7 changes: 7 additions & 0 deletions .changeset/giant-jars-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@omnigraph/json-schema': patch
'@omnigraph/openapi': patch
---

Fix issue regarding allOf operator that prevents overlapping sub-properties of objects provided to
the operator from being merged
133 changes: 129 additions & 4 deletions packages/loaders/json-schema/src/getComposerFromJSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ import {
EnumTypeComposerValueConfigDefinition,
InputTypeComposer,
InputTypeComposerFieldConfigAsObjectDefinition,
InputTypeComposerFieldConfigMap,
InterfaceTypeComposer,
isSomeInputTypeComposer,
ListComposer,
ObjectTypeComposer,
ObjectTypeComposerFieldConfigMap,
ObjectTypeComposerFieldConfigMapDefinition,
ScalarTypeComposer,
SchemaComposer,
ThunkComposer,
UnionTypeComposer,
} from 'graphql-compose';
import {
Expand Down Expand Up @@ -944,8 +947,37 @@ export function getComposerFromJSONSchema(
} else {
const inputTypeElemFieldMap = inputTypeComposer.getFields();
for (const fieldName in inputTypeElemFieldMap) {
const field = inputTypeElemFieldMap[fieldName];
inputFieldMap[fieldName] = field;
const newInputField = inputTypeElemFieldMap[fieldName] as any;
const existingInputField = inputFieldMap[fieldName] as any;
if (!existingInputField) {
inputFieldMap[fieldName] = newInputField;
} else {
/*
If the new field collides with an existing field:

- If both the existing and the new field have an input type composer, combine their subfields
- Otherwise, replace the existing field with the new one
*/
const existingInputFieldUnwrappedTC =
typeof existingInputField.type?.getUnwrappedTC === 'function'
? existingInputField.type.getUnwrappedTC()
: undefined;
const newInputFieldUnwrappedTC =
typeof newInputField.type?.getUnwrappedTC === 'function'
? newInputField.type.getUnwrappedTC()
: undefined;
if (
existingInputFieldUnwrappedTC instanceof InputTypeComposer &&
newInputFieldUnwrappedTC instanceof InputTypeComposer
) {
deepMergeInputTypeComposerFields(
existingInputFieldUnwrappedTC.getFields(),
newInputFieldUnwrappedTC.getFields(),
);
} else {
inputFieldMap[fieldName] = newInputField;
}
}
}
}

Expand Down Expand Up @@ -980,8 +1012,37 @@ export function getComposerFromJSONSchema(

const typeElemFieldMap = outputTypeComposer.getFields();
for (const fieldName in typeElemFieldMap) {
const field = typeElemFieldMap[fieldName];
fieldMap[fieldName] = field;
const newField = typeElemFieldMap[fieldName] as any;
const existingField = fieldMap[fieldName] as any;
if (!existingField) {
fieldMap[fieldName] = newField;
} else {
/*
If the new field collides with an existing field:

- If both the existing and the new field have an object type composer, combine their subfields
- Otherwise, replace the existing field with the new one
*/
const existingFieldUnwrappedTC =
typeof existingField.type?.getUnwrappedTC === 'function'
? existingField.type.getUnwrappedTC()
: undefined;
const newFieldUnwrappedTC =
typeof newField.type?.getUnwrappedTC === 'function'
? newField.type.getUnwrappedTC()
: undefined;
if (
existingFieldUnwrappedTC instanceof ObjectTypeComposer &&
newFieldUnwrappedTC instanceof ObjectTypeComposer
) {
deepMergeObjectTypeComposerFields(
existingFieldUnwrappedTC.getFields(),
newFieldUnwrappedTC.getFields(),
);
} else {
fieldMap[fieldName] = newField;
}
}
}
}
}
Expand Down Expand Up @@ -1434,4 +1495,68 @@ export function getComposerFromJSONSchema(
}
},
});

function deepMergeInputTypeComposerFields(
existingInputTypeComposerFields: InputTypeComposerFieldConfigMap,
newInputTypeComposerFields: InputTypeComposerFieldConfigMap,
) {
for (const [newFieldKey, newFieldValue] of Object.entries(newInputTypeComposerFields)) {
const existingFieldValue = existingInputTypeComposerFields[newFieldKey];
if (!existingFieldValue) {
existingInputTypeComposerFields[newFieldKey] = newFieldValue;
} else {
const existingFieldUnwrappedTC =
typeof (existingFieldValue.type as ThunkComposer)?.getUnwrappedTC === 'function'
? (existingFieldValue.type as ThunkComposer)?.getUnwrappedTC()
: undefined;
const newFieldUnwrappedTC =
typeof (newFieldValue.type as ThunkComposer).getUnwrappedTC === 'function'
? (newFieldValue.type as ThunkComposer).getUnwrappedTC()
: undefined;
if (
existingFieldUnwrappedTC instanceof InputTypeComposer &&
newFieldUnwrappedTC instanceof InputTypeComposer
) {
deepMergeInputTypeComposerFields(
existingFieldUnwrappedTC.getFields(),
newFieldUnwrappedTC.getFields(),
);
} else {
existingInputTypeComposerFields[newFieldKey] = newFieldValue;
}
}
}
}

function deepMergeObjectTypeComposerFields(
existingObjectTypeComposerFields: ObjectTypeComposerFieldConfigMap<any, any>,
newObjectTypeComposerFields: ObjectTypeComposerFieldConfigMap<any, any>,
) {
for (const [newFieldKey, newFieldValue] of Object.entries(newObjectTypeComposerFields)) {
const existingFieldValue = existingObjectTypeComposerFields[newFieldKey];
if (!existingFieldValue) {
existingObjectTypeComposerFields[newFieldKey] = newFieldValue;
} else {
const existingFieldUnwrappedTC =
typeof (existingFieldValue.type as ThunkComposer)?.getUnwrappedTC === 'function'
? (existingFieldValue.type as ThunkComposer)?.getUnwrappedTC()
: undefined;
const newFieldUnwrappedTC =
typeof (newFieldValue.type as ThunkComposer).getUnwrappedTC === 'function'
? (newFieldValue.type as ThunkComposer).getUnwrappedTC()
: undefined;
if (
existingFieldUnwrappedTC instanceof ObjectTypeComposer &&
newFieldUnwrappedTC instanceof ObjectTypeComposer
) {
deepMergeObjectTypeComposerFields(
existingFieldUnwrappedTC.getFields(),
newFieldUnwrappedTC.getFields(),
);
} else {
existingObjectTypeComposerFields[newFieldKey] = newFieldValue;
}
}
}
}
}
124 changes: 124 additions & 0 deletions packages/loaders/json-schema/test/getComposerFromSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,130 @@ type ExampleAllOf {
`.trim(),
);
});

it('should generate deeply-merged object types from allOf definitions', async () => {
const inputSchema: JSONSchema = {
title: 'ExampleAllOf',
allOf: [
{
type: 'object',
title: 'Foo',
properties: {
attributes: {
type: 'object',
title: 'Attributes',
properties: {
id: {
type: 'string',
},
age: {
type: 'number',
},
address: {
type: 'object',
title: 'Address',
properties: {
street: {
type: 'string',
},
number: {
type: 'number',
},
},
},
},
required: ['id'],
},
},
},
{
type: 'object',
title: 'Bar',
properties: {
attributes: {
type: 'object',
title: 'Attributes',
properties: {
name: {
type: 'string',
},
age: {
type: 'string', // Overriding type
},
address: {
type: 'object',
title: 'Address',
properties: {
number: {
type: 'string', // Overriding type
},
zipCode: {
type: 'string',
},
},
},
},
required: ['name'],
},
},
},
],
};
const result = await getComposerFromJSONSchema(inputSchema, logger);
expect(
(result.input as InputTypeComposer).toSDL({
deep: true,
omitDescriptions: true,
omitScalars: true,
}),
).toContain(
/* GraphQL */ `
input ExampleAllOf_Input {
attributes: Attributes_Input
}

input Attributes_Input {
id: String!
age: String
address: Address_Input
name: String!
}

input Address_Input {
street: String
number: String
zipCode: String
}
`.trim(),
);
expect(
(result.output as InputTypeComposer).toSDL({
deep: true,
omitDescriptions: true,
omitScalars: true,
}),
).toContain(
/* GraphQL */ `
type ExampleAllOf {
attributes: Attributes
}

type Attributes {
id: String!
age: String
address: Address
name: String!
}

type Address {
street: String
number: String
zipCode: String
}
`.trim(),
);
});

it('should generate container types and fields for allOf definitions that contain scalar types', async () => {
const title = 'ExampleAllOf';
const inputSchema: JSONSchema = {
Expand Down
Loading