"Subscription" is a reserved word that means something else to a lot of SaaS businesses
See original GitHub issueIntended outcome:
I work at a SaaS company. We have a web app. Our data model includes accounts
which have subscriptions
to products
. There I was, building a form for updating an account’s subscriptions, and my Apollo Client started behaving in an unexpected way. Reloading the page no longer updated the code for my newest changes. The data coming into the front end didn’t match what was coming out of the database. So strange!
I had defined Types on the backend:
class Types::SubscriptionType < Types::BaseObject
implements GraphQL::Relay::Node.interface
use_adapter Adapters::SubscriptionTypeObjectAdapter
global_id_field :id
field :account, Types::AccountType, null: false
field :product, Types::ProductType, null: false
field :quantity, Integer, null: false
field :display_name, String, null: false
field :price, Float, null: false
end
class Types::AccountType < Types::BaseObject
use_adapter Adapters::AccountTypeObjectAdapter
global_id_field :id
field :subscriptions, [Types::SubscriptionType], null: true
end
I had a query on the front end:
const MY_SUBSCRIPTION_QUERY = gql`
query AccountSubscriptionsQuery($accountId: Int!) {
accountById(accountId: $accountId) {
id
subscriptions {
id
displayName
expirationDate
quantity
product {
id
price
name
}
}
}
}
`
I used the Apollo client’s useQuery
to query the backend for the data.
import { useQuery, gql } from '@apollo/client';
import SubscriptionForm from './SubscriptionForm';
const SubscriptionFormDataFetcher = () => {
const { data, loading, error } = useQuery(MY_SUBSCRIPTION_QUERY, {
variables: { accountId: 'accountId' },
});
if (loading) {
return 'loading...';
}
const account = data.accountById;
return (
<SubscriptionForm account={account} />
);
}
The query should have returned an account with an array of subscription
objects. The correct JSON blob got to the browser in the network tab:
data: {
accountById: {
id: 'accountId',
subscriptions: [
{ id: 'firstSubscriptionId' },
{ id: 'secondSubscriptionId' }
]
}
}
But the data returned from useQuery
contained multiple copies of the first subscription
object in the array. When I pasted my query into graphiql, it returned the correct data, but something inside of Apollo Client was transforming the account.subscriptions
array from [subscription1, subscription2]
to [subscription1, subscription1]`.
data: {
accountById: {
id: 'accountId',
subscriptions: [
{ id: 'firstSubscriptionId' },
{ id: 'firstSubscriptionId' }
]
}
}
Actual outcome:
It took me a day or two to figure out that the problem was the word “Subscriptions” itself. Apollo Client uses subscription
as a reserved word. But there was no error message, no warning, no explanation from the software itself to tell me that I couldn’t define a SubscriptionType
.
Surely I’m not the only developer working on a Software-as-a-Service platform that uses subscriptions
as part of our business logic and data model. It’s a hole that I stepped in, and I’m sure it’s affected other developers as well. For now, my GraphQL schema is calling them subskriptions
but I think it would be swell if the software itself could return a helpful error message or warning when someone defines a SubscriptionType
.
How to reproduce the issue:
Create a type named SubscriptionType
and try to query for it.
Or use my demo: https://github.com/abbyhowell/react-apollo-error-demo
Versions
npx: installed 1 in 1.779s
System: OS: macOS 10.15.6 Binaries: Node: 10.16.0 - ~/.asdf/installs/nodejs/10.16.0/bin/node Yarn: 1.21.1 - /usr/local/bin/yarn npm: 6.9.0 - ~/.asdf/installs/nodejs/10.16.0/bin/npm Browsers: Chrome: 84.0.4147.135 Firefox: 77.0.1 Safari: 13.1.2 npmPackages: @apollo/client: ^3.0.2 => 3.1.3
Issue Analytics
- State:
- Created 3 years ago
- Reactions:2
- Comments:5 (2 by maintainers)
@abbyhowell Thanks for the thorough explanation and reproduction!
I’m convinced by your framing of this problem that we should allow
Subscription
as a user-defined type, even though it also happens to be the default type name for GraphQL subscription operations. I will have this fixed in the next release of@apollo/client
(likely 3.1.4).Technically, the
Query
,Mutation
, andSubscription
type names are not completely hard-coded, since they can be changed by aschema
declaration:See this section of the spec for more details.
I suppose this means you could have solved your problem by changing the name of the root
subscription
type in your schema, and then tellingInMemoryCache
about it like so:If you’re a die-hard GraphQL spec literalist, this might be the solution for you! However, in the case of subscriptions, this extremely obscure configuration has no real benefits besides preventing collisions with your other
Subscription
type, because (as I learned today) you can’t even request the__typename
of a subscription operation, because subscriptions must only have a single root field.In short, my proposed solution is to allow both
Subscription
andMutation
as user-defined types: #6914. This should solve your problem (with no changes to your code) becausecache.identify({ __typename: "Subscription", id })
will now beSubscription:${id}
(or whatever you configure it to be, usingdataIdFromObject
orkeyFields
) rather thanROOT_SUBSCRIPTION
(a behavior that was never actually useful, as far as I can tell).I hope I don’t end up regretting this change, but I’m comfortable sending folks to this issue to understand why it felt more important to address the problem than to leave things the way they were.
@abbyhowell This should be working as desired in v3.1.4 (released last night). Closing now, but feel free to reopen or comment here if you have any additional troubles. Thanks again for the surprising but undeniable bug report!