Skip to content
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

GraphQL default schema issues and potential for customisation #5777

Closed
omairvaiyani opened this issue Jul 6, 2019 · 7 comments
Closed

GraphQL default schema issues and potential for customisation #5777

omairvaiyani opened this issue Jul 6, 2019 · 7 comments
Labels
type:feature New feature or improvement of existing feature

Comments

@omairvaiyani
Copy link
Contributor

omairvaiyani commented Jul 6, 2019

I'd first like to congratulate the core contributors for an excellent job at introducing GraphQL into Parse. It really is a big step forward at keeping the Parse project modern and exciting for new developers.

I can see that the approach has been one of plug-n-play; all types, queries and mutations are automatically generated which means that for many teams, they can swap out many of their existing queries with GraphQL. This is pretty amazing stuff - but it does have inherent tradeoffs. I'll list these concerns out in hopes of some productive discussion to guide future development before the community's usage of the ParseGraphQLServer becomes too reliant on the status quo.

Schema overload

Autogenerating the graph using the entire database schema, and adding all default mutations for each class is like bending GraphQL into an exact map of what Parse already offers. The real power of GraphQL is the ability to generate representations of your database that you would like to make available to your clients, incrementally and evolutionarily. Its an opportunity for enterprises and scaling teams to re-think their schema, and decide what really should be accessed and mutated.

I think an ability to configure the Parse GraphQL server should be introduced that does one or two of the following:

  • Turn the default schema generation off, and instead pass in ones own schema
  • Override specific types/queries/mutations for selected classes
  • Add completely custom types, queries and mutations that do not necessarily tie into the default schema

Now, we don't want to throw the baby out with the bathwater here. There is some impressive coding done here by the contributors around the use of resolvers and mapping of GraphQL query constraints with that of Parse. Careful consideration is needed to maintain these helpful modules whilst allowing developers to extend the ParseGraphQLServer to their specific needs.

Nested mutations lose atomicity

Mutations differ from queries in that they run in series, rather than in parallel. This is to allow atomic mutations. Unfortunately, nesting mutations as is the case with the Parse implementation, loses this synchronous execution and results in tradeoff of atomic mutations.

mutation StockMarketAction {
  // executes first
   incrementStockPrice(ticker: "GOOGL", incrementBy: 1) {
     price
   }
  // waits for the above to complete, then executes
   sellStock(ticker: "GOOGL", numShares: 50) {
     numShares
   }
}

As it currently stands, Parse's GraphQL Schema nests all mutations under objects (and users) like so:

mutation StockMarketAction {
   objects {
      // runs immediately
      updateStock(objectId: "googl", fields: { stockPrice: 50 }) {
        updatedAt
      }
      // runs immediately, above stock price update may not be updated before this order is executed
      createStockOrder(fields: { type: "sell", numShares: "50", stockId: "googl"  }) {
        objectId
      }
   }

}

No obvious location for Cloud Functions

CloudFunctions don't really have a home in mutations as it stands. This means that clients will have to maintain two separate network requests layers should they require full-usage of a parse server API. Whilst we are striving to reduce our reliance of RPC's, there are many instances where an RPC approach is required, and so we'd very much like CloudFunctions to be first-class citizens in ParseGraphQLServer.

--

I was tempted to begin forking and adding PRs to establish some of the above suggestions, but I think it's absolutely critical that some discussion is done before further code is cooked.

@davimacedo
Copy link
Member

davimacedo commented Jul 6, 2019

Hi @omairvaiyani

First of all, I'd like to thank you for trying the brand new GraphQL API, do this detailed analysis and share your feedback. You are very welcome to contribute to the project in discussions and PRs.

The current version of the GraphQL API is just what we considered as the minimum to run a GraphQL API on top of Parse Server, so we could get feedback from the community (like yours) and continue working. There is still a lot that can be done to leverage all the GraphQL potential into Parse Server project. We created a first short to do list here. Feel free to suggest new tasks and/or work in any of them.

I agree with most of your points. Let me comment each of them (maybe we could create a separate issue/task for each of them):

  • Plug-and-Play: In this first PR we made sure to keep the same features that the current community loves in the REST API: auto-generation, schemaless, plug-and-play approach... There are for sure some tradeoffs that we can now start discussing and addressing based on the community feedback.
  • Schema overload: I completely agree with you. There is already in our roadmap a task in which the developer will be able to select which classes' operations they want to be automatically generated. Then the developer should also be able to create additional queries/mutations using cloud code.
  • Nested Mutations: We decided to create the "packages" to organize users, objects, etc, operations because we figured out that, with the auto generation, it would be created so many operations in addition to the large amount that Parse has by default and it would be much harder for a new develoepr to understand how it should work if not grouped by "packages". We were not aware about the risk of loosing the serial execution functionality, but I think there is no problem, since you can run something like this (I'm not sure that it will execute in series, though. Would you be willed to create a test case for this?):
mutation StockMarketAction {
   updateStock: objects {
      updateStock(objectId: "googl", fields: { stockPrice: 50 }) {
        updatedAt
      }
   }
  createStockOrder: objects {
      createStockOrder(fields: { type: "sell", numShares: "50", stockId: "googl"  }) {
        objectId
      }
  }
}
  • Coud Functions: It is already in our roadmap to include support for cloud functions in the GraphQL API.

Again, thanks for your help. Let's keep the discussion for each of the items and tackle them as a team!

@TomWFox TomWFox added discussion type:feature New feature or improvement of existing feature labels Jul 6, 2019
@omairvaiyani
Copy link
Contributor Author

omairvaiyani commented Jul 7, 2019

Hi @davimacedo - I'd be happy to join the project and start ticking off some of the items on your todo list.

I agree about creating separate tasks for some of the issues above; overall I'm just pleased to know that you have a plan in place already and it seems that you already have considered most of the concerns I've highlighted!

Plug-and-play
This was more of an observation and I think it's the right approach to get the "MVP", so I think it can be left as is.

Schema Overload
This as you said is covered by this task here. It's the one I'm most interested in getting right - are you happy to assign it to me? In terms of further discussions around it, I'm thinking the best approach is to let me create a PR with some preliminary work to let us have a base to discuss from?

Nested Mutations:
So a few points here. Given that we have schema autogeneration and plan to let developers issue their own in the near future, nesting the default mutations under predefined keys is overall a really good idea - to be honest, one of my gripes with mutations in GraphQL is that of namespacing/modularity, but this has been a point of contention over a 3 year long discussion with yet no resolve. Your solution for the serial execution is a good compromise and on the face of it, does work:

mutation SyncExecutionTest {
  first: objects {
    createGraphTest(fields: { name: "Numero Uno", delay: 3000 }) {
      createdAt
    }
  }
  second: objects {
    createGraphTest(fields: { name: "Numero Dos" }) {
      createdAt
    }
  }
}
{
  "data": {
    "first": {
      "createGraphTest": {
        "createdAt": "2019-07-07T11:24:56.951Z" 
      }
    },
    "second": {
      "createGraphTest": {
        "createdAt": "2019-07-07T11:25:00.123Z"
      }
    }
  }
}

Logs:

// Saving "Numero Uno"
// await 3000
// Saved
// Saving "Numero Dos"
// Saved

Cloud Functions:
I have a feeling that this one will be relatively straightforward in deciding where it belongs in the graph. It'll be interesting to decide whether each registered function gets its own node, or whether we have one node called "cloudFunction" that accepts the function name as a parameter.

Hope this helps, I'm happy to get started on the Schema Overload issue as soon as possible.

@davimacedo
Copy link
Member

@omairvaiyani we are very happy to have you on board! Please find my comments below.

Schema Overload
This as you said is covered by this task here. It's the one I'm most interested in getting right - are you happy to assign it to me? In terms of further discussions around it, I'm thinking the best approach is to let me create a PR with some preliminary work to let us have a base to discuss from?

I assigned the task to you and I will discuss it further in your PR

Nested Mutations:
So a few points here. Given that we have schema autogeneration and plan to let developers issue their own in the near future, nesting the default mutations under predefined keys is overall a really good idea - to be honest, one of my gripes with mutations in GraphQL is that of namespacing/modularity, but this has been a point of contention over a 3 year long discussion with yet no resolve. Your solution for the serial execution is a good compromise and on the face of it, does work:

I've seen this discussion about namespaces and that's why I've decided to fo with the nested mutations. So, in your opinion, should we keep the current approach?

Cloud Functions:
I have a feeling that this one will be relatively straightforward in deciding where it belongs in the graph. It'll be interesting to decide whether each registered function gets its own node, or whether we have one node called "cloudFunction" that accepts the function name as a parameter.

I'd love to have both options:

  • a generic mutation called call that receives the function name and an input object, and return an output object
  • a custom operation for each function in which the developer will be able to specify the schema
    What do you think?

@omairvaiyani
Copy link
Contributor Author

I've seen this discussion about namespaces and that's why I've decided to fo with the nested mutations. So, in your opinion, should we keep the current approach?

I think let's keep the nesting as it's the lesser of two evils. Introducing 100+ mutations and queries all in one namespace will be a deterrent for end users trying to construct queries. Without nesting, we would also be forced to share the space between the base types and any additional types from schema stitching future. The workaround for atomicity issue will do fine for those that need it.

I'd love to have both options:
a generic mutation called call that receives the function name and an input object, and return an output object
a custom operation for each function in which the developer will be able to specify the schema
What do you think?

Given how flexible our upcoming Parse GraphQL Config is, I don't see any reason why we can't have both! A generic mutation should definitely be set as a default, just as we set the generic create, update, destroy and similar mutations. We can decide whether to pre-load all registered cloud functions as their mutations under the namespace cloud, or whether to leave it as hidden unless configured to be added in the config. My one concern here would be registering the cloud function names which in Parse can be registered as any valid string. But special characters will cause syntax issues with graphql. For example, the function "moduleA-doThing" is perfectly acceptable in Cloud Code, but is not accepted in .graphql files where a query would be constructed. A workaround would be to clean the function name before registering it as a mutation under graphql, though this may lead to confusion.

@davimacedo
Copy link
Member

Nice! It sounds we have a plan!

Talking about the function names, I'd prefer to validate the name and, in the case it does not pass, we can just log a warn and do not generate the mutation for it. It would have to be called using the generic mutation then. We could additionally have some way to enter the GraphQL mutation name. But these details I think we can discuss further when tackling this feature.

douglasmuraoka pushed a commit that referenced this issue Jul 18, 2019
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.

Therefore, regarding #5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with #5782 and #5818, when merged, closes the issue.

How it works:

1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
      parseGraphQLServer = new ParseGraphQLServer(parseServer, {
        graphQLPath: '/graphql',
        graphQLCustomTypeDefs: gql`
          extend type Query {
            custom: Custom @namespace
          }
           type Custom {
            hello: String @resolve
            hello2: String @resolve(to: "hello")
            userEcho(user: _UserFields!): _UserClass! @resolve
          }
        `,
      });
```

Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.

2. Once the schema was set, you just need to write regular cloud code functions:
```
      Parse.Cloud.define('hello', async () => {
        return 'Hello world!';
      });

      Parse.Cloud.define('userEcho', async req => {
        return req.params.user;
      });
```

3. Now you are ready to play with your new custom api:
```
query {
  custom {
    hello
    hello2
    userEcho(user: { username: "somefolk" }) {
      username
    }
  }
}
```
should return
```
{
  "data": {
    "custom": {
      "hello": "Hello world!",
      "hello2": "Hello world!",
      "userEcho": {
        "username": "somefolk"
      }
    }
  }
}
```
@davimacedo
Copy link
Member

@omairvaiyani do you think that we've already addressed everything here and we can close this issue?

@omairvaiyani
Copy link
Contributor Author

Yup, very quick turnaround I'd say!

UnderratedDev pushed a commit to UnderratedDev/parse-server that referenced this issue Mar 21, 2020
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.

Therefore, regarding parse-community#5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with parse-community#5782 and parse-community#5818, when merged, closes the issue.

How it works:

1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
      parseGraphQLServer = new ParseGraphQLServer(parseServer, {
        graphQLPath: '/graphql',
        graphQLCustomTypeDefs: gql`
          extend type Query {
            custom: Custom @namespace
          }
           type Custom {
            hello: String @resolve
            hello2: String @resolve(to: "hello")
            userEcho(user: _UserFields!): _UserClass! @resolve
          }
        `,
      });
```

Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.

2. Once the schema was set, you just need to write regular cloud code functions:
```
      Parse.Cloud.define('hello', async () => {
        return 'Hello world!';
      });

      Parse.Cloud.define('userEcho', async req => {
        return req.params.user;
      });
```

3. Now you are ready to play with your new custom api:
```
query {
  custom {
    hello
    hello2
    userEcho(user: { username: "somefolk" }) {
      username
    }
  }
}
```
should return
```
{
  "data": {
    "custom": {
      "hello": "Hello world!",
      "hello2": "Hello world!",
      "userEcho": {
        "username": "somefolk"
      }
    }
  }
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:feature New feature or improvement of existing feature
Projects
None yet
Development

No branches or pull requests

4 participants