Salt la conținutul principal

The data modeling language extension for GraphQL

This article describes the current capabilities of the Data modeling language extension for GraphQL (DML).

The DML extension lets you control both the data model-centric, and the view-centric —the solution data model—management of a Data Modeling Service (DMS) based schema.

notă

The data modeling REST API (DMS) in combination with the Cognite Python SDK offers more power and flexibility than the DML extension. They're the preferred tools for managing industrial knowledge graphs in Cognite Data Fusion (CDF).

Basics

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

Each type can contain properties that reference other types, forming a relationship. You can also specify the properties as a list of references to nodes of other types.

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

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

This example is a simple industry data model:

type Equipment {
"Unique identifier for the equipment"
id: int!
"name of the piece of equipment"
name: String!
"the parts included to build this equipment"
parts: [Part]
"the documents describing use and maintenance for the equipment"
documentation: [Document] @reverseDirectRelation(throughProperty: "equipment")
}

type Part {
id: int!
name: String!
documentation: [Document]
}

type Document {
id: int!
name: String!
source: String!
"the equipment this document describes"
equipment: Equipment
"the parts this document describes"
parts: [Part]
}

Where:

  • Equipment type has the properties id, and name properties.
    • id must be an Integer type, and must exist on all nodes of Equipment.
    • name must be a String type, and must exist on all nodes of Equipment.
  • Equipment type has a parts relationship to a list/array of one or more Parts.
  • Equipment type has a documentation relationship to a list/array of one or more Documents. This is a reverse direct relation, meaning the direct relation property on the Document view is used for graph traversal.
  • Part has the properties id and name,
    • id must be an Integer type, and must exist on all nodes of Part.
    • name must be a String type, and must exist on all nodes of Part.
  • Part has a documentation relationship to a list/array of one or more Documents.
  • Document type has the properties id, name, and File.
    • id must be an Integer type, and must exist on all nodes of Document.
    • name must be a String type, and must exist on all nodes of Document.
    • source must be a String type, and must exist on all nodes of Document.
  • Document has a equipment relationship to a single (piece of) Equipment.
  • Equipment.name has a comment/description (name of the piece of equipment).

Direct relations can also be lists. To model this, add the @directRelation directive to the field.

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 is no difference between a type being an object or interface. However, only interface can be extended / implemented upon.

EquipmentInterface and EquipmentObject will here have the same query capabilities:

interface EquipmentInterface {
name: String
}

type EquipmentObject {
name: String
}

Enums

DML also supports GraphQL enum types, which can be defined using the keyword enum:

enum Status {
ACTIVE
INACTIVE
}

type Equipment {
name: String!
status: Status
}
what happens in the background

In our DML, an object or interface type represents a view by default. When a @view directive isn't specified, the view version will be managed by the GraphQL service. When a new view version is needed due to breaking changes, the data model version will also have to be changed. View versions can also be overriden by directives.

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. You can also extend multiple interfaces by separating them with &.

This is an 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 type is versioned, and the version changes when the view in the data model is updated with a breaking change. As of now, all changes to the structure of a type are considered breaking changes, but if you leave the type unchanged it will not get a new version. Since you can query each version of a data model individually, consumers are not directly affected by breaking changes.

Relations

We support a number of different types of relations - they can point to a single target instance or multiple target instances, and they can be backed by either edges or direct relations.

A relation with a single target instance, for example document: Document, is backed by a direct relation. A direct relation is a container property storing a reference to another node. These can be used to describe both one to one and one to many relations - for example, an equipment can point to a single document describing it, but multiple equipments may point to the same document.

If you want to define a relation pointing to more than a single instance, you can create a field with a list data type, for example documents: [Document]. These can be used to describe both one to many and many to many relations - for example, an equipment can point to multiple documents describing it and multiple equipments can point to the same documents. These relations are by default backed by edges, and they need to be ingested as separate instances. It is possible to specify that relation should be backed by a list of direct relations instead by using the @directRelation directive.

what happens in the background

documents: [Document] will generate a view connection property that we call a relation in graphql. A relation is 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 is unique per space, so is the edge.type.

If you want to describe a many to many relation, you simply need to add in a 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
)
}

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 type that is 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 native resource types

Primitive data types are mapped as following:

GraphQL nameAPI nameDescriptionSupported
File
TimeSeries
Sequence

1 Beta

We don't support the union GraphQL types, as well as defining input and extends despite the feature being available in GraphQL.

How GraphQL SDL concepts are mapped to data models API

The main goal of the DML is to re-use concepts from the GraphQL schema definition language to the extent possible. However, the GraphQL SDL is designed to describe a schema, while we need to translate it into an implementation. There are multiple implementations conforming to the same GraphQL schema. For convenience, the translation from a GraphQL schema to a set of industrial knowledge graph schema components - containers and views - is based on sane defaults. However, this behavior can be overridden as needed by using directives.

Specification of directives

The following is a list of the directive definitions, with descriptions. There are more extensive guides describing how each is used, and the purpose 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.
* version: Overrides the version of the view, which by default is the same as the data model version.
* rawFilter: Sets a filter on the view, accepts a JSON string.

"""
directive @view(
space: String
version: String
rawFilter: JSON
) on OBJECT | INTERFACE

"""
Specifies that a type is an edge type.
The view can then be used to expand an edge node with a node instance with properties
as specified in the view with the @edge directive.
The edge should be annotated with `@relation(edgeSource: "name_of_edge_view")`.
"""
directive @edge ON OBJECT | INTERFACE

"""
Specifies that a type is importing another view and its backing container.
Currently, this annotation must be combined with @view to specify the
view and view version to import. In order to extend the view in your data
model, specify it as an interface and then create a new data type
that implements the interface and adds additional properties.
In the future, this directive will be extended to simplify importing.
"""
directive @import 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

* type: The reference to the edge type this relation is mapped to.
* name: Overrides the name property of the relation definition. This is merely metadata, and should not be confused with the
property identifier!
* edgeSource: The name of the edge view to use for expanding the edge node with a node instance.
* 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
edgeSource: 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

"""
Sets the default value of a field.

* value: Specifies the default value. The input is always in string format, however all dms supported default value types are also supported.

"""
directive @default(
value: String
) on FIELD_DEFINITION

"""
Sets the unit on a field.

* externalId: specifies a unit by using an external ID value. `externalId` can only be assigned to the types `Float`, `Float32`, `Float64`, or to an array of listed floating types and must match a unit in the unit catalog.
* sourceUnit: a free text field to store arbitrary unit information like unit name in a source system.

"""
directive @unit(
externalId: String,
sourceUnit: String
) on FIELD_DEFINITION

"""
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.
* cursorable: With cursorable index we can efficiently query with custom sort options, and queries will emit cursors that can be used to paginate through the results. Only supported for BTREE indexType.
"""
input _IndexDefinition {
identifier: String!
indexType: _IndexType
fields: [String!]!
cursorable: Boolean
}

"""
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 using the @mapping directive. 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. Fields that are mapped using the mapping directive, must exist. In other words, there are no auto-generation of fields mapped.

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