From fa4f22fef2b15bf9a9447ebc3c59368e3397c5bd Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Tue, 21 Jul 2020 13:42:18 -0700 Subject: [PATCH] Implement new composition format --- .../__tests__/printComposedSdl.test.ts | 170 ++++++++++++------ .../src/service/printComposedSdl.ts | 106 +++++------ .../__fixtures__/schemas/accounts.ts | 1 - 3 files changed, 175 insertions(+), 102 deletions(-) diff --git a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts b/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts index 6a00aeb7349..616f9b3a803 100644 --- a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts +++ b/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts @@ -1,14 +1,15 @@ +import { lexicographicSortSchema } from 'graphql'; +import gql from 'graphql-tag'; import { fixtures } from '../../../../apollo-gateway/src/__tests__/__fixtures__/schemas'; -import { composeServices } from '../../composition'; +import { composeAndValidate, ServiceDefinition } from '../../composition'; import { printComposedSdl } from '../printComposedSdl'; -import { lexicographicSortSchema } from 'graphql'; describe('printComposedSdl', () => { - let { schema } = composeServices(fixtures); - const sorted = lexicographicSortSchema(schema); - it('prints a full, composed schema', () => { - expect(printComposedSdl(sorted, fixtures)).toMatchInlineSnapshot(` + let { schema, errors } = composeAndValidate(fixtures); + schema = lexicographicSortSchema(schema); + expect(errors).toHaveLength(0); + expect(printComposedSdl(schema, fixtures)).toMatchInlineSnapshot(` "schema @graph(name: \\"accounts\\", url: \\"https://api.accounts.com\\") @graph(name: \\"books\\", url: \\"https://api.books.com\\") @@ -34,30 +35,36 @@ describe('printComposedSdl', () => { union Body = Image | Text - type Book implements Product @key(fields: \\"isbn\\") { - details: ProductDetailsBook - inStock: Boolean + type Book implements Product + @owner(graph: \\"books\\") + @key(fields: \\"isbn\\", graph: \\"books\\") + { + details: ProductDetailsBook @resolve(graph: \\"product\\") + inStock: Boolean @resolve(graph: \\"inventory\\") isbn: String! - isCheckedOut: Boolean + isCheckedOut: Boolean @resolve(graph: \\"inventory\\") metadata: [MetadataOrError] - name(delimeter: String = \\" \\"): String @requires(fields: \\"title year\\") - price: String - relatedReviews: [Review!]! @requires(fields: \\"similarBooks { isbn }\\") - reviews: [Review] + name(delimeter: String = \\" \\"): String @resolve(graph: \\"product\\") @requires(fields: \\"title year\\") + price: String @resolve(graph: \\"product\\") + relatedReviews: [Review!]! @resolve(graph: \\"reviews\\") @requires(fields: \\"similarBooks { isbn }\\") + reviews: [Review] @resolve(graph: \\"reviews\\") similarBooks: [Book]! - sku: String! + sku: String! @resolve(graph: \\"product\\") title: String - upc: String! + upc: String! @resolve(graph: \\"product\\") year: Int } union Brand = Amazon | Ikea - type Car implements Vehicle @key(fields: \\"id\\") { + type Car implements Vehicle + @owner(graph: \\"product\\") + @key(fields: \\"id\\", graph: \\"product\\") + { description: String id: String! price: String - retailPrice: String @requires(fields: \\"price\\") + retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"price\\") } type Error { @@ -65,15 +72,19 @@ describe('printComposedSdl', () => { message: String } - type Furniture implements Product @key(fields: \\"upc\\") @key(fields: \\"sku\\") { + type Furniture implements Product + @owner(graph: \\"product\\") + @key(fields: \\"upc\\", graph: \\"product\\") + @key(fields: \\"sku\\", graph: \\"product\\") + { brand: Brand details: ProductDetailsFurniture - inStock: Boolean - isHeavy: Boolean + inStock: Boolean @resolve(graph: \\"inventory\\") + isHeavy: Boolean @resolve(graph: \\"inventory\\") metadata: [MetadataOrError] name: String price: String - reviews: [Review] + reviews: [Review] @resolve(graph: \\"reviews\\") sku: String! upc: String! } @@ -96,22 +107,28 @@ describe('printComposedSdl', () => { value: String! } - type Library @key(fields: \\"id\\") { + type Library + @owner(graph: \\"books\\") + @key(fields: \\"id\\", graph: \\"books\\") + { id: ID! name: String - userAccount(id: ID! = 1): User @requires(fields: \\"name\\") + userAccount(id: ID! = 1): User @resolve(graph: \\"accounts\\") @requires(fields: \\"name\\") } union MetadataOrError = Error | KeyValue type Mutation { - deleteReview(id: ID!): Boolean - login(password: String!, username: String!): User - reviewProduct(body: String!, upc: String!): Product - updateReview(review: UpdateReviewInput!): Review + deleteReview(id: ID!): Boolean @resolve(graph: \\"reviews\\") + login(password: String!, username: String!): User @resolve(graph: \\"accounts\\") + reviewProduct(body: String!, upc: String!): Product @resolve(graph: \\"reviews\\") + updateReview(review: UpdateReviewInput!): Review @resolve(graph: \\"reviews\\") } - type PasswordAccount @key(fields: \\"email\\") { + type PasswordAccount + @owner(graph: \\"accounts\\") + @key(fields: \\"email\\", graph: \\"accounts\\") + { email: String! } @@ -140,18 +157,23 @@ describe('printComposedSdl', () => { } type Query { - body: Body! - book(isbn: String!): Book - books: [Book] - library(id: ID!): Library - product(upc: String!): Product - topCars(first: Int = 5): [Car] - topProducts(first: Int = 5): [Product] - topReviews(first: Int = 5): [Review] - vehicle(id: String!): Vehicle - } - - type Review @key(fields: \\"id\\") { + body: Body! @resolve(graph: \\"documents\\") + book(isbn: String!): Book @resolve(graph: \\"books\\") + books: [Book] @resolve(graph: \\"books\\") + library(id: ID!): Library @resolve(graph: \\"books\\") + me: User @resolve(graph: \\"accounts\\") + product(upc: String!): Product @resolve(graph: \\"product\\") + topCars(first: Int = 5): [Car] @resolve(graph: \\"product\\") + topProducts(first: Int = 5): [Product] @resolve(graph: \\"product\\") + topReviews(first: Int = 5): [Review] @resolve(graph: \\"reviews\\") + user(id: ID!): User @resolve(graph: \\"accounts\\") + vehicle(id: String!): Vehicle @resolve(graph: \\"product\\") + } + + type Review + @owner(graph: \\"reviews\\") + @key(fields: \\"id\\", graph: \\"reviews\\") + { author: User @provides(fields: \\"username\\") body(format: Boolean = false): String id: ID! @@ -159,7 +181,10 @@ describe('printComposedSdl', () => { product: Product } - type SMSAccount @key(fields: \\"number\\") { + type SMSAccount + @owner(graph: \\"accounts\\") + @key(fields: \\"number\\", graph: \\"accounts\\") + { number: String } @@ -180,19 +205,22 @@ describe('printComposedSdl', () => { id: ID! } - type User @key(fields: \\"id\\") { + type User + @owner(graph: \\"accounts\\") + @key(fields: \\"id\\", graph: \\"accounts\\") + { account: AccountType birthDate(locale: String): String - goodAddress: Boolean @requires(fields: \\"metadata { address }\\") - goodDescription: Boolean @requires(fields: \\"metadata { description }\\") + goodAddress: Boolean @resolve(graph: \\"reviews\\") @requires(fields: \\"metadata { address }\\") + goodDescription: Boolean @resolve(graph: \\"inventory\\") @requires(fields: \\"metadata { description }\\") id: ID! metadata: [UserMetadata] name: String - numberOfReviews: Int! - reviews: [Review] - thing: Thing + numberOfReviews: Int! @resolve(graph: \\"reviews\\") + reviews: [Review] @resolve(graph: \\"reviews\\") + thing: Thing @resolve(graph: \\"product\\") username: String - vehicle: Vehicle + vehicle: Vehicle @resolve(graph: \\"product\\") } type UserMetadata { @@ -201,11 +229,14 @@ describe('printComposedSdl', () => { name: String } - type Van implements Vehicle @key(fields: \\"id\\") { + type Van implements Vehicle + @owner(graph: \\"product\\") + @key(fields: \\"id\\", graph: \\"product\\") + { description: String id: String! price: String - retailPrice: String @requires(fields: \\"price\\") + retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"price\\") } interface Vehicle { @@ -217,4 +248,41 @@ describe('printComposedSdl', () => { " `); }); + + it('fixes the block description bug', () => { + const serviceDefinitions: ServiceDefinition[] = [{ + name: 'service', + url: 'https://service.api.com', + typeDefs: gql` + type Query { + """ + Block description with "double quotes" + """ + fieldWithBlockDescription: String + } + `, + }]; + + let { schema, errors } = composeAndValidate(serviceDefinitions); + schema = lexicographicSortSchema(schema); + + expect(errors).toHaveLength(0); + expect(printComposedSdl(schema, serviceDefinitions)) + .toMatchInlineSnapshot(` + "schema + @graph(name: \\"additional\\", url: \\"https://additional.api.com\\") + @composedGraph(version: 1) + { + query: Query + } + + type Query { + \\"\\"\\" + Block description with \\"double quotes\\" + \\"\\"\\" + additional: String @resolve(graph: \\"additional\\") + } + " + `); + }); }); diff --git a/packages/apollo-federation/src/service/printComposedSdl.ts b/packages/apollo-federation/src/service/printComposedSdl.ts index 2ea89775d87..19f0831ef7f 100644 --- a/packages/apollo-federation/src/service/printComposedSdl.ts +++ b/packages/apollo-federation/src/service/printComposedSdl.ts @@ -75,6 +75,7 @@ export function printIntrospectionSchema( ): string { return printFilteredSchema( schema, + [], isSpecifiedDirective, isIntrospectionType, options, @@ -152,37 +153,6 @@ function printComposedGraphDirective() { return `\n @composedGraph(version: 1)`; } -/** - * GraphQL schema define root types for each type of operation. These types are - * the same as any other type and can be named in any manner, however there is - * a common naming convention: - * - * schema { - * query: Query - * mutation: Mutation - * } - * - * When using this naming convention, the schema description can be omitted. - */ -function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { - const queryType = schema.getQueryType(); - if (queryType && queryType.name !== 'Query') { - return false; - } - - const mutationType = schema.getMutationType(); - if (mutationType && mutationType.name !== 'Mutation') { - return false; - } - - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType && subscriptionType.name !== 'Subscription') { - return false; - } - - return true; -} - export function printType(type: GraphQLNamedType, options?: Options): string { if (isScalarType(type)) { return printScalar(type, options); @@ -229,23 +199,23 @@ function printObject(type: GraphQLObjectType, options?: Options): string { printDescription(options, type) + `type ${type.name}` + printImplementedInterfaces(type) + - // Federation addition for printing @key usages - printKeyDirectives(type) + + // Federation addition for printing @owner and @key usages + printEntityDirectives(type) + printFields(options, type) ); } -// Federation change: print usages of the @key directive. -function printKeyDirectives(type: GraphQLObjectType): string { +// Federation change: print usages of the @owner and @key directives. +function printEntityDirectives(type: GraphQLObjectType): string { const metadata = type.extensions?.federation; if (!metadata) return ''; const { serviceName, keys } = metadata; if (!keys) return ''; - return " " + keys[serviceName].map( - (key: FieldDefinitionNode[]) => `@key(fields: "${key.map(print)}")`, - ).join(" "); + return `\n @owner(graph: "${serviceName}")` + keys[serviceName].map( + (key: FieldDefinitionNode[]) => `\n @key(fields: "${key.map(print)}", graph: "${serviceName}")`, + ).join(""); } function printInterface(type: GraphQLInterfaceType, options?: Options): string { @@ -296,6 +266,7 @@ function printFields( options: Options | undefined, type: GraphQLObjectType | GraphQLInterfaceType, ) { + const fields = Object.values(type.getFields()).map( (f, i) => printDescription(options, f, ' ', !i) + @@ -305,9 +276,14 @@ function printFields( ': ' + String(f.type) + printDeprecated(f) + - printFederationFieldDirectives(f), + printFederationFieldDirectives(f, type), ); - return printBlock(fields); + + // Federation change: for entities, we want to print the block on a new line. + // This is just a formatting nice-to-have. + const isEntity = Boolean(type.extensions?.federation?.keys); + + return printBlock(fields, isEntity); } export function printWithReducedWhitespace(ast: ASTNode): string { @@ -316,23 +292,53 @@ export function printWithReducedWhitespace(ast: ASTNode): string { .trim(); } -function printFederationFieldDirectives(field: GraphQLField): string { - if (!field.extensions?.federation) return ""; - - let printed = ""; - const { provides = [], requires = [] } = field.extensions.federation; - if (provides.length > 0) { - printed += ` @provides(fields: "${provides.map(printWithReducedWhitespace).join(" ")}")`; +/** + * Federation change: print @resolve, @requires, and @provides directives + * + * @param field + * @param parentType + */ +function printFederationFieldDirectives( + field: GraphQLField, + parentType: GraphQLObjectType | GraphQLInterfaceType, +): string { + if (!field.extensions?.federation) return ''; + + const { + serviceName, + requires = [], + provides = [], + } = field.extensions.federation; + + let printed = ''; + // If a `serviceName` exists, we only want to print a `@resolve` directive + // if the `serviceName` differs from the `parentType`'s `serviceName` + if ( + serviceName && + serviceName !== parentType.extensions?.federation?.serviceName + ) { + printed += ` @resolve(graph: "${serviceName}")`; } + if (requires.length > 0) { - printed += ` @requires(fields: "${requires.map(printWithReducedWhitespace).join(" ")}")`; + printed += ` @requires(fields: "${requires.map(printWithReducedWhitespace).join(' ')}")`; + } + + if (provides.length > 0) { + printed += ` @provides(fields: "${provides.map(printWithReducedWhitespace).join(' ')}")`; } return printed; } -function printBlock(items: string[]) { - return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : ''; +// Federation change: `onNewLine` is a formatting nice-to-have for printing +// types that have a list of directives attached, i.e. an entity. +function printBlock(items: string[], onNewLine?: boolean) { + return items.length !== 0 + ? onNewLine + ? '\n{\n' + items.join('\n') + '\n}' + : ' {\n' + items.join('\n') + '\n}' + : ''; } function printArgs( diff --git a/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/accounts.ts b/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/accounts.ts index 57ad64b24ef..bb229788455 100644 --- a/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/accounts.ts +++ b/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/accounts.ts @@ -13,7 +13,6 @@ export const typeDefs = gql` } extend type RootQuery { - """Testing doc block""" user(id: ID!): User me: User }