Skip to main content

Computed Data

Computed Data is a core concept in @interaqt/runtime. The @interaqt/runtime provides a series of tools to help you define what the data content is. Once defined, how data changes become automatic. It is just like reactivity in backend. This is also a major difference from other frameworks. Below are all the types of computed data provided by the system. Note that the term 'Record' here is a general term for Entity and Relation.

Computed Data Representing Entity/Relation Data

MapActivity

Map each activity into an Entity/Relation/State or a Property of an Entity/Relation. It is commonly used in scenarios where information from an activity is integrated. For example, integrate all information from the "createFriendRelation" activity into an entity named Request:

export const requestEntity = Entity.create({
name: 'Request',
computedData: MapActivity.create({
items: [
MapActivityItem.create({
activity: createFriendRelationActivity,
triggerInteractions: [sendInteraction, approveInteraction, rejectInteraction], // interations that trigger data computation
map: function map(stack) { // compute data
const sendRequestEvent = stack.find((i: any) => i.interaction.name === 'sendRequest')

if (!sendRequestEvent) {
return undefined
}

const handled = !!stack.find((i: any) => i.interaction.name === 'approve' || i.interaction.name === 'reject')

return {
from: sendRequestEvent.data.user,
to: sendRequestEvent.data.payload.to,
message: sendRequestEvent.data.payload.message,
handled,
}
}
})
]
})
})

MapInteraction

Map each interaction to an Entity/Relation/State or a Property of an Entity/Relation. It is typically used to associate user information and Payload within an interaction, or to record information from the interaction.

Example: Associate the user sending a request with the sent request to create relation data.

const sendRequestRelation = Relation.create({
source: RequestEntity,
sourceProperty: 'from',
target: UserEntity,
targetProperty: 'request',
relType: 'n:1',
properties: [
Property.create({
name: 'createdAt',
type: 'string'
})
],
computedData: MapInteraction.create({
items: [
MapInteractionItem.create({
interaction: createInteraction, // interaction that triggers data computation
map: function map(event: any) {
return {
source: event.payload.request,
createdAt: Date.now().toString(), // data recorded on the relation
target: event.user,
}
}
}),
],
}),
})

It can also be used to record information from the payload of an interaction. Example: Once a user performs an approval action, record it in the 'result' field of the relation between the user and the request.

Property.create({
name: 'result',
type: 'string',
collection: false,
computedData: MapInteraction.create({
items: [
MapInteractionItem.create({
interaction: approveInteraction,
map: () => 'approved',
computeSource: async function (this: Controller, event) {
return {
"source.id": event.payload.request.id,
"target.id": event.user.id
}
}
}),
],
})
})

MapRecordMutation

Map a RecordMutation to an Entity/Relation/State or a specific Property of an Entity/Relation. It is commonly used for recording changes and can be utilized to generate historical versions of these changes. Example: Create a historical version for each modification of a post.

const postRevisionEntity = Entity.create({
name: 'PostRevision',
properties: [
Property.create({ name: 'content', type: PropertyTypes.String })
],
computedData: MapRecordMutation.create({
map: async function (this: Controller, event:RecordMutationEvent, events: RecordMutationEvent[]) {
if (event.type === 'update' && event.recordName === 'Post') {
return {
content: event.oldRecord!.content,
current: {
id: event.oldRecord!.id
}
}
}
}
})
})

RelationStateMachine

Use a state machine to represent the creation/deletion/modification of a relation. The document is coming soon.

Representing fields in Entity/Relation

RelationBasedAny

Create a boolean field to indicate whether there exists at least one entity related to the current entity of a certain relation type and whether the data on the associative relationship meets a specific condition. This can only be used in a Property. Example: "Whether the current request has been rejected."

Property.create({
name: 'rejected',
type: 'boolean',
collection: false,
computedData: RelationBasedAny.create({
relation: receivedRequestRelation,
relationDirection: 'source',
match:
(_, relation) => {
return relation.result === 'rejected'
}

})
})

RelationBasedEvery

Create a boolean field to indicate whether every related entity of a certain relation type of the current entity and the data on the associative relationship meet a specific condition. This can only be used in a Property. Example: "Whether the current request has been approved."

Property.create({
name: 'approved',
type: 'boolean',
collection: false,
computedData: RelationBasedEvery.create({
relation: receivedRequestRelation,
relationDirection: 'source',
notEmpty: true,
match:
(_, relation) => {
return relation.result === 'approved'
}

})
})

RelationCount

Used to calculate the total of existing data for a specific Relation of an entity. This can only be used in a Property.

Example: The number of my pending requests.

Property.create({
name: 'pendingRequestCount',
type: 'number',
collection: false,
computedData: RelationCount.create({
relation: reviewerRelation,
relationDirection: 'target',
match: function (request, relation) {
return request.result === 'pending'
}
})
})

RelationBasedWeightedSummation

perform a weighted calculation based on the data of associated entities and relationships of a certain Relation type. This can only be used in a Property.

Representing global state

Any

Determines whether there exists a record that satisfies a condition. This can only be used in a State.

Example: Whether globally any request has been processed:

const anyRequestHandledState = State.create({
name: 'anyRequestHandled',
type: 'boolean',
collection: false,
computedData: Any.create({
record: requestEntity,
match: (request) => {
return request.handled
}
})
})

Every

Determines whether all data of a certain Record type meet a condition. This can only be used in a State.

Example: Whether globally all requests have been processed:

const everyRequestHandledState = State.create({
name: 'everyRequestHandled',
type: 'boolean',
collection: false,
computedData: Every.create({
record: requestEntity,
match: (request) => {
return request.handled
}
})
})

Count

Counts the total number of a certain type of Record. This can only be used in a State.

Example: The total number of all friendships globally.

const totalFriendRelationState = State.create({
name: 'totalFriendRelation',
type: 'number',
collection: false,
computedData: Count.create({
record: friendRelation,
match: () => true
})
})

WeightedSummation

Based on a weighted calculation of all Records. This can only be used in a State.

Example: Suppose there is an entity in the system called Request, which has two boolean properties: approved and rejected. We aim to define a global value called approveX, which is the weighted sum of all Requests. A Request with approved as true has a weight of +2, and a Request with rejected as true has a weight of -1.

const approveXState = State.create({
name: 'approveX',
type: 'number',
collection: false,
computedData: WeightedSummation.create({
records: [requestEntity],
matchRecordToWeight: (request) => {
return request.approved ? 2 : (request.rejected ? -1 : 0)
}
})
})

Note that we can mix different types of Records for calculation. Users need to distinguish the types of Records themselves in the matchRecordToWeight function.

Computed based on its own Properties in Entity/Relation

Sometimes, our Entity/Relation may have some properties that can be directly calculated based on other properties. If we wish to use these as matching criteria during a search, then we should use the computed field directly when creating the Property. Example: On the Request entity, there are already boolean properties approved and rejected. We also want to have a string-type property result, which is calculated based on approved and rejected.

RequestEntity.properties.push(
Property.create({
name: 'approved',
type: 'boolean',
collection: false,
computedData: RelationBasedEvery.create({
relation: reviewerRelation,
relationDirection: 'source',
notEmpty: true,
match:
(_, relation) => {
return relation.result === 'approved'
}
})
}),
Property.create({
name: 'rejected',
type: 'boolean',
collection: false,
computedData: RelationBasedAny.create({
relation: reviewerRelation,
relationDirection: 'source',
match:
(_, relation) => {
return relation.result === 'rejected'
}
})
}),
Property.create({
name: 'result',
type: 'string',
collection: false,
computed: (request: any) => {
// Here you can directly read the value of request and perform computation
return request.approved ? 'approved' : (request.rejected ? 'rejected' : 'pending')
}
}),
)

When a record is created or updated, any Property with the computed attribute will be automatically recalculated.