AMPize uses its own data framework called Alambic to connect and query heterogeneous data sources. Alambic is based on Facebook GraphQL and relies on a declarative and strong-types data model, which describes the types of objects that can be returned, the relations between them and the queries endpoints.
Models
A basic data model configuration describes:
- the data structure: Types
- how data can be fetched: Connectors
Built in Connectors
AMPize.me currently supports three different types of connectors, also know as data sources:
- Websites
- Databases
- REST API's
More connectors will be added in the future, including product specific API's, such as Magento API for example.
Types are described through Json files that can be uploaded in AMPize during the configuration process of REST API's and databases data sources.
Let's take a dive into this JSON configuration.
Types
At the heart of any GraphQL implementation is a description of what types of objects it can return, described in a GraphQL Type system and returned in the GraphQL Schema.
The Alambic Type system extends the GraphQL initial format by adding information about data validation and relations between objects, along with new internal Types, such as ImageUrl, Date, ... these internal Types that can be used to compose your own custom Type definition.
Internal Object Types
Internal Type | Description |
---|---|
String | String |
Int | Integer |
Float | Float |
Boolean | Boolean, TRUE or FALSE |
ID | String or numeric |
ImageUrl | Image url |
Date | Date format |
DateTime | Datetime format |
Time | Tme format |
HTML | HTML, will automatically be converted to AMP HTML |
JSON | JSON data |
JSONArray | JSON array data |
Custom Object Types
Every object field that composes your custom Type must belong to one of the internal or custom Types:
{
"Product": {
"name": "Product",
"description": "A product Type",
"fields": {
"productId": {
"type": "ID",
...
},
"Name": {
"type": "String",
...
},
"Price": {
"type": "Float",
...
},
"Weight": {
"type": "Int",
...
},
"InStock": {
"type": "Boolean",
...
},
"Stores": {
"type": "Store",
...
}
}
}
}
- the key "Product" is the name of the Type, it MUST be unique across all data sources
- the "name" value MUST be equal to the Type key
- each field is described by a number of properties
Fields
Fields are part of Object Types definitions.
They are described by the following properties:
Property | Type | Required | Description |
---|---|---|---|
name | String | No | Name of the field. If not set GraphQL will use the key of fields array |
type | Type | Yes | Must be an Internal Type or an existing foreign or embedded object Type |
description | String | No | Field description for clients |
required | Boolean | No | Is this field required? default to false |
args | Array | No | The args array is usually used to describe the fields that can be retrieved from a foreign object Type |
relation | Key:Value | No | Describes the relation between the current and the foreign object Type. Key = foreign key name; Value = local key Name. |
Relations
Individual object Types are easy to setup, but when building real applications developers will often need to connect objects together and build relations.
You can define the following relations between object Types:
- One to One
- One to Many
- Many to Many
- Embedded Types
One to One
A one to one relation between object Type A and object Type B defines that every instance of A "has one" instance of B. For example every "Post" has one "Author".
The declaring Type (Post) has a foreign key property (authorId) that references the primary key (id) of the target Type (Author).
{
"Post": {
"fields": {
"id": {...},
"text": {...},
"author": {
"type": "Author",
"relation": {
"id": "authorId"
}
}
}
}
}
The relation is always built as a "key:value" expression where the key is the target Type key name and the value is the declaring Type key name.
One to many
A one to many relation between object Type A and object Type B defines that every instance of A "has many" instances of B. For example every "Author" has many "Posts".
The target Type (Post) has a foreign key property that references the primary key of the declaring Type (Author).
The only difference between one-to-one and one-to-many declarations is that the "multivalued" option is set to true for one-to-many.
{
"Author": {
"fields": {
"id": {...},
"name": {...},
"posts": {
"type": "Post",
"multivalued": true,
"relation": {
"authorId": "id"
}
}
}
}
}
Many to Many
A many to many relation between two object Types can be established through a third Type.
For example Author and Blog can share many posts.
{
"Post": {
"fields": {
"id": {...},
"text": {...},
"Author": {
"type": "Author",
"relation": {
"id": "authorId"
}
},
"Blog": {
"type": "Blog",
"relation": {
"id": "blogId"
}
}
}
}
}
Embedded Types
Embedded relations are used to represent a Type that embeds another Type.
For example a post embeds one or many comments.
{
"id": 1,
"text": "My first blog post on alambic",
"comments": [
{"author": "John Doe", "text": "Awesome!"},
{"author": "Jane Doe", "text": "Very useful"}
]
}
In Alambic every foreign field Type declared without the "relation" property is considered as embedded.
{
"Comment": {
"fields": {
"author": {...},
"text": {...}
}
}
}
{
"Post": {
"fields": {
"id": {...},
"text": {...},
"comments": {
"type": "Comment",
"multivalued": true
** no relation **
}
}
}
}
Connectors settings
Once you described the data structure (Types, fields) and the relations between Types, you'll need to set up a few more attributes in your model to specify how to fetch data from the original source.
Attribute | Description |
---|---|
expose | Boolean, usually true if this Type can be directly queried |
singleEndpoint | Describes the API endpoint used to retrieve one item |
multiEndpoint | Describes the API endpoint used to retrieve a list of items |
connector | Describes the connector settings specific to this Type |
The configuration can be slightly different depending on the source Type: Database or REST API.
REST API settings
{
"content": {
"name": "content",
"description": "The Guardian API content",
"fields": {
"id": {...},
...
},
"expose": true,
"multiEndpoint": {
"name": "contents",
"args": {
"q": {
"type": "String"
}
}
},
"singleEndpoint": {
"name": "content",
"args": {
"ids": {
"type": "String",
"required": true,
"modelField": "id",
"description": "Content ID"
}
}
},
"connector": {
"type": "guardian",
"configs": {
"segment": "search",
"resultsPath": "response.results",
"startFieldName": "page",
"limitFieldName": "page-size",
"orderByFieldName": "order-by"
}
}
}
}
In this configuration:
- a "content" object can be retrieved one at a time by passing a required "ids" string parameter to the API. This parameter is mapped to the "id" field.
"singleEndpoint": {
"name": "content",
"args": {
"ids": {
"type": "String",
"required": true,
"modelField": "id",
"description": "Content ID"
}
}
}
- a list of "contents" can be also be retrieved by passing a "q" string argument to the API.
"multiEndpoint": {
"name": "contents",
"args": {
"q": {
"type": "String"
}
}
}
The connector configuration is done through the following properties:
"connector": {
"type": "guardian",
"configs": {
"segment": "search",
"resultsPath": "response.results",
"detailResultsPath": "response.results.0",
"startFieldName": "page",
"limitFieldName": "page-size",
"orderByFieldName": "order-by",
"orderByDirectionFieldName": "order-direction",
"ascendantValue": "ASC",
"descendantValue": "DESC"
}
}
- the "type" value must be the name given to the data source
- the "segment" value is concatenated with the to base API url of the data source
- the segment can accept vars substitution from the endpoint arguments with the syntax "{argName}"
- the API response is expected to yield results in the "response.results" path
- the single endpoint is expected to yield results as the first item of the "response.results" list
- pagination can be triggered by passing the "page" (start) and "page-size" (limit) arguments
- the order of the results can be set by passing the "order-by" argument
- the sort direction can be set by passing the "order-direction" argument
- Ascendant sort direction is set to "ASC"
- Descendant sort direction is set to "DESC"
Beware that:
- depending on your API, pagination and sorting may not be available
- Ascendant and Descendant values may be omitted if they match the default values: "ASC" and "DESC"
Database settings
In AMPize.me the database model is automatically generated from the database schema. A Type is generated from each table.
{
"user": {
"name": "user",
"description": "My user table in MySQL",
"fields": {
"id": {...},
...
},
"expose": true,
"multiEndpoint": {
"name": "user_multi",
"args": {}
},
"singleEndpoint": {
"name": "user_single"
},
"connector": {
"type": "myDBDataSource",
"configs": {
"table": "user"
}
}
}
}
- the multi endpoint is named {typeName} + "_multi" by default, but you set your own name
- the single endpoint is named {typeName} + "_single" by default, but you set your own name
- you don't need to set any arguments for endpoints, they will be automatically injected in the model, based on the table fields
- the connector config only takes a "table" attribute that maps the type to a db table
Examples
These files are used in AMPize samples: