Skip to main content

GraphQL data modeling language specification

The GraphQL Data Modeling Language (DML) can translate to data models API definitions. The language is compatible with the GraphQL schema definition language. This document defines the current feature set.

Basics

The data model format is GraphQL Schema Definition Language. Each data model has many types, consisting of fields with a specified type.

Each type contains fields that reference another type, forming a relationship. You can also specify the fields as an array (list) of another type.

Use ! to indicate that a field is required and that each instance of the template must contain the value.

To add comments, enter "..." above the field/type definition.

Limits

A data model may consist of at most 100 types, each having at most 300 fields. A data model can contain up to 10 million instances (nodes and edges).

Example data model

This example is a simple data model for movies.

type Movie {
"name of the movie"
name: String!
actors: [Actor]
watchedIt: Boolean
}
type Actor {
name: String!
age: Int
}

Where:

  • Movie type has the fields name and watchedIt fields.
    • name must be a String type, and must exist on all instances of Movie.
    • watchedIt must be a Boolean type.
  • Movie type has an actors relationship to a list of Actors.
  • Actor has the fields name and age,
    • name must be a String type, and must exist on all instances of Actor.
    • age must be an Int type.
  • Movie.name has a comment/description (name of the movie).

Types and Interfaces

An object type in GraphQL is defined by using the keyword type, for example type Equipment defines an object type in GraphQL called Equipment.

An interface type in GraphQL is defined by using the keyword interface, for example interface Equipment.

From a data querying or population perspective, there are no difference between a type being an object or interface. However, only interface can be extended / implemented upon.

EquipmentInterface and EquipmentObject will have the same query capabilities, in the example below.

interface EquipmentInterface {
name: String
}
type EquipmentObject {
name: String
}
what happens in the background

In our DML we assume that an object or interface type represents a view. By default a GraphQL type is interpreted as having a @view directive. Furthermore we assume this view has the same version as the data model version, see directives for how to override this version.

We call types having a '@view' directive for view types. A view type can extend from other view types, allowing for graphql inheritance, more in the next section.

Extending types via inheritance

We rely on the inheritance rules as defined by GraphQL, so to extend another view type, that base view type has to be an interface.

A view type can be extendable via inheritance by defining the type to be an interface. A view type can then extend the base type by using implements.

Example of a view extending another view

interface Common {
name: String
}
type Equipment implements Common {
name: String
}

There are some caveats with fields that are imported from inheritance, learn more in the advanced section.

Descriptions

GraphQL descriptions are supported and will be converted to the corresponding description property on view, container and property definitions. Furthermore, we have syntax for setting the name of a view / container or a field. It is set by starting a line in the description with @name. The rest of the line will then be parsed as the name.

Example of GraphQL descriptions

"""
This is a common interface for common models
@name Common View
"""
interface Common {
name: String
}

type Equipment implements Common {
"""
This is the description for the property Equipment.name
@name Name
"""
name: String
}

Versioning

Every template group is versioned, and the version changes when the data model is updated with a breaking change. Since you can query each version individually, consumers are not directly affected by breaking changes.

Also, each version has its own set of template instances. The data is tied to each version of a template group, and there is no automatic migration between versions.

Direct relation vs. relation

DMS supports two types of relations: direct relations and relations.

A direct relation is a container property storing a reference to another node (commonly referred as one to one relationship). It only supports storing a single reference. In the DML, specifying a single reference, for example document: Document, generates a direct relation automatically.

If you want to describe a one to many relation, you have to create a field whose data type is a list, For example document: [Document].

what happens in the background

This will generate a view property definition that we call a relation. A relation represents a traversal of edges defined by the:

  • relational direction (edge direction), which by default is outwards
  • the edge.type to follow. The default is set to {your_typename}.{your_fieldname}.
  • the space is by default set to be the same as the view that the field resides in.

However, just as type's name are unique per space, so is the edge.type.

If you want to describe a many to many relation, you simply need to add in an reference in the opposite direction, and use @relation to specify that this relation goes inwards instead of the default outwards. For example,

type Equipment implements Common @view(space: "core") {
name: String
documents: [Document] # This is identical to setting @relation(type: { space: "core", externalId: "Equipment.documents" }, direction: OUTWARDS)
}
>
interface Document {
name: String
equipments: [Equipment]
@relation(
type: { space: "core", externalId: "Equipment.documents" }
direction: INWARDS
)
}

More details on how to do this in the bi-directional relationship guide. See directives for more options.

Categories of types

The following types are available:

  • Primitives - simple types like String, Int, Int64, Float, Boolean, JSONObject, TimeSeries, and Timestamp.
  • User defined - any other types that' are defined in the data model. In the example above, Actor and Movie are examples of user defined types, forming relationships between types.

All of these types can exist in a list (for example [String]) or in singular form (for example String) through [...].

Primitive data types

Primitive data types are mapped as following:

GraphQL nameAPI nameDescription
Stringtext
Int or Int32int3232-bit integer number
Int64int6464-bit integer number
Float or Float64float6464-bit number encoded in IEEE 754
Float32float3232-bit number encoded in IEEE 754
TimestamptimestampTimestamp encoded in ISO 8601
JSONObjectjsonJSON encoded data
DatedateDate encoded in ISO 8601, but without time
Booleanboolean

CDF data types

Primitive data types are mapped as following:

GraphQL nameAPI nameDescriptionSupported
TimeSeries
AssetOn the roadmap
SyntheticTimeSeries
GeospacialOn the roadmap
3DOn the roadmap
Sequence
FileOn the roadmap
EventOn the roadmap
GraphQL Union

We do not support unions graphql types, as well as defining input and extends within the DML despite the feature being available in GraphQL.

How GraphQL SDL concepts is mapped to data models API

The main goal of the DML is to re-use as much concepts as possible from the GraphQL schema definition language. However the GraphQL SDL is designed to describe a schema, while we need to translate this it into an implementation. There exists multiple implementations conforming to the same GraphQL schema. For convenience, the translation from a GraphQL schema to the data model definitions, will be based on sane defaults. However, the behavior can be overridden when needed by using directives.

Directives

This is the more concrete list of all the directive definition and their description. We have better guides on how each is used and what purposes they serve in the advanced data modeling guides.

"""
Specifies that a type is a view type.

* space: Overrides the space, which by default is the same as the data model.
* name: Overrides the name of the view, which by default is the same as the externalId.
* version: Overrides the version of the view, which by default is the same as the data model version.
"""
directive @view(
space: String
name: String
version: String
) on OBJECT | INTERFACE

"""
Overrides the mapping of a field. Can only be used in a view type and can not be used on imported fields.

* space: Overrides the space, which by default is the same as the data model space.
* container: Overrides the container externalId, which by default is the same as the externalId of the view postfixed with 'Container'.
* property: Overrides the container property identifier being mapped.
"""
directive @mapping(
space: String
container: String
property: String
) on FIELD_DEFINITION

"""
Defines the relation field's details

* name: Overrides the name property of the relation definition. This is merely metadata, and should not be confused with the property identifier!
* direction: The direction to follow the edges filtered by 'type'.
* type: Specifies the edge type, namespaced by 'space', where the 'externalId' corresponds to the edge type name.
"""
directive @relation(
type: _DirectRelationRef
name: String
direction: _RelationDirection
) on FIELD_DEFINITION

"""
Configures the backing container of the view.
It can be used to configure indexes or constraints for the properties of a container.
"""
directive @container(
constraints: [_ConstraintDefinition!]
indexes: [_IndexDefinition!]
) on OBJECT | INTERFACE

"""
Represents an index definition.
* identifier: An unique identifier for the index
* indexType: Is the type of index
* fields: List of field names to define the index across. Only supported for non-inherited or mapped fields. The order of the fields matters.
"""
input _IndexDefinition {
identifier: String!
indexType: _IndexType
fields: [String!]!
}

"""
Represents a constraint definition. The constraint definition can either be an uniquness constraint or requires.
In case 'constraintType' is set to 'REQUIRES', the 'require' argument must be set.
And if it's set to 'UNIQUENESS', the 'fields' argument must be set.

* identifier: An unique identifier for the constraint
* constraintType: Is the type of constraint
* fields: List of field names to define the unique constraint across. Only supported for non-inherited or mapped fields. The order of the fields matters.
* require: Specify a container (by space and externalId), which is required to exist on the node, if the current container is to be used.
"""
input _ConstraintDefinition {
identifier: String!
constraintType: _ConstraintType
require: _DirectRelationRef
fields: [String!]
}

enum _ConstraintType {
UNIQUENESS
REQUIRES
}

enum _IndexType {
BTREE
}

input _DirectRelationRef {
space: String!
externalId: String!
}

enum _RelationDirection {
INWARDS
OUTWARDS
}

Advanced: mapping and importing fields

A view type can also map directly to properties in a container. Thus the fields of a type defined in GraphQL can be a mix of imported properties and mapped properties.

Fields that are imported via inheritance can not be mapped. Also fields that are mapped using the mapping directive, must exist. In other words there are no auto-generation of fields mapped.

Fields that are not "imported" is by default mapped to a generated container with the same name as the type name. For example above, EquipmentObject will create a view and container of the same externalId.