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

Add cloud code resolver example #771

Merged
merged 5 commits into from Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
163 changes: 154 additions & 9 deletions _includes/graphql/customisation.md
Expand Up @@ -71,7 +71,7 @@ interface ParseGraphQLConfiguration {

// By default, all write mutation types are
// exposed for all included classes. Use this to disable
// the available mutation types for this class and optionally
// the available mutation types for this class and optionally
// override the default generated name with aliases.
mutation?: {
create?: boolean;
Expand Down Expand Up @@ -99,7 +99,7 @@ We have provided a public API in `ParseGraphQLServer` which accepts the above JS
// ... ParseGraphQLConfiguration
};

await parseGraphQLServer.setGraphQLConfig(config);
await parseGraphQLServer.setGraphQLConfig(config);
```

### Include or Exclude Classes
Expand Down Expand Up @@ -207,14 +207,14 @@ By default, the schema exposes a `get` and `find` operation for each class, for
"query": {
"get": true,
"find": false
}
}
},
{
"className": "Review",
"query": {
"get": false,
"find": true
}
}
}
]
}
Expand All @@ -229,13 +229,13 @@ By default, generated query names use pluralized version of `className`. You can
"className": "Likes",
"query": {
"getAlias": "like"
}
}
},
{
"className": "Data",
"query": {
"findAlias": "findData"
}
}
}
]
}
Expand All @@ -255,15 +255,15 @@ By default, the schema exposes a `create`, `update` and `delete` operation for e
"create": true,
"update": true,
"destroy": true
}
}
},
{
"className": "Review",
"mutation": {
"create": true,
"update": false,
"destroy": true
}
}
}
]
}
Expand All @@ -282,8 +282,153 @@ You can optionally override the default generated mutation names with aliases:
"createAlias": "newRecord",
"updateAlias": "changeRecord",
"destroyAlias": "eraseRecord"
}
}
}
]
}
```

## Cloud Code Resolvers

The Parse GraphQL API supports the use of custom user-defined schema. The [Adding Custom Schema](#adding-custom-schema) section explains how to get started using this feature.

Cloud Code functions can then be used as custom resolvers for your user-defined schema.

### Query Resolvers

Here's an example of a custom query and its related cloud code function resolver in action:

```graphql
# schema.graphql
extend type Query {
hello: String! @resolve
}
```

```js
// main.js
Parse.Cloud.define("hello", () => "Hello, world!");
```

```js
// Header
{
"X-Parse-Application-Id": "APPLICATION_ID",
"X-Parse-Master-Key": "MASTER_KEY" // (optional)
}
```

```graphql
query hello {
hello
}
```

The code above should resolve to this:

```js
// Response
{
"data": {
"hello": "Hello, world!"
}
}
```

### Mutation Resolvers

At times, you may need more control over how your mutations modify data than what Parse's auto-generated mutations can provide. For example, if you have classes named `Item` and `CartItem` in the schema, you can create an `addToCart` custom mutation that tests whether a specific item is already in the user's cart. If found, the cart item's quantity is incremented by one. If not, a new `CartItem` object is created.

The ability to branch your resolver logic enables you to replicate functionality found in Parse's auto-generated `createCartItem` and `updateCartItem` mutations and combine those behaviors into a single custom resolver.

```graphql
# schema.graphql
extend type Mutation {
addToCart(id: ID!): CartItem! @resolve
}
```

**Note**: The `id` passed in to your Cloud Code function from a GraphQL query is a [Relay Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm); it is **not** a Parse `objectId`. Most of the time the `Relay Node Id` is a `Base64` of the `ParseClass` and the `objectId`. Cloud code does not recognize a `Relay Node Id`, so converting it to a Parse `objectId` is required.

Decoding and encoding `Relay Node Ids` in Cloud Code is needed in order to smoothly interface with your client-side GraphQL queries and mutations.

First, install the [Relay Library for GraphQL.js](https://www.npmjs.com/package/graphql-relay) as a required dependency to enable decoding and encoding `Relay Node Ids` in your cloud code functions:

```sh
$ npm install graphql-relay --save
```

Then, create your `main.js` cloud code file, import `graphql-relay`, and build your `addToCart` function:

```js
// main.js
const { fromGlobalId, toGlobalId } = require('graphql-relay');

Parse.Cloud.define("addToCart", async (req) => {
const { user, params: { id } } = req;

// Decode the incoming Relay Node Id to a
// Parse objectId for Cloud Code use.
const { id: itemObjectId } = fromGlobalId(id);

// Query the user's current cart.
const itemQuery = new Parse.Query("Item");
const item = await itemQuery.get(itemObjectId);
const cartItemQuery = new Parse.Query("CartItem");
cartItemQuery.equalTo("item", item);
cartItemQuery.equalTo("user", user);
const [existingCartItem] = await cartItemQuery.find();
let savedCartItem;

if (existingCartItem) {
// The item is found in the user's cart; increment its quantity.
const quantity = await existingCartItem.get("quantity");
existingCartItem.set("quantity", quantity + 1);
savedCartItem = await existingCartItem.save();
} else {
// The item has not yet been added; create a new cartItem object.
const CartItem = Parse.Object.extend("CartItem");
const cartItem = new CartItem();
savedCartItem = await cartItem.save({ quantity: 1, item, user });
}

// Encode the Parse objectId to a Relay Node Id
// for Parse GraphQL use.
const cartItemId = toGlobalId('CartItem', savedCartItem.id);

// Convert to a JSON object to handle adding the
// Relay Node Id property.
return { ...savedCartItem.toJSON(), id: cartItemId };
});
```

```js
// Header
{
"X-Parse-Application-Id": "APPLICATION_ID",
"X-Parse-Session-Token": "r:b0dfad1eeafa4425d9508f1c0a15c3fa"
}
```

```graphql
mutation addItemToCart {
addToCart(id: "SXRlbTpEbDVjZmFWclRI") {
id
quantity
}
}
```

The code above should resolve to something similar to this:

```js
// Response
{
"data": {
"addToCart": {
"id": "Q2FydEl0ZW06akVVTHlGZnVpQw==",
"quantity": 1
}
}
}
```
65 changes: 65 additions & 0 deletions _includes/graphql/getting-started.md
Expand Up @@ -93,6 +93,71 @@ After starting the app, you can visit [http://localhost:1337/playground](http://

⚠️ Please do not mount the GraphQL Playground in production as anyone could access your API Playground and read or change your application's data. [Parse Dashboard](#running-parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps. If you want to secure your API in production take a look at [Class Level Permissions](/js/guide/#class-level-permissions).

## Adding Custom Schema

The Parse GraphQL API supports the use of custom user-defined schema. You can write your own types, queries, and mutations, which will be merged with the ones that are automatically generated. Your custom schema is resolved via [Cloud Code](#cloud-code-resolvers) functions.

First, add a utility for parsing GraphQL queries as a required dependency:

```sh
$ npm install graphql-tag --save
```

Then, modify your `index.js` file to include your custom schema, along with the path to your cloud code file:

```js
const gql = require('graphql-tag');

const parseServer = new ParseServer({
appId: 'APPLICATION_ID',
cloud: './cloud/main.js',
});

const parseGraphQLServer = new ParseGraphQLServer(
parseServer,
{
graphQLPath: '/graphql',
playgroundPath: '/playground',
graphQLCustomTypeDefs: gql`
extend type Query {
hello: String! @resolve
hello2: String! @resolve(to: "hello")
}
`,
}
);
```

Alternatively, you can create your custom schema in a dedicated `schema.graphql` file and reference the file in your `index.js`:

```js
const gql = require('graphql-tag');
const fs = require('fs');
const customSchema = fs.readFileSync('./cloud/schema.graphql');

const parseServer = new ParseServer({
appId: 'APPLICATION_ID',
cloud: './cloud/main.js',
});

const parseGraphQLServer = new ParseGraphQLServer(
parseServer,
{
graphQLPath: '/graphql',
playgroundPath: '/playground',
graphQLCustomTypeDefs: gql`${customSchema}`,
}
);
```

```graphql
# schema.graphql
extend type Query {
hello: String! @resolve
hello2: String! @resolve(to: "hello")
}
```

## Running Parse Dashboard

[Parse Dashboard](https://github.com/parse-community/parse-dashboard) is a standalone dashboard for managing your Parse Server apps, including your objects' schema and data, logs, jobs, CLPs, and push notifications. Parse Dashboard also has a built-in GraphQL Playground that you can use to play around with your auto-generated Parse GraphQL API. It is the recommended option for **production** applications.
Expand Down
2 changes: 1 addition & 1 deletion _includes/graphql/objects.md
Expand Up @@ -70,7 +70,7 @@ mutation createAGameScore {
}
```

**Note:** The `id` is a [Relay Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm), it's **not** a Parse `objectId`. Most of the time the `Relay Node Id` is a `Base64` of the `ParseClass` and the `objectId`.
**Note:** The `id` is a [Relay Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm); it's **not** a Parse `objectId`. Most of the time the `Relay Node Id` is a `Base64` of the `ParseClass` and the `objectId`.

## Update

Expand Down