diff --git a/package.json b/package.json index b47ece6efe7..e21e015f08c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "jest": "26.0.1", "ts-jest": "26.1.0", "typescript": "3.9.5", - "@typescript-eslint/eslint-plugin": "3.1.0", + "@typescript-eslint/eslint-plugin": "3.2.0", "@typescript-eslint/parser": "3.2.0", "bob-the-bundler": "1.0.2", "eslint": "7.2.0", diff --git a/packages/schema/src/addResolversToSchema.ts b/packages/schema/src/addResolversToSchema.ts index e034859504f..4225ac03d22 100644 --- a/packages/schema/src/addResolversToSchema.ts +++ b/packages/schema/src/addResolversToSchema.ts @@ -99,40 +99,60 @@ export function addResolversToSchema( type[fieldName] = resolverValue[fieldName]; } }); + } else if (isEnumType(type)) { + const values = type.getValues(); + + Object.keys(resolverValue).forEach(fieldName => { + if ( + !fieldName.startsWith('__') && + !values.some(value => value.name === fieldName) && + !allowResolversNotInSchema + ) { + throw new Error(`${type.name}.${fieldName} was defined in resolvers, but not present within ${type.name}`); + } + }); + } else if (isUnionType(type)) { + Object.keys(resolverValue).forEach(fieldName => { + if (!fieldName.startsWith('__') && !allowResolversNotInSchema) { + throw new Error( + `${type.name}.${fieldName} was defined in resolvers, but ${type.name} is not an object or interface type` + ); + } + }); + } else if (isObjectType(type) || isInterfaceType(type)) { + Object.keys(resolverValue).forEach(fieldName => { + if (!fieldName.startsWith('__')) { + const fields = type.getFields(); + const field = fields[fieldName]; + + if (field == null && !allowResolversNotInSchema) { + throw new Error(`${typeName}.${fieldName} defined in resolvers, but not in schema`); + } + + const fieldResolve = resolverValue[fieldName]; + if (typeof fieldResolve !== 'function' && typeof fieldResolve !== 'object') { + throw new Error(`Resolver ${typeName}.${fieldName} must be object or function`); + } + } + }); } } }); schema = updateResolversInPlace - ? addResolversToExistingSchema({ - schema, - resolvers, - defaultFieldResolver, - allowResolversNotInSchema, - }) - : createNewSchemaWithResolvers({ - schema, - resolvers, - defaultFieldResolver, - allowResolversNotInSchema, - }); + ? addResolversToExistingSchema(schema, resolvers, defaultFieldResolver) + : createNewSchemaWithResolvers(schema, resolvers, defaultFieldResolver); checkForResolveTypeResolver(schema, requireResolversForResolveType); return schema; } -function addResolversToExistingSchema({ - schema, - resolvers, - defaultFieldResolver, - allowResolversNotInSchema, -}: { - schema: GraphQLSchema; - resolvers: IResolvers; - defaultFieldResolver: GraphQLFieldResolver; - allowResolversNotInSchema: boolean; -}): GraphQLSchema { +function addResolversToExistingSchema( + schema: GraphQLSchema, + resolvers: IResolvers, + defaultFieldResolver: GraphQLFieldResolver +): GraphQLSchema { const typeMap = schema.getTypeMap(); Object.keys(resolvers).forEach(typeName => { if (typeName !== '__schema') { @@ -143,6 +163,24 @@ function addResolversToExistingSchema({ Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { type[fieldName.substring(2)] = resolverValue[fieldName]; + } else if (fieldName === 'astNode' && type.astNode != null) { + type.astNode = { + ...type.astNode, + description: (resolverValue as GraphQLScalarType)?.astNode?.description ?? type.astNode.description, + directives: (type.astNode.directives ?? []).concat( + (resolverValue as GraphQLScalarType)?.astNode?.directives ?? [] + ), + }; + } else if (fieldName === 'extensionASTNodes' && type.extensionASTNodes != null) { + type.extensionASTNodes = ([] ?? type.extensionASTNodes).concat( + (resolverValue as GraphQLScalarType)?.extensionASTNodes ?? [] + ); + } else if ( + fieldName === 'extensions' && + type.extensions != null && + (resolverValue as GraphQLScalarType).extensions != null + ) { + type.extensions = Object.assign({}, type.extensions, (resolverValue as GraphQLScalarType).extensions); } else { type[fieldName] = resolverValue[fieldName]; } @@ -154,12 +192,25 @@ function addResolversToExistingSchema({ Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { config[fieldName.substring(2)] = resolverValue[fieldName]; - } else if (!enumValueConfigMap[fieldName]) { - if (allowResolversNotInSchema) { - return; - } - throw new Error(`${type.name}.${fieldName} was defined in resolvers, but not present within ${type.name}`); - } else { + } else if (fieldName === 'astNode' && config.astNode != null) { + config.astNode = { + ...config.astNode, + description: (resolverValue as GraphQLScalarType)?.astNode?.description ?? config.astNode.description, + directives: (config.astNode.directives ?? []).concat( + (resolverValue as GraphQLEnumType)?.astNode?.directives ?? [] + ), + }; + } else if (fieldName === 'extensionASTNodes' && config.extensionASTNodes != null) { + config.extensionASTNodes = config.extensionASTNodes.concat( + (resolverValue as GraphQLEnumType)?.extensionASTNodes ?? [] + ); + } else if ( + fieldName === 'extensions' && + type.extensions != null && + (resolverValue as GraphQLEnumType).extensions != null + ) { + type.extensions = Object.assign({}, type.extensions, (resolverValue as GraphQLEnumType).extensions); + } else if (enumValueConfigMap[fieldName]) { enumValueConfigMap[fieldName].value = resolverValue[fieldName]; } }); @@ -169,15 +220,7 @@ function addResolversToExistingSchema({ Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { type[fieldName.substring(2)] = resolverValue[fieldName]; - return; - } - if (allowResolversNotInSchema) { - return; } - - throw new Error( - `${type.name}.${fieldName} was defined in resolvers, but ${type.name} is not an object or interface type` - ); }); } else if (isObjectType(type) || isInterfaceType(type)) { Object.keys(resolverValue).forEach(fieldName => { @@ -190,23 +233,14 @@ function addResolversToExistingSchema({ const fields = type.getFields(); const field = fields[fieldName]; - if (field == null) { - if (allowResolversNotInSchema) { - return; - } - - throw new Error(`${typeName}.${fieldName} defined in resolvers, but not in schema`); - } - - const fieldResolve = resolverValue[fieldName]; - if (typeof fieldResolve === 'function') { - // for convenience. Allows shorter syntax in resolver definition file - field.resolve = fieldResolve; - } else { - if (typeof fieldResolve !== 'object') { - throw new Error(`Resolver ${typeName}.${fieldName} must be object or function`); + if (field != null) { + const fieldResolve = resolverValue[fieldName]; + if (typeof fieldResolve === 'function') { + // for convenience. Allows shorter syntax in resolver definition file + field.resolve = fieldResolve; + } else { + setFieldProperties(field, fieldResolve); } - setFieldProperties(field, fieldResolve); } }); } @@ -231,17 +265,11 @@ function addResolversToExistingSchema({ return schema; } -function createNewSchemaWithResolvers({ - schema, - resolvers, - defaultFieldResolver, - allowResolversNotInSchema, -}: { - schema: GraphQLSchema; - resolvers: IResolvers; - defaultFieldResolver: GraphQLFieldResolver; - allowResolversNotInSchema: boolean; -}): GraphQLSchema { +function createNewSchemaWithResolvers( + schema: GraphQLSchema, + resolvers: IResolvers, + defaultFieldResolver: GraphQLFieldResolver +): GraphQLSchema { schema = mapSchema(schema, { [MapperKind.SCALAR_TYPE]: type => { const config = type.toConfig(); @@ -250,6 +278,24 @@ function createNewSchemaWithResolvers({ Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { config[fieldName.substring(2)] = resolverValue[fieldName]; + } else if (fieldName === 'astNode' && config.astNode != null) { + config.astNode = { + ...config.astNode, + description: (resolverValue as GraphQLScalarType)?.astNode?.description ?? config.astNode.description, + directives: (config.astNode.directives ?? []).concat( + (resolverValue as GraphQLScalarType)?.astNode?.directives ?? [] + ), + }; + } else if (fieldName === 'extensionASTNodes' && config.extensionASTNodes != null) { + config.extensionASTNodes = config.extensionASTNodes.concat( + (resolverValue as GraphQLScalarType)?.extensionASTNodes ?? [] + ); + } else if ( + fieldName === 'extensions' && + config.extensions != null && + (resolverValue as GraphQLScalarType).extensions != null + ) { + config.extensions = Object.assign({}, type.extensions, (resolverValue as GraphQLScalarType).extensions); } else { config[fieldName] = resolverValue[fieldName]; } @@ -268,12 +314,25 @@ function createNewSchemaWithResolvers({ Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { config[fieldName.substring(2)] = resolverValue[fieldName]; - } else if (!enumValueConfigMap[fieldName]) { - if (allowResolversNotInSchema) { - return; - } - throw new Error(`${type.name}.${fieldName} was defined in resolvers, but not present within ${type.name}`); - } else { + } else if (fieldName === 'astNode' && config.astNode != null) { + config.astNode = { + ...config.astNode, + description: (resolverValue as GraphQLScalarType)?.astNode?.description ?? config.astNode.description, + directives: (config.astNode.directives ?? []).concat( + (resolverValue as GraphQLEnumType)?.astNode?.directives ?? [] + ), + }; + } else if (fieldName === 'extensionASTNodes' && config.extensionASTNodes != null) { + config.extensionASTNodes = config.extensionASTNodes.concat( + (resolverValue as GraphQLEnumType)?.extensionASTNodes ?? [] + ); + } else if ( + fieldName === 'extensions' && + config.extensions != null && + (resolverValue as GraphQLEnumType).extensions != null + ) { + config.extensions = Object.assign({}, type.extensions, (resolverValue as GraphQLEnumType).extensions); + } else if (enumValueConfigMap[fieldName]) { enumValueConfigMap[fieldName].value = resolverValue[fieldName]; } }); @@ -288,17 +347,8 @@ function createNewSchemaWithResolvers({ const config = type.toConfig(); Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { - // this is for isTypeOf and resolveType and all the other stuff. config[fieldName.substring(2)] = resolverValue[fieldName]; - return; } - if (allowResolversNotInSchema) { - return; - } - - throw new Error( - `${type.name}.${fieldName} was defined in resolvers, but ${type.name} is not an object or interface type` - ); }); return new GraphQLUnionType(config); @@ -308,22 +358,10 @@ function createNewSchemaWithResolvers({ const resolverValue = resolvers[type.name]; if (resolverValue != null) { const config = type.toConfig(); - const fields = config.fields; Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { config[fieldName.substring(2)] = resolverValue[fieldName]; - return; - } - - const field = fields[fieldName]; - - if (field == null) { - if (allowResolversNotInSchema) { - return; - } - - throw new Error(`${type.name}.${fieldName} defined in resolvers, but not in schema`); } }); @@ -334,22 +372,10 @@ function createNewSchemaWithResolvers({ const resolverValue = resolvers[type.name]; if (resolverValue != null) { const config = type.toConfig(); - const fields = config.fields; Object.keys(resolverValue).forEach(fieldName => { if (fieldName.startsWith('__')) { config[fieldName.substring(2)] = resolverValue[fieldName]; - return; - } - - const field = fields[fieldName]; - - if (field == null) { - if (allowResolversNotInSchema) { - return; - } - - throw new Error(`${type.name}.${fieldName} defined in resolvers, but not in schema`); } }); @@ -367,9 +393,6 @@ function createNewSchemaWithResolvers({ // for convenience. Allows shorter syntax in resolver definition file newFieldConfig.resolve = fieldResolve; } else { - if (typeof fieldResolve !== 'object') { - throw new Error(`Resolver ${typeName}.${fieldName} must be object or function`); - } setFieldProperties(newFieldConfig, fieldResolve); } return newFieldConfig; diff --git a/packages/schema/tests/schemaGenerator.test.ts b/packages/schema/tests/schemaGenerator.test.ts index b8c5dc609ef..c59f32e8126 100644 --- a/packages/schema/tests/schemaGenerator.test.ts +++ b/packages/schema/tests/schemaGenerator.test.ts @@ -838,6 +838,35 @@ describe('generating schema from shorthand', () => { }); }); + test('retains original scalar directives when passing in scalars in resolve functions', () => { + const schema = makeExecutableSchema({ + typeDefs: ` + directive @test on SCALAR + + scalar Test @test + + type Query { + test: Test + } + `, + resolvers: { + Test: new GraphQLScalarType({ + name: 'Test', + description: 'Test resolver', + serialize: (value) => value, + parseValue: (value) => value, + }), + Query: { + test: () => 42, + }, + }, + }); + + const testType = schema.getType('Test'); + expect(testType).toBeInstanceOf(GraphQLScalarType); + expect(testType.astNode.directives.length).toBe(1); + }); + test('retains scalars after walking/recreating the schema', () => { const shorthand = ` scalar Test diff --git a/yarn.lock b/yarn.lock index 5e07fda1de5..9b2e822d6bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2345,27 +2345,17 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" - integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== +"@typescript-eslint/eslint-plugin@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.2.0.tgz#7fb997f391af32ae6ca1dbe56bcefe4dd30bda14" + integrity sha512-t9RTk/GyYilIXt6BmZurhBzuMT9kLKw3fQoJtK9ayv0tXTlznXEAnx07sCLXdkN3/tZDep1s1CEV95CWuARYWA== dependencies: - "@typescript-eslint/experimental-utils" "3.1.0" + "@typescript-eslint/experimental-utils" "3.2.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" - integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.1.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - "@typescript-eslint/experimental-utils@3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.2.0.tgz#4dab8fc9f44f059ec073470a81bb4d7d7d51e6c5" @@ -2386,19 +2376,6 @@ "@typescript-eslint/typescript-estree" "3.2.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" - integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== - dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" - "@typescript-eslint/typescript-estree@3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.2.0.tgz#c735f1ca6b4d3cd671f30de8c9bde30843e7ead8"