Using ActiveQL
ActiveQL basic concepts

ActiveQL concepts

We believe - as Alan Kay puts it - "Simple things should be simple, complex things should be possible". Let's see the simplicity and power of a domain driven API design by looking at this very simple example of a business domain expressed in YAML.

Example

enum:
  CarBrand:
    - Mercedes
    - BMW
    - Audi
    - Porsche
 
entity:
  Car:
    attributes:
      license: Key
      brand: CarBrand!
      mileage: Int!
      color: String

This simple example configuration would generate a

full fledged GraphQL API

  • type object for entity
  • query for the type (by id)
  • query for a list of items of types (with filter, sort and paging)
  • filter types for object type
  • filter types for all object type fields (e.g. filtering by String)
  • create mutation
  • update mutation
  • delete mutation
  • input type for the create mutation
  • input type for the update mutation
  • result type for create and update mutations
  • enumeration type
  • resolver for the queries and mutations that read/write to a datastore (per default a document based database)
  • statistics queries to get the number of entries, latest updates etc.
  • some helper types, queries and more

Some more API Features (not included in this example)

  • validation of mutation input (configuration or function)
  • relationships between entities
  • role based permission management to grant a principal fine-grained access to queries and mutations
  • handling of file uploads
  • seed data for testing your API or filling the datastore with initial data
  • custom code (hooks) to influence the behavior of the resolver
  • possible replacement for default implementation of
    • datastore
    • resolver
    • validation
    • file save
    • data seeding
  • and more

Let's break the generated GraphQL schema down, to show some of the ActiveQL conventions.

Type

type Car {
  id: ID!
  license: String!
  brand: CarBrand!
  mileage: Int!
  color: String
  createdAt: DateTime!
  updatedAt: DateTime!
}

From the Car entity a GraphQL object type is generated with attributes from the Entity definition becoming fields of this type. As you see, in addition the entity's attributes the type gets a unique id field, that is used to identify an entity item. This id is also used to implement the relationship of entities. The id is assigned by ActiveQL (in fact the actual DataStore implementation) when you call the create mutation.

You might notice that the "license" attribute was configured as type Key but the resulting field is of type String!. Key is in fact an ActiveQL shortcut, for a more complex attribute configuration. Actually all attribute configurations of this example are shortcuts - we will cover this in Attribute Configuration. The attribute "license" could have been written explicitly as

license:
  type: String
  required: true
  unique: true
  updateInput: false
  queryBy: true

Meaning it is a mandatory String field, that will be validated as unique and can only be set in a create mutation but not in an update mutation. Thus making it immutable and allowing it to unambiguously identify an entity item separately from the artificial id. The queryBy configuration will add a carBylicense query to easily get a car by its license.

Every entity type has also createdAt and updatedAt timestamp fields. That are set automatically by the mutation resolvers.

Mutations

type Mutation {
  ping(some: String): String
  seed(truncate: Boolean): String
  createCar(car: CarCreateInput): SaveCarMutationResult
  updateCar(car: CarUpdateInput): SaveCarMutationResult
  deleteCar(id: ID): [String]
}

Any schema includes a mutation ping that simply sends the value back and can be used to test whether a GraphQL API can be accessed without the need to build any specific query or mutation.

There is also a seed mutation you can call to seed the datastore with test or initial data. We did not include any seed data to this example definition, so this would have no effect. For more - check out Seed Configuration.

Entity Mutations

For every entity a create, update and delete mutation is generated - in this example createCar, updateCar and deleteCar.

Input Types

The mutations use input types to hold the value of items that should be created or updated.

input CarCreateInput {
  license: String!
  brand: CarBrand!
  mileage: Int!
  color: String
}
 
input CarUpdateInput {
  id: ID!
  brand: CarBrand
  mileage: Int
  color: String
}

As you see there are separate types generated for the create and update mutations. The CreateType does not have an id while the UpdateType does. The id determines which entity item should be updated.

Also note that the "color" attribute is configured as required in the entity configuration. Therefore it results in a mandatory field in the Car type and in the CarCreateInput type. But not in the CarUpdateInput type - this is because a client is not forced to pass all attributes in the update mutation but only those it wants to change. Making required attributes mandatory in the GraphQL schema for the update input would not allow to leave the others (e.g. "brand" or "mileage") untouched.

You might also have noticed that there is no "license" field in the CarUpdateInput. This is because we have configured the attribute as Key and therefore it is considered to be immutable.

Mutation Result Types

The create and update mutations return different data, depending on whether the operation could be executed or was prevented by validation errors.

type SaveCarMutationResult {
  validationViolations: [ValidationViolation]!
  car: Car
}
 
type ValidationViolation {
  path: String
  message: String!
}

If the mutation could be executed it returns the successfully created/updated entity items.

If there were validation errors it returns a list validation violations to inform an API client about the failed validations. Apart from the unique nature of the "license" attribute we do not have any other validations. See Attribute Validation and Entity Validation how to add business validations.

Please be aware that invalid GraphQL requests, e.g. not providing a mandatory field or having a value that does not match to the field type, will be handled by the GraphQL layer - resulting in an error.

So default mutation can have three possible responses:

SituationMutation Response
GraphQL request valid, all business validations passed, mutation was executedthe created / updated entity item
GraphQL request valid, one or more business validations did not passlist of ValidationViolations
GraphQL request invalid or error while executing mutationerror array - with stack-trace

Create Mutation

For any entity a create mutation is generated to create new entity items. As we've seen, it uses the create input type for the attribute values and the ValidationViolation type to inform a client about possible validation errors.

```graphql mutation { createCar( car: { license: "HH AQ 2020" mileage: 310000 }){ car{ id brand mileage license } validationViolations } } ``` ```json { "error": { "errors": [ { "message": "Field \"CarCreateInput.brand\" of required type \"CarBrand!\" was not provided.", } ] } } ``` ```graphql mutation { createCar( car: { license: "HH AQ 2020" brand: PORSCHE mileage: 310000 }){ car{ id brand mileage license } validationViolations } } ``` ```json { "data": { "createCar": { "car": { "id": "hUFcoso8KUs6oXcW", "license": "HH AQ 2020", "brand": "MERCEDES", "mileage": 10000 }, "validationViolations": [] } } } ```

Update Mutation

For any entity an update mutation is generated to create new entity items. It uses the update input type for the attribute values and the ValidationViolation type to inform a client about possible validation errors.

```graphql mutation { updateCar( car: { id: "hUFcoso8KUs6oXcW" mileage: 45000 }){ car { id license brand mileage createdAt updatedAt } validationViolations } } ``` ```json { "data": { "updateCar": { "car": { "id": "hUFcoso8KUs6oXcW", "license": "HH AQ 2020", "brand": "MERCEDES", "mileage": 45000, "createdAt": "2020-12-15T14:07:19.320Z", "updatedAt": "2020-12-15T14:09:04.840Z" }, "validationViolations": [] } } } ```

See how we only provided the "mileage" value and the rest of the attribute values remain untouched and also how ActiveQL added and updated the createdAt and updatedAt fields of the entity item.

Delete Mutation

``` mutation { deleteCar( id: "7547" ) { id validationViolations } } ``` ``` { "data": { "deleteCar": { "id": "7547", "validationViolations": [] } } } ```

You might wonder why the deleteCar mutation has a ValidationValidation in the return type. When the deletion of an entity item fails it would be an error and not a ValidationValidation. But there can business requirements preventing an item from being deleted (e.g. insufficient permissions or a invalid state of state machine) and this is what the API client is informed about as a ValidationValidations.

Queries

type Query {
  ping: String
  car(id: ID): Car
  cars(filter: CarFilter, sort: CarSort, paging: EntityPaging): [Car]
  carBylicense(license: String!): Car
  carsStats(filter: CarFilter): EntityStats
}

Any schema includes a query ping that simply sends back the value pong and can be used whether a GraphQL API can be accessed without the need to build any specific query or mutation.

Entity Type Query

If a client knows the id of an entity item it can gets the item via the type query, e.g.

```graphql query { car( id: "a4Rcu5mAEBAqbnzk" ){ brand mileage color createdAt updatedAt } } ``` ```json { "data": { "car": { "brand": "PORSCHE", "mileage": 8000, "createdAt": "2020-12-14T11:35:47.211Z", "updatedAt": "2020-12-14T11:35:47.211Z" } } } ```

If the id does not exist an exception will be thrown.

Types Query

A client can request a result set of entity items with filtering, sorting and paging. The following query will return all car entities in no specific order.

```graphql query { cars( filter: { brand: { isNot: BMW }, mileage: { lowerOrEqual: 45000 } } ){ id brand mileage } } ``` ```json { "data": { "cars": [ { "id": "a4Rcu5mAEBAqbnzk", "brand": "PORSCHE", "mileage": 8000 }, { "id": "Wnt9lTvjiwHn39uX", "brand": "PORSCHE", "mileage": 12500 }, { "id": "BHE30fMAn2zXTqtN", "brand": "PORSCHE", "mileage": 45000 } ] } } ``` ```graphql query { cars { id license brand mileage } } ``` ```json { "data": { "cars": [ { "id": "iXDeS6AcslDcb0Cv", "license": "M TR 2017", "brand": "PORSCHE", "mileage": 55000 }, { "id": "hUFcoso8KUs6oXcW", "license": "HH AQ 2020", "brand": "MERCEDES", "mileage": 45000 }, { "id": "RvG2Fyb2wa9z0HjM", "license": "HH TR 1970", "brand": "AUDI", "mileage": 5000 }, { "id": "KDJjtbCIQgCkeHol", "license": "M TR 2016", "brand": "MERCEDES", "mileage": 15000 }, { "id": "8Tp3PD340q7idiK4", "license": "HH TM 2020", "brand": "MERCEDES", "mileage": 10000 } ] } } ```

Filter

input CarFilter {
  id: ID
  brand: StringFilter
  mileage: IntFilter
}

As we have seen, for every entity a FilterType is created that can be used in the types query. For every attribute a corresponding filter type is determined. E.g. the StringFilter for the "brand" attribute, the IntFilter for the "mileage" attribute. This filter-types are defined by the datastore since their implementation is dependent to the datastore (e.g. database). The default datastore (document based database) provides filter-types for all scalar types, enums and some internal types (like for assocFrom relations). In this example we use the StringFilter and IntFilter.

input StringFilter {
  is: String
  isNot: String
  in: [String]
  notIn: [String]
  contains: String
  doesNotContain: String
  beginsWith: String
  endsWith: String
  caseSensitive: Boolean
}
 
input IntFilter {
  is: Int
  isNot: Int
  lowerOrEqual: Int
  lower: Int
  greaterOrEqual: Int
  greater: Int
  isIn: [Int]
  notIn: [Int]
  between: [Int]
}

They could be used to filter/search for entity items like so:

```graphql query { cars( filter: { brand: { isNot: BMW }, mileage: { lowerOrEqual: 45000 } } ){ id brand mileage } } ``` ```json { "data": { "cars": [ { "id": "a4Rcu5mAEBAqbnzk", "brand": "PORSCHE", "mileage": 8000 }, { "id": "Wnt9lTvjiwHn39uX", "brand": "PORSCHE", "mileage": 12500 }, { "id": "BHE30fMAn2zXTqtN", "brand": "PORSCHE", "mileage": 45000 } ] } } ```

Sort

For any entity a SortEnum is generated with all attributes that can be used for sorting. For every attribute (unless configured otherwise) two enum values are created. One for ascending order (e.g. brand_ASC) and one for descending order (e.g. mileage_DESC).

enum CarSort {
  brand_ASC
  brand_DESC
  mileage_ASC
  mileage_DESC
  id_ASC
  id_DESC
}

An API client may use this entity sort enum in the types query for the entity, to determine the sort order of the result.

Paging

Any types query support paging. An API client can request to limit the result to a certain number and to skip results (based on a sort order).

input EntityPaging {
  page: Int!
  size: Int!
}

If a query to the types query wants to use a subset instead of the whole result it can use the paging input type. Pages start with 0. If you don't give a sort order, the result set will be ordered by id.

You could request the first 10 items like so of a result set of the types query like so:

query { cars( sort: mileage_ASC, paging: { page: 0, size: 10 } ) }

For the next 10 items it would be (and so on)

query { cars( sort: mileage_ASC, paging: { page: 1, size: 10 } ) }

A more sophisticated usage of the cars types query:

```graphql query { cars( filter: { brand: { in: [ MERCEDES, PORSCHE] } } sort: mileage_DESC paging: { page: 0, size: 3 } ){ id brand mileage } } ``` ```json { "data": { "cars": [ { "id": "iXDeS6AcslDcb0Cv", "brand": "PORSCHE", "mileage": 55000 }, { "id": "hUFcoso8KUs6oXcW", "brand": "MERCEDES", "mileage": 45000 }, { "id": "KDJjtbCIQgCkeHol", "brand": "MERCEDES", "mileage": 15000 } ] } } ```

This query would return the three cars of the brand "Mercedes" and "Porsche" with the highest milage. See more about filter, sort and paging below or in Queries and mutation.

Statistics

A client can request some basic statistics about the entity. This can also be used for filtered result sets.

  • count the number of items
  • createdFirst the date when the first item was created
  • createdLast the date when the last item was created
  • updatedLast the date when the first item was updated
type EntityStats {
  count: Int!
  createdFirst: Date
  createdLast: Date
  updatedLast: Date
}

To know how many cars of the brand 'Porsche' exist a client can use the following query:

  query {
    carsStats( filter: {brand: "Porsche" } ){ count }
  }