Neurelo API Reference (GraphQL)

Introduction

This API documentation is intended to provide an overview of how Neurelo generates APIs based on your data definition. It will go into detail on how to perform operations using a sample schema definition. You can then apply these concepts and patterns to your own schema. Reference documentation generated specifically for your data definition is included in your specific project on Neurelo's Data Access Platform.

Neurelo provides two types of APIs that are automatically generated and available for use.

  • The REST API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.

  • The GraphQL API exposes a single endpoint where clients can send GraphQL queries or mutations and a strictly-typed schema that be be shared across consumers.

You can toggle between the GraphQL and REST API documentation when reviewing the documentation in the platform.

API Headers

Authentication

API calls to Neurelo are authenticated using an API Key for all API requests. This api key defines which environment the API request is interacting with. You can create an api key under the settings of the specific environment that you are trying to access. API Keys cannot be used across environments.

To utilize an api key, include it as a header with your API Request, using X-API-KEY as the key.

Query Strategy

Neurelo generated APIs can be tuned to fetch data using either the selects or joins strategy. The main difference is that selects executes multiple queries behind the scenes to gather the desired data, whereas joins uses a single query when possible using database-level joins, to fetch the results.

By default, joins strategy is enabled. This can be changed using an HTTP header. For example if you are using cURL, then to fetch all the users and their corresponding posts using selects strategy:

curl -X POST https://$ENV_API_URL/graphql/
    --data '{"query": "query { findManyUser { name } }", "variables": {}}'
    --header 'X-READ-STRATEGY: selects'

Similarly, 'X-READ-STRATEGY: joins' can be used.

joins strategy is suitable for scenarios where you want to minimize the number of queries made to the database, especially when the relation expressions are relatively simpler, and there's less risk of data duplication.

selects can be faster in scenarios with multiple many-to-many or one-to-many relations. This is because the database's flat record list from joins can sometimes contain a significant amount of repeated information. Despite joins' quick execution in the database, the process of moving and parsing this redundant data into JSON can incur high costs.

Query Viz

To review the database queries that Neurelo will be sending to your data source, you can use the X-QUERY-VIZ HTTP header. For example, to enable query viz while fetching all the users and their corresponding posts:

curl https://$ENV_API_URL/graphql/
    --data '{"query": "query { findManyUser { name } }", "variables": {}}'
    --header 'X-QUERY-VIZ: true'

This header will return both the query string and its corresponding query parameters.

Similarly, 'X-QUERY-VIZ: false' can be used to disable the viz. By default, this header is disabled.

IMPORTANT: When query viz is enabled, the queries will still run against your selected environment, and they may modify data in that environment depending on your queries.

Operations

Creating Objects

You can create objects by utilizing one of two mutations available for creating objects. These are createOneObject and createManyObject, where Object is the name of your object.

In this example, we will use a sample schema with a User object and a BlogPost object. A user has many posts, and a post has one user.

Create one User (CreateInput)

To create a single user, assuming that we have the sample schema running in an environment, we can issue a CreateOne mutation, with the data argument set to the UserCreateInput input type. For example:

mutation {
  createOneUser(data: { name: "User 1", country: "Canada" }) {
    id
    name
    country
  }
}

The response from this insert request will look like this, which is the object that was inserted:

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "Admin User",
      "country": "United States"
    }
  }
}

To prevent the insertion of records with unique fields or identifiers that already exist, we can use skipDuplicates. For example:

mutation {
  createOneUser(data: [{ id: 1, name: "User 1", country: "Canada" }, { id: 1, name: "User 2", country: "United States" }]) {
    id
    name
    country
  }
}

The response from this insert request will look like this, which is the inserted object(s):

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "Admin User",
      "country": "United States"
    }
  }
}

In the above example, as User IDs were same, the second record was prevented from being inserted.

NOTE: Currently, skipDuplicates is not supported for MongoDB.

Creating multiple objects (CreateManyInput)

To create multiple users, assuming that we have the sample schema running in an environment, we can issue a CreateMany mutation, with the data argument set to an array of UserCreateInput types.

mutation {
  createManyUser(data: [{ name: "User 1", country: "Canada" }, { name: "User 2", country: "United States" }]) {
    id
  }
}

The response (AffectedRowsOutput) from this insert request will look like this, which is the count of the inserted rows:

{
  "data": {
    "createManyUser": {
      "count": 2
    }
  }
}

NOTE: Currently, related objects cannot be created or associated with the 'root' object using CreateManyInput. To achieve this, it is necessary to use CreateInput.

Creating with relations

Currently, Neurelo for MongoDB does not directly support the creation of objects with relations. However, you can create inner objects (which are equivalent to embedded documents, meaning documents nested within another document) or use references to accommodate your data modeling needs. Refer to the Modeling Relationships for MongoDB with Inner Objects and References section for more information.

When creating a single user, you can create or connect related object in the same transaction.

On an object with a has one relationship to another object, you can use the create input to create a new related object automatically associated with the 'root' object. Alternatively, you can use the connect input to link an existing related object to the 'root' object. When using connect, an error will be thrown if the linked object is not found. To address this, use connectOrCreate, which links an existing related object to the 'root' object or creates a new related object if it doesn't exist.

On an object with a has many relationship to another object, you can also utilize create, connect, and connectOrCreate. Additionally, you can use createMany to create many related objects.

create

To demonstrate creating a root object and its related object, we will use a sample schema with a User object and a BlogPost object. A user has many posts, and a post has one user.

mutation {
  createOneUser(data: { name: "User 1", country: "Canada", posts: { create: [{ title: "Post Title" }] } }) {
    id
    name
    posts {
      id
      title
    }
  }
}

The response from this insert request will look like this, which is the inserted object(s):

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "User 1",
      "posts": [
        {
          "id": 1,
          "title": "Post Title"
        }
      ]
    }
  }
}

connect

To create a root object and link it with an existing related object in a has many relationship, assuming that we have the sample schema running in an environment, we can use the connect input.

mutation {
  createOneUser(data: { name: "User 1", country: "Canada", posts: { connect: [{ id: 1 }] } }) {
    id
    name
    country
    posts {
      id
      title
    }
  }
}

The response from this insert request will look like this, which is the inserted object(s):

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "User 1",
      "country": "Canada",
      "posts": [
        {
          "id": 1,
          "title": "Post Title"
        }
      ]
    }
  }
}

In the above example, the Post with an ID of 1 previously existed and is now being associated with a relationship with the new root object.

connectOrCreate

To create a root object and link it with an existing related object or create a new related object if it doesn't exist (when the root and related object are in a has many relationship), assuming that we have the sample schema running in an environment, we can use the connectOrCreate input. For example,

mutation {
  createOneUser(data: { name: "User 1", country: "Canada", posts: { connectOrCreate: [{ create: { title: "Post 1" }, where: { id: 3 } }] } }) {
    id
    name
    country
    posts {
      id
      title
    }
  }
}

The response from this insert request will look like this, which is the inserted object(s):

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "User 1",
      "country": "Canada",
      "posts": [
        {
          "id": 2,
          "title": "Post 1"
        }
      ]
    }
  }
}

In the above example, by observing the Post's ID, we can conclude that the ID 3 was not found in the Post object. As a result, a new object was created.

createMany

To create a root object and link it with many related objects in a has many relationship, assuming that we have the sample schema running in an environment, we can use the createMany input.

mutation {
  createOneUser(data: { name: "User 1", country: "Canada", posts: { createMany: { data: [{ title: "Post Title 1" }, { title: "Post Title 2" }] } } }) {
    id
    name
    country
    posts {
      id
      title
    }
  }
}

The response from this insert request will look like this, which is the inserted object(s):

{
  "data": {
    "createOneUser": {
      "id": 1,
      "name": "User 1",
      "country": "Canada",
      "posts": [
        {
          "id": 1,
          "title": "Post Title 1"
        },
        {
          "id": 2,
          "title": "Post Title 2"
        }
      ]
    }
  }
}

Similar to createInput, you can use skipDuplicates in createMany to prevent the insertion of records with unique fields or identifiers that already exist.

Retrieving Objects

You can retrieve objects by utilizing a few different queries that are generated for each object. There are three find queries available per object, they are findFirstObject, findUniqueObject and findManyObject.

In this example, we will use a sample schema with a User object and a BlogPost object. A user has many posts, and a BlogPost has one user. A user has a relationship property to posts called posts and the post has a relationship property to users called author.

Retrieving unique object

To retrieve a unique user, assuming that we have the sample schema running in an environment, we can issue a query to findUniqueUser with a where argument that filters on unique properties. For example:

query {
  findUniqueUser(where: { id: 1 }) {
    name
  }
}

The response from this request will look like this, which is the retrieved object:

{
  "data": {
    "findUniqueUser": {
      "name": "User 1"
    }
  }
}

Retrieving all objects

To retrieve all users, assuming that we have the sample schema running in an environment, we can issue a query with findManyUser. For example:

query {
  findManyUser {
    name
  }
}

The response from this request will look like this, which is an array of the retrieved objects:

{
  "data": {
    "findManyUser": [
      {
        "name": "User 1"
      },
      {
        "name": "User 2"
      }
    ]
  }
}

Filtering for specific objects

To filter specific objects, we can utilize the where query argument. For complete documentation of all the options available to you with the query argument, take a look at the WhereInput input type documentation below.

To filter for specific User objects, assuming that we have the sample schema running in an environment, we can issue a query with the where argument set to the UserWhereInput input type.

query {
  findManyUser(where: { name: { contains: "Jane" } }) {
    name
  }
}

The response from this request will look like this, which is an array of the retrieved objects:

{
  "data": {
    "findManyUser": [
      {
        "name": "Jane"
      }
    ]
  }
}

To retrieve all users with their posts, assuming that we have the sample schema running in an environment, we can issue a query with findManyUser. For example:

query {
  findManyUser {
    name
    posts {
      name
      words
    }
  }
}

The response from this request will look like this, which is an array of the retrieved objects and their related objects:

{
  "data": {
    "findManyUser": [
      {
        "name": "User 1",
        "posts": [
          {
            "name": "My Trip to Egypt",
            "words": 1200
          },
          {
            "name": "My Trip to Canada",
            "words": 800
          }
        ]
      },
      {
        "name": "User 2",
        "posts": [
          {
            "name": "Greek mythology: Part 1",
            "words": 1000
          }
        ]
      }
    ]
  }
}

Assume that in our sample schema, there is a new object Activity, such that each post has many activities. A post has a relationship property to activities called activity.

To retrieve all the users, their post names, and activities on each post, assuming that we have the sample schema running in an environment, we can issue a query with findManyUser. For example:

query {
  findManyUser {
    name
    posts {
      name
      activity {
        name
        action
      }
    }
  }
}

The response from this request will look like this, which is an array of the retrieved objects and their specific related objects:

{
  "data": {
    "findManyUser": [
      {
        "name": "User 1",
        "posts": [
          {
            "name": "My Trip to Egypt",
            "activity": [
              {
                "name": "User 123",
                "action": "Like"
              },
              {
                "name": "User 234",
                "action": "Share"
              }
            ]
          },
          {
            "name": "My Trip to Canada",
            "activity": [
              {
                "name": "User 345",
                "action": "Like"
              }
            ]
          }
        ]
      }
    ]
  }
}

Updating Objects

You can update objects by using one of two update mutations, these are similar to the create mutations, but include a where argument to specify which objects should be updated. There are updateOneObject and a updateManyObject mutations available, Object being replaced by the object that you are performing the update on.

Updating unique object (UpdateInput)

Updating a unique object is similar to updating many, except that the where argument is limited to filtering on unique properties. For example, the id property on User is a unique property and is guaranteed to be unique to a single User object.

mutation {
  updateOneUser(data: { name: "John" }, where: { id: 1 }) {
    name
  }
}

The response from this request will look like this, which is the updated object:

{
  "data": {
    "updateOneUser": {
      "id": 1,
      "name": "John"
    }
  }
}

Updating multiple objects (UpdateManyInput)

To update multiple users, we can utilize the updateManyObject mutation with the data and where arguments to provide both our updated properties and a condition set to limit which objects are updated.

mutation {
  updateManyUser(data: { name: "John" }, where: { name: { contains: "Jane " } }) {
    count
  }
}

The response (AffectedRowsOutput) from this update request will look like this, which is the count of the updated rows:

{
  "data": {
    "updateManyUser": {
      "count": 1
    }
  }
}

Updating scalars using special operations

push

When using the array type for a property, the UpdateInput provides a push operation. This inserts a value or array of values at the end of the array. If the value is null, it would initialize the array with the value(s) provided. For example,

mutation {
  updateOnePost(data: { name: "My updated .bashrc file", tags: { push: ["alias"] } }) {
    id
    name
    tags
  }
}

The response from this request will look like this, which is the updated object:

{
  "data": {
    "id": 1,
    "name": "My updated .bashrc file",
    "tags": ["termcolor", "alias"]
  }
}

Updating with relations

Currently, Neurelo for MongoDB does not directly support the updation of objects with relations. However, you can update inner objects (which are equivalent to embedded documents, meaning documents nested within another document) or use references to accommodate your data modeling needs. Refer to the Modeling Relationships for MongoDB with Inner Objects and References section for more information.

When updating a single object, you can create, update, disconnect or delete related object in the same transaction.

On an object with a has one relationship to another object, you can use the update input to update a related object automatically associated with the 'root' object. Alternatively, you can use the disconnect input to unlink an existing related object with the 'root' object. When using update, an error will be thrown if the linked object is not found. To address this, use upsert, which updates an existing related object to the 'root' object or creates a new related object if it doesn't exist. You can also use delete input to delete an existing related object.

The connect, connectOrCreate, and create inputs (as explained in the Creating objects with relations section) are also supported here.

On an object with a has many relationship to another object, you can also utilize create, connect, connectOrCreate, update, upsert, delete, and disconnect inputs. Additionally, you can use createMany, updateMany, and deleteMany to create, update, and delete many related objects, respectively.

createMany is explained in the Creating objects with relations section.

update

To demonstrate updating a root object and its related object, we will use a sample schema with a User object and a BlogPost object. A user has many posts, and a post has one user.

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User 1", posts: { update: { where: { id: 2 }, data: { name: "My system configuration" } } } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this update request will look like this, which shows the updated object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User 1",
    "posts": [
      {
        "id": 1,
        "name": "Test"
      },
      {
        "id": 2,
        "name": "My system configuration"
      }
    ]
  }
}

In the above example, the Post with an ID of 2 previously existed and is now being updated along with the name of the user.

upsert

To update a root object and update its corresponding related object or create a new related object if it doesn't exist (when the root and related object are in a has many relationship), assuming that we have the sample schema running in an environment, we can use the upsert input. For example,

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User 1", posts: { upsert: { where: { id: 2 }, update: { name: "Hardware Info" }, create: { name: "Hardware Info" } } } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this upsert request will look like this, which is the upsert object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User 1",
    "posts": [
      {
        "id": 1,
        "name": "Test"
      },
      {
        "id": 2,
        "name": "Hardware Info"
      }
    ]
  }
}

disconnect

To unlink an existing related object with the 'root' object (when the root and related object are in a has many relationship), assuming that we have the sample schema running in an environment, we can use the disconnect input. For example,

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User 1", posts: { disconnect: { id: 2 } } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this request will look like this, which is the updated object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User 1",
    "posts": [
      {
        "id": 1,
        "name": "Test"
      }
    ]
  }
}

In the above example, the Post with an ID of 2 previously existed and still exists, but it is now being unlinked with respect to the 'root' object. Furthermore, the name of the user also gets updated.

delete

To delete an existing related object asccociated with a 'root' object (when the root and related object are in a has many relationship), assuming that we have the sample schema running in an environment, we can use the delete input. For example,

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User 1", posts: { delete: { id: 2 } } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this request will look like this, which is the updated object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User 1",
    "posts": [
      {
        "id": 1,
        "name": "Test"
      }
    ]
  }
}

In the above example, the Post with an ID of 2 is now deleted. Furthermore, the name of the user also gets updated.

updateMany

To update a root object and its many related objects in a has many relationship, assuming that we have the sample schema running in an environment, we can use the updateMany input.

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User 1", posts: { updateMany: [{ where: { id: 1 }, data: { name: "Test Blog" } }, { where: { id: 2 }, data: { name: "Hardware Info Blog" } }] } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this updateMany request will look like this, which is the updated object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User 1",
    "posts": [
      {
        "id": 1,
        "name": "Test Blog"
      },
      {
        "id": 2,
        "name": "Hardware Info Blog"
      }
    ]
  }
}

deleteMany

To update a root object and delete its many related objects in a has many relationship, assuming that we have the sample schema running in an environment, we can use the deleteMany input.

mutation {
  updateManyUser(where: { id: 1 }, data: { name: "Admin User Empty", posts: { deleteMany: [{ id: 1 }, { id: 2 }] } }) {
    id
    name
    posts {
      id
      name
    }
  }
}

The response from this deleteMany request will look like this, which is the deleted object(s):

{
  "data": {
    "id": 1,
    "name": "Admin User Empty",
    "posts": []
  }
}

Deleting Objects

You can delete objects by utilizing one of two mutations. These mutations are deleteOneObject and a deleteManyObject mutations available, where Object is the name of the object that you are deleting.

In this example, we will use a sample schema with a User object.

Delete unique object

To delete a unique user, assuming that we have the sample schema running in an environment, we can issue a query to deleteOneUser. For example:

mutation {
  deleteOneUser(where: { id: 1 }) {
    id
    name
    country
  }
}

The response from this request will look like this, which is the deleted object:

{
  "data": {
    "id": 1,
    "name": "User 1",
    "country": "Canada"
  }
}

Delete all objects

To delete all users, you can issue a deleteManyUser mutation with arguments present. For example:

mutation {
  deleteManyUser {
    count
  }
}

The response from this request will look like this, which is the count of the deleted object(s):

{
  "data": {
    "deleteManyUser": {
      "count": 1
    }
  }
}

Delete specific objects

To delete specific objects, we can utilize the where argument. For complete documentation of all the options available to you with the query argument, take a look at the WhereInput input type documentation below.

To retrieve all specific, we can issue deleteManyUser with the where argument specified. For example:

mutation {
  deleteManyUser(where: { name: { contains: "John" } }) {
    count
  }
}

The response from this request will look like this, which is the count of the deleted object(s):

{
  "data": {
    "deleteManyUser": {
      "count": 1
    }
  }
}

Aggregating Objects

You can aggregate objects by utilizing the aggregateObject query that is generated for each object. The following aggregate functions are supported: _count, _sum, _avg, _min, and _max.

In this example, we will use a sample schema with a Post object.

Aggregating specific objects

To aggregate post objects, assuming that we have the sample schema running in an environment, we can issue a query with aggregatePost. For example, to average the number of words for post objects:

query {
  aggregatePost {
    _avg {
      words
    }
  }
}

The response (AggregateOutput) from this request will look like this, which contains the aggregated value:

{
  "data": {
    "aggregatePost": {
      "_avg": {
        "words": 1000.0
      }
    }
  }
}

Aggregating all objects

Presently, only _count function supports aggregating all objects.

To count all objects, assuming that we have the sample schema running in an environment, we can issue a query with aggregatePost with the _count function and _all value specified for it. For example,

query {
  aggregatePost {
    _count {
      _all
    }
  }
}

The response (AggregateOutput) from this request will look like this, which contains the aggregated value:

{
  "data": {
    "aggregatePost": {
      "_count": {
        "_all": 2
      }
    }
  }
}

Grouping Objects

In this example, we will use a sample schema with a User object.

You can group by objects by utilizing the groupByObject query that is generated for each object. It is mandatory to include a by option to group records by one or more fields. This option takes an array of field names (Array), to specify the fields to group by with.

For example, to count the number of users by country:

query {
  groupByUser(by: [country]) {
    country
    _count {
      id
    }
  }
}

The response from this request will look like this, which is the grouped and aggregated object(s):

{
  "data": {
    "groupByUser": [
      {
        "country": "United States",
        "_count": {
          "id": 1
        }
      },
      {
        "country": "Canada",
        "_count": {
          "id": 1
        }
      }
    ]
  }
}

Group By with Filtering

To filter the fields before grouping, we can utilize the where option. For example,

query {
  groupByUser(where: { country: { startsWith: "C" } }, by: [country]) {
    country
    _count {
      id
    }
  }
}

The response from this request will look like this, which is the grouped and aggregated object:

{
  "data": {
    "groupByUser": [
      {
        "country": "Canada",
        "_count": {
          "id": 1
        }
      }
    ]
  }
}

To filter the groups by aggregate functions, we can utilize the having option.

query {
  groupByUser(having: { id: { _count: { lte: 2 } } }, by: [country]) {
    country
    _count {
      id
    }
  }
}

The response from this request will look like this, which is the grouped and aggregated object(s):

{
  "data": {
    "groupByUser": [
      {
        "country": "United States",
        "_count": {
          "id": 1
        }
      },
      {
        "country": "Canada",
        "_count": {
          "id": 1
        }
      }
    ]
  }
}

Input Types

In this section, we’ll take a look at some arguments that apply to many different operations. The avaliability of each argument is dependent on the operation being performed, for example, the WhereInput argument is not available on Insert operations.

Once you are familiar with these arguments, we hope that utilizing individual operations on your objects comes naturally and will not require continous referral to the generated reference documentation.

where (WhereInput)

The WhereInput argument allows you to add conditions to your query. This argument can be used when retrieving objects, as well as when updating objects where it will act as the condition on which objects are updated.

Filtering with Scalars

Each scalar has it’s own set of operations that can be performed to assert a condition, for example, an integer has the following operators available to it: equals, not, in, notIn, lt, lte, gt, gte. With the exception of in and notIn, all of these operators will accept the matching scalar, in and notIn will take an array of the matching scalar.

Here is a list of all available operators: equals, not, in, notIn, lt, lte, gt, gte, contains, startsWith, and endsWith. The availiblity of these is dependant on the scalar type, for example, you cannot utilize the gt operator on a string.

A where condition for a given object includes all of the scalars present on the object with their respective scalar filter type. To construct a where condition utilizing only an object’s scalar properties, you can use the name of the property as the key, and then a where object for that specific scalar type.

Here is a more comprehensive overview of these operators:

Operator

Description

Example

equals

Filter for field values equal to a specific value

To filter when age equals 21, { age: { equals: 21 } }

lt/lte

Filter for field values "less than / less than or equal to" a specific value

To filter when age is less than 21, { age: { lt: 21 } }

gt/gte

Filter for field values "greater than / greater than or equal to" a specific value

To filter when age is greater than or equal to 30, { age: { gte: 30 } }

not

Filters when field value does not equal a specific value

To filter records that have all ids except 100, {id: {not: 100}}

in/notIn

Checks if a field value exists / does not exist in a list, and filters accordingly.

To filter records having ids as either 1, 3 or 6, { id: { in: [1, 3, 6] } }

startsWith/endsWith

Filter when a specific value is at the start or end of a field value. Case-sensitive by default. _ wildcard can be used to match one or more characters. $ wildcard can be used to match zero or more characters.

To filter names that start with "Ron" (case-insensitive), {name: {startsWith: "Ron", mode: "insensitive"} }. To filter names that start with "R", followed by one or more characters, and then "d", {name: {startsWith: "R_d"} }. This matches names such as "Roland" and "Rudd".

contains

Filters when a specific value is contained in a field value. Case-sensitive by default. _ wildcard can be used to match one or more characters. $ wildcard can be used to match zero or more characters.

To filter country names that contain the word "United", {country: {contains: "United" } }

search

Filters using full-text search capabilities. This operator is specific to strings and currently, it is not supported with MongoDB. Currently, this operator is not supported with MongoDB. For querying, PostgreSQL and MySQL's native full-text search capabilities are leveraged. For example, in Postgres, & can be used to perform a boolean AND on two strings. Similarly, | and ! can be used to perform boolean OR and boolean NOT, respectively. For MySQL, + and - are used as boolean AND and boolean NOT, respectively. Boolean OR in MySQL is represented using no operators; for example, Tomato Orange would check if the text contains Tomato or Orange.

In PostgreSQL, to filter records with the engine as Firefox or Chrome, use {engine: {search: "Firefox | Chrome"}}. In MySQL, to filter records with Firefox but not Chrome in the report, use {report: {search: "+Firefox -Chrome"}}.

Combining conditions

When you add conditions to a WhereInput object, you are asserting that all of the listed conditions must evaluate to true for an object to be returned, but you are also able to further customize your query’s condition by utilizing the AND, OR, and NOT properties. All of these properties accept an array of the object’s WhereInput type, allowing you to nest and compose different filtering conditions to construct your query.

Here is a more comprehensive overview of these operators:

Operator

Description

Example

OR

Filters for field values when one or more conditions is true

To filter names starting with either "A" or "B", {OR: [{name: {startsWith: "A"}}, {name: {startsWith: "B"}}]}. Furthermore, to filter names starting with "A" or ending with "n", {OR: [{name: {startsWith: "A"}}, {name: endsWith: "n"}]}.

AND

Filters for field values when all conditions are true

To filter for first names starting with "A" and last names ending with "n", {AND: [{firstName: {startsWith: "A"}}, {lastName: {endsWith: "n"}}]}. Furthermore, to filter names starting with "B" and ending with "n", we can use operators such as startsWith and endsWith in a single object itself, like {AND: [{name: {startsWith: "B", endsWith: "n"}}]}.

NOT

Filters for field values when all the conditions are false

To filter for names that are not "Alan" and not "George", {NOT: [{name: {equals: "Alan"}}, {name: {equals: "George"}}]}. Furthermore, to filter names not starting with "C" and not ending with "n", we can use operators such as startsWith and endsWith in a single object itself, like {NOT: [{name: {startsWith: "C", endsWith: "n"}}]}.

Note on Scope of Logical operators:

  • Implicit AND: Combining conditions within one object, {name: {startsWith: "A", endsWith: "n"}}, uses an implicit AND logic, meaning all conditions must be met.

  • Explicit Logical Operators: Using OR, AND, or NOT to combine multiple conditions, such as {OR: [ {...}, {...} ]}, processes each condition in its own scope before applying the logical operator. This structure allows for complex filtering, evaluating from the inner-most conditions outwards.

Furthermore, you can combine these operators to do more powerful filtering. For example,

  • NOT and AND: To filter for names that do no start with "A" and end with "n", {NOT: {AND: [{name: {startsWith: "A"}}, {name: {endsWith: "n"}}]}}.

  • OR and AND: To filter for ids that are between 0 to 10 or 30 to 40, {OR: [{AND: [{id: {gte: 0}}, {id: {lte: 10}}]}, {AND: [{id: {gte: 30}}, {id: {lte: 40}}]}]}.

Filtering with List

You can use the following operators to perform filtering with lists: has, hasSome, hasEvery, and isEmpty.

Additionally, you can employ the equals operator introduced earlier.

Here is a more comprehensive overview of these operators:

Operator

Description

Example

has

Filters for lists where a given value exists.

To filter when Canada is in a locations list, {locations: {has: "Canada"}}

hasSome

Filters for lists where at least one value from the search list exists.

To filter when Canada or United States are in a locations list, {locations: {hasSome: ["Canada", "United States"]}}

hasEvery

Filters for lists where all values from the search list exists.

To filter when Canada, United States, and Spain are in a locations list, {locations: {hasEvery: ["Canada", "United States", "Spain"]}}

isEmpty

Filters for empty lists.

To filter for empty locations list, {locations: {isEmpty: true}}. Similarly, to filter for non-empty lists, use {isEmpty: false}.

Filtering with JSON

You can use the following operators to perform JSON filtering: array_starts_with, array_ends_with, array_contains, string_starts_with, string_ends_with, and string_contains.

Additionally, you can employ operators introduced earlier, such as equals, gt, gte, lt, lte, and not.

To filter a specific part of the JSON object, each of these operators requires a specified path. For example, consider a JSON object like {Produce: {Fruits: {Organic: ["Orange", "Mango", "Banana"], NonOrganic: ["Apple", "Melon"]}}}. In Postgres, the path Produce -> Fruits -> Organic can be represented as ["Produce", "Fruits", "Organic"]. Similarly, for MySQL, it can be represented as '$.Produce.Fruits.Organic'.

Here is a more comprehensive overview of these operators (all the paths are specific to Postgres):

Operator

Description

Example

array_starts_with

Filters for JSON objects where a specified path has a list that begins with a specific value.

To filter when the list at ["Fruits", "Organic"] path starts with Orange, {Crops: {path: ["Fruits", "Organic"], array_starts_with: "Orange"}}

array_ends_with

Filters for JSON objects where a specified path has a list that ends with a specific value.

To filter when the list at ["Fruits", "Organic"] path ends with Apple , {Crops: {path: ["Fruits", "Organic"], array_starts_with: "Apple"}}

array_contains

Filters for JSON objects where a specified path has a list that contains a specific value.

To filter when the list at ["Schedule", "Heathrow"] path contains 9 , {Misc: {path: ["Schedule", "Heathrow"], array_contains: 9}}

string_starts_with

Filters for JSON objects where a specified path has a string that begins with a specific value.

To filter when the string at ["Premium Customers", "NYC", "George"] path starts with Q2F0cw== , {IDs: {path: ["Premium Customers", "NYC", "George"], string_starts_with: "Q2F0cw=="}}

string_ends_with

Filters for JSON objects where a specified path has a string that ends with a specific value.

To filter when the string at ["Premium Customers", "NYC", "George"] path ends with TmV1cmVsbw== , {IDs: {path: ["Premium Customers", "NYC", "George"], string_ends_with: "TmV1cmVsbw=="}}

string_contains

Filters for JSON objects where a specified path has a string that contains a specific value.

To filter when the string at ["FullReport", "Browser"] path contains Firefox , {Details: {path: ["FullReport", "Browser"], string_contains: "Firefox"}}

Filtering with Relations

You are able to utilize related objects in a condition too, though the manner in which you do so will depend on the type of relationship that this object has with it’s related object.

When a relationship has one of another object, you can utilize the is and isNot filters to construct conditions on a related object. For example, if we have a Post object, which has one User object, we can create a condition utilizing the User’s objects properties like so: { user: { is: { name: { equal: "George" } } } }. This filter would return all Post objects where the related User’s name is equal to George.

When a relationship has many of another object, you can utilize the some, every, and none conditions to construct conditions on a set of related objects. For example, if we have a User object that has many Posts, we can create a condition utilizing the Post’s objects properties like so:{ posts: some: [ { title: { equal: "Cats" } } ] }. This will return a list of users where at least one of their posts has the title of “Cats”.

Here is a more comprehensive overview of these operators:

Operator

Description

Example

is

Filters a record when related record "is" equal to the filter condition. Can only be used when a relationship has one of another object.

If every user has one post, to filter when name equals Alan, { user: { is: { name: { equal: "Alan" } } } }

isNot

Filters a record when related record "is not" equal to the filter condition. Can only be used when a relationship has one of another object.

If every user has one post, to filter when name does not start with "A", { user: { isNot: { name: { startsWith: "A" } } } }

some

Filters a record when related record has "some" filter conditions as true. Can only be used when a relationship has many of another object.

If a brand has many sauces, to find out brands that have "Paprika" in some of their sauces, { sauces: { some: [ { ingr: { contains: "paprika" } } ] } }

every

Filters a record when related record has "every" filter conditions as true. Can only be used when a relationship has many of another object.

If a brand has many sauces, to find out brands that have "Paprika" in all of their sauces, { sauces: { every: [ { ingr: { contains: "paprika" } } ] } }

none

Filters a record when related record has "none" of its filter conditions as true. Can only be used when a relationship has many of another object.

If a brand has many sauces, to find out brands that do not have "Paprika" in any of their sauces, { sauces: { none: [ { ingr: { contains: "paprika" } } ] } }

Note: You can combine the three sections above to construct complex queries.

order_by (OrderByWithRelationInput)

The orderBy argument takes an array of objects, with the following definition:

{ [key: ScalarName]: "asc" | "desc" }

You can use any scalar on the object being queried as a key, along with the direction of the sort as the value in the object. Our GraphQL APIs support ordering of nested fields.

For example, given a sample schema with a User object:

{
  findManyUser(orderBy: { id: desc }) {
    id
    name
    posts(orderBy: { id: asc }) {
      id
      name
    }
  }
}

The response from this request will look like this:

{
  "data": {
    "findManyUser": [
      {
        "id": 2,
        "name": "User 2",
        "posts": [
          {
            "id": 1,
            "name": "Post 1"
          },
          {
            "id": 2,
            "name": "Post 2"
          }
        ]
      },
      {
        "id": 1,
        "name": "User 1",
        "posts": []
      }
    ]
  }
}

skip & take

Pagination with Neurelo’s APIs works using the skip and take options as an implementation of offset based pagination. You can pass an integer to each option to limit or skip the resulting objects.

For example, given a sample schema with a User object, to skip one record and take the next record:

{
  findManyUser(skip: 1, take: 1) {
    id
    name
  }
}

The response from this request will look like this:

{
  "data": {
    "findManyUser": [
      {
        "id": 2,
        "name": "User 2"
      }
    ]
  }
}

by

The by option takes an array of field names (Array), to specify the fields to group by with. It must be ensured that every selected scalar field in your graphql body that is not part of an aggregation must be included in by option's array of field names.

For example, let us use a sample schema with a User object,

# If the selected scalar fields in graphql body are name and country, then by needs to equal ["name", "country"].
query {
  groupByUser(by: [name, country]) {
    name
    country
    _count {
      id
    }
  }
}

The response from this request will look like this:

{
  "data": {
    "groupByUser": [
      {
        "country": "United States",
        "_count": {
          "id": 1
        }
      },
      {
        "country": "Canada",
        "_count": {
          "id": 1
        }
      }
    ]
  }
}

having (HavingInput)

having option is used in conjuction with by option to filter the groups by aggregate functions. It is a special kind of where which accepts all the where options, together with the ability to filter aggregate functions.

To filter by aggregate function, the following definition can be used:

{ [field: ScalarName]: { [key: aggregateFunction]: whereCondition } }

Supported aggregate functions are: _count, _sum, _avg, _min, and _max. whereCondition is the same as a where body and accepts all the where options.

For example, let us use a sample schema with a Post object,

query {
  groupByPost(by: [location], having: { num_words: { _sum: { gte: 1000000 } } }) {
    location
    _sum {
      num_words
    }
  }
}

The response from this request will look like this, which is the grouped and aggregated object(s):

{
  "data": {
    "groupByPost": [
      {
        "location": "United States",
        "_sum": {
          "id": 5179839
        }
      },
      {
        "location": "India",
        "_sum": {
          "id": 3044357
        }
      }
    ]
  }
}

Furthermore, similar to filter, you can use AND, OR, and NOT operators to do more powerful filtering. For example, to filter when the number of words is between 1000 and 10000, you can use {"OR": [{"id": {"_sum": {"gte": 1000}}}, {"id": {"_sum": {"lte": 10000}}}]}.

cursor

cursor option is used to retrieve results before or after a specified cursor. This cursor is a unique identifier or a combination of unique identifiers.

Implicitly an ordering is performed based on the unique identifiers specified in the cursor, to ensure a consistent and predictable sequence of results.

For example, let us consider a sample schema with a User object. To fetch all users with IDs less than or equal to 2, the following can be used:

query {
  findManyUser(cursor: { id: 2 }, orderBy: { id: desc }) {
    id
    name
  }
}

The response from this request will look like this:

{
  "data": {
    "findManyUser": [
      {
        "id": 2,
        "name": "User 2"
      },
      {
        "id": 1,
        "name": "User 1"
      }
    ]
  }
}

Furthermore, to fetch the user after skipping two users, starting from ID 3, we can use the following:

query {
  findManyUser(cursor: { id: 3 }, skip: 2, take: 1) {
    id
    name
  }
}

The response from this request will look like this:

{
  "data": {
    "findManyUser": [
      {
        "id": 5,
        "name": "User 5"
      }
    ]
  }
}

In the above example, User IDs 3 and 4 were skipped, and the result set of User ID 5 was returned.

NOTE: Currently, the REST APIs do not have an equivalent functionality. As a workaround, use order_by to sort the primary keys, followed by filter to exclude the primary keys. For instance, cursor: { id: 3 } would have an equivalent of order_by=[{"id": "asc"}] with filter={"id": {"gte": 3}}.

Modeling Relationships for MongoDB with Inner Objects and References

Currently, Neurelo for MongoDB doesn't support direct manipulation of objects with relations. However, you can work with inner objects (equivalent to embedded documents, i.e., documents nested within another document) or use references to model such relationships.

To decide which data modeling approach suits your needs best, refer to our guide on how to work with embedded documents and references in MongoDB.

In summary, inner objects enhance read performance by reducing database queries and ensuring atomic updates within a single document. They are optimal for closely related data that is frequently accessed together, remains relatively static, and adheres to the 16MB document size limit. However, they might lead to data redundancy and scalability challenges as your database grows.

On the other hand, references are more suitable for large or frequently changing data. They excel in many-to-many relationships by offering the flexibility to link documents across different collections, preventing data duplication and maintaining database normalization. The trade-off, however, is the need for additional queries to get related data, which could affect your performance.

Inner Objects

Neurelo supports the management of inner objects at the schema level. To learn how to create inner objects for your schema, please refer to the Neurelo Schema Language reference.

Now, assume that will have a sample schema with a User object and an Address inner object. A user has one address. For example,

{
  "objects": {
    "user": {
      "properties": {
        "id": {
          "type": "string",
          "sourceName": "_id",
          "sourceType": "ObjectId",
          "default": {
            "function": "auto"
          },
          "identifier": true
        },
        "name": {
          "type": "string"
        },
        "address": {
          "$ref": "#/innerObjects/Address",
          "nullable": true
        }
      }
    }
  },
  "innerObjects": {
    "Address": {
      "properties": {
        "id": {
          "type": "string",
          "sourceType": "ObjectId"
        },
        "street": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "zipcode": {
          "type": "string"
        }
      }
    }
  }
}

Create

To create a single user with its address, assuming that we have the sample schema running in an environment, we can issue a CreateOne mutation, with the data argument set to the userCreateInput input type. For example:

mutation {
  createOneuser(data: { name: "Admin User", address: { set: { id: "5ca4bbc7a2dd94ee5816238d", street: "Diagon Alley", zipcode: "27000", city: "Nowhere Land" } } }) {
    id
    name
    address {
      id
      street
      city
      zipcode
    }
  }
}

Note that, at present, our APIs do not allow the user to auto generate inner object ids, hence we had to manually specify it in the above examples.

The response from this insert request will look like this, which is the object that was inserted:

{
  "data": {
    "createOneuser": {
      "id": "65eb623e591811da1638040a",
      "name": "Admin User",
      "address": {
        "id": "5ca4bbc7a2dd94ee5816238d",
        "street": "Diagon Alley",
        "city": "Nowhere Land",
        "zipcode": "27000"
      }
    }
  }
}

To create multiple users with their address, we can issue a CreateMany mutation, with the data argument set to an array of userCreateInput types.

mutation {
  createManyuser(data: [{ name: "Admin User", address: { set: { id: "5ca4bbc7a2dd94ee5816238d", street: "Diagon Alley", zipcode: "27000", city: "Nowhere Land" } } }, { name: "Admin User 2", address: { set: { id: "5ca4bbc7a2dd94ee5816238e", street: "King's Court", zipcode: "27100", city: "Nashville" } } }]) {
    count
  }
}

The response (AffectedRowsOutput) from this insert request will look like this, which is the count of the inserted rows:

{
  "data": {
    "createManyuser": {
      "count": 2
    }
  }
}

Retrieve

To retrieve a unique user with its address, assuming that we have the sample schema running in an environment, we can issue a query to findUniqueUser with a where argument that filters on unique properties. For example:

query {
  findUniqueuser(where: { { id: "65eb623e591811da1638040a" } }) {
    id
    name
    address {
      id
      street
      city
      zipcode
    }
  }
}

The response from this request will look like this, which is the retrieved object:

{
  "data": {
    "findUniqueuser": {
      "id": "65eb623e591811da1638040a",
      "name": "Admin User",
      "address": {
        "id": "5ca4bbc7a2dd94ee5816238d",
        "street": "Diagon Alley",
        "city": "Nowhere Land",
        "zipcode": "27000"
      }
    }
  }
}

Similarly, to retrieve all users with their address, assuming that we have the sample schema running in an environment, we can issue a query with findManyuser. For example:

query {
  findManyuser {
    id
    name
    address {
      id
      city
      street
      zipcode
    }
  }
}

The response from this request will look like this, which is an array of the retrieved objects (with their inner objects):

{
  "data": {
    "findManyuser": [
      {
        "id": "65eb623e591811da1638040a",
        "name": "Admin User",
        "address": {
          "id": "5ca4bbc7a2dd94ee5816238d",
          "street": "Diagon Alley",
          "city": "Nowhere Land",
          "zipcode": "27000"
        }
      }
    ]
  }
}

Update

To update multiple users with their address, we can utilize the updateManyObject mutation with the data and where arguments to provide both our updated properties and a condition set to limit which objects are updated.

mutation {
  updateManyuser(data: { address: { set: { id: "5ca4bbc7a2dd94ee5816238d", street: "High Street", city: "Nowhere Land", zipcode: "27000" } } }, where: { address: { is: { id: { equals: "5ca4bbc7a2dd94ee5816238d" } } } }) {
    count
  }
}

The response (AffectedRowsOutput) from this update request will look like this, which is the count of the updated rows:

{
  "data": {
    "updateManyuser": {
      "count": 1
    }
  }
}

Delete

To delete specific users with their address, you can issue a deleteOneuser mutation with arguments present. For example:

mutation {
  deleteOneuser(where: { id: "65eb623e591811da1638040a" }) {
    id
    name
    address {
      id
      street
      city
      zipcode
    }
  }
}

The response from this request will look like this, which is the deleted object:

{
  "data": {
    "deleteOneuser": {
      "id": "65eb623e591811da1638040a",
      "name": "Admin User",
      "address": {
        "id": "5ca4bbc7a2dd94ee5816238d",
        "street": "Diagon Alley",
        "city": "Nowhere Land",
        "zipcode": "27000"
      }
    }
  }
}

Note that this will delete both the object and its inner object. To remove the contents of just the inner object, you can use the UPDATE request with an unset:

mutation {
  updateManyuser(data: { address: { unset: true } }, where: { id: { equals: "65eb623e591811da1638040a" } }) {
    count
  }
}

This approach requires setting the inner object of your schema, specifically the unique ID, to have nullable set to true.

API Errors

Errors for the API calls will be presented in a list format so that multiple issues can be communicated at once. Each error object will contain an errors property that contains the error message and the location of the error.

Two types of errors can be returned as a response:

  • Validation errors - request is invalid or contains invalid data

  • Execution errors - request execution failed

For example, a sample schema and a sample request like:

{
  "objects": {
    "actor": {
      "properties": {
        "actor_id": {
          "type": "integer",
          "identifier": true
        },
        "first_name": {
          "type": "string"
        },
        "last_name": {
          "type": "string"
        },
        "movie_count": {
          "type": "integer"
        }
      }
    }
  }
}
query {
  findManyActor(where: { movie_count: { gt: "3" } }) {
    id
    firstname
  }
}

Will generate the following response:

{
  "data": null,
  "errors": [
    {
      "message": "Invalid value for argument \"where.movie_count.gt\", expected type \"Int\"",
      "locations": [
        {
          "line": 2,
          "column": 18
        }
      ]
    },
    {
      "message": "Unknown field \"firstname\" on type \"Actor\". Did you mean \"first_name\"?",
      "locations": [
        {
          "line": 4,
          "column": 9
        }
      ]
    }
  ]
}

The above example contains two errors, each with an error message and the specific location of the invalid attribute in the details section. The location parameter contains a JSON path to the attribute that failed the validation.

Last updated