question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

"Subscription" is a reserved word that means something else to a lot of SaaS businesses

See original GitHub issue

Intended 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:closed
  • Created 3 years ago
  • Reactions:2
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

9reactions
benjamncommented, Aug 26, 2020

@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, and Subscription type names are not completely hard-coded, since they can be changed by a schema declaration:

type MyQueryRootType {...}
type MyMutationRootType {...}
type MySubscriptionRootType {...}

schema {
  query: MyQueryRootType
  mutation: MyMutationRootType
  subscription: MySubscriptionRootType
}

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 telling InMemoryCache about it like so:

new InMemoryCache({
  typePolicies: {
    MySubscriptionRootType: {
      subscriptionType: true,
    },
    // This also works, for completeness:
    MyQueryRootType: {
      queryType: true,
    },
    MyMutationRootType: {
      mutationType: true,
    },
  },
})

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 and Mutation as user-defined types: #6914. This should solve your problem (with no changes to your code) because cache.identify({ __typename: "Subscription", id }) will now be Subscription:${id} (or whatever you configure it to be, using dataIdFromObject or keyFields) rather than ROOT_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.

2reactions
benjamncommented, Aug 28, 2020

@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!

Read more comments on GitHub >

github_iconTop Results From Across the Web

90+ Key SaaS Terms Everyone Should Know About
In this article, I will list some key SaaS terms sales teams, business owners and any marketing team should know.
Read more >
SaaS subscriptions may be short-serving your customers
However, software as a service (SaaS) has perhaps become a bit too interchangeable with subscription models. Every software company now looks to ...
Read more >
SaaS Finance Terminologies: Acronyms of the Trade
As the name suggests, cohorts in SaaS means the customer group that signed up for a subscription around the same time or were...
Read more >
Software as a Service (SaaS) - TechTarget
This definition explains software as a service (SaaS), a common cloud computing model in which a third-party provider hosts applications that customers can ......
Read more >
How the subscription business model is changing - Klipfolio
Companies that provide software as a service – SaaS companies – often sell their services using a subscription model. The concept is simple: ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found