Skip to content

Allow to resolve automatically Parse Type fields from Custom Schema #6562

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

Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"url": "https://opencollective.com/parse-server",
"logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary"
},
"publishConfig": { "registry": "https://npm.pkg.github.com/" },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parse-server"
Expand Down
126 changes: 83 additions & 43 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10769,57 +10769,70 @@ describe('ParseGraphQLServer', () => {
robot: { value: 'robot' },
},
});
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
customQuery: {
type: new GraphQLNonNull(GraphQLString),
args: {
message: { type: new GraphQLNonNull(GraphQLString) },
const SomeClassType = new GraphQLObjectType({
name: 'SomeClass',
fields: {
nameUpperCase: {
type: new GraphQLNonNull(GraphQLString),
resolve: (p) => p.name.toUpperCase(),
},
type: { type: TypeEnum },
language: {
type: new GraphQLEnumType({
name: 'LanguageEnum',
values: {
fr: { value: 'fr' },
en: { value: 'en' },
},
resolve: (p, { message }) => message,
},
}),
resolve: () => 'fr',
},
}),
types: [
new GraphQLInputObjectType({
name: 'CreateSomeClassFieldsInput',
fields: {
type: { type: TypeEnum },
},
}),
new GraphQLInputObjectType({
name: 'UpdateSomeClassFieldsInput',
fields: {
type: { type: TypeEnum },
},
}),
new GraphQLObjectType({
name: 'SomeClass',
},
}),
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
nameUpperCase: {
customQuery: {
type: new GraphQLNonNull(GraphQLString),
resolve: (p) => p.name.toUpperCase(),
args: {
message: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: (p, { message }) => message,
},
type: { type: TypeEnum },
language: {
type: new GraphQLEnumType({
name: 'LanguageEnum',
values: {
fr: { value: 'fr' },
en: { value: 'en' },
},
}),
resolve: () => 'fr',
customQueryWithAutoTypeReturn: {
type: SomeClassType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (p, { id }) => {
const obj = new Parse.Object('SomeClass');
obj.id = id;
await obj.fetch();
return obj.toJSON();
},
},
},
}),
],
}),
});
types: [
new GraphQLInputObjectType({
name: 'CreateSomeClassFieldsInput',
fields: {
type: { type: TypeEnum },
},
}),
new GraphQLInputObjectType({
name: 'UpdateSomeClassFieldsInput',
fields: {
type: { type: TypeEnum },
},
}),
SomeClassType,
],
}),
});

parseGraphQLServer.applyGraphQL(expressApp);
await new Promise((resolve) =>
Expand Down Expand Up @@ -10857,6 +10870,33 @@ describe('ParseGraphQLServer', () => {
expect(result.data.customQuery).toEqual('hello');
});

it('can resolve a custom query with auto type return', async () => {
const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const result = await apolloClient.query({
variables: { id: obj.id },
query: gql`
query CustomQuery($id: String!) {
customQueryWithAutoTypeReturn(id: $id) {
objectId
nameUpperCase
name
type
}
}
`,
});
expect(result.data.customQueryWithAutoTypeReturn.objectId).toEqual(
obj.id
);
expect(result.data.customQueryWithAutoTypeReturn.name).toEqual('aname');
expect(result.data.customQueryWithAutoTypeReturn.nameUpperCase).toEqual(
'ANAME'
);
expect(result.data.customQueryWithAutoTypeReturn.type).toEqual('robot');
});

it('can resolve a custom extend type', async () => {
const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' });
Expand Down
67 changes: 37 additions & 30 deletions src/GraphQL/ParseGraphQLSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class ParseGraphQLSchema {
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap();
Object.values(customGraphQLSchemaTypeMap).forEach(
customGraphQLSchemaType => {
(customGraphQLSchemaType) => {
if (
!customGraphQLSchemaType ||
!customGraphQLSchemaType.name ||
Expand All @@ -215,40 +215,45 @@ class ParseGraphQLSchema {
autoGraphQLSchemaType &&
typeof customGraphQLSchemaType.getFields === 'function'
) {
const findAndAddLastType = type => {
if (type.name) {
if (!this.graphQLAutoSchema.getType(type)) {
// To avoid schema stitching (Unknow type) bug on variables
// transfer the final type to the Auto Schema
this.graphQLAutoSchema._typeMap[type.name] = type;
const findAndReplaceLastType = (parent, key) => {
if (parent[key].name) {
if (
this.graphQLAutoSchema.getType(parent[key].name) &&
this.graphQLAutoSchema.getType(parent[key].name) !==
parent[key]
) {
// To avoid unresolved field on overloaded schema
// replace the final type with the auto schema one
parent[key] = this.graphQLAutoSchema.getType(
parent[key].name
);
}
} else {
if (type.ofType) {
findAndAddLastType(type.ofType);
if (parent[key].ofType) {
findAndReplaceLastType(parent[key], 'ofType');
}
}
};

Object.values(customGraphQLSchemaType.getFields()).forEach(
field => {
findAndAddLastType(field.type);
if (field.args) {
field.args.forEach(arg => {
findAndAddLastType(arg.type);
});
}
(field) => {
findAndReplaceLastType(field, 'type');
}
);
autoGraphQLSchemaType._fields = {
...autoGraphQLSchemaType._fields,
...customGraphQLSchemaType._fields,
...autoGraphQLSchemaType.getFields(),
...customGraphQLSchemaType.getFields(),
};
} else {
this.graphQLAutoSchema._typeMap[
customGraphQLSchemaType.name
] = customGraphQLSchemaType;
}
}
);
this.graphQLSchema = mergeSchemas({
schemas: [
this.graphQLSchemaDirectivesDefinitions,
this.graphQLCustomTypeDefs,
this.graphQLAutoSchema,
],
mergeDirectives: true,
Expand All @@ -271,24 +276,24 @@ class ParseGraphQLSchema {
}

const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => {
Object.keys(graphQLSchemaTypeMap).forEach((graphQLSchemaTypeName) => {
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
if (
typeof graphQLSchemaType.getFields === 'function' &&
this.graphQLCustomTypeDefs.definitions
) {
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
definition => definition.name.value === graphQLSchemaTypeName
(definition) => definition.name.value === graphQLSchemaTypeName
);
if (graphQLCustomTypeDef) {
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
Object.keys(graphQLSchemaTypeFieldMap).forEach(
graphQLSchemaTypeFieldName => {
(graphQLSchemaTypeFieldName) => {
const graphQLSchemaTypeField =
graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
if (!graphQLSchemaTypeField.astNode) {
const astNode = graphQLCustomTypeDef.fields.find(
field => field.name.value === graphQLSchemaTypeFieldName
(field) => field.name.value === graphQLSchemaTypeFieldName
);
if (astNode) {
graphQLSchemaTypeField.astNode = astNode;
Expand Down Expand Up @@ -319,7 +324,9 @@ class ParseGraphQLSchema {
) {
if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
this.graphQLTypes.find(existingType => existingType.name === type.name) ||
this.graphQLTypes.find(
(existingType) => existingType.name === type.name
) ||
(!ignoreConnection && type.name.endsWith('Connection'))
) {
const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
Expand Down Expand Up @@ -409,20 +416,20 @@ class ParseGraphQLSchema {
if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) {
let includedClasses = allClasses;
if (enabledForClasses) {
includedClasses = allClasses.filter(clazz => {
includedClasses = allClasses.filter((clazz) => {
return enabledForClasses.includes(clazz.className);
});
}
if (disabledForClasses) {
// Classes included in `enabledForClasses` that
// are also present in `disabledForClasses` will
// still be filtered out
includedClasses = includedClasses.filter(clazz => {
includedClasses = includedClasses.filter((clazz) => {
return !disabledForClasses.includes(clazz.className);
});
}

this.isUsersClassDisabled = !includedClasses.some(clazz => {
this.isUsersClassDisabled = !includedClasses.some((clazz) => {
return clazz.className === '_User';
});

Expand Down Expand Up @@ -467,19 +474,19 @@ class ParseGraphQLSchema {
}
};

return parseClasses.sort(sortClasses).map(parseClass => {
return parseClasses.sort(sortClasses).map((parseClass) => {
let parseClassConfig;
if (classConfigs) {
parseClassConfig = classConfigs.find(
c => c.className === parseClass.className
(c) => c.className === parseClass.className
);
}
return [parseClass, parseClassConfig];
});
}

async _getFunctionNames() {
return await getFunctionNames(this.appId).filter(functionName => {
return await getFunctionNames(this.appId).filter((functionName) => {
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
return true;
} else {
Expand Down