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.
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 fieldsname
andwatchedIt
fields.name
must be aString
type, and must exist on all instances ofMovie
.watchedIt
must be aBoolean
type.
Movie
type has anactors
relationship to a list ofActor
s.Actor
has the fieldsname
andage
,name
must be aString
type, and must exist on all instances ofActor
.age
must be anInt
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
andEquipmentObject
will have the same query capabilities, in the example below.interface EquipmentInterface {
name: String
}
type EquipmentObject {
name: String
}
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]
.
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 type
s are available:
- Primitives - simple types like
String
,Int
,Int64
,Float
,Boolean
,JSONObject
,TimeSeries
, andTimestamp
. - User defined - any other types that' are defined in the data model. In the example above,
Actor
andMovie
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 name | API name | Description |
---|---|---|
String | text | |
Int or Int32 | int32 | 32-bit integer number |
Int64 | int64 | 64-bit integer number |
Float or Float64 | float64 | 64-bit number encoded in IEEE 754 |
Float32 | float32 | 32-bit number encoded in IEEE 754 |
Timestamp | timestamp | Timestamp encoded in ISO 8601 |
JSONObject | json | JSON encoded data |
Date | date | Date encoded in ISO 8601, but without time |
Boolean | boolean |
CDF data types
Primitive data types are mapped as following:
GraphQL name | API name | Description | Supported |
---|---|---|---|
TimeSeries | ✅ | ||
Asset | On the roadmap | ❌ | |
SyntheticTimeSeries | ❌ | ||
Geospacial | On the roadmap | ❌ | |
3D | On the roadmap | ❌ | |
Sequence | ❌ | ||
File | On the roadmap | ❌ | |
Event | On the roadmap | ❌ |
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.