Design GraphQL APIs
The easiest way to design your custom GraphQL data API with DataSQRL is to start with the GraphQL schema that is generated by the compiler.
docker run --rm -v $PWD:/build datasqrl/cmd compile myscript.sqrl -a graphql
When you add the -a graphql
option, the compiler writes a schema.graphqls
GraphQL schema file to the local directory where the compiler was invoked.
Change the name of the GraphQL schema file before you customize it. That way you avoid accidentally overwriting your changes if you run the command with the -a
option again.
Script to GraphQL Schema Mapping
DataSQRL maps the tables, fields, and relationships defined in the SQRL script to a GraphQL schema which exposes the data through a GraphQL API.
Tables are mapped to types in the GraphQL schema and table columns map to fields of that type. The field data type is the same as the column data type from the table. If the type isn't available in GraphQL, it is serialized into a generic type like string
.
Relationship columns of a table map to fields of the type associated with the table. The type of that field is the type associated with the table that the relationship links to.
The mapping between tables and types and (relationship) columns and fields is established by case-insensitive name. That means, to expose the table Orders
in the API we have to create a type Orders
or orders
in the GraphQL schema.
For example, the Orders
table we imported in the DataSQRL tutorial maps onto the following type in GraphQL
type Orders {
id: Int!
customerid: Int!
time: String!
items(productid: Int, quantity: Int, unit_price: Float, discount: Float, total: Float): [items!]
totals: totals
}
The type has one field for each column in the table with the same name as the column. The data type of the field is determined by the data type of the column. The exclamation mark !
indicates that a field is non-null, which is also inferred from the data type of the column.
The type has a relationship field items
that links to the nested Orders.items
table which is mapped to the type items
in the GraphQL schema. For nested tables, the type name is equal to the name of the table and not the full table path.
The field totals
is a relationship field to the nested Orders.totals
table that aggregates the total price and savings for each order. The compiler infers whether a relationship has a to-one or to-many multiplicity. The totals
relationship returns a single object of type totals
whereas the items
relationship returns an array of [items!]
. For to-one relationships, no optional argument filters are generated.
The relationship field accepts optional arguments for each field of the related type to filter on. That means, if we traverse the items
relationship and provide an argument for product id productid: 10
, then the relationship field only returns those items where the product id is equal to 10
.
The nested table Users.spending
is mapped to the following type in GraphQL
type spending {
week: String!
spend: Float!
saved: Float!
parent: Users!
}
The type has a field for each column in the table. In addition, it has the relationship field parent
that relates a nested table record to its parent record from the Users
table.
Non-nested tables in the script map onto query entry-points of the same name as the table which can be used to query the table. In the DataSQRL tutorial, we have the root (i.e. non-nested) tables Orders
and Users
which are exposed through the following query endpoints:
type Query {
Orders(id: Int, customerid: Int, time: String): [orders!]
Users(id: Int): [Users!]
}
Like relationship fields, query fields have optional arguments for each field of the returned type to filter on. For example, Users(id: 5)
returns the user with id equal to 5
.
GraphQL Schema Customization
The compiler generates complete GraphQL schemas, which means that the schema contains all tables, fields, and relationships defined in the SQRL script as well as field filters for all fields. In most cases, we don't want to expose all of those in the data API.
You can create your own custom GraphQL schema by trimming the generated schema and only expose those tables, fields, relationships, and filters that are required by your data API.
In our DataSQRL tutorial we don't need any filters for the items in each order, so we remove all the arguments from that field.
type Orders {
id: Int!
customerid: Int!
time: String!
items: [items!]
totals: totals
}
We also don't need to navigate from the nested Users.spending
back to Users
so we remove the parent
relationship field.
type spending {
week: String!
spend: Float!
saved: Float!
}
For the query endpoints, we only want to filter Orders
by time
and return a single user by id, with the id
being a required argument:
type Query {
Orders(time: String): [Orders!]
Users(id: Int!): Users
}
Adding Pagination
Pagination allows the user of an API to page through the results when there are too many results to return them all at once.
For our example, we might have thousands of orders and wouldn't want to return all of them when the user accesses the Orders()
query end point of our API.
To limit the number of results the API returns and allow the user to page through the results to retrieve them incrementally, we add limit
and offset
arguments to query endpoints and relationship fields in GraphQL schema.
type Query {
Orders(time: String, limit: Int!, offset: Int = 0): [Orders!]
Users(id: Int!): Users
}
The Orders()
query endpoint requires a limit and an optional offset which defaults to 0
.
type Users {
id: Int!
purchases(limit: Int!, offset: Int): [Orders!]
spending(week: String, limit: Int = 20): [spending!]
}
In the type definition for Users
above, we use limit
and offset
arguments to allow users of the API to page through the purchase history of a user and return a limited amount of spending analysis.
Full Example GraphQL Schema
The following is the final customized GraphQL schema for our DataSQRL tutorial example.
type Query {
Orders(time: String, limit: Int!, offset: Int = 0): [Orders!]
Users(id: Int!): Users
}
type Users {
id: Int!
purchases(limit: Int!, offset: Int): [Orders!]
spending(week: String, limit: Int = 20): [spending!]
}
type spending {
week: String!
spend: Float!
saved: Float!
}
type Orders {
id: Int!
customerid: Int!
time: String!
items: [items!]
totals: totals
}
type items {
productid: Int!
quantity: Int!
unit_price: Float!
discount: Float
total: Float!
}
type totals {
price: Float!
saving: Float!
}
Mutations
To supporting adding data through the API you add a mutation to the GraphQL schema. The mutation should have a single argument with the input type of the data to be added. The return type of the mutation can contain all or some of the fields of the input type in addition to the special field _source_time
which contains the timestamp when the data was added.
For example, in the DataSQRL tutorial we added the following mutation to capture product page visits:
type Mutation {
ProductVisit(event: VisitEvent!): CreatedProductVisit
}
input VisitEvent {
userid: Int!
productid: Int!
}
type CreatedProductVisit {
_source_time: String!
productid: Int!
userid: Int!
}
This defines a single mutation called ProductVisit
which accepts input data of the type VisitEvent
.
The added data can then be imported into a SQRL script by using the GraphQL schema file name as the package name and the name of the mutation as the table name.
For example, if the GraphQL schema filename is seedshop.graphqls
then we can import the product page visit data via
IMPORT seedshop.ProductVisit;
Additional Reading
Refer to the API chapter of the intro tutorial for a step-by-step guide to customizing GraphQL APIs.