Navigation

Define a Custom Resolver

Overview

You can define custom resolvers that extend the GraphQL API for your app’s use cases. Custom resolvers allow you to define new root-level operations that are more complex or specific than the generated query and mutation resolvers. You can also add new computed fields to generated document types that dynamically evaluate a result whenever an operation reads a document of the extended type.

Procedure

1

Create a New Custom Resolver

In the Realm UI, click GraphQL in the navigation sidebar and then select the Custom Resolvers tab.

Click the Add a Custom Resolver button to open the configuration screen for a new custom resolver.

The custom resolvers screen in the Realm UI
2

Define the Resolver Field Name

Specify a descriptive name for the resolver in the GraphQL Field Name input. Realm exposes the custom resolver in its parent type using this name, so the name should describe what the resolver does in a way that is useful to developers who work with the GraphQL API.

The GraphQL Field Name input in the Realm UI
3

Define the Parent Type

Realm exposes every custom resolver as a field on a parent type. The parent type can be a root-level query, mutation, or a generated document type.

In the Parent Type dropdown, select one of the following options:

Option Description
Query

The resolver is a root-level query operation:

Example

A custom resolver for a query named myCustomQuery has the following generated schema:

type Query {
  myCustomQuery: DefaultPayload
  ...
}
Mutation

The resolver is a root-level mutation operation:

Example

A custom resolver for a mutation named myCustomMutation has the following generated schema:

type Mutation {
  myCustomMutation: DefaultPayload
  ...
}
Document Type

The resolver is a computed property on the specified document type. Any query or mutation that returns the document type can also ask for computed properties defined by custom resolvers on the type.

Example

A custom resolver that defines a computed property on the Task type named myCustomTaskProperty has the following generated schema:

type Task {
  myCustomTaskProperty: DefaultPayload
  ...
}
4

Define the Input Type

A custom resolver can accept input parameters from the incoming query or mutation. You can use an existing generated input type or define a new custom input type specifically for the resolver.

If you specify an input type, Realm exposes the input parameter in the custom resolver’s generated GraphQL schema definition as an optional parameter that accepts the specified input type. If you don’t specify an input type, the custom resolver does not accept any arguments.

In the Input Type dropdown, select one of the following options:

Option Description
None

The resolver does not accept any input.

A custom resolver configuration that does not accept any input parameters.

Example

A custom resolver named myCustomQuery that does not accept an input has the following generated schema:

type Query {
  myCustomQuery: DefaultPayload
  ...
}
Scalar

The resolver uses an existing scalar type from the generated GraphQL schema.

In the second dropdown input, select either a single scalar or an array of multiple scalars of the same type.

A custom resolver configuration that specifies an existing scalar type.

Example

A custom resolver named myCustomQuery that uses the Scalar Type option to specify an input type of DateTiem has the following generated schema:

type Query {
  myCustomQuery(input: DateTime): DefaultPayload
  ...
}
Existing Type

The resolver uses an existing input type from the generated GraphQL schema.

In the second dropdown input, select either a single input object or an array of multiple input objects of the same type.

A custom resolver configuration that specifies an existing input type.

Example

A custom resolver named myCustomQuery that uses the Existing Type option to specify an input type of TaskInsertInput has the following generated schema:

type Query {
  myCustomQuery(input: TaskInsertInput): DefaultPayload
  ...
}
Custom Type

Realm generates a new input type specifically for the resolver based on a JSON schema that you define. The schema must be an object that contains at least one property and a title field that defines a unique name for the generated input type.

A custom resolver configuration for a custom input type.

Example

A custom resolver named myCustomQuery that uses the Custom Type option with an input type named MyCustomQueryInput has the following generated schema:

input MyCustomQueryInput {
  someArgument: String;
}
type Query {
  myCustomQuery(input: MyCustomQueryInput): DefaultPayload
  ...
}
5

Define the Payload Type

All GraphQL resolvers must return a payload that conforms to a specific type in the schema. For a custom resolver, you can use an existing generated document type, define a new custom payload type specifically for the resolver, or use a default payload. Realm includes the specified payload type in the custom resolver’s generated GraphQL schema definition.

In the Payload Type dropdown, select one of the following options:

Option Description
DefaultPayload

The resolver returns the automatically generated DefaultPayload type which has the following signature:

type DefaultPayload {
  status: String!
}

The status field will always resolve to "complete" regardless of the resolver function’s return value.

{
  status: "complete"
}
A custom resolver configuration that uses the default payload type.

Example

A custom resolver named myCustomQuery that uses the DefaultPayload option has the following generated schema:

type Query {
  myCustomQuery: DefaultPayload
  ...
}
Scalar

The resolver uses an existing scalar type from the generated GraphQL schema.

In the second dropdown input, select either a single scalar or an array of multiple scalars of the same type.

A custom resolver configuration that specifies an existing scalar type.

Example

A custom resolver named myCustomQuery that uses the Scalar Type option to specify a payload type of DateTime has the following generated schema:

type Query {
  myCustomQuery: DateTime
  ...
}
Existing Type

The resolver returns an existing document type from the generated GraphQL schema.

In the second dropdown input, select either a single document type or an array of multiple documents of the same type.

A custom resolver configuration that specifies an existing document payload type.

Example

A custom resolver named myCustomQuery that uses the Existing Type option to specify an input type of TaskInsertInput has the following generated schema:

A custom resolver named myCustomQuery that uses the Existing Type option to specify a payload type of [Task] has the following generated schema:

type Query {
  myCustomQuery: [Task]
  ...
}
Custom Type

Realm generates a new payload type specifically for the resolver based on a JSON schema that you define. The schema must be an object that contains at least one property and a title field that defines a unique name for the generated input type.

A custom resolver configuration that defines a new custom payload type.

Example

A custom resolver named myCustomQuery that uses the Custom Type option with a payload type named MyCustomQueryPayload has the following generated schema:

input MyCustomQueryPayload {
  someValue: String;
}
type Query {
  myCustomQuery: MyCustomQueryPayload
  ...
}
6

Define the Resolver Function

When a user calls a custom resolver Realm executes the resolver function and returns the result, which must conform to the resolver’s Payload Type.

Realm passes the function any input data from the operation, if applicable. If the resolver is a computed property on a document type, Realm passes the function the specific document that the resolver was called on.

A custom resolver function has one of two possible signatures, depending on whether or not it accepts an input:

exports = function myCustomResolver(input, source) {
  // The `input` parameter that contains any input data provided to the resolver.
  // The type and shape of this object matches the resolver's input type.
  const { someArgument } = input;

  // If the resolver is a computed property, `source` is the parent document.
  // Otherwise `source` is undefined.
  const { _id, name } = source;

  // The return value must conform to the resolver's configured payload type
  return {
    "someValue": "abc123",
  };
}
exports = function myCustomResolver(source) {
  // If the resolver is a computed property, `source` is the parent document.
  // Otherwise `source` is undefined.
  const { _id, name } = parent;

  // The return value must conform to the resolver's configured payload type
  return {
    "someValue": "abc123",
  };
}

To define the resolver function, click the Function dropdown and either select an existing function or create a new one.

A custom resolver configured to use the `myCustomResolver` function
7

Save and Deploy the Resolver

Once you have configured the resolver, click Save and then deploy your application. Once deployed, you can call the custom resolver through the GraphQL API.

Custom Resolver Examples

Scenario & Schemas

Consider a hypothetical dashboard that a sales team uses to show various statistics and other performance metrics for a given time period. The dashboard uses the custom resolvers in this section to handle some of its specific use cases.

The resolvers all reference Sale documents, which have the following schema:

type Sale {
  _id: ObjectId!
  customer_id: String!
  year: String!
  month: String!
  saleTotal: Float!
  notes: [String]
}
{
  "title": "Sale",
  "bsonType": "object",
  "required": ["_id", "customer_id", "year", "month", "saleTotal"],
  "properties": {
    "_id": { "bsonType": "objectId" },
    "customer_id": { "bsonType": "string" },
    "year": { "bsonType": "string" },
    "month": { "bsonType": "string" },
    "saleTotal": { "bsonType": "decimal" },
    "notes": {
      "bsonType": "array",
      "items": { "bsonType": "string" }
    }
  }
}

Custom Query Resolver

The sales team’s hypothetical dashboard uses a custom query resolver that returns aggregated sales data for a specific month.

Realm generates schema definitions for the resolver’s custom input and payload types and adds the resolver to its parent type, the root-level Query:

type Query {
  averageSaleForMonth(input: AverageSaleForMonthInput): AverageSaleForMonthPayload
}

input AverageSalesForMonthInput {
  month: String!;
  year: String!;
}

type AverageSaleForMonthPayload {
  month: String!;
  year: String!;
  averageSale: Float!;
}

Configuration

The resolver uses the following configuration:

Option Description
Parent Type Query
GraphQL Field Name averageSaleForMonth
Input Type

Custom Type: AverageSaleForMonthInput

{
  "bsonType": "object",
  "title": "AverageSaleForMonthInput",
  "required": ["month", "year"],
  "properties": {
    "month": {
      "bsonType": "string"
    },
    "year": {
      "bsonType": "string"
    }
  }
}
input AverageSalesForMonthInput {
  month: String!;
  year: String!;
}
Payload Type

Custom Type: ``AverageSaleForMonthPayload`

{
  "bsonType": "object",
  "title": "AverageSaleForMonthPayload",
  "required": ["month", "year", "averageSale"],
  "properties": {
    "month": {
      "bsonType": "string"
    },
    "year": {
      "bsonType": "string"
    },
    "averageSale": {
      "bsonType": "decimal"
    }
  }
}
type AverageSaleForMonthPayload {
  month: String!;
  year: String!;
  averageSale: Float!;
}
Function
exports = async function averageSaleForMonth({ month, year }) {
  const cluster = context.services.get("mongodb-atlas");
  const sales = cluster.db("corp").collection("sales");

  const averageSalePayload = await sales
    .aggregate([
      { $match: { month: month, year: year } },
      {
        $group: {
          _id: "$month",
          month: "$month",
          year: "$year",
          averageSale: { $avg: "$saleTotal" },
        },
      },
    ])
    .next();

  return averageSalePayload;
};

Example Usage

To call this custom query, you could use the following operation and variables:

query GetAverageSaleForMonth($averageSaleInput: AverageSaleForMonthInput!) {
  averageSaleForMonth(input: $averageSaleInput) {
    month
    year
    averageSale
  }
}
{
  "variables": {
    "averageSaleInput": { month: "March", year: "2020" }
  }
}

Custom Mutation

The sales team’s hypothetical dashboard uses a custom mutation resolver that adds a string note to a specific Sale document, identified by its _id.

Realm generates schema definitions for the resolver’s custom input type and adds the resolver to its parent type, the root-level Mutation:

type Mutation {
  addNoteToSale(input: AddNoteToSaleInput): Sale
}

input AddNoteToSaleInput {
  sale_id: ObjectId!;
  note: String!;
}

Configuration

The resolver uses the following configuration:

Option Description
Parent Type Mutation
GraphQL Field Name addNoteToSale
Input Type

Custom Type: AddNoteToSaleInput

{
  "bsonType": "object",
  "title": "AddNoteToSaleInput",
  "required": ["sale_id", "note"],
  "properties": {
    "sale_id": {
      "bsonType": "objectId"
    },
    "note": {
      "bsonType": "string"
    }
  }
}
input AddNoteToSaleInput {
  sale_id: ObjectId!;
  note: String!;
}
Payload Type

Existing Type: Sale

type Sale {
  _id: ObjectId!
  customer_id: String!
  year: String!
  month: String!
  saleTotal: Float!
  notes: [String]
}
{
  "title": "Sale",
  "bsonType": "object",
  "required": ["_id", "customer_id", "year", "month", "saleTotal"],
  "properties": {
    "_id": { "bsonType": "objectId" },
    "customer_id": { "bsonType": "string" },
    "year": { "bsonType": "string" },
    "month": { "bsonType": "string" },
    "saleTotal": { "bsonType": "decimal" },
    "notes": {
      "bsonType": "array",
      "items": { "bsonType": "string" }
    }
  }
}
Function
exports = async function addNoteToSale({ sale_id, note }) {
  const cluster = context.services.get("mongodb-atlas");
  const sales = cluster.db("corp").collection("sales");

  const sale = await sales.findOneAndUpdate(
    { _id: sale_id },
    { $push: { notes: note } },
    { returnNewDocument: true }
  );
  return sale;
}

Example Usage

To call this custom query, you could use the following operation and variables:

mutation AddNoteToSale($addNoteToSaleInput: AddNoteToSaleInput) {
  addNoteToSale(input: $addNoteToSaleInput) {
    _id
    customer_id
    month
    year
    saleTotal
    notes
  }
}
{
  "variables": {
    "addNoteToSaleInput": {
      "sale_id": "5f3c2779796615b661fcdc25",
      "note": "This was such a great sale!"
    }
  }
}

Computed Properties

The sales team’s hypothetical dashboard uses a custom resolver that adds a new computed property to each Sale document. When an operation requests the computed field for a given Sale, the resolver queries an external system and returns support cases filed by the associated customer.

Realm generates schema definitions for the resolver’s custom payload type and adds the resolver to its parent type, Sale:

type Sale {
  _id: ObjectId!
  customer_id: String!
  year: String!
  month: String!
  saleTotal: Float!
  notes: [String]
  customerSupportCases: [CustomerSupportCase]
}

type CustomerSupportCase {
  caseId: String!
  description: String!
}

Configuration

The resolver uses the following configuration:

Option Description
Parent Type Sale
GraphQL Field Name customerSupportCases
Input Type None
Payload Type

Custom Type: [CustomerSupportCase]

{
  "bsonType": "array",
  "items": {
    "title": "CustomerSupportCase",
    "bsonType": "object",
    "required": ["caseId", "description"],
    "properties": {
      "caseId": { "bsonType": "string" },
      "description": { "bsonType": "string" }
    }
  }
}
type CustomerSupportCase {
  caseId: String!
  description: String!
}

type Sale {
  _id: ObjectId!
  customer_id: String!
  year: String!
  month: String!
  saleTotal: Float!
  notes: [String]
  customerSupportCases: [CustomerSupportCase]
}
Function
exports = async function customerSupportCases(sale) {
  // Return a list of objects from some external system
  const cases = await fetchCustomerSupportCases({
    customerId: sale.customer_id
  });
  return cases;
};

Example Usage

To use this custom computed property, you could run the following operation:

query GetSalesWithSupportCases {
  sales {
    _id
    customer_id
    year
    month
    saleTotal
    notes
    customerSupportCases {
      caseId
      description
    }
  }
}