-
Notifications
You must be signed in to change notification settings - Fork 780
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
refactor(experimental): graphql: add multipleAccounts
root query
#2587
base: master
Are you sure you want to change the base?
refactor(experimental): graphql: add multipleAccounts
root query
#2587
Conversation
|
This stack of pull requests is managed by Graphite. Learn more about stacking. Join @buffalojoec and the rest of your teammates on Graphite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are probably a number of good and perhaps surprising reasons not to make a plural field version of this, but I’ll have to spend some time explaining why. I’ll get to it tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, so let's talk about plural fields.
Let's break down the use cases for multiple account fetches.
Use cases
Case 1: You know the accounts statically
If your app straight up knows which accounts it needs, either literally statically (the addresses are constants in the code) or via arguments (there are always n accounts it needs, and the addresses of those will be supplied as inputs to the application) then it should declare them each in the query.
const data = useFragment(graphql`
fragment Home_query on Query {
userProfileAccount: account(address: $userProfileAccountAddress) {
data
}
gameStateAccount: account(address: $gameStateAccountAddress) {
data
}
}
`, props.query);
Pros
- Each
account()
fetch can declare its own arguments (eg.commitment
) independent of the others. - Each account can be refetched independently.
Case 2: You need an unknown number of accounts, you don't have the addresses, but you know the root
In this case, the schema usually offers a field (consider walletAccounts
in the example below) that resolves zero-or-more objects given some context/parent.
const data = useFragment(graphql`
fragment UserProfile_user on User {
walletAccounts(dataSlice: { length: 0, offset: 0 }) {
...Wallet_account
address
}
}
`, props.user);
return (
<ul>
{data.walletAccounts.map(account =>
<Wallet
account={account}
key={account.address}
/>
}
</ul>
);
Pros
- You need not know the addresses of the entities up front to be able to fetch zero-or-more of them
Cons
- The entire fetch for the list of accounts must share the same arguments (eg.
dataSlice
). - You get all of the entities over the wire, whether your app has space to render them or not. Plural fields offer no way to paginate.
Case 3: You need a dynamic list of accounts for which you have the addresses
Consider an app that lets the user check the balance of multiple accounts, by literally supplying the accounts.
You could do this:
const [data, refetch] = useRefetchableFragment(graphql`
fragment AccountBalances_query on Query
@refetchable(queryName: "AccountBalancesRefetchQuery") {
accounts(accounts: $accounts) {
...Balance_account
address
}
}
`, props.query);
return (
<>
<textarea onChange={(e) => {
const addresses = e.target.value.split('\n');
refetch({ accounts: addresses });
}}></textarea>
<ul>
{data.accounts.map(account =>
<Balance
account={account}
key={account.address}
/>
}
</ul>
</>
);
Cons
- Every time an address is added or removed, the entire account list gets fetched again, even the ones you already have locally.
- The entire fetch for the list of accounts must share the same arguments (eg.
commitment
).
Discussion
- The only use case that requires a plural root field is case 3.
- Case 3 can be implemented using a dynamic set of entrypoints instead of a single page that queries a plural root field. Imagine each result UI being, itself, a mini-page based on the address as input. When the user adds or removes an address to their list, a new entrypoint is fetched and rendered, but the others don't change. Think of entrypoints as islands of one or more root queries that make up a larger app, and share data with the entire app.
@@ -2,6 +2,7 @@ export const rootTypeDefs = /* GraphQL */ ` | |||
type Query { | |||
account(address: Address!, commitment: Commitment, minContextSlot: Slot): Account | |||
block(slot: Slot!, commitment: CommitmentWithoutProcessed): Block | |||
multipleAccounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually just follow pluralization rules.
multipleAccounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account] | |
accounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account] |
@@ -2,6 +2,7 @@ export const rootTypeDefs = /* GraphQL */ ` | |||
type Query { | |||
account(address: Address!, commitment: Commitment, minContextSlot: Slot): Account | |||
block(slot: Slot!, commitment: CommitmentWithoutProcessed): Block | |||
multipleAccounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This field, as written, returns a nullable array. This will force the consumer to treat null
and []
the same with two different checks. What you actually want is to unconditionally return an array, maybe empty, but in any case with null
for every unfetchable account.
multipleAccounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account] | |
multipleAccounts(addresses: [Address!]!, commitment: Commitment, minContextSlot: Slot): [Account]! |
@steveluscher Thanks for the detailed breakdown. It sounds to me like this is probably not The Way™. I'm mostly fixated on the fact that case 3 is the most valid use for such a query, and the cons I think are too much to bear. Admittedly, I viewed Case3: Con 2 as a potential Pro, but now I'm starting to see how it could very easily become either one. I think I may abandon this change. With that being said, I was open to the idea of at least introducing the I'll look more into pagination and connections before circling back to this problem. |
Problem
The GraphQL resolver currently supports root queries for
account
andprogramAccounts
, but not formultipleAccounts
. A user can craft multipleaccount
queries in one source, but why go through the trouble if all fields andinput parameters are the same?
Additionally, if the GraphQL resolver had a
resolveMultipleAccounts
resolverfunction, it would make things much easier to add list-based account lookups
on schema types that would otherwise return
[Address]
. With this new resolver,they can instead return
[Account]
, which would allow nested queries on theselists.
Summary of Changes
Introduce the
mutipleAccounts
root query.I've reimplemented the same tests for this new
multipleAccounts
queryas those already provided for the existing
accounts
query.src/__tests__/account-test.ts
src/loaders/__tests__/account-loader-test.ts
ie:
src/__tests__/multiple-accounts-test.ts
src/loaders/__tests__/multiple-accounts-loader-test.ts