Skip to content
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

Can we merge schemas with overriding types in fields? #287

Closed
nodkz opened this issue Sep 21, 2020 · 5 comments
Closed

Can we merge schemas with overriding types in fields? #287

nodkz opened this issue Sep 21, 2020 · 5 comments

Comments

@nodkz
Copy link
Member

nodkz commented Sep 21, 2020

@FluorescentHallucinogen described his needs in ardatan/graphql-tools#2051

@nodkz
Copy link
Member Author

nodkz commented Sep 21, 2020

/* @flow */

import { buildSchema, GraphQLSchema } from 'graphql';
import { SchemaComposer, dedent } from '../..';

describe('github issue #: Object directives are removed from schema', () => {
  it('should keep @test directive on TestObject', () => {
    const schemaA = buildSchema(`
      type Query {
        field1: Int
        """KEEP ME"""
        field2: Int
      }
    `);
    const schemaB = buildSchema(`
      type Query {
        """BBB"""
        field1: String
        field3: String
        field22: Int
      }
    `);
    const sc = new SchemaComposer(schemaA);
    sc.merge(schemaB);

    expect(sc.toSDL({ omitScalars: true, omitDirectiveDefinitions: true })).toEqual(dedent`
      type Mutation

      type Query {
        """BBB"""
        field1: String
      
        """KEEP ME"""
        field2: Int
        field3: String
        field22: Int
      }

      type Subscription
    `);

    expect(sc.buildSchema()).toBeInstanceOf(GraphQLSchema);
  });
});

nodkz added a commit that referenced this issue Sep 21, 2020
@nodkz nodkz closed this as completed Sep 21, 2020
@FluorescentHallucinogen

Code:

const schemaA = buildSchema(`
# An object with an ID
interface Node {
  # The id of the object.
  id: ID!
}

type Post implements Node {
  id: ID!
  content: String
  fieldA: Int
  fieldB: String
}

type Query {
  post: Post
  # Fetches an object given its ID
  node(
    # The ID of an object
    id: ID!
  ): Node
}
`);

const schemaB = buildSchema(`
# An object with an ID
interface Node {
  # The id of the object.
  id: ID!
}

type Post implements Node {
  id: ID!
  content: String
}

type Query {
  post: Post
  # Fetches an object given its ID
  node(
    # The ID of an object
    id: ID!
  ): Node
}
`);

const sc = new SchemaComposer(schemaA);
sc.merge(schemaB);

const schemaC = sc.toSDL();

console.log('schemaC', schemaC);

Expected result:

Type Post should contain fieldA and fieldB.

The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
"""
scalar ID

type Mutation

interface Node {
  id: ID!
}

type Post implements Node {
  id: ID!
  content: String
  fieldA: Int
  fieldB: String
}

type Query {
  post: Post
  node(id: ID!): Node
}

"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""
scalar String

type Subscription

Actual result:

Type Post doesn't contain fieldA and fieldB.

The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
"""
scalar ID

type Mutation

interface Node {
  id: ID!
}

type Post implements Node {
  id: ID!
  content: String
}

type Query {
  post: Post
  node(id: ID!): Node
}

"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""
scalar String

type Subscription

@FluorescentHallucinogen

Moreover, if I remove post field from Query type in schemaA and schemaB, then schemaC will not contain type Post.

@FluorescentHallucinogen

If schemaA and schemaB don't contain type Query, then the following will be printed:

type Mutation

type Query

type Subscription

I expected the following:

The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
"""
scalar ID

type Mutation

interface Node {
  id: ID!
}

type Post implements Node {
  id: ID!
  content: String
  fieldA: Int
  fieldB: String
}

"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""
scalar String

type Query

type Subscription

@FluorescentHallucinogen
const typeDefsA = `
type AggregatePost {
  count: Int!
}

type BatchPayload {
  """The number of nodes that have been affected by the Batch operation."""
  count: Long!
}

"""
The Long scalar type represents non-fractional signed whole numeric values.
Long can represent values between -(2^63) and 2^63 - 1.
"""
scalar Long

type Mutation {
  createPost(data: PostCreateInput!): Post!
  updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post
  deletePost(where: PostWhereUniqueInput!): Post
  upsertPost(where: PostWhereUniqueInput!, create: PostCreateInput!, update: PostUpdateInput!): Post!
  updateManyPosts(data: PostUpdateManyMutationInput!, where: PostWhereInput): BatchPayload!
  deleteManyPosts(where: PostWhereInput): BatchPayload!
}

enum MutationType {
  CREATED
  UPDATED
  DELETED
}

"""An object with an ID"""
interface Node {
  """The id of the object."""
  id: ID!
}

"""Information about pagination in a connection."""
type PageInfo {
  """When paginating forwards, are there more items?"""
  hasNextPage: Boolean!

  """When paginating backwards, are there more items?"""
  hasPreviousPage: Boolean!

  """When paginating backwards, the cursor to continue."""
  startCursor: String

  """When paginating forwards, the cursor to continue."""
  endCursor: String
}

type Post implements Node {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
}

"""A connection to a list of items."""
type PostConnection {
  """Information to aid in pagination."""
  pageInfo: PageInfo!

  """A list of edges."""
  edges: [PostEdge]!
  aggregate: AggregatePost!
}

input PostCreateInput {
  id: ID
  title: String!
  content: String!
  published: Boolean
}

"""An edge in a connection."""
type PostEdge {
  """The item at the end of the edge."""
  node: Post!

  """A cursor for use in pagination."""
  cursor: String!
}

enum PostOrderByInput {
  id_ASC
  id_DESC
  title_ASC
  title_DESC
  content_ASC
  content_DESC
  published_ASC
  published_DESC
  updatedAt_ASC
  updatedAt_DESC
  createdAt_ASC
  createdAt_DESC
}

type PostPreviousValues {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
}

type PostSubscriptionPayload {
  mutation: MutationType!
  node: Post
  updatedFields: [String!]
  previousValues: PostPreviousValues
}

input PostSubscriptionWhereInput {
  """Logical AND on all given filters."""
  AND: [PostSubscriptionWhereInput!]

  """Logical OR on all given filters."""
  OR: [PostSubscriptionWhereInput!]

  """Logical NOT on all given filters combined by AND."""
  NOT: [PostSubscriptionWhereInput!]

  """The subscription event gets dispatched when it's listed in mutation_in"""
  mutation_in: [MutationType!]

  """
  The subscription event gets only dispatched when one of the updated fields names is included in this list
  """
  updatedFields_contains: String

  """
  The subscription event gets only dispatched when all of the field names included in this list have been updated
  """
  updatedFields_contains_every: [String!]

  """
  The subscription event gets only dispatched when some of the field names included in this list have been updated
  """
  updatedFields_contains_some: [String!]
  node: PostWhereInput
}

input PostUpdateInput {
  title: String
  content: String
  published: Boolean
}

input PostUpdateManyMutationInput {
  title: String
  content: String
  published: Boolean
}

input PostWhereInput {
  """Logical AND on all given filters."""
  AND: [PostWhereInput!]

  """Logical OR on all given filters."""
  OR: [PostWhereInput!]

  """Logical NOT on all given filters combined by AND."""
  NOT: [PostWhereInput!]
  id: ID

  """All values that are not equal to given value."""
  id_not: ID

  """All values that are contained in given list."""
  id_in: [ID!]

  """All values that are not contained in given list."""
  id_not_in: [ID!]

  """All values less than the given value."""
  id_lt: ID

  """All values less than or equal the given value."""
  id_lte: ID

  """All values greater than the given value."""
  id_gt: ID

  """All values greater than or equal the given value."""
  id_gte: ID

  """All values containing the given string."""
  id_contains: ID

  """All values not containing the given string."""
  id_not_contains: ID

  """All values starting with the given string."""
  id_starts_with: ID

  """All values not starting with the given string."""
  id_not_starts_with: ID

  """All values ending with the given string."""
  id_ends_with: ID

  """All values not ending with the given string."""
  id_not_ends_with: ID
  title: String

  """All values that are not equal to given value."""
  title_not: String

  """All values that are contained in given list."""
  title_in: [String!]

  """All values that are not contained in given list."""
  title_not_in: [String!]

  """All values less than the given value."""
  title_lt: String

  """All values less than or equal the given value."""
  title_lte: String

  """All values greater than the given value."""
  title_gt: String

  """All values greater than or equal the given value."""
  title_gte: String

  """All values containing the given string."""
  title_contains: String

  """All values not containing the given string."""
  title_not_contains: String

  """All values starting with the given string."""
  title_starts_with: String

  """All values not starting with the given string."""
  title_not_starts_with: String

  """All values ending with the given string."""
  title_ends_with: String

  """All values not ending with the given string."""
  title_not_ends_with: String
  content: String

  """All values that are not equal to given value."""
  content_not: String

  """All values that are contained in given list."""
  content_in: [String!]

  """All values that are not contained in given list."""
  content_not_in: [String!]

  """All values less than the given value."""
  content_lt: String

  """All values less than or equal the given value."""
  content_lte: String

  """All values greater than the given value."""
  content_gt: String

  """All values greater than or equal the given value."""
  content_gte: String

  """All values containing the given string."""
  content_contains: String

  """All values not containing the given string."""
  content_not_contains: String

  """All values starting with the given string."""
  content_starts_with: String

  """All values not starting with the given string."""
  content_not_starts_with: String

  """All values ending with the given string."""
  content_ends_with: String

  """All values not ending with the given string."""
  content_not_ends_with: String
  published: Boolean

  """All values that are not equal to given value."""
  published_not: Boolean
}

input PostWhereUniqueInput {
  id: ID
}

type Query {
  posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post]!
  post(where: PostWhereUniqueInput!): Post
  postsConnection(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection!

  """Fetches an object given its ID"""
  node(
    """The ID of an object"""
    id: ID!
  ): Node
}

type Subscription {
  post(where: PostSubscriptionWhereInput): PostSubscriptionPayload
}
`;

const typeDefsB = `
type AggregatePost {
  count: Int!
}

type BatchPayload {
  # The number of nodes that have been affected by the Batch operation.
  count: Long!
}

# The Long scalar type represents non-fractional signed whole numeric values.
# Long can represent values between -(2^63) and 2^63 - 1.
scalar Long

type Mutation {
  createPost(data: PostCreateInput!): Post!
  updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post
  deletePost(where: PostWhereUniqueInput!): Post
  upsertPost(
    where: PostWhereUniqueInput!
    create: PostCreateInput!
    update: PostUpdateInput!
  ): Post!
  updateManyPosts(
    data: PostUpdateManyMutationInput!
    where: PostWhereInput
  ): BatchPayload!
  deleteManyPosts(where: PostWhereInput): BatchPayload!
}

enum MutationType {
  CREATED
  UPDATED
  DELETED
}

# An object with an ID
interface Node {
  # The id of the object.
  id: ID!
}

# Information about pagination in a connection.
type PageInfo {
  # When paginating forwards, are there more items?
  hasNextPage: Boolean!
  # When paginating backwards, are there more items?
  hasPreviousPage: Boolean!
  # When paginating backwards, the cursor to continue.
  startCursor: String
  # When paginating forwards, the cursor to continue.
  endCursor: String
}

type Post implements Node {
  id: ID!
  content: String
  ololo: String
  azaza: Int
}

# A connection to a list of items.
type PostConnection {
  # Information to aid in pagination.
  pageInfo: PageInfo!
  # A list of edges.
  edges: [PostEdge]!
  aggregate: AggregatePost!
}

input PostCreateInput {
  id: ID
  title: String!
  azaza: String
  content: String!
  published: Boolean
}

# An edge in a connection.
type PostEdge {
  # The item at the end of the edge.
  node: Post!
  # A cursor for use in pagination.
  cursor: String!
}

enum PostOrderByInput {
  id_ASC
  id_DESC
  title_ASC
  title_DESC
  content_ASC
  content_DESC
  published_ASC
  published_DESC
  updatedAt_ASC
  updatedAt_DESC
  createdAt_ASC
  createdAt_DESC
}

type PostPreviousValues {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
}

type PostSubscriptionPayload {
  mutation: MutationType!
  node: Post
  updatedFields: [String!]
  previousValues: PostPreviousValues
}

input PostSubscriptionWhereInput {
  # Logical AND on all given filters.
  AND: [PostSubscriptionWhereInput!]
  # Logical OR on all given filters.
  OR: [PostSubscriptionWhereInput!]
  # Logical NOT on all given filters combined by AND.
  NOT: [PostSubscriptionWhereInput!]
  # The subscription event gets dispatched when it's listed in mutation_in
  mutation_in: [MutationType!]
  # The subscription event gets only dispatched when one of the updated fields names is included in this list
  updatedFields_contains: String
  # The subscription event gets only dispatched when all of the field names included in this list have been updated
  updatedFields_contains_every: [String!]
  # The subscription event gets only dispatched when some of the field names included in this list have been updated
  updatedFields_contains_some: [String!]
  node: PostWhereInput
}

input PostUpdateInput {
  title: String
  content: String
  published: Boolean
}

input PostUpdateManyMutationInput {
  title: String
  content: String
  published: Boolean
}

input PostWhereInput {
  # Logical AND on all given filters.
  AND: [PostWhereInput!]
  # Logical OR on all given filters.
  OR: [PostWhereInput!]
  # Logical NOT on all given filters combined by AND.
  NOT: [PostWhereInput!]
  id: ID
  # All values that are not equal to given value.
  id_not: ID
  # All values that are contained in given list.
  id_in: [ID!]
  # All values that are not contained in given list.
  id_not_in: [ID!]
  # All values less than the given value.
  id_lt: ID
  # All values less than or equal the given value.
  id_lte: ID
  # All values greater than the given value.
  id_gt: ID
  # All values greater than or equal the given value.
  id_gte: ID
  # All values containing the given string.
  id_contains: ID
  # All values not containing the given string.
  id_not_contains: ID
  # All values starting with the given string.
  id_starts_with: ID
  # All values not starting with the given string.
  id_not_starts_with: ID
  # All values ending with the given string.
  id_ends_with: ID
  # All values not ending with the given string.
  id_not_ends_with: ID
  title: String
  # All values that are not equal to given value.
  title_not: String
  # All values that are contained in given list.
  title_in: [String!]
  # All values that are not contained in given list.
  title_not_in: [String!]
  # All values less than the given value.
  title_lt: String
  # All values less than or equal the given value.
  title_lte: String
  # All values greater than the given value.
  title_gt: String
  # All values greater than or equal the given value.
  title_gte: String
  # All values containing the given string.
  title_contains: String
  # All values not containing the given string.
  title_not_contains: String
  # All values starting with the given string.
  title_starts_with: String
  # All values not starting with the given string.
  title_not_starts_with: String
  # All values ending with the given string.
  title_ends_with: String
  # All values not ending with the given string.
  title_not_ends_with: String
  content: String
  # All values that are not equal to given value.
  content_not: String
  # All values that are contained in given list.
  content_in: [String!]
  # All values that are not contained in given list.
  content_not_in: [String!]
  # All values less than the given value.
  content_lt: String
  # All values less than or equal the given value.
  content_lte: String
  # All values greater than the given value.
  content_gt: String
  # All values greater than or equal the given value.
  content_gte: String
  # All values containing the given string.
  content_contains: String
  # All values not containing the given string.
  content_not_contains: String
  # All values starting with the given string.
  content_starts_with: String
  # All values not starting with the given string.
  content_not_starts_with: String
  # All values ending with the given string.
  content_ends_with: String
  # All values not ending with the given string.
  content_not_ends_with: String
  published: Boolean
  # All values that are not equal to given value.
  published_not: Boolean
  azaza: String
}

input PostWhereUniqueInput {
  id: ID
}

type Query {
  posts(
    where: PostWhereInput
    orderBy: PostOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): [Post]!
  post(where: PostWhereUniqueInput!): Post
  postsConnection(
    where: PostWhereInput
    orderBy: PostOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): PostConnection!
  # Fetches an object given its ID
  node(
    # The ID of an object
    id: ID!
  ): Node
}

type Subscription {
  post(where: PostSubscriptionWhereInput): PostSubscriptionPayload
}
`;

const schemaA = buildSchema(typeDefsA);
const schemaB = buildSchema(typeDefsB);

const sc = new SchemaComposer(schemaA);
sc.merge(schemaB);
const schemaC = sc.buildSchema();

produces the following error:

c:\graphql-voyager-visual-diff\node_modules\graphql\type\schema.js:262
      throw new Error("Schema must contain uniquely named types but contains multiple types named \"".concat(namedType.name, "\"."));
            ^
Error: Schema must contain uniquely named types but contains multiple types named "Node".
    at typeMapReducer (c:\graphql-voyager-visual-diff\node_modules\graphql\type\schema.js:262:13)
    at typeMapReducer (c:\graphql-voyager-visual-diff\node_modules\graphql\type\schema.js:286:20)
    at Array.reduce (<anonymous>)
    at new GraphQLSchema (c:\graphql-voyager-visual-diff\node_modules\graphql\type\schema.js:145:28)
    at SchemaComposer.buildSchema (c:\graphql-voyager-visual-diff\node_modules\graphql-compose\lib\SchemaComposer.js:153:12)
    at Object.<anonymous> (c:\graphql-voyager-visual-diff\src\index.ts:656:19)
    at Module._compile (internal/modules/cjs/loader.js:1138:30)
    at Module.m._compile (c:\graphql-voyager-visual-diff\node_modules\ts-node\src\index.ts:858:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
    at Object.require.extensions.<computed> [as .ts] (c:\graphql-voyager-visual-diff\node_modules\ts-node\src\index.ts:861:12)

nodkz added a commit that referenced this issue Sep 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants