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

* add package

* Allow real GraphQL Schema via ParseServer.start

* Allow resolve fields from auto graphQL Schema

* Simple merge

* fix + improve

* Add tests
This commit is contained in:
Antoine Cormouls
2020-04-21 19:15:00 +02:00
committed by GitHub
parent ad027c2822
commit f2f772084f
3 changed files with 126 additions and 78 deletions

View File

@@ -122,6 +122,7 @@
"url": "https://opencollective.com/parse-server", "url": "https://opencollective.com/parse-server",
"logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary" "logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary"
}, },
"publishConfig": { "registry": "https://npm.pkg.github.com/" },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/parse-server" "url": "https://opencollective.com/parse-server"

View File

@@ -10769,57 +10769,70 @@ describe('ParseGraphQLServer', () => {
robot: { value: 'robot' }, robot: { value: 'robot' },
}, },
}); });
parseGraphQLServer = new ParseGraphQLServer(parseServer, { const SomeClassType = new GraphQLObjectType({
graphQLPath: '/graphql', name: 'SomeClass',
graphQLCustomTypeDefs: new GraphQLSchema({ fields: {
query: new GraphQLObjectType({ nameUpperCase: {
name: 'Query', type: new GraphQLNonNull(GraphQLString),
fields: { resolve: (p) => p.name.toUpperCase(),
customQuery: {
type: new GraphQLNonNull(GraphQLString),
args: {
message: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: (p, { message }) => message,
},
}, },
}), type: { type: TypeEnum },
types: [ language: {
new GraphQLInputObjectType({ type: new GraphQLEnumType({
name: 'CreateSomeClassFieldsInput', name: 'LanguageEnum',
fields: { values: {
type: { type: TypeEnum }, fr: { value: 'fr' },
}, en: { value: 'en' },
}),
new GraphQLInputObjectType({
name: 'UpdateSomeClassFieldsInput',
fields: {
type: { type: TypeEnum },
},
}),
new GraphQLObjectType({
name: 'SomeClass',
fields: {
nameUpperCase: {
type: new GraphQLNonNull(GraphQLString),
resolve: (p) => p.name.toUpperCase(),
}, },
type: { type: TypeEnum }, }),
language: { resolve: () => 'fr',
type: new GraphQLEnumType({ },
name: 'LanguageEnum', },
values: {
fr: { value: 'fr' },
en: { value: 'en' },
},
}),
resolve: () => 'fr',
},
},
}),
],
}), }),
}); 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) },
},
resolve: (p, { message }) => message,
},
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); parseGraphQLServer.applyGraphQL(expressApp);
await new Promise((resolve) => await new Promise((resolve) =>
@@ -10857,6 +10870,33 @@ describe('ParseGraphQLServer', () => {
expect(result.data.customQuery).toEqual('hello'); 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 () => { it('can resolve a custom extend type', async () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' }); await obj.save({ name: 'aname', type: 'robot' });

View File

@@ -200,7 +200,7 @@ class ParseGraphQLSchema {
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') { if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap(); const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap();
Object.values(customGraphQLSchemaTypeMap).forEach( Object.values(customGraphQLSchemaTypeMap).forEach(
customGraphQLSchemaType => { (customGraphQLSchemaType) => {
if ( if (
!customGraphQLSchemaType || !customGraphQLSchemaType ||
!customGraphQLSchemaType.name || !customGraphQLSchemaType.name ||
@@ -215,40 +215,45 @@ class ParseGraphQLSchema {
autoGraphQLSchemaType && autoGraphQLSchemaType &&
typeof customGraphQLSchemaType.getFields === 'function' typeof customGraphQLSchemaType.getFields === 'function'
) { ) {
const findAndAddLastType = type => { const findAndReplaceLastType = (parent, key) => {
if (type.name) { if (parent[key].name) {
if (!this.graphQLAutoSchema.getType(type)) { if (
// To avoid schema stitching (Unknow type) bug on variables this.graphQLAutoSchema.getType(parent[key].name) &&
// transfer the final type to the Auto Schema this.graphQLAutoSchema.getType(parent[key].name) !==
this.graphQLAutoSchema._typeMap[type.name] = type; 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 { } else {
if (type.ofType) { if (parent[key].ofType) {
findAndAddLastType(type.ofType); findAndReplaceLastType(parent[key], 'ofType');
} }
} }
}; };
Object.values(customGraphQLSchemaType.getFields()).forEach( Object.values(customGraphQLSchemaType.getFields()).forEach(
field => { (field) => {
findAndAddLastType(field.type); findAndReplaceLastType(field, 'type');
if (field.args) {
field.args.forEach(arg => {
findAndAddLastType(arg.type);
});
}
} }
); );
autoGraphQLSchemaType._fields = { autoGraphQLSchemaType._fields = {
...autoGraphQLSchemaType._fields, ...autoGraphQLSchemaType.getFields(),
...customGraphQLSchemaType._fields, ...customGraphQLSchemaType.getFields(),
}; };
} else {
this.graphQLAutoSchema._typeMap[
customGraphQLSchemaType.name
] = customGraphQLSchemaType;
} }
} }
); );
this.graphQLSchema = mergeSchemas({ this.graphQLSchema = mergeSchemas({
schemas: [ schemas: [
this.graphQLSchemaDirectivesDefinitions, this.graphQLSchemaDirectivesDefinitions,
this.graphQLCustomTypeDefs,
this.graphQLAutoSchema, this.graphQLAutoSchema,
], ],
mergeDirectives: true, mergeDirectives: true,
@@ -271,24 +276,24 @@ class ParseGraphQLSchema {
} }
const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap(); const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => { Object.keys(graphQLSchemaTypeMap).forEach((graphQLSchemaTypeName) => {
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName]; const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
if ( if (
typeof graphQLSchemaType.getFields === 'function' && typeof graphQLSchemaType.getFields === 'function' &&
this.graphQLCustomTypeDefs.definitions this.graphQLCustomTypeDefs.definitions
) { ) {
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find( const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
definition => definition.name.value === graphQLSchemaTypeName (definition) => definition.name.value === graphQLSchemaTypeName
); );
if (graphQLCustomTypeDef) { if (graphQLCustomTypeDef) {
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields(); const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
Object.keys(graphQLSchemaTypeFieldMap).forEach( Object.keys(graphQLSchemaTypeFieldMap).forEach(
graphQLSchemaTypeFieldName => { (graphQLSchemaTypeFieldName) => {
const graphQLSchemaTypeField = const graphQLSchemaTypeField =
graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName]; graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
if (!graphQLSchemaTypeField.astNode) { if (!graphQLSchemaTypeField.astNode) {
const astNode = graphQLCustomTypeDef.fields.find( const astNode = graphQLCustomTypeDef.fields.find(
field => field.name.value === graphQLSchemaTypeFieldName (field) => field.name.value === graphQLSchemaTypeFieldName
); );
if (astNode) { if (astNode) {
graphQLSchemaTypeField.astNode = astNode; graphQLSchemaTypeField.astNode = astNode;
@@ -319,7 +324,9 @@ class ParseGraphQLSchema {
) { ) {
if ( if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) || (!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')) (!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.`; const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
@@ -409,7 +416,7 @@ class ParseGraphQLSchema {
if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) { if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) {
let includedClasses = allClasses; let includedClasses = allClasses;
if (enabledForClasses) { if (enabledForClasses) {
includedClasses = allClasses.filter(clazz => { includedClasses = allClasses.filter((clazz) => {
return enabledForClasses.includes(clazz.className); return enabledForClasses.includes(clazz.className);
}); });
} }
@@ -417,12 +424,12 @@ class ParseGraphQLSchema {
// Classes included in `enabledForClasses` that // Classes included in `enabledForClasses` that
// are also present in `disabledForClasses` will // are also present in `disabledForClasses` will
// still be filtered out // still be filtered out
includedClasses = includedClasses.filter(clazz => { includedClasses = includedClasses.filter((clazz) => {
return !disabledForClasses.includes(clazz.className); return !disabledForClasses.includes(clazz.className);
}); });
} }
this.isUsersClassDisabled = !includedClasses.some(clazz => { this.isUsersClassDisabled = !includedClasses.some((clazz) => {
return clazz.className === '_User'; return clazz.className === '_User';
}); });
@@ -467,11 +474,11 @@ class ParseGraphQLSchema {
} }
}; };
return parseClasses.sort(sortClasses).map(parseClass => { return parseClasses.sort(sortClasses).map((parseClass) => {
let parseClassConfig; let parseClassConfig;
if (classConfigs) { if (classConfigs) {
parseClassConfig = classConfigs.find( parseClassConfig = classConfigs.find(
c => c.className === parseClass.className (c) => c.className === parseClass.className
); );
} }
return [parseClass, parseClassConfig]; return [parseClass, parseClassConfig];
@@ -479,7 +486,7 @@ class ParseGraphQLSchema {
} }
async _getFunctionNames() { 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)) { if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
return true; return true;
} else { } else {