-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[RFC] exporting variables between queries #377
Comments
Thanks a lot for starting this discussion, @bbakerman! These are very good discussion points. In my current implementation, I took a quite pragmatic approach. I realize that the decisions that I took might be suboptimal. So, the feature is marked as experimental ATM and my hope is to change semantics as soon as better options emerge:
I also would like to add some point that I found quite interesting as I was working on it. In the following query: query q1 {
person(id: 1) {
email @export(as: "emails")
}
}
query q2 {
person(id: 2) {
email @export(as: "emails")
}
}
mutation SendEmails {
notify(emails: $emails, message: "🍪 Free cookies! 🍪")
} There are several things to consider and define semantics for:
@leebyron In your presentation you haven't touched on the implementation details. At this point, it would be very interesting to learn how you defined the discussed semantics in your implementation. It would be great if you could share this and your thoughts on these points :) |
Wow, I never noticed that in the presentation, there were no explicit variable declarations (from the link above): I wonder if this could be simpler if we used declarations in both cases, for example: query NewsFeed($ids: [ID!]) {
feed {
stories {
id @export(as: $ids)
}
}
}
query StoryComments($ids: [ID!]) {
stories(ids: $ids) {
# ...
}
} In that way, both operations are already syntactically valid and we remove the need for maybe-ambiguous type inference. But, are there shortcomings to that approach? |
Just wanted to revive this conversation, as it would be a huge win for my team right now. Would it be helpful to make the |
Another thought: My team's current requirements could probably be satisfied in a single operation with multiple mutations inside it, rather than multiple operations. This could potentially simplify the export variable type declarations, which could go in a directive next to the normal declared variables of the operation. Something like: mutation CreateAndFollowUser($input: UserInput!) @exported($userId: String) {
createUser(input: $input) {
user {
id @export(as: "userId")
}
}
followUser(userId: $userId) {
success
}
} Here are a couple limitations right off the bat:
I think the first limitation could be remedied by using followUser(userId: $userId) @defer {
...
} And then the I'm not sure I have a remedy for the second limitation yet, but maybe we could punt on that until multiple operation support makes it to spec? 🤷♂️ |
I like the concept of the input ChargePaymentMethodInput {
clientMutationId: String
paymentMethodId: ID!
transaction: TransactionInput!
} mutation TokenizeAndChargePaymentMethod($tokenizeInput: TokenizeCreditCardInput!, $chargeInput: ChargePaymentMethodInput!) {
tokenizeCreditCard(input: $tokenizeInput) {
paymentMethod {
id @export("paymentMethodId")
}
}
chargePaymentMethod(input: $chargeInput) {
transaction {
id
status
}
}
} How were you thinking of passing the exported |
@cmonty I think you basically have to inline all inputs down to the level of the exported variable, but anything deeper you could still include as an operation variable. So your example would be: mutation TokenizeAndChargePaymentMethod(
$tokenizeInput: TokenizeCreditCardInput!,
$transactionInput: TransactionInput!
) @exported($paymentMethodId: String) {
tokenizeCreditCard(input: $tokenizeInput) {
paymentMethod {
id @export(as: "paymentMethodId")
}
}
chargePaymentMethod(input: {
paymentMethodId: $paymentMethodId,
transaction: $transactionInput
}) {
transaction {
id
status
}
}
} |
i wanted to share a couple of thoughts on this Topic 1: Determining the type of an exported variableI think it is better to statically determine type of an exported variable in the following way: case 1if the graph path to the exported field contains any list type field in it then the type should be a list of the exported field type. For example, in query A {
users
{
email @export(as:"emails")
}
}
case 2otherwise, it is just has the type of the exported field. For example, in query A($userId: ID) {
user(userId: $userId)
{
email @export(as:"email")
}
}
case 3if multiple For example, in query A($userId1: ID) {
user(userId: $userId1)
{
email @export(as:"emails")
}
}
query B($userId2: ID) {
user(userId: $userId2)
{
email @export(as:"emails")
}
}
later when using this variable as an input to another operation strict type matching would be used rather than deciding the casting method here. Note: I didn't put to many thoughts on nullablity but i think it can be inferred too. Topic 2. Using the exported variableThe examples that export a list of ids i've seen so far force the mutation that takes the them as an input to be prepared for this, ie: to take a list of ids. And while that may be more efficient in some cases, it forces the implementor of the schema to provide such mutation, rather than letting the client make use of existing mutations that take a id. For example, if we have a query to collect emails but a mutation that takes as input only send one email at a time clients could use it like this: query A {
users
{
email @export(as:"emails")
}
}
mutation B {
sendEmail(email : $email, subject: "hello") @expand(all: "emails", as: "email") {
status
}
} In addition to this, other directives could be provided, such as:
Some uniqueness support could be provided:
Note: Strictly speaking the collected values will sometimes be a List and other times be a Bag (like an unordered Set with duplicates) depending on when the export is declared, for example:
It may not be worth doing, but ideally whether a variable is a bag or a list would be tracked by the server executing the query (not necessarily in the type system), and it should prevent "first" and "last" to be extracted from bags, allowing only "any" and "single" for those cases. Topic 3: Value pairsEdit: Now I believe this is an overkill for a corner case, but sharing anyway. Most of the time just a single or list of identifiers is exported but there are cases when identifier pairs may be needed. For example query A {
friendships(filter: { age: { gt: 10 }})
{
friend1 {
id @export(as: "ids1")
}
friend2 {
id @export(as: "ids2")
}
}
}
mutation B {
upgradeFriendship(id1: $ids1, id1: $ids2) { # how should the separate lists be combined???
status
}
} there is no clear to specify this way imo. To do this maybe it would be better to be able to export not only leaf types but also object types. query A {
friendships(filter: { age: { gt: 10 }}) @export(as: "oldFriendships")
{
friend1 {
id @export(as: "ids1")
}
friend2 {
id @export(as: "ids2")
}
}
}
mutation B {
upgradeFriendship(id1: $oldFriendship.friend1.id, id1: $oldFriendship.friend2.id) { @expand(all: "oldFriendships", as: "oldFriendship")
status
}
} although since dots are not allowed in variable names either that is changed or this will need something like: mutation B {
upgradeFriendship(id1: $id1, id1: $id2) { @expand(all: "oldFriendships", as: "oldFriendship") @unwrap(path: "oldFriendship.friend1.id", as:"id1") @unwrap(path: "oldFriendship.friend2.id", as:"id2")
status
}
} The type of the Topic 4: Mutations in any object typeI've seen comments from people already doing this. People create fields in their object types that are actually mutations (and act as object oriented design methods). Something like this: query A {
friendships(filter: { age: { gt: 10 }})
{
friend1 {
id
}
friend2 {
id
}
upgradeFriendship
}
} It seems somewhat elegant but from what I can tell it violates the spec, object fields should not have side effects. And it is also unclear by looking at the query.
type Friendship {
friend1: User!
friend2: User!
mutation upgradeFriendship : UpgradeFriendshipResult!
# or the uglier alternative:
upgradeFriendship : UpgradeFriendshipResult! @mutation
}
query A {
friendships(filter: { age: { gt: 10 }})
{
friend1 {
id
}
friend2 {
id
}
mutation upgradeFriendship
# or the uglier alternative:
upgradeFriendship @mutation
}
} Topic 5: How much server side scripting do we want?This is no specific proposal, just a reflection. |
Any update on this? |
Hope this feature is prioritized as it has serious performance benefits. Bump! |
RFC exporting variables between queries
This RFC calls for the ability to "export" variables from one query into another.
This allows a single server call to be made and outputs from one query to be fed into
others as variables.
So given:
Output values from operation A would be exported as the variables named "droidId", "r2d2Friends", and "friendNames"
The operation B would be detected to be dependent on operation A and would be executed after operation A completes. It would receive the
value of "droidId" as a variable from operation A
Multiple operation support
This RFC is dependent on #375, that is the ability to execute multiple operations in one server interaction
Variable arity
There are design questions around variable arity. In the example above the "droidId" exported variable can be thought of as a singleton value because the surrounding types are not lists types.
Where as "r2d2Friends" can be thought of as a list value because the surrounding types are list values and hence multiple exported variable values are possible.
One can imagine in certain cases some one might want map like semantics so that "the first / last" value of a list is considered a singleton value.
Naming could be used to indicate behaviour say, for example where plural export names imply a list while singlar variable names imply a singleton value. eg: "droidId" would always be considered a singleton value where as "friendNames" would always be considered a list value.
This is a great discussion point we think.
Consumer Supplied Variable Values
If the consumer caller supplies their own variable values, these will be passed to each operation. The exported variables will be placed into the variables map and will overwrite any existing variables with the same name key.
Exported Variable Value Validation
Validation of the variable values would follow the same rules as consumer supplied variable values.
Validation errors would be placed on each operation execution result
Current implementations
Sangria has this capability today https://sangria-graphql.org/learn/#batch-executor and graphql-java has a PR exploring the idea graphql-java/graphql-java#808
These are based on the ideas presented in a Facebook presentation about capabilities used within Facebook today.
We would love to see the Facebook learnings represented back out to the greater graphql community.
The text was updated successfully, but these errors were encountered: