diff --git a/package.json b/package.json index 6f6b109c..0e181a55 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 1e5ecf4c..4534ffe7 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -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) }, - }, - resolve: (p, { message }) => message, - }, + const SomeClassType = new GraphQLObjectType({ + name: 'SomeClass', + fields: { + nameUpperCase: { + type: new GraphQLNonNull(GraphQLString), + resolve: (p) => p.name.toUpperCase(), }, - }), - types: [ - new GraphQLInputObjectType({ - name: 'CreateSomeClassFieldsInput', - fields: { - type: { type: TypeEnum }, - }, - }), - 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: { + type: new GraphQLEnumType({ + name: 'LanguageEnum', + values: { + fr: { value: 'fr' }, + en: { value: 'en' }, }, - type: { type: TypeEnum }, - language: { - type: new GraphQLEnumType({ - name: 'LanguageEnum', - values: { - fr: { value: 'fr' }, - en: { value: 'en' }, - }, - }), - resolve: () => 'fr', - }, - }, - }), - ], + }), + 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); await new Promise((resolve) => @@ -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' }); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index dd071b1f..9616baa8 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -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 || @@ -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, @@ -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; @@ -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.`; @@ -409,7 +416,7 @@ 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); }); } @@ -417,12 +424,12 @@ class ParseGraphQLSchema { // 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'; }); @@ -467,11 +474,11 @@ 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]; @@ -479,7 +486,7 @@ class ParseGraphQLSchema { } 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 {