Skip to content
  • There are no suggestions because the search field is empty.

Data API reference

Reference to data API queries, sorting, filtering, pagination, access tokens, API limits and rate limiting.

Data API allows you to retrieve and manipulate data from your Betty Blocks applications. This article covers querying properties and model relations, setting up sorting, filtering, pagination, etc. Additionally, it includes details on access tokens, API limits, and how requests are managed using buckets.

Connecting to GraphQL playground

Once you’ve set up a connection with the GraphQL playground, as explained in Getting started with data API, you can start querying data.

The GraphQL playground provides a comprehensive list of available queries (see more); however, this article will focus on practical examples using a Task model. Adapt these examples to your models as necessary.

Note: Connecting through the playground will also give you an entire list of queries available. Our interest here is in querying data from the Task model.

 

Querying data

Get all Tasks

This query returns an overview of all Task records, retrieving the IDs, names and end dates.

{
allTask {
results {
id
name
endDate
}
}
}

Output example:

 

Filtering Tasks with a 'where' condition

Use filters to narrow down the results. The following example retrieves Tasks where the ID is either 3 or 4.

{
allTask(where: { id: { in: [3, 4] } }) {
results {
id
name
endDate
}
}
}

Output example:

 

Retrieve a single task

This query returns Task 10, which matches the provided filter.

{
oneTask(where: { id: { eq: 10 } }) {
id
name
description
}
}

Output example:

 

Sorting and pagination

Sorting Tasks by a specific field

You can order the results by a specific field in ascending (ASC) or descending (DESC) order. The following query sorts Tasks by ID in descending order.

{
allTask(sort: { field: id, order: DESC }) {
results {
id
}
}
}

Pagination

To retrieve data in chunks, you can use pagination options like limit and offset. For example, the following query retrieves the first 5 Tasks:

{
allTask(limit: 5, offset: 0) {
results {
id
task
}
}
}

Querying relational data

You can retrieve related data along with the main task. In the following example, a task is fetched along with its associated project details (the Task model is related to the Project model).

{
oneTask(where: { id: { eq: 3 } }) {
id
name
project {
name
}
}
}

Output example:

 

Note: Ensure that authorization on relational data is properly set. If not, an 'Unauthorized' error may occur

 

Handling different data types

The following query demonstrates how to retrieve records that include file property values, such as name and URL.

{
allArtists {
results {
name
songs {
art {
name
url
}
}
}
}
}

Access tokens

Requesting data through the data API can be available to everyone (public data) or only to authorized users (private data). For the second group, a valid access token is required. With the refresh token flow for the data API, users can extend their active sessions, instead of starting a new one.

Refresh token

The login mutation has been updated to return a refreshToken:

mutation login{
login(
authProfileUuid: "<authProfileUuid>"
username: "<username>",
password: "<password>",
){
isValid
jwtToken
refreshToken
}
}

This token can be used in a new mutation refreshToken to extend your login by returning a new access JWT token. Be aware that for security reasons, the refresh token can only be used once, and the user will receive a new one after the mutation has been completed:

mutation refresh{
refreshToken(
token: "<refreshToken>"
){
isValid
jwtToken
refreshToken
refreshExpiresIn
}
}


Revoking a token

A refresh token can also be revoked at any time, using the revokeRefreshToken mutation:

mutation revoke{
revokeRefreshToken(
token: "<refreshToken>"
){
removed
refreshId
}
}

Maximum and minimum token lifetimes

  • Access token

    • Maximum lifetime = 10800 seconds (3 hours)

    • Minimum lifetime = 60 seconds (1 minute)

  • Refresh token

    • Maximum lifetime = 1209600 seconds (336 hours, 14 days)

    • Minimum lifetime = 60 seconds (1 minute)

You can adjust token lifetimes in the Authentication profiles settings, but you cannot exceed the maximum values.

 

Mutating data

Create a Task

This mutation will create a new Task record and insert it into the database. Any fields added to $input will be applied to the record. As with all mutations, $input will be subject to validations specified in the model.

const mutation = `
mutation {
createTask(input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { input: { task: "New Task" } });

Create many Tasks

This mutation can be used to create multiple Task records simultaneously. This works exactly like the createTask mutation, but the $input variable expects a list of inputs. IDs will be automatically generated by the database.​

Example use cases:

  • Add posts to a blog

  • Add items to inventory

const mutation = `
mutation {
createManyTask(input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { input: [
{ task: "Mow lawn" },
{ task: "Wash dishes" },
{ task: "Build todo application" },
]})

Delete a Task

This mutation will delete a Task record from the database. The only required input is the ID of the record to be deleted. Deleting a record will not delete any of its relationships.

const mutation = `
mutation {
deleteTask(id: $id) {
id
}
}
`

const { data, errors } = await gql(mutation, { id: 1 });

Delete many Tasks

This mutation can be used to delete multiple Task records simultaneously. The $input variable expects a list of IDs, as formatted in the example below. Filtering with $where is currently not supported, so this mutation is best combined with a separate query.​

Example use cases:

  • Delete all expired items

  • Delete users based on the domain of their email address

const mutation = `
mutation {
deleteManyTask(input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { input: { ids: [1, 2, 3] } } });

Update a Task

This mutation will update a single Task record. The record to be updated is selected by the $id. Any properties present in $input will replace the existing values on the record.

const mutation = `
mutation {
updateTask(id: $id, input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { id: 1, input: { task: "Updated Task" } });

Update many Tasks

Use this mutation to update multiple Task records at once. Specify which records to update using the $where filter, and define the new property values in the $input variable. All selected records will be updated with the same values.

Example use case:

  • Mark all posts older than a year as "archived"

  • Set the locale of users who are located at the office to English

const mutation = `
mutation {
updateManyTask(where: $where, input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { where: { task: { eq: "Existing Task" } }, input: { task: "Updated Task" } });

Upsert a Task

This mutation can be used to insert or update a Task. The $input expects Task data, without an ID property, and the $uniqueBy field expects a list of property names. The properties on $uniqueBy will be used to identify if the record already exists. If it does, the record will be updated; otherwise, a new record will be created.

It is important to know that all fields used on $uniqueBy must also be included in the $input, so that we can check which value to use.

Example use case:

  • After importing data, you want to update a field of a specific record, but you don't know if the record is already imported, so you can then select this record based on a different unique field.

const mutation = `

mutation {
upsertTask(input: $input, uniqueBy: $uniqueBy) {
id
}
}
`

// externalId is a property on the Task model, for instance an identifier from an external system/service
const { data, errors } = await gql(mutation, { input: { externalId: "unique:123", task: "Updated Task" }, uniqueBy: ["externalId"]});

Upsert many Tasks

This mutation can be used to update or insert multiple Tasks at the same time. The $input expects a list of Task data, each with its own ID and properties. For each item in the list, the mutation will first check if there is already a record with the same ID. If a record exists, it will be updated with the data from the item. If there is no record with this ID, it will be created with the given input.

Example use cases:

  • Fetch records from your inventory with a status "old" or "unprocessed"

  • Process inventory items and set the new status

  • Update the entire inventory in one mutation, using upsertMany

const mutation = `

mutation {
upsertManyTask(input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { input: [
{ id: 1, task: "Mow lawn" },
{ id: 2, task: "Wash dishes" },
{ id: 3, task: "Build todo application" },
]});

Upsert many can also have a uniqueBy argument just like the single upsert mutation.

Changing relationships

In 'Create' and 'Update' mutations, it is possible to add or remove relationships between records. The syntax for changing relationships is different for each type. Below you will find examples of how to change each relationship type. In this section, we will use the WebUser, Company and Role models as an example.

Belongs to

Belongs to relationships are set directly on the model, which belongs to another model. Adding the ID for the related model in the input will associate the new WebUser with Company 1. Updating relationships will use the same syntax.

const mutation = `
mutation {
createWebUser(input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { input: {
firstName: "Betty",
company: {id: 1},
}});

Has many

The inverse of a belongs to a relationship. In this example, we are taking a Company and associating the WebUser with ID 5. The WebUser will be detached from its current Company and attached to Company 1.

const mutation = `
mutation {
updateCompany(id: 1, input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, {
input: {
webUsers: { _add: [{ id: 5 }] },
}
})

Here you'll notice the _add syntax, which allows you to add new relations to the existing record. Both the Has many and Has and belongs to many will have three different functions to manipulate the list of relations. The other two are _remove, which allows for removal of existing relations and _replace, which will completely replace the existing relations with the ones provided in the input.

Has and belongs to many

Here are some examples:

  • Creating a WebUser with roles

When creating a record you should use the "_add" operation:

const mutation = `
mutation {
createWebUser(id: $id, input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { id: 1, input: {
firstName: "Betty",
...
roles: {_add: [{id: 1}, {id: 2}, {id: 3}]},
}})

Updating roles on a WebUser

Assume that the WebUser with id 1 has no roles to start with. Each mutation will be done in sequence, so we keep going with the result from each mutation:

const mutation = `
mutation {
updateWebUser(id: $id, input: $input) {
id
}
}
`

const { data, errors } = await gql(mutation, { id: 1, input: {
roles: {_add: [{id: 1}, {id: 2}, {id: 3}]},
}})

The user is now associated with the roles with ids 1, 2 and 3.

const { data, errors } = await gql(mutation, { id: 1, input: {
roles: {_remove: [{id: 2}, {id: 3}]},
}})

The association with roles 2 and 3 are removed. That means the user is only associated with role 1.

const { data, errors } = await gql(mutation, { id: 1, input: {
roles: {_replace: [{id: 3}, {id: 4}]},
}})

All existing roles on the user were removed, and the user is now associated with only roles 3 and 4.

Authentication

To send the mutations mentioned above, you don't need to specify authentication. These are only available from the Actions context, so authentication is handled by the Actions runtime. In most cases, authentication for the API can be done using the login mutation. This only concerns the HTTP API used in the Pages / custom frontends.​

There is also an additional login mutation available in Actions: generateJWT. This mutation allows you to generate a JWT for a given user, and you can optionally add custom fields to the resulting token. You can use the mutation to create custom authentication flows.

Here is an example of how you can send the mutation:

const mutation = `
mutation {
generateJwt(userId: $userId, authProfileUuid: $authProfileUuid, customFields: $customFields) {
jwtToken
}
}
`

const { data: {generateJwt: { jwtToken }} } = await gql(mutation, {
userId: 1,
authProfileId: "your-auth-profile-id",
customFields: { some: ["extra", "fields"] }
})

// The JWT payload contains the extra fields
const payload = JSON.parse(atob(jwtToken.split('.')[1]))

console.log(payload.custom_fields) // { some: ["extra", "fields"] }

Limits on data API

To ensure smooth operation, there are limits in place for the data API:

  • Read request rate:

    • Production: 200 per minute

    • Non-production and sandboxes: 100 per minute

  • Database query timeout: 5 seconds

  • Maximum queries per request: 10 queries

  • Maximum nesting: 4 layers

Rate limiting and buckets

Rate limiting counts the requests made to our server and applies a method called 'count' to determine when to block further requests. Both read and write requests are counted, and each application or sandbox has its own limit.

Buckets

To count and check the amount of requests you make, and to make sure applications don't make too many, they are being collected in something called a bucket. Multiple buckets handle a separate limit for a specified time.

In the new setup, 3 buckets are defined; one with a refresh rate per minute, one with an hourly refresh rate, and one with a daily refresh rate:

1-minute bucket: 200 requests

1-hour bucket: 2600 requests

1-day bucket: 1150 requests

Requests are first taken from the 1-minute bucket. If it’s empty, the hourly or daily bucket is used. Buckets refresh automatically when their time limit expires.

Note: In sandbox applications, these limits will be divided by 2. The reason for this is that sandboxes are more likely to contain inefficient pages and are used to test new code setups, which can slow down the server.