This guide will give a detailed explanation of object types, field types, field-constraints and directives in the Space Cloud schema definition language.
An object type is used to define a table/collection with the keyword type
.
Example:
type post {
id: ID! @primary
title: String!
text: String!
is_published: Boolean
}
The above example will create a post
table/collection which has the id
, title
, text
and is_published
columns/fields.
Fields are the building blocks of an object type. A field either refers to a scalar type, a nested type or a relation.
ID
An ID
is used to hold a string value of up to 50 characters. You use ID
to store prominent strings in your model like the unique identifier of a row/document.
Space Cloud auto-generates the value of ID
fields with ksuid (sortable unique identifiers) if you don’t provide their value during an insert operation.
Example: Uniquely identify an order in an e-commerce app:
type order {
id: ID! @primary
amount: Float!
}
String
A String
holds text. String
is used for fields like the title of a blog post or anything that is best represented as text.
Example:
type post {
id: ID! @primary
title: String!
text: String!
}
In queries or mutations, String
fields have to be specified using enclosing double quotes: string: "some-string"
.
Integer
An Integer
is a number that cannot have decimals. Use this to store values such as the number of items purchased or the combat power 💪🏻 of a pokemon.
Note: Int values are stored as 64 bit (ranging from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807).
Example:
type pokemon {
id: ID! @primary
name: String!
combat_power: Integer!
}
In queries or mutations, Integer
fields have to be specified without any enclosing characters: integer: 42
.
Float
A Float
is a number that can have decimals. Float
helps you store values such as the price of an item or the result of some complex calculation.
Example:
type item {
id: ID! @primary
name: String!
price: Float!
description: String
}
In queries or mutations, Float
fields have to be specified without any enclosing characters and an optional decimal point: float: 42, float: 4.2
.
Boolean
A Boolean
can have the value true
or false
. Boolean
can help you keep track of settings such as whether a post is published or if a pokemon is marked favourite by his trainer 😛.
Example:
type pokemon {
id: ID! @primary
name: String!
is_favourite: Boolean!
}
In queries or mutations, Boolean
fields have to be specified without any enclosing characters: boolean: true, boolean: false
.
Date
A Date
stores date values. A good example might be a person’s date of birth or the date a blog post was published.
Example:
type post {
id: ID! @primary
title: String!
is_published: Boolean
published_date: Date
}
In queries or mutations, Date
fields have to be specified in ISO 8601 format with enclosing double quotes: date: "2015-11-22"
Time
A Time
stores time values.
Example:
type post {
id: ID! @primary
title: String!
is_published: Boolean
published_time: Time
}
In queries or mutations, Time
fields have to be specified in hh:mm:ss
format with enclosing double quotes: time: "04:05:06"
.
DateTime
A DateTime
stores date and time values. A good example might be a person’s date of birth or the date a blog post was published.
Example:
type post {
id: ID! @primary
title: String!
is_published: Boolean
published_date: DateTime
}
In queries or mutations, DateTime
fields have to be specified either in ISO 8601 format with enclosing double quotes or in milliseconds since epoch without enclosing double quotes:
datetime: "2015-11-22"
datetime: "2015-11-22T13:57:31.123Z"
datetime: 1571207400000
JSON fields
A JSON
type stores JSON data. It is typically used when your data has nested structures rather than a flat structure.
Note: This is only available in PostgreSQL and MySQL as of now. Space Cloud uses the
JSONB
type for Postgres andJSON
type for MySQL underneath to provide this feature.
For example: We might want to store the address
of each user in the user
table. However, address
field itself can have other fields like pincode
, city
, etc. You can model such data easily in the following way using the JSON
type:
type user {
id: ID! @primary
email: ID!
name: String!
address: JSON
}
Providing such a schema allows you to use JSON objects in your mutations and queries directly. Serializing and unserializing of the JSON data is handled automatically.
Nested/embedded fields
Document oriented databases like MongoDB have first-class support of nested structures. Modelling nested structures in such databases is as easy as writing a separate schema for the nested structure and using that schema in the parent schema.
Note: This feature is only available in MongoDB.
Example: Let’s say each document in post
collection has an embedded document called author
:
type post {
id: ID! @primary
title: String!
text: String!
is_published: Boolean
author: author
}
type author {
id: String!
name: String!
}
Note: All the embedded types for a collection are provided in the schema of the collection itself.
Fields can be configured with certain constraints to add further semantics to your data model.
Not null constraint
You use a not-null constraint for required fields in your app. You can add a not-null constraint by adding an exclamation mark in front of the field type.
Example: Making email a required field for a user:
type user {
id: ID! @primary
email: String!
name: String
}
Primary key constraint
Primary key constraint is used to make a field as a unique identifier of the table/collection.
Example: Making order_id the unique identifier of an order:
type order {
order_id: ID! @primary
amount: Float!
}
Note: Space Cloud doesn’t support composite primary keys (Github issue) yet.
Example: Using primary key constraint on autoincrement/serial field:
type order {
order_id: Integer! @primary(autoIncrement: true)
amount: Float!
}
Unique constraint
A unique constraint is used to ensure that a field always has a unique value.
Example: Making username of a user unique:
type user {
id: ID! @primary
username: ID! @unique
email: String!
}
Example: Composite unique keys:
type user {
id: ID! @primary
first_name: String! @unique("group": "unique_name", order: 1)
last_name: String! @unique("group": "unique_name", order: 2)
}
Note: The
@unique
index only works with typeID
,Integer
,Float
,Boolean
andDateTime
.
The above example creates a composite unique key on two columns - first_name
and last_name
. Read more about @unique
directive from [here].
Foreign key constraint
A foreign key constraint is used to maintain the integrity of a relation.
Example: Create a foreign key on the id
field of author
for post
table:
type author {
id: ID! @primary
name: String! @unique
}
type post {
id: ID! @primary
title: String!
author: ID @foreign(table: "author", field: "id")
}
Example: Specifying onDelete
behaviour of foreign key:
type author {
id: ID! @primary
name: String! @unique
}
type post {
id: ID! @primary
title: String!
author: ID @foreign(table: "author", field: "id", onDelete: "cascade")
}
If the
onDelete
is not specified, then the default behaviour is to restrict the deletes violating the foreign key contraint.
Directives are used to provide additional information in your data model. They look like this: @name(argument: "value")
or simply @name
when there are no arguments.
The @primary
directive is used to make a field as the primary key in that table/collection.
Note: Only one field in a type can have
@primary
directive.
Example:
type order {
id: ID! @primary
amount: Float!
}
Note: You can specify the
autoIncrement
argument inside the@primary
directive forInteger
fields to use autoincrementing or serial IDs.
Example: Using autoincrementing primary field:
type order {
id: Integer! @primary(autoIncrement: true)
amount: Float!
}
The @unique
directive is used to put a unique constraint/index on a field(s). In its simplest form it looks like this:
type user {
id: ID! @primary
email: ID! @unique
name: String!
}
The above schema creates an unique index on the email
field. (i.e. No two users will have the same email
)
Note: The
@unique
index only works with typeID
,Integer
,Float
,Boolean
andDateTime
.
You can provide the following arguments in order to customize the unique index:
group
: (String) If two or more fields have the same group
, then they form a composite unique index.order
: (Integer starting from 1) Used to set the order of the column within the index . Required in case of composite unique index.Full fledged example: Make sure that the combination of first_name
and last_name
is unique:
type user {
id: ID! @primary
first_name: ID! @unique(group: "user_name", order: 1),
last_name: ID! @unique(group: "user_name", order: 2)
}
The @unique
directive is used to put a unique constraint/index on a field(s). It takes the following arguments:
group
: Optional. A string used to name the unique index. If two or more fields have the same group
, then they form a composite unique key.order
: Optional. An integer used to set the order of the column within the index . Required in case of composite unique key.The @default
directive is used to assign a column / field a default value. During an insert, if the field containing the @default
directive wasn’t set, the default value is used
Example: Setting the default value of role
to user
:
type account {
id: ID! @primary
name: ID!
role: ID! @default(value: "user")
}
Note: The
@default
directive only works with typeID
,Integer
,Float
,Boolean
andDateTime
.
The @foreign
directive is used to create a foreign key constraint. Foreign keys are used to maintain the integrity of relations in your data model.
Example: Create a foreign key between the order
and its customer
:
type customer {
id: ID! @primary
name: String!
}
type order {
id: ID! @primary
order_date: DateTime!
amount: Float!
customer_id: ID! @foreign(table: "customer", field: "id")
}
In the above example, a foreign key is created from the customer_id
field of order
table to the id
field of customer
table.
Note: Both the fields involved in the foreign key (in this case
order.customer_id
andcustomer.id
) should have the same type (ID
in this case).
Example: Specifying onDelete
behaviour of foreign key:
type customer {
id: ID! @primary
name: String!
}
type order {
id: ID! @primary
order_date: DateTime!
amount: Float!
customer_id: ID! @foreign(table: "customer", field: "id", onDelete: "cascade")
}
If the
onDelete
is not specified, then the default behaviour is to restrict the deletes violating the foreign key contraint.
Links are used to model relational data. They help you fetch a type along with its related type with a simple query.
Links don’t require you to create foreign keys either. So you can use it on relational and non relational databases.
Links are not physical fields in table. They are virtual fields which help Space Cloud to perform join operations on the backend.
The @link
directive is used to link a field to another field/table/link in the same or a different database.
You can pass the following arguments in the @link
directive:
table
: The table to link to.from
: The field in the current table used for linking both the tables.to
: The field in the linked table that used for linking both the tables.field
: Optional. The field in the linked table that you want to link to. Used if you want to link to a field/link.db
: Optional. The alias name of the database to link to. Used in cross-database links.In this example, we are going to link the orders of a customer to orders
field in customer
so that you can query a customer along with all his orders. Here’s a schema example to achieve this:
type customer {
id: ID! @primary
name: String!
orders: [order] @link(table: "order", from: "id", to: "customer_id")
}
type order {
id: ID! @primary
order_date: DateTime!
amount: Float!
customer_id: ID! @foreign(table: "customer", field: "id")
}
Note: There is no physical
orders
field in thecustomer
table. Thecustomer.orders
is a virtual field linked to another table (order table in this case).
So now you can perform this query on frontend:
query {
customer @mysql {
id
name
orders {
id
amount
order_date
}
}
}
The above query results in a join between the customer and order table with the condition - customer.id == order.customer_id
. This condition is described by the from
and to
arguments in the @link
directive.
Let’s say you want to query a customer along with the dates of all his orders. For that, we need to link the order_dates
of all the orders placed by a customer
. Here’s a schema example to achieve this:
type customer {
id: ID! @primary
name: String!
order_dates: [DateTime] @link(table: "order", field: "order_date", from: "id", to: "customer_id")
}
type order {
id: ID! @primary
order_date: DateTime!
amount: Float!
customer_id: ID! @foreign(table: "customer", field: "id")
}
So now you can perform this query on frontend:
query {
customer @mysql {
id
name
order_dates
}
}
Many to many relationships in SQL are tracked by a third table called the tracking table.
Let’s say we want to fetch all the orders with their items. In this case, we first link the order
table to the items
field in order_item
table (tracking table), which then links to the item
table. Here’s how you can model the schema for this example:
type order {
id: ID! @primary
order_date: DateTime!
items: [item] @link(table: "order_item", field: "items", from : "id", to: "order_id")
}
type order_item {
id: ID! @primary
order_id: ID! @foreign(table: "order", field: "id")
item_id: ID! @foreign(table: "item", field: "id")
items: [item] @link(table: "item", from: "item_id", to: "id")
}
type item {
id: ID! @primary
name: String!
description: String!
price: Float!
}
So now you can perform this query on frontend:
query {
order @mysql {
id
order_date
items {
id
name
description
price
}
}
}
When we want to link a field in one database to a field/table/link in another database, we use the db
argument of the @link
directive.
Let’s say we have our customer
table in one database (db1
) and the order
table in another database (db2
). Here’s how we can link them together with the db
argument:
type customer {
id: ID! @primary
name: String!
orders: [order] @link(db: "db2", table: "order", from: "id", to: "customer_id")
}
type order {
id: ID! @primary
order_date: DateTime!
amount: Float!
customer_id: ID!
}
The @index
directive is used to create an index on your table/collection. In its simplest form it looks like this:
type user {
id: ID! @primary
email: ID! @index
name: String!
}
The above schema creates an index on the email
field.
Note: The
@index
directive isn’t available on MongoDB yet and only works with typeID
,Integer
,Float
,Boolean
andDateTime
.
You can provide the following arguments in order to customize the index:
group
: (String) If two or more fields have the same group
, then they form a composite index.order
: (Integer starting from 1) Used to set the order of the column within the index . Required in case of composite index.sort
: (String - asc
|desc
) Used to set the sorting of the index.Full fledged example:
type user {
id: ID! @primary
first_name: ID! @index(group: "user_name", order: 1, sort: "asc"),
last_name: ID! @index(group: "user_name", order: 2, sort: "desc")
}
If you want to capture the creation time of a document/row, you should use the @createdAt
directive. A good example is the order date of order:
type order {
id: ID! @primary
order_date: DateTime! @createdAt
amount: Float!
}
In an insert mutation, you don’t need to provide values for the fields with @createdAt
directive. Space Cloud automatically inserts the values for them.
Note: You can use
@createdAt
directive only with a DateTime field.
If you want to store the time of the last update made to a document/row, you should use the @updatedAt
directive. A good example is showing the last modified date on a blog post:
type post {
id: ID! @primary
title: String!
last_edited: DateTime! @updatedAt
content: String!
}
In an update mutation, you don’t need to provide values for the fields with @updatedAt
directive. Space Cloud automatically updates the values for them.
Note: You can use
@updatedAt
directive only with a DateTime field.