Domain configuration
Entities
Entity configuration

Entity Configuration

A business domain is mainly described by its entities and their relation to each other. Think of an entity as any thing in your business domain. We highly recommend using a Domain Driven Design (opens in a new tab) approach.

optiondescription
attributesthe attributes of the entity
descriptiondescription to add to the schema documentation
assocTohas-one relationship to another entity
assocToManyhas-many relationship with references to the other entity at this entity
assocFromhas-many as relationship to another entity as opposite site of a has-one or has-many relationship with references at the other entity
assocByhas-one as relationship to another entity as opposite site of a has-one relationship with references at the other entity
seedsgenerating seed or test data
validationcallback for complex or non-attribute based validation
permissionsrole-based rights permission configuration
uniondefines a a union relationship between entities
interfacedefines a common interface other entities can implement
implementsdefines the implementation of an interface entity
subscriptionsadd subscriptions for create, update, delete of items
onInitcallback on initialization of the entity
typeOnlyif true skip all queries and resolvers and only add type to the schema
typeQueryskip or replace default type query resolver with custom resolver
typesQueryskip or replace default types query resolver with custom resolver
createMutationskip or replace default create mutation resolver with custom resolver
updateMutationskip or replace default update mutation resolver with custom resolver
deleteMutationskip or replace default delete mutation resolver with custom resolver
statsQueryskip or replace default entity statistics query resolver with custom resolver
typeNamethe name of the entity object type
pluralthe plural of the entity, used for generated queries and others
singularthe singular of the entity, used for generated queries and others
collectionthe name of the collection (e.g. table) in the datastore for this entity
paththe name of the path in a UI (e.g. ActiveQL Admin UI) for this entity
uiUI configuration - only relevant for the ActiveQL AdminUI
foreignKeythe name to use as foreignKey for has-one relationships for this entity
foreignKeysthe name to use as foreignKey for has-many relationships for this entity
createInputTypeNamethe name of the GraphQL input type for the create mutation
updateInputTypeNamethe name of the GraphQL input type for the update mutation
filterTypeNamethe name of the GraphQL input type for the filter of the types query
sorterEnumNamethe name of the GraphQL enum for sorting of the types query
typeFieldthe name of the GraphQL field when this entity is part of a polymorphic relationship
typesEnumNamethe name of the GraphQL enum for all types that are union types of this entity or implement this entity as interface
createMutationNamethe name of the create mutation for this entity
updateMutationNamethe name of the update mutation for this entity
deleteMutationNamethe name of the delete mutation for this entity
mutationResultNamethe name of the GraphQL type that is returned for the create and update mutation
typeQueryNamethe name of the type query for this entity
typesQueryNamethe name of the types query for this entity
statsQueryNamethe name of the stats query for this entity

We suggest writing the entity configuration in YAML files and simply includes these files in your Domain Configuration.

From the definition of an entity a lot of GraphQL schema types, queries and mutations are generated, incl. resolver to read and write its items to and from a datastore. The behavior is strongly opinionated and uses mostly conventions; nonetheless you can configure most of the details. You will probably only ever use the first few of these configuration options, unless you know very well what do you want to achieve:

Properties of Entity configuration

attributes

attributes?:{[name:string]:string|AttributeConfig}

The attributes of an entity as key/value pairs. See Attribute Configuration for details.

entity:    
  Car: 
    attributes: 
      brand: string!
      mileage: int

description

description?:string

You can add a description or documentation to your entity. This will become part of the schema and the public API.

entity: 
  Car: 
    description: >
      A simple version of a car entity, purely for demonstration purposes.
      We will try to demonstrate as much ActiveQL features as possible. 
      If you like ActiveQL - feel free to contribute more examples at
      https://github.com/betterobjects/activeql

interface

TypeDefaultDescription
booleanfalseIf true creates the entity's type as an GraphQL interface

As a convenience feature you can define attributes in an interface entity that all entities that implement this interface will inherit.

entity:    
  Car: 
    interface: true    
    attributes:
      brand: string!
  Convertible:
    implements: Car
    attributes:
      softtop: boolean
  Sedan:
    implements: Car
    attributes:
      doors: int

implements

TypeDefaultDescription
string | string[]Configures the entity to implement all interfaces from this entity names
entity:    
  Car: 
    interface: true    
    attributes:
      brand: string!
  Convertible:
    implements: Car
    attributes:
      softtop: boolean
  Sedan:
    implements: Car
    attributes:
      doors: int

Schema (excerpt)

interface Car {
  id: ID!
  brand: String!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
type Convertible implements Car {
  id: ID!
  softtop: Boolean
  createdAt: DateTime!
  updatedAt: DateTime!
  brand: String!
}
 
type Mutation {
  deleteCar(id: ID): [String]
  createConvertible(
    convertible: ConvertibleCreateInput
  ): SaveConvertibleMutationResult
  updateConvertible(
    convertible: ConvertibleUpdateInput
  ): SaveConvertibleMutationResult
  deleteConvertible(id: ID): [String]
  createSedan(sedan: SedanCreateInput): SaveSedanMutationResult
  updateSedan(sedan: SedanUpdateInput): SaveSedanMutationResult
  deleteSedan(id: ID): [String]
}
 
type Query {
  cars(filter: CarFilter, sort: CarSort): [Car]
  convertible(id: ID): Convertible
  convertibles(filter: ConvertibleFilter, sort: ConvertibleSort): [Convertible]
  sedan(id: ID): Sedan
  sedans(filter: SedanFilter, sort: SedanSort): [Sedan]
}
 
type Sedan implements Car {
  id: ID!
  doors: Int
  createdAt: DateTime!
  updatedAt: DateTime!
  brand: String!
}

Request

mutation addCars {
  createSedan( sedan: { brand: "VOLKSWAGEN"  doors: 4 } ) 
  createConvertible( convertible: { brand: "PORSCHE", softtop: true } )
}
query listCars {
  cars { 
  	... on Convertible {
      softtop
    }
    ... on Sedan {
      doors
    }
    brand
    __typename 
  }
}

Result

{
  "data": {
    "cars": [
      {
        "softtop": true,
        "brand": "PORSCHE",
        "__typename": "Convertible"
      },
      {
        "doors": 4,
        "brand": "VOLKSWAGEN",
        "__typename": "Sedan"
      },
      {
        "softtop": null,
        "brand": "PORSCHE",
        "__typename": "Convertible"
      },
      {
        "doors": 4,
        "brand": "VOLKSWAGEN",
        "__typename": "Sedan"
      }
    ]
  }
}

union

TypeDefaultDescription
string[]Configures the entity's type to be the union of 2 or more entity types
entity:
  Convertible:
    attributes:
      brand: string
      softtop: boolean
  Sedan:
    attributes:
      brand: string
      doors: int
  Car:
    union:
      - Convertible
      - Sedan

Schema (excerpt)

union Car = Convertible | Sedan
type Convertible {
  id: ID!
  brand: String
  softtop: Boolean
  createdAt: DateTime!
  updatedAt: DateTime!
}
type Sedan {
  id: ID!
  brand: String
  doors: Int
  createdAt: DateTime!
  updatedAt: DateTime!
}

GraphQL

mutation addCars {
  createSedan( sedan: { brand: "VOLKSWAGEN", doors: 4 } ){ sedan: { id } }
	createConvertible( convertible: { brand: "PORSCHE", softtop: true }){ convertible: { id } } }
}
query listCars {
  cars { 
  	... on Convertible {
      brand
      softtop
    }
    ... on Sedan {
      brand
      doors
    }
    __typename 
  }
}

Result

{
  "data": {
    "cars": [
      {
        "brand": "PORSCHE",
        "softtop": true,
        "__typename": "Convertible"
      },
      {
        "brand": "VOLKSWAGEN",
        "doors": 4,
        "__typename": "Sedan"
      }
    ]
  }
}

seeds

Sometime you want to define some seed data to your application, you can easily initialize the DataStore with. You can configure this in the seeds section.

For more details e.g. how to reference type items see Seed Configuration.

entity:
  Car:
    attributes:
      brand: string!
      license: string!
      mileage: int
    seeds:
    - items:
        - brand: VOLKSWAGEN
          license: AA 1234
          mileage: 10000
        - brand: Mercedes
          license: BB 2345
          mileage: 20000
        - brand: PORSCHE
          license: CC 3456
          mileage: 30000
    - environment: test
      items:
        100: 
          brand:
            faker: vehicle.manufacturer
          licence: 
            rfs: alphaUpper(1, 3).append(['-', ' ']).alphaUpper( 2, 3 ).append(' ').numeric( 3, 4 )
          mileage: 
            random: 
              min: 10000
              max: 120000

validation

Often validation takes place on the attribute level. Check Attribute Validation for details.

But sometimes you want to add validation to the entity itself, when its logic is not bound to an attribute, e.g. if it reflects two or more attributes - in this case you can add a function that is called every time before the entity item is created or updated.

validation?:( item:any, runtime:Runtime ) => ValidationReturnType

The ValidationReturnType is usually of type ValidationViolation or a string.

ValidationViolation

PropertyDescription
path (optional) should point to the path in the result data where the ValidationViolation occurred
messagea descriptive message

ValidationReturnType

The ValidationReturnType can be

Type Description
 undefined signals the validation passes
ValidationViolationwill be added to the list of other ValidationViolation and sent to the client
ValidationViolation[]all will be added to the list of other ValidationViolation and sent to the client
string will be handled as ValidationViolation with this string as message and no path
 Promise<...> any of the types above can also be returned as Promise

Your custom method will get the item that is about to be saved. Please note that for updates the item is complete, that means even if a client only attempts to update one attribute, all other existing attribute values are set prior the call to this method.

If a validate method is present and returns something else but undefined or [] the validation fails and the entity item will not be saved. The return message (probably combined with other ValidationViolation messages) will be send to the client.

For more information please check Validation.

Example

export const example3:DomainConfiguration = {
  entity:{
    Car: {
      attributes: {
        brand: 'string!',
        mileage: 'int'
      },
      validation: (item:any) => (item.brand !== 'Mercedes' && item.mileage > 300000 ) ? 
        'I wouldnt believe that' : undefined
    }
  }
}

Request

mutation {
  createCar( 
    car: { brand: "Kia", mileage: 310000 } 
  )
  {
    car{ id brand mileage }
    validationViolations 
  }
}

Response

{
  "data": {
    "createCar": {
      "car": null,
      "validationViolations": [
        {
          "path": null,
          "message": "I wouldnt believe that"
        }
      ]
    }
  }
}

Request

mutation {
  createCar( 
    car: { brand: "Mercedes", mileage: 310000 } 
  )
  {
    car{ id brand mileage }
    validationViolations 
  }
}

Response

{
  "data": {
    "createCar": {
      "car": {
        "id": "5fa87cc40dd707984e939524",
        "brand": "Mercedes",
        "mileage": 310000
      },
      "validationViolations": []
    }
  }
}

permissions

Role based declaration of permissions to access this entity's

  entity: {
    Car: {
      attributes: {
        brand: 'String!',
        mileage: 'Int!',
      },
      permissions: {
        admin: true,
        manager: { read: true, create: true, update: true },
        assistant: ({action}) => action === CRUD.READ ? { brand: { $ne: 'Ferrari'} } : false
      }
    }
  }

You can allow/disallow access to any query and mutation (via CRUD actions) based on the role(s) of a Principal and even filter allowed entity items via expressions. For details see Permissions.

subscriptions

Per default any lifecycle action is published as an EntityLifecycle event at the Runtime. You can disable this by setting this to false.

Any Callback can register to the EntityLifecycle event and get the following payload:

{
  entity: string, 
  id: string, 
  event: 'CREATED' | 'UPDATED' | 'DELETED'
}

If DomainConfiguration.subscription is not disabled API clients can also subscribe to this via GraphQL subscription.

onInit

Callback that is called when this entity is initialized. This is a good place to add hooks or augment any configuration.

Example

In this example we want to allow any client not to query more than 100 car items in any query at once. Therefore we add a hook in the onInit method. If you need the runtime, you can get it via entity.runtime.

export const domainConfiguration: DomainConfiguration = {
  entity: {
    Car: {
      onInit: entity => {
        entity.hooks.afterTypesQuery.push( resolved => {
          if( ! _.isArray( resolved ) ) throw new Error('wrong resolved value');
          if( resolved.length > 100 ) resolved.splice(0, 99);
        });
      }
    }
  }
}

typeName

The typeName is the name of the GraphQL type in the schema. Per default it is capitalized name of this entity in the domain configuration. Only set this if know very well, what you want to achieve.

entity: 
  car: 
  Driver: 
    typeName: Chauffeur
const domainConfiguration = {
  car: {}, 
  Driver: { typeName: 'Chauffeur' }
}

Schema (excerpt)

type Car {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
 
type Chauffeur {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
type Mutation {
  createCar(car: CarCreateInput): SaveCarMutationResult
  updateCar(car: CarUpdateInput): SaveCarMutationResult
  deleteCar(id: ID): [String]
  createChauffeur(chauffeur: ChauffeurCreateInput): SaveChauffeurMutationResult
  updateChauffeur(chauffeur: ChauffeurUpdateInput): SaveChauffeurMutationResult
  deleteChauffeur(id: ID): [String]
}
 
type Query {
  car(id: ID): Car
  cars(filter: CarFilter, sort: CarSort): [Car]
  chauffeur(id: ID): Chauffeur
  chauffeurs(filter: ChauffeurFilter, sort: ChauffeurSort): [Chauffeur]
}
 
type SaveCarMutationResult {
  validationViolations: [ValidationViolation]!
  car: Car
}
 
type SaveChauffeurMutationResult {
  validationViolations: [ValidationViolation]!
  chauffeur: Chauffeur
}

We suggest to use the capitalized version of the type name as key in the entity object of the configuration.

singular

The singular of an entity is used to infer the name of the type if not stated otherwise. This name is used when referring to an instance of a type. Per default it is determined via inflection of the name of the entity (lower letter). Only set this if you know very well what you want to achieve.

YAML

entity:
  Car: 
    attributes: 
      brand: string!
  Driver:
    attributes:
      name: string!
    assocTo: Car

Schema for Car (excerpt)

type Driver {
  car: Car
}
 
input DriverCreateInput {
  carId: ID
}
 
input DriverFilter {
  carId: IDFilter
}
 
input DriverUpdateInput {
  carId: ID
}
 
type Mutation {
  createCar(car: CarCreateInput): SaveCarMutationResult
  updateCar(car: CarUpdateInput): SaveCarMutationResult
}
 
type Query {
  car(id: ID): Car
}
 
type SaveCarMutationResult {
  car: Car
}

plural

The plural of an entity is used to infer the name of a list of types if not stated otherwise. Per default it is determined via inflection of the name of the entity (lower letter). Only set this if you know very well what you want to achieve.

YAML

entity:
  Car: 
    attributes: 
      brand: string!
    assocFrom: Driver
  Driver:
    attributes:
      name: string!
    assocTo: Car

Schema for Driver (excerpt)

type Car {
  drivers: [Driver]
}
 
type Query {
  drivers(filter: DriverFilter, sort: DriverSort): [Driver]
}

collection

The collection property defines the collection or database table a data-store should use to read and write entities.

Per default this is the plural of the entity.

path

The path property is the identifier for this entity in the UI, also it is the path in the url.

Per default this is the underscore version of the plural of the entity.

YAML

entity:    
  TransportFleet: 
    ...

Result

// path: transport_fleets

ui

Usually you configure the behavior of the ActiveQL Admin UI in the adminConfig of your Angular application. You can however choose to add any static configuration in your entity configuration in ActiveQL as well.

YAML

entity:    
  Car: 
    attributes: 
      color: String
      brand: String
      age: Int
      damage: Int
 
    ui:
      listTitle: Fleet
      itemTitle: Vehicle
      resources: 
        label: 
          damage: Reported Damage 
 
      index:
        fields: 
          - brand
          - color
          - age

In this example the UI would use Fleet instead of Cars to refer to any list of cars and Vehicle instead of Car for any car item.

On the index view the table with the car items would include the given fields in that order, instead of all attributes in unreliable order.

Any ui configuration here will be merged with any configuration in your ActiveQL Admin UI client configuration, with the client's configuration preceding this configuration.

Any entry under resources will be merged to the resources object on AdminUI for this entity.

typeQueryName

The name this entity can be accessed in the API. Default is the typeName with lower first character.