-
At present, Type Merging can only be setup using static config. Thinking about a trajectory forward, it would be nice to support it as an SDL directive that would allow subservices to be autonomous, and would facilitate the possible future addition of hot reloads of the gateway schema. So, I've got a proposal here that I wanted to bounce off the powers that be – if there's interest here, then I'd be happy to get a library setup that supports this feature: scalar _SelectionKey # may be a string or an array of strings
scalar _Any
directive @merge(selectionSet: String, key: _SelectionKey!, keyArg: String!, args: _Any, types: [String!]) on FIELD_DEFINITION With that directive, we should be able to support just about any standard merge configuration. Basic example that selects simple fields: type Query {
# scalar key field
post1(id: ID!): Post @merge(
selectionSet: "{ id }",
key: "id",
keyArg: "id"
)
# {
# fieldName: 'post1',
# selectionSet: '{ id }'
# args: (id) => ({ id }),
# }
# array of scalar keys, recognized by array argument in schema
post1A(ids: [ID!]!): [Post]! @merge(
selectionSet: "{ id }",
key: "id",
keyArg: "id"
)
# {
# fieldName: 'post1A',
# selectionSet: '{ id }'
# key: ({ id }) => id,
# args: (ids) => ({ ids }),
# }
} In the array scenario above, we should be able to inspect the schema to know that the argument we're referencing is an array. Example that builds an argument object: input PostKey {
id: ID!
networkId: ID!
}
type Query {
# single object key
post2(key: PostKey!): Post @merge(
selectionSet: "{ id networkId }",
key: ["id", "networkId"],
keyArg: "key"
)
# {
# fieldName: 'post2',
# selectionSet: '{ id networkId }'
# args: ({ id, networkId }) => ({ key: { id, networkId } }),
# }
# array of object keys
post2A(keys: [PostKey!]!): [Post]! @merge(
selectionSet: "{ id networkId }",
key: ["id", "networkId"],
keyArg: "keys"
)
# {
# fieldName: 'post2A',
# selectionSet: '{ id networkId }'
# key: ({ id, networkId }) => ({ id, networkId }),
# args: (keys) => ({ keys }),
# }
} Then, we need to account for selection paths, argument aliases, and additional args: input PostKey {
id: ID!
networkId: ID!
}
type Query {
# with selection path
post3(id: ID!): Post @merge(
selectionSet: "{ network { id } }",
key: "network.id",
keyArg: "id"
)
# {
# fieldName: 'post3',
# selectionSet: '{ network { id } }'
# args: (root) => ({ id: root.network.id }),
# }
# with argument alias
post4(key: PostKey!): Post @merge(
selectionSet: "{ id nid }",
key: ["id", "networkId:nid"],
keyArg: "key"
)
# {
# fieldName: 'post4',
# selectionSet: '{ id nid }'
# args: ({ id, nid }) => ({ key: { id, networkId: nid } }),
# }
# with additional arguments
post5(id: ID!, scope: String!): Post @merge(
selectionSet: "{ id }",
key: "id",
keyArg: "id",
args: { scope: "Blah" }
)
# {
# fieldName: 'post5',
# selectionSet: '{ id }'
# args: ({ id }) => ({ id, scope: 'Blah' }),
# }
} Then for federation style entities services, we provide a few helpers: input ProductRepresentation {
id: ID!
price: Float
weight: Int
}
union _Entity = Post | Product
type Query {
# with wildcard "all" selections and "__typename"
# sets up merges for all types in the _Entity abstract:
_entities1(representations: [_Any]!): [_Entity] @merge(
selectionSet: "{ id price weight }",
key: ["*", "__typename"],
keyArg: "representations"
)
# {
# fieldName: '_entities1',
# selectionSet: '{ id }'
# // collects all selectionSet fields from base and @requires fields,
# // also include the GraphQL __typename for this merged type:
# key: ({ id, ... }) => ({ id, ..., __typename: 'Product' }),
# args: (representations) => ({ representations }),
# }
# with limited types...
# does the same thing as above, but only for the Product type
_entities1(representations: [_Any]!): [_Entity] @merge(
selectionSet: "{ id price weight }",
key: ["*", "__typename"],
keyArg: "representations",
types: ["Product"]
)
} So, that's my proposal. It doesn't get into the behavior offered by the |
Beta Was this translation helpful? Give feedback.
Replies: 14 comments 41 replies
-
Hi. Just linking the conversation we had in Discord here similar to this for visibility: https://discord.com/channels/625400653321076807/631489932334202882/773245011466518610 Looks like you were the one who wrote this: https://product.voxmedia.com/2020/11/2/21494865/to-federate-or-stitch-a-graphql-gateway-revisited That was the base for the discussion 😅 |
Beta Was this translation helpful? Give feedback.
-
Straw man question... Because the spec does not yet support custom metadata/directives by introspection, we will have to come up with a way for services to submit their schema to a schema registry. That is also ideal because not all servers want to enable introspection... In which case, the submission format might not necessarily be any easier for end users to use SDL/directives, they could just use code. Basically, first we need to think about schema registry logic, and then we can afterwards figure out whether we want/need these directives... At least that is what I am thinking about.... In terms of how the schema registry should then work, it basically is a code repository, I'm not sure that we should reinvent the wheel, maybe it should just be a private npm registry via verdaccio, or a GitHub repo, and the gateway should just poll for pull requests, patch updates, etc. A lot of great choices! we should probably open a new discussion to discuss that... Using graphql-helix, also, incredibly simple to implement hot reload. |
Beta Was this translation helpful? Give feedback.
-
Actually, thought of a different approach to above that I found appealing, so taking this up again. Currently, you suggested this: type Query {
# scalar key field
post1(id: ID!): Post @merge(
selectionSet: "{ id }",
key: "id",
keyArg: "id"
)
# {
# fieldName: 'post1',
# selectionSet: '{ id }'
# args: (id) => ({ id }),
# } What about something like this: type Query {
# scalar key field
post1(id: ID!): Post @stitch(
query: "post1(id: $id)",
variables: [{
name: "$id",
value: "id",
}],
)
# {
# fieldName: 'post1',
# selectionSet: '{ id }'
# args: (id) => ({ id }),
# } Where the expressions are evaluated in the context of the original result.... Have to think about it a bit more to see if it covers all of your examples... |
Beta Was this translation helpful? Give feedback.
-
The idea is interesting. My initial reaction is that the query param is a
bit redundant with the method signature sitting right next to it, but I see
the intent there. I also like that it keeps the declaration on the
stitching service, allowing lots of supporting information to be inflected,
rather than on the stitched type declaration.
…On Sat, Nov 7, 2020 at 3:29 PM Yaacov Rydzinski ***@***.***> wrote:
Actually, thought of a different approach to above that I found appealing,
so taking this up again.
Currently, you suggested this:
type Query { # scalar key field
post1(id: ID!): Post @merge(
selectionSet: "{ id }",
key: "id",
keyArg: "id"
) # { # fieldName: 'post1', # selectionSet: '{ id }' # args: (id) => ({ id }), # }
What about something like this:
type Query { # scalar key field
post1(id: ID!): Post @stitch(
query: "post1(id: $id)",
variables: [{
name: "$id",
value: "id",
}],
) # { # fieldName: 'post1', # selectionSet: '{ id }' # args: (id) => ({ id }), # }
Where the expressions are evaluated in the context of the original
result....
Have to think about it a bit more to see if it covers all of your
examples...
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2022 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROGIFTSNVT5UCO7NOWLSOWUZFANCNFSM4RHQS7YA>
.
|
Beta Was this translation helpful? Give feedback.
-
Iterating. . type Query {
# scalar key field
post1(id: ID!): Post @merge(
arguments: [{
name: "id",
value: "originalResult.id",
}],
)
# {
# fieldName: 'post1',
# selectionSet: '{ id }'
# args: (id) => ({ id }),
# } We can infer the id selectionSet from its presence in arguments... |
Beta Was this translation helpful? Give feedback.
-
That works for basic scalar args, but doesn’t facilitate the construction
of input objects (which have application outside of just the federation
usecase).
…On Sat, Nov 7, 2020 at 3:54 PM Yaacov Rydzinski ***@***.***> wrote:
Iterating. .
type Query { # scalar key field
post1(id: ID!): Post @merge(
arguments: [{
name: "id",
value: "originalResult.id",
}],
) # { # fieldName: 'post1', # selectionSet: '{ id }' # args: (id) => ({ id }), # }
We can infer the id selectionSet from its presence in arguments...
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2022 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROFG34SHVHIHOH5GWFLSOWXXZANCNFSM4RHQS7YA>
.
|
Beta Was this translation helpful? Give feedback.
-
I think we can just allow objects within the value field. So I don't see that as a huge problem... I I'm wondering how we can avoid specification of a key within the type declaration either with your method above or my method... Seems like syntax you have above would not distinguish between different entities that have identically named fields which are part of a key for one entity but not for another.... |
Beta Was this translation helpful? Give feedback.
-
@yaacovCR Awesome. This is exactly what I wanted to have as a final goal but suggested otherwise since I was not sure if you had the bandwidth to take the SDL approach. Btw, as I understand here, you will still be pushing the SDL with directives to registry right? Since there is no introspection support as of now as you rightly mentioned. So, will the registry parse the directives and do the stitching from that? Also, I wanted to make sure that by doing this, we would not loose the advantage of every service being self-contained. So, even when the SDL might define directives on how it has to be merged with other service(s), I hope that we will still be able to use the service in isolation without registry or any other component, have the GQL endpoint and test the component in isolation since that is a huge differentiator as opposed to federation. Cause for individual service to be valid in isolation, I guess these directives have to be define in every service in addition to the regular SDL. and then merged in the registry which is then pulled by the gateway if I am not wrong. |
Beta Was this translation helpful? Give feedback.
-
Fuller sketch of above... NOTE: The below has been edited based upon feedback below. type Query {
# scalar key field
post1(id: ID!): Post @merge(args: "id: $key.id")
# {
# fieldName: 'post1',
# selectionSet: '{ id }'
# args: (id) => ({ id }),
# }
# array of scalar keys, recognized by array argument in schema
post1A(ids: [ID!]!): [Post]! @merge(args: "ids: [[$key.id]]")
# {
# fieldName: 'post1A',
# selectionSet: '{ id }'
# key: ({ id }) => id,
# args: (ids) => ({ ids }),
# }
} Examples that builds an argument object: input PostKey {
id: ID!
networkId: ID!
}
type Query {
# single object key
post2(key: PostKey!): Post @merge(args: "key: { id: $key.id , networkId: $key.networkId }")
)
# {
# fieldName: 'post2',
# selectionSet: '{ id networkId }'
# args: ({ id, networkId }) => ({ key: { id, networkId } }),
# }
# array of object keys
post2A(keys: [PostKey!]!): [Post]! @merge(args: "keys: [[{ id: $key.id , networkId: $key.networkId }]]")
# {
# fieldName: 'post2A',
# selectionSet: '{ id networkId }'
# key: ({ id, networkId }) => ({ id, networkId }),
# args: (keys) => ({ keys }),
# }
} Examples that builds a nested argument object: input PostInput {
key: PostKey!
}
input PostsInput {
keys: [PostKey!]!
}
input PostKey {
id: ID!
networkId: ID!
}
type Query {
# single object key
post2B(input: PostInput!): Post @merge(args: "input: { key: [[{ id: $key.id , networkId: $key.networkId }]] }")
)
# {
# fieldName: 'post2B',
# selectionSet: '{ id networkId }'
# args: ({ id, networkId }) => ({ input: { key: { id, networkId } } }),
# }
# array of object keys
post2C(input: PostsInput!): [Post]! @merge(args: "input: { { keys: [[{ id: $key.id , networkId: $key.networkId }]] }")
# {
# fieldName: 'post2C',
# selectionSet: '{ id networkId }'
# key: ({ id, networkId }) => ({ id, networkId }),
# args: (keys) => ({ input: { keys } }),
# }
} Then, we need to account for selection paths: input PostKey {
id: ID!
networkId: ID!
}
type Query {
# with selection path
post3(id: ID!): Post @merge(args: "id: $key.network.id")
# {
# fieldName: 'post3',
# selectionSet: '{ network { id } }'
# args: (root) => ({ id: root.network.id }),
# } As well as argument aliases: type Query {
post4(key: PostKey!): Post @merge(args: "id: $key.id, networkId: $key.nid")
# {
# fieldName: 'post4',
# selectionSet: '{ id nid }'
# args: ({ id, nid }) => ({ key: { id, networkId: nid } }),
# } And additional args: type Query {
post5(id: ID!, scope: String!): Post @merge(args: "id: $key.id, scope: \"Blah\"")
# {
# fieldName: 'post5',
# selectionSet: '{ id }'
# args: ({ id }) => ({ id, scope: 'Blah' }),
# }
} Then for federation style entities services: input ProductRepresentation {
id: ID!
price: Float
weight: Int
}
union _Entity = Post | Product
type Product @base(selectionSet: "{ id }") {
id: ID!
shippingEstimate: Float! @computed(selectionSet: "{ price weight }")
...
}
type Post @key(selectionSet: "{ id ") {
...
}
type Query {
_entities1(representations: [_Any]!): [_Entity] @merge(args: "representations: [[$key]]")
# {
# fieldName: '_entities1',
# selectionSet: '{ id }'
# key: ({ id, ... }) => ({ id, ..., __typename: 'Product' }),
# argsFromKeys: (representations) => ({ representations }),
# }
# with limited types...
# does the same thing as above, but only for the Product type
_entities1(representations: [_Any]!): [_Entity] @merge(
args: "representations: [[$key]]",
types: ["Product"]
)
} Thoughts? To me, syntax seems simpler, although we have to do more custom parsing of plain text for these expressions? |
Beta Was this translation helpful? Give feedback.
-
Going to respond to a few items individually here.. type Post @requires(selectionSet: "{ id nid }") {
...
}
type Query {
post5(id: ID!, scope: String!): Post @merge(
args: [{
name: "id",
value: "{ id: $originalResult.id }",
}, {
name: "scope",
value: "\"Blah\"",
}],
)
# {
# fieldName: 'post5',
# selectionSet: '{ id }'
# args: ({ id }) => ({ id, scope: 'Blah' }),
# }
} The Then concerning this: Personally I find it a bit tawdry implementing a structured object within a plain string. It seems way too easy to write incorrectly without a structured spec to validate against. I guess, theoretically that is JavaScript embedded within GraphQL? I have to ask, why not just permit structure object inputs all the way down... ie:
|
Beta Was this translation helpful? Give feedback.
-
Then for type Query {
topProducts(first: Int = 5): [Product]
__products(upcs: [String]): [Product] @merge(args: { name: "upcs", value: "$originalResult.upc" })
} Is this to say you can only submit a single argument with a given name and value? I'd expect args to be structured more like this using some kind of JSONObject/ type Query {
__products(upcs: [String]): [Product] @merge(args: { upcs: "$originalResult.upc" })
} Which would allow for the definition of additional static arguments, such as: type Query {
__products(upcs: [String]): [Product] @merge(args: { upcs: "$originalResult.upc", limit: 50 })
} Off the top of my head I can't remember what GraphQL permits for unstructured object scalars, so perhaps that is not valid syntax. At which time, I'd think at the very least |
Beta Was this translation helpful? Give feedback.
-
Then let's talk about the federation example: input ProductRepresentation {
upc: String
price: Float
weight: Int
}
type Query {
__products(representations: [ProductRepresentation]): [Product] @merge(args: { name: "representations", value: "$originalResult" })
}
type Product @requires(selectionSet: "upc") {
upc: String!
inStock: Boolean
shippingEstimate: Int @computed(selectionSet: "{ price weight }")
} Passing |
Beta Was this translation helpful? Give feedback.
-
See #2227, working on having your cake + eating, i.e. the At least that's my current approach. Making slow, steady progress. |
Beta Was this translation helpful? Give feedback.
-
See https://github.com/gmac/schema-stitching-demos under stitching directives sdl |
Beta Was this translation helpful? Give feedback.
See https://github.com/gmac/schema-stitching-demos under stitching directives sdl