Domain configuration
Entities
Entity state engine

Entity State Engine

The entity state engine implements concepts from a finite state machine (opens in a new tab)

ActiveQL uses the following terms

TermDescription
StateEnginethe configuration of a state machine for a StateEntity
StateEntitythe entity that has the StateEngine configuration
stateAttributethe attribute of the StateEntity holding the state value
StateEnumthe enum defining all possible state values
TransitionEnumthe enum defining all possible transitions - this will generated
StateEntity itema specific item of the StateEntity
transitionan allowed state change from one state value to another
associated entitiesentities having a assoc to or from the StateEntity, also over other associated entities
associated entity iteman item of an associated entity that references or is referenced by a specific StateEntity item
entityStateUpdate mutationa mutation that allows the application a transition to a StateEntity item - this will generated
entityState querya query allowing to see the current state value and all allowed mutations from this state for a StateEntity item - this will generated

An entity can have one (or more) state engines that manage a state attribute. A state changes it's value either

  • through a generated mutation based on allowed transitions
  • after an observed mutation was successfully executed

Adding a StateEngine configuration will

  • make the stateAttribute of the StateEntity read-only
  • generate an entityStateUpdate mutation - allowing to apply a transition and execute the state change
  • generate an entityState query, allowing to see the current state value and all allowed mutations from this state
  • adds a validation to all observed mutations to ensure they will only be executed if any associated StateEntity item has a state value that is allowed
  • adds a afterHook to all observed mutations that can optionally change the state value of any associated StateEntity item after the mutation's execution.

Execution of a stateUpdate mutation

Any configured transition can be applied by an API client by calling the generated entityStateUpdate mutation. This is an overview of the execution flow.

updateStateMutation

Execution of an observed mutation

Since the state of an StateEntity item can influence and is dependent of other entity items, you can observe mutations of other entities. This would be an overview of the execution flow of such a mutation.

observed-mutation

EntityStateEngineConfig

PropertyTypeDefaultDescription
stateAttributeStringstatethe entity attribute holding the state; must be of type Enum
initialStringnewthe default value of the stateAttribute; overrides any existing defaultValue
stateQueryNameString[entity][State]the name of the generated Query showing the current value for the stateAttribute and allowed mutations
stateMutationNameString[entity][State]Updatethe name of the generated Mutation allowing to apply a transition
transitionEnumNameString[Entity][State]Transitionthe name of the generated Enum holding all configured transition names
exposebooleantruewhether the transitions should be available as an API mutation
observeEntityStateEngineObserve[]list of mutations that should be under control of this StateEngine
transitionMap of StateEngineTransitionmap of transitions that can be applied to change the stateAttribute value

stateAttribute

stateAttribute: undefined | string
Config ValueDescription
undefinedsame as "state"
stringName of the entity attribute holding the state

If the stateAttribute value does not match an attribute of the entity or if the attribute has another type than an Enum, the StateMachine will not be initialized.

Adding a stateAttribute will make the corresponding attribute read-only - it can only be manipulated through the generated entityStateUpdate mutation-

inital

initial: undefined | string
Config ValueDescription
undefinedsame as "new"
stringDefault value of the stateAttribute when e new StateEntity item is created

If this does not match a value of the Enum type the stateAttribute will nonetheless be of this value, but the state engine will probably not work as expected.

transitionEnumName

transitionEnumName: undefined | string
Config ValueDescription
undefinedsame as [EntityName][StateAttributeName]Transition
stringName of the generated Enum holding all configured transition names

There is usually no need for this to be configured.

expose

expose: undefined | boolean
Config ValueDescription
undefinedsame as true
trueany transition (that does not have an expose: false) will be callable via the StateMutation
falseno transition (except it does have an expose: true itself) will be callable via the StateMutation

If expose: false and no transition overrides this, no StateMutation will be generated.

stateQueryName

Config ValueDescription
undefinedsame as [entityName][StateAttributeName]
stringName of the generated Query showing the current value for the stateAttribute and allowed mutations

There is usually no need for this to be configured.

entity: 
  Rental: 
    stateEngine: 
      stateQueryName: myRentalState

stateMutationName

Config ValueDescription
undefinedsame as [entityName][StateAttributeName]Update
stringName of the generated Mutation allowing an API client to apply a transition to the StateEngine

There is usually no need for this to be configured.

entity: 
  Rental: 
    stateEngine: 
      stateMutationName: myRentalStateUpdate

transition

Every transition configuration adds value to the TransitionEnum that can be applied by a client by calling the entityStateUpdate mutation.

PropertyTypeDefaultDescription
fromundefined | string | string[]state(s) in which this transition is allowed
tostringvalue the state should be after this transition was successfull
validationundefined | EntityStateEngineValidation | EntityStateEngineValidation[]validation to determine whether this transition should execute the state-change to the to state-value
failedundefined | stringvalue the state should be after the validation of this transition failed
exposeundefined | booleanundefinedper default a transition according to the expose configuration of the StateEngine; you can override this here
      transition:
        confirm:
          from: requested
          to: confirmOrReject # this is not an enum value, therfore resolved from the context
 
        cancel:
          from: requested
          to: status
 
        conclude:
          from: confirmed
          to: concluded
          
        reopen:
          from: 
            - rejected
            - canceled
            - secluded
          to: requested

from

Config ValueDescription
undefinedthis transition is allowed from any state
stringsame as string[]
string[]if the state has any of these values, the transition is allowed; if an API client attempts to apply this transition in any other state a ValidationViolation will be sent to the client and the state keeps unchanged. Any existing validation is not eveluated.

to

Config ValueDescription
stringvalue the state should be after this transition was successfull; a transition is successfull when it is allowed and either no validation exists or the validation does not fail

The to value is a string that is handled as follows:

  • if it is a value of the StateEnum (type of the stateAttribute) the value is used as the new state value
  • otherwise the value is looked up the in the context
    • if such a value can be found in the context and it is a value of the StateEnum this value is used as the new state value
    • if the value
      • can not be found in the context or
      • the value is undefined or
      • the value is not a value of the StateEnum - the state keeps unchanged

validation

Config ValueDescription
FeelExpressionConfigany valid FEEL Expression
DtConfigany valid Decision Table configuration
DtRefany valid reference to a Decision Table configuration in the DomainGraph

The expressions and decision table rules have access to the context that as the following content:

Context valueDescription
typeQueryNamethe StateEngine item is available under the type query name of the entity, e.g. the Rental item would be available as rental
principalif a Principal is present in the API call it is available under principal
localethe current locale as determined from the runtime configuration or API call
associated entitiesany associated entity item as defined in the context configuration
variablesany variable defined by the context configuration

The return values from the expression or decision table are considered as follows:

Return ValueDescription
true | undefined | null | empty stringvalidation did pass
falsevalidation failed; a ValidationViolation with a generic message is send to the API client
string | string[]validation failed; the return values are send as message of a ValidationViolation to the client

failed

Config ValueDescription
undefinedwhen a validation failes, the state keeps unchanged
stringvalue the state should be after this the validation fails

For the eveluation of the actual new state value the same rules apply as for to.

observe

Any mutation of an associated entity that can only be executed in certain states can be added to the observe list.

PropertyTypeDescription
mutationstring | string[]mutation that should be oserved
entitystring | string[]entity which mutations should be oserved
querystringwhen the entity of the the observed mutation(s) has no direct assoc to the StateEntity you must provide a way to find the affected StateEntity items
fromstring | string[]state value(s) in which the mutations is allowed
tostringnew state value of the StateEntity items after the execution of the mutation
validationFeelExpressionConfig | DtConfig | DtRef | ConfigSourceFnvalidation if the StateEntity item would be in a valid state after the mutation was executed
      observe:
        - mutation: createRental
          to: requested        
 
        - mutation: 
            - updateRental
            - deleteRental
          from: 
            - requested
            - rejected
 
        - mutation: updateCar
          from: 
            - requested
            - rejected
            - canceled
 
        - mutation: deleteCar
          from: 
            - requested
            - confiremed
            - rejected
            - canceled
          to: canceled
 
        - mutation: updateDriver          
          from: 
            - requested
            - rejected
            - canceled          
 
        - mutation: deleteDriver          
          to: canceled
 
        - entity: Destination # meaning create, update and delete mutations
          from: requested
 
        - entity: SpeedViolation
          assoc: driver.rentals
          from: 
            - requested
            - canceled

mutation

An observe configuration must have either a mutation or entity config value.

Config ValueDescription
stringsame as string[]
string[]list of mutations that are observed by this StateEngine

entity

When providing an entity it is treated the same as adding the

  • createMutation
  • updateMutation
  • createMutation
Config ValueDescription
stringsame as string[]
string[]list of entities that are observed by this StateEngine

query

When you oberve mutations of an entity the StateEngine from the StateEntity hooks itself into the lifecycle methods of the observed entity. When a mutation is resolved it has to decide if there are any items of the observed entity are associated to the item that is handled by the mutation resolve.

As long as there is a direct assoc between the observed entity and the StateEntity this can be determined by this assoc. When the observed entity and the StateEntity are linked through other entity assocs you must provide the query path from the item of the observed entity to any item of the StateEntity.

The query path is the same as you would use in query.

from

Config ValueDescription
undefinedthis transition is allowed from any state
stringsame as string[]
string[]if the state has any of these values, the observed mutation is allowed; if an API client attempts to apply this mutation in any other state a ValidationViolation will be added to muatation response to the client and the state keeps unchanged. Any other validation is not eveluated.

to

Config ValueDescription
undefinedthe state remains unchanged after the observed mutation was executed
stringvalue the state should be after the observed muation was successfully executed; meaning no validation exists or the validation does not fail

It is evaluated like the to property of the transition.

validation

It is evaluated like the validation property of the transition. Please refer to the documentation there.

context

Any validation or evaluation of a state uses a context. This context always has the following default properties:

PropertyTypeDefaultDescription
runtimeRuntimethe current runtime instance
principalPrincipalthe principal present in the current request or undefined
localestringenthe locale present in the current request
logErrorLogthe log from the current runtime
nowDatenew Date()the date at creation of the context as determined by the runtime.now function
dataany{ [typeQueryName]: Item }
validationViolationsValidationViolation[][]

When you provide a resolve function for a config source, this is the context you will get your callback.

When using FEEL expressions or Decision Tables the only relevant property is the data object. The data object is the evaluation context of your expressions.

assoc

Per default you find the item of the StateEntity that is evaluated whether a transition or mutation is allowed under its typeQueryName in the data object. E.g. when you have a Rental as StateEntity the data object will just have the rental item as follows:

{
  rental: {
    status: 'requested',
    from: '2023-12-01',
    till: '2023-12-03',
    carId: 'xoFcO7DtgnNHEoQ2',
    driverIds: [ 'xlDGd0CQY1N3EtCh', '54srKaQFFdzntImP' ],
    id: 'JYD6EQBTq4PrYe3b',
    indication: '[Rental:JYD6EQBTq4PrYe3b]',
    createdAt: '2023-12-20T17:37:03.042Z',
    updatedAt: '2023-12-20T17:37:03.048Z',
    __typename: "Rental"
  }
}

We can use this data/object in any FEEL expressions or Decision Tables. But in most real life scenarios we would also need information from associated entities. You can add any to associated entities as you would in a GrahQL query.

With the following paths in the assoc array ...

      context:
        assoc: 
          - car
          - drivers.speedViolations
          - drivers.address
          - destinations

... we will have the all these objects in the context.data:

{
  rental: {
    status: 'requested',
    from: '2023-12-01',
    till: '2023-12-03',
    carId: 'uPLW6HBLn8vQ6oNa',
    driverIds: [ 'zZaIjIUA5TescTOI', '0jWAeZjQ1rkSmpIW' ],
    createdAt: '2023-12-20T17:53:46.836Z',
    updatedAt: '2023-12-20T17:53:46.842Z',
    __typename: 'Rental',
    id: 'fPd5HDCdPKlLWJPz',
    indication: '[Rental:fPd5HDCdPKlLWJPz]',
    car: {
      brand: 'Smart',
      power: 94,
      createdAt: '2023-12-20T17:53:46.672Z',
      updatedAt: '2023-12-20T17:53:46.752Z',
      __typename: 'Car',
      id: 'uPLW6HBLn8vQ6oNa',
      indication: 'Smart'
    },
    drivers: [
      {
        lastname: 'Ortiz',
        birthdate: '1950-08-27',
        createdAt: '2023-12-20T20:10:31.359Z',
        updatedAt: '2023-12-20T20:10:31.458Z',
        __typename: 'Driver',
        id: 'qH87NKOdkzypjMaK',
        indication: 'Ortiz',
        speedViolations: [],
        address: undefined
      },
      {
        lastname: 'Kemmer',
        birthdate: '1951-08-15',
        createdAt: '2023-12-20T20:10:31.330Z',
        updatedAt: '2023-12-20T20:10:31.402Z',
        __typename: 'Driver',
        id: 'Cwv6aXuKb8miEMVk',
        indication: 'Kemmer',
        speedViolations: [],
        address: undefined
      }
    ],
    destinations: []
  }
}

variable

variable: EntityStateEngineContextVariable | EntityStateEngineContextVariable[]

type EntityStateEngineContextVariable = {[name:string]: EntityStateEngineExpression} 
type EntityStateEngineExpression = FeelExpressionConfig|DtConfig|DtRef|string|boolean|number

You might need the same evaluation in more than one location. You can add any value to the data object that becomes available for all following evaluations. If one variable is dependent on another you can put the variable definition(s) in a list. Any variable definitions will be evaluated in that order.

        variable: 
          - driversWithoutViolations: 
              expression: count( rental.drivers )
            youngestDriver: 
              expression: age( min( map( @drivers, "birthdate") ) )              
 
          - status: canceled