Contember provides a GraphQL API for fetching and modifying data. There are three types of queries available for each entity: "get" for fetching a single record by a unique field, "list" for simple listing, and "paginate" for pagination similar to the Relay specification. The "get" query requires a "by" parameter that allows filtering by a primary column or any other unique columns. The "list" query offers additional options such as filtering using complex conditions, ordering the result, and paging using a limit and offset. The "paginate" query works similarl to the "list" query but returns a connection object that includes pagination information such as the total count and edges for each record.

For following entity:

import { c } from "@contember/schema-definition";

export class Post {
  title = c.stringColumn().notNull();
  publishedAt = c.dateTimeColumn();
}

GraphQL schema will be similar to this (some types are omitted in the example for clarity):

query {
  getPost(by: PostUniqueWhere!, filter: PostWhere): Post
  listPost(filter: PostWhere, orderBy: [PostOrderBy!], offset: Int, limit: Int): [Post!]
  paginatePost(filter: PostWhere, orderBy: [PostOrderBy!], skip: Int, first: Int): PostConnection!
}
input PostUniqueWhere {
  id: UUID
}
input PostOrderBy {
  id: OrderDirection
  publishedAt: OrderDirection
}
input PostWhere {
  id: UUIDCondition
  publishedAt: DateTimeCondition
  and: [PostWhere!]
  or: [PostWhere!]
  not: PostWhere
}
enum OrderDirection {
  asc
  desc
}
type PostConnection {
    pageInfo: PageInfo!
    edges: [PostEdge!]!
}

type PageInfo {
    totalCount: Int!
}

type PostEdge {
    node: Post!
}

DateTime serialization

By default, DateTime columns are serialized through the GraphQL DateTime scalar, which formats the value with a JavaScript Date. This produces a valid ISO 8601 string but truncates the time to millisecond precision (e.g. 2024-01-01T11:22:33.444Z), dropping any sub-millisecond digits stored in PostgreSQL.

Two schema settings under content change this behavior.

fullDateTimeResponse

When enabled, the Content API serializes DateTime values directly from PostgreSQL instead of going through the JavaScript Date. This preserves the full microsecond precision of the underlying timestamptz column.

This setting is enabled by the v2.0 settings preset and above, so new projects have it on by default.

import { createSchema } from '@contember/schema-definition'
import * as model from './model'

export default {
  ...createSchema(model),
  settings: {
    content: {
      fullDateTimeResponse: true,
    },
  },
}

dateTimeResponseFormat

When fullDateTimeResponse is enabled, dateTimeResponseFormat controls the exact textual representation of the serialized value. It accepts two values:

ValueExample outputNotes
legacy (default)2024-01-01 11:22:33.444444+00Raw PostgreSQL text representation — a space separator and a numeric UTC offset, not a valid ISO 8601 string.
iso86012024-01-01T11:22:33.444444ZValid ISO 8601 string with a T separator and a Z suffix, formatted on the database side so microsecond precision is preserved.

The default is legacy for backward compatibility, because the raw PostgreSQL text format is what fullDateTimeResponse has always produced. To get standards-compliant ISO 8601 output, opt in to iso8601:

import { createSchema } from '@contember/schema-definition'
import * as model from './model'

export default {
  ...createSchema(model),
  settings: {
    content: {
      fullDateTimeResponse: true,
      dateTimeResponseFormat: 'iso8601',
    },
  },
}

dateTimeResponseFormat has no effect unless fullDateTimeResponse is also enabled. Both the scalar (single value) and list (dateTimeColumn().list()) variants of DateTime columns are formatted consistently, and null values are preserved as null.

Fetching a single record

To fetch a single record using the "get" query in Contember. For example, if you have an entity called "Post", there will be a field called "getPost" with a parameter called "by". To fetch a record, you need to know the unique field that identifies the record.

Example: fetching a single Post with the id "c4ae3a0f-d91b-42a8-ad3c-5ca6b9f407c2":

query {
	getPost(by: { id: "c4ae3a0f-d91b-42a8-ad3c-5ca6b9f407c2" }) {
		title
		publishedAt
	}
}

The "by" parameter allows you to filter by any unique column (or columns in case of a compound unique key). By default, the unique field is "id", but you can specify other unique fields using the .unique() method on the column or using the @c.Unique(...) class annotation.

You can then specify the fields you want to retrieve in the GraphQL query. In the example above, we are retrieving the "title" and "publishedAt" fields of the Post with the specified id.

You can also use the "get" query to filter by other unique fields, as long as they are defined as unique in your Contember schema. For example, if you have a unique field called "slug" in your Post entity, you can fetch a single Post using the following query:

query {
	getPost(by: { slug: "my-awesome-post" }) {
		title
		publishedAt
	}
}

The filter argument in a get query allows you to apply additional filters on the result in addition to the unique filter provided by the by argument. You can use the same filter conditions as in the list and paginate queries.

Example of how to use the filter argument in a get query:

query {
	getPost(
		by: { id: "c4ae3a0f-d91b-42a8-ad3c-5ca6b9f407c2" },
		filter: {
			publishedAt: { lte: "2019-12-20" }
			category: { name: { eq: "Graphql" } }
		}
	) {
		title
		publishedAt
	}
}

This query will fetch a single Post record with specified ID and also filter the result to include only those that were published before "2019-12-20" and are in the category with the name "Graphql".

Fetching a list of records

You can use the list query to fetch a list of records from a specific entity. The list query takes several arguments, including filter, orderBy, offset, and limit, which allow you to narrow down the results and specify the order and pagination of the returned data.

Example of how to use the list query to fetch a list of posts:

query {
  listPost(
    filter: {
      publishedAt: { lte: "2019-12-20" }
      category: { name: { eq: "Graphql" } }
    }
    orderBy: [{ publishedAt: asc }]
    limit: 10
  ) {
    title
    publishedAt
  }
}

This query will fetch a list of posts published before a certain date, ordered by their publication date in ascending order, with a limit of 10 results.

You can use the filter argument to apply conditions on the returned data. You can use comparison operators, such as eq, lt, contains, etc., to specify the values that a certain field should or should not have. You can also use logical operators, such as and, or, and not, to combine multiple conditions. For details, see filters section bellow.

The orderBy argument allows you to specify the order of the returned data. You can use the asc and desc values to specify ascending or descending order, respectively. For details, see sorting results bellow.

The offset and limit arguments allow you to specify the pagination of the returned data. The offset argument specifies the number of records to skip, while the limit argument specifies the maximum number of records to return.

Records pagination

There is an alternative to a list queries with a similar structure - a "paginate" queries. This query aims to be Relay compatible in the future.

The skip parameter determines the number of records to skip before returning results, while the first parameter determines the maximum number of records to return. The filter and orderBy parameters work the same way as they do in the list query.

Cursor based pagination is not yet supported.

In addition to fields for fetching a list of records, there is a pageInfo object with totalCount field, which tells you the total number of records that match the filter and sorting criteria. You can use this value to calculate the total number of pages and implement a pagination UI in your application.

Example of how to use the "paginate" query

query {
  paginatePost(
    skip: 1
    first: 2
    filter: { author: { name: { eq: "John Doe" } } }
    orderBy: [{ publishedAt: asc }]
  ) {
    pageInfo {
      totalCount
    }
    edges {
      node {
        id
        title
        author {
          name
        }
      }
    }
  }
}

Filters

To filter the results of a query in Contember's GraphQL API, you can use the filter argument. This argument takes an input object with fields corresponding to the columns of the entity you are querying. The value of each field can be a condition object specifying how the column should be filtered.

Comparison operators

GraphQL nameDescriptionExampleSupported columns
isNullis (not) null{isNull: true} or {isNull: false}All column types
eqequal to{eq: "value"}All column types except JSON
notEqnot equals to{notEq: "value"}All column types except JSON
inis in list{in: ["A", "B"]}All column types except JSON
notInis not in list{notIn: ["A", "B"]}All column types except JSON
ltless than{lt: 100}All column types except JSON
lteless than or equals to{lte: 100}All column types except JSON
gtgreater than{gt: 100}All column types except JSON
gtegreater than or equals to{gte: 100}All column types except JSON
containscontains a string (case sensitive){contains: "contember"}String only
containsCIcontains a string (case insensitive){containsCI: "contember"}String only
startsWithstarts with a string (case sensitive){startsWith: "contember"}String only
startsWithCIstarts with a string (case insensitive){startsWithCI: "contember"}String only
endsWithends with a string (case sensitive){endsWith: "contember"}String only
endsWithCIends with a string (case insensitive){endsWithCI: "contember"}String only
similarfuzzy match by overall trigram similarity{similar: "contember"}String only (requires pg_trgm)
wordSimilarfuzzy match against any word in the text{wordSimilar: "contmbr"}String only (requires pg_trgm)
includeschecks if array includes value or JSON contains value{includes: "value"}Array/List columns and JSON columns
minLengthminimum array length{minLength: 1}Array/List columns
maxLengthmaximum array length{maxLength: 5}Array/List columns

Example: GraphQL type for String condition

input StringCondition {
  and: [StringCondition!]
  or: [StringCondition!]
  not: StringCondition
  null: Boolean
  isNull: Boolean
  eq: String
  notEq: String
  in: [String!]
  notIn: [String!]
  lt: String
  lte: String
  gt: String
  gte: String
  contains: String
  startsWith: String
  endsWith: String
  containsCI: String
  startsWithCI: String
  endsWithCI: String
  similar: String
  wordSimilar: String
  always: Boolean
  never: Boolean
}

Fuzzy text search (trigram similarity)

The similar and wordSimilar operators perform fuzzy text matching based on trigram similarity, which is useful for search-as-you-type and for tolerating typos. They are backed by PostgreSQL's pg_trgm extension:

  • similar compares the overall trigram similarity between the column and the value (PostgreSQL column % value). A row matches when the similarity is above pg_trgm.similarity_threshold (default 0.3).
  • wordSimilar checks whether the value is similar to any word within the column text (PostgreSQL value <% column). A row matches when the word similarity is above pg_trgm.word_similarity_threshold (default 0.6). This is well suited for matching a short query against longer texts.

Example: fuzzy search by title

query {
  listPost(filter: { title: { wordSimilar: "contmbr" } }) {
    id
    title
  }
}
Requires the pg_trgm extension

These operators rely on the PostgreSQL pg_trgm extension. It must be enabled in your database before the operators can be used, otherwise queries fail with operator does not exist: text % text. Enable it once per database with:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

For good performance on larger tables, also add a GIN trigram index on the searched column — see Trigram indexes.

Note

The match thresholds are controlled by PostgreSQL settings (pg_trgm.similarity_threshold and pg_trgm.word_similarity_threshold), not by Contember. Adjust them at the database/session level if the defaults are too strict or too loose.

Logical operators

You can also use the and, or, and not operators to combine multiple filters:

GraphQL nameExample
and{and: [{ gte: "2019-12-20" }, { lte: "2019-12-30" }]}
or{or: [{isNull: true}, {eq: "value"}]}
not{not: {eq: "value"}}

Example how to combine two operators using AND

query {
  listPost(
    filter: {
      publishedAt: { and: [{ gte: "2019-12-20" }, { lte: "2019-12-30" }] }
    }
  ) {
    id
    title
  }
}

In this example, the and logical operator is used to specify that the publishedAt field must be greater than or equal to "2019-12-20" and less than or equal to "2019-12-30". This filters the result to only include posts with a publishedAt value within this range. The and operator can take an array of conditions, which must all be met in order for the overall condition to be true. The or operator can also be used in a similar manner, with the difference being that at least one of the conditions in the array must be true for the overall condition to be true. The not operator can be used to negate a condition, so that only records that do not match the condition will be included in the result.

Note

It is not possible to combine multiple fields in a single object. You have to wrap it using and or or fields.

Relation filters

Filtering over relationships allows you to apply filters on related entities in a query. This is useful for narrowing down the results of a query by using the properties of related entities. Relation filters can be used on all types of relationships, including "has many".

Example: filtering over relation

query {
  listPost(
    filter: {
      author: { name: { eq: "John Doe" } }
      tag: { caption: { eq: "graphql" } }
    }
  ) {
    id
    title
  }
}

In the above example, the listPost query is being filtered by two relationships - author and tag. For the author relationship, the filter is specifying that the name of the related author must be "John Doe". For the tag relationship, the filter is specifying that the caption of the related tag must be "graphql". This will return all posts that have an author with a name of "John Doe" and a tag with a caption of "graphql".

Sorting result

To sort the results of a query, you can use the orderBy argument in the query. This argument takes an array of objects, where each object represents a field to sort by and the sorting direction (ascending or descending). You can specify multiple fields to sort by and you can also sort by fields on related entities.

Example how to sort a list of posts first by the author's name in ascending order and then by the publishedAt field in descending order:

query {
  listPost(
      orderBy: [
        { author: { name: asc } },
        { publishedAt: desc }
      ]
  ) {
    id
    title
  }
}

In addition to asc and desc, there are also ascNullsFirst and descNullsLast which can be used to specify how to handle null values in the sorting. ascNullsFirst will place null values at the beginning of the sorted list, while descNullsLast will place them at the end.

Nested objects

When working with GraphQL, it is often necessary to traverse relationships between objects to retrieve the data needed.

Example

query {
  listPost {
    id
    title
    category {
      name
    }
    author {
      name
    }
  }
}

Additionally, you can traverse multiple levels of relationships by nesting fields within each other.

Example how to retrieve the author of each post within a category:

query {
	listCategory {
		id
		title
		posts {
			title
			author {
				name
			}
		}
	}
}

On "has many" relations, you can also set a filter, orderBy and limit with an offset.

Example how to use parameters on has-many relations

query {
  listCategory {
    id
    title
    posts(limit: 3, orderBy: [{ publishedAt: desc }]) {
      title
    }
  }
}

On both "has many" and "has one" relations, you can apply additional filters using the same syntax as with "list" queries.

Example how to fetch all posts, but category is filtered to only match given filter:

query {
	listPost {
		id
		title
		category(filter: { name: { eq: "GraphQL" } }) {
			name
		}
	}
}

Narrowed Has Many

The narrowed has-many feature allows you to efficiently filter a "has-many" relation by one or more fields of a compound unique key, where one part of the unique key references the entity you are querying. This lets you access a specific record within the "has-many" relation using a simplified query structure.

Example Schema: Categories and Translations

export class Category {
	translations = c.oneHasMany(CategoryTranslation, 'category')
	internalNote = c.stringColumn()
}

@c.Unique('category', 'locale')
export class CategoryTranslation {
	category = c.manyHasOne(Category, 'translations').cascadeOnDelete().notNull()
	locale = c.stringColumn().notNull()
	name = c.stringColumn()
}

In this schema:

  • The translations field defines a has-many relationship between Category and CategoryTranslation.
  • The CategoryTranslation entity has a compound unique key (category, locale) that ensures uniqueness for translations by category and locale.

Querying with Narrowed Has Many

Specific Narrowed Field

Contember automatically generates a narrowed field for each unique component of the relationship. In this case, it generates translationsByLocale, allowing you to filter the translations relation by the locale field:

query {
	listCategory {
		id
		translationsByLocale(by: { locale: "en" }) {
			name
		}
	}
}

This query returns the translation for the specified locale ("en") for each category in the result set, even though the relation is defined as has-many.

Engine 2.0+ Universal Narrowed Field

To provide more flexibility, Contember also generates a universal narrowed field, named <relationName>By (e.g., translationsBy). This field allows querying by all fields in the unique key except for the back-referencing field, which is auto-completed. The same query can be written as:

query {
	listCategory {
		id
		translationsBy(by: { locale: "en" }) {
			name
		}
	}
}

Transactions

It's important to note that transactions are not automatically started for queries. This is done to improve performance, but it also means that you may experience inconsistency in the results.

The transaction field allows you to wrap multiple queries into a single transaction. This ensures that all the queries are executed atomically.

Example how to use transactions in queries

query {
  transaction {
    listPost {
      title
    }
    listAuthor {
      name
    }
  }
}

This will ensure that the listPost and listAuthor queries are executed within a single transaction, ensuring consistency in the results.