interaqt 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agent/agentspace/knowledge/generator/api-reference.md +28 -5
- package/agent/agentspace/knowledge/generator/computation-implementation.md +61 -14
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +2 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +158 -101
- package/agent/agentspace/knowledge/usage/05-interactions.md +67 -22
- package/agent/agentspace/knowledge/usage/08-activities.md +10 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +1 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +52 -99
- package/agent/agentspace/knowledge/usage/14-api-reference.md +22 -5
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +89 -28
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +10 -8
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +11 -2
- package/dist/builtins/interaction/Action.d.ts.map +1 -1
- package/dist/builtins/interaction/Activity.d.ts.map +1 -1
- package/dist/builtins/interaction/Attributive.d.ts.map +1 -1
- package/dist/builtins/interaction/Condition.d.ts.map +1 -1
- package/dist/builtins/interaction/Conditions.d.ts.map +1 -1
- package/dist/builtins/interaction/Data.d.ts.map +1 -1
- package/dist/builtins/interaction/Event.d.ts.map +1 -1
- package/dist/builtins/interaction/Gateway.d.ts.map +1 -1
- package/dist/builtins/interaction/Interaction.d.ts.map +1 -1
- package/dist/builtins/interaction/Payload.d.ts.map +1 -1
- package/dist/builtins/interaction/PayloadItem.d.ts.map +1 -1
- package/dist/builtins/interaction/activity/ActivityCall.d.ts.map +1 -1
- package/dist/core/Entity.d.ts.map +1 -1
- package/dist/core/EventSource.d.ts +37 -0
- package/dist/core/EventSource.d.ts.map +1 -1
- package/dist/core/Relation.d.ts.map +1 -1
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/drivers/Mysql.d.ts +4 -3
- package/dist/drivers/Mysql.d.ts.map +1 -1
- package/dist/drivers/PGLite.d.ts +2 -2
- package/dist/drivers/PGLite.d.ts.map +1 -1
- package/dist/drivers/PostgreSQL.d.ts +2 -2
- package/dist/drivers/PostgreSQL.d.ts.map +1 -1
- package/dist/drivers/SQLite.d.ts +3 -3
- package/dist/drivers/SQLite.d.ts.map +1 -1
- package/dist/drivers.d.ts +2 -0
- package/dist/drivers.js +984 -0
- package/dist/drivers.js.map +1 -0
- package/dist/index.js +5930 -5736
- package/dist/index.js.map +1 -1
- package/dist/runtime/ComputationSourceMap.d.ts +11 -0
- package/dist/runtime/ComputationSourceMap.d.ts.map +1 -1
- package/dist/runtime/Controller.d.ts.map +1 -1
- package/dist/runtime/Scheduler.d.ts +21 -3
- package/dist/runtime/Scheduler.d.ts.map +1 -1
- package/dist/runtime/System.d.ts +1 -0
- package/dist/runtime/System.d.ts.map +1 -1
- package/dist/runtime/computations/Any.d.ts +17 -52
- package/dist/runtime/computations/Any.d.ts.map +1 -1
- package/dist/runtime/computations/Average.d.ts +15 -49
- package/dist/runtime/computations/Average.d.ts.map +1 -1
- package/dist/runtime/computations/Computation.d.ts +8 -0
- package/dist/runtime/computations/Computation.d.ts.map +1 -1
- package/dist/runtime/computations/Count.d.ts +21 -64
- package/dist/runtime/computations/Count.d.ts.map +1 -1
- package/dist/runtime/computations/Custom.d.ts.map +1 -1
- package/dist/runtime/computations/Every.d.ts +18 -52
- package/dist/runtime/computations/Every.d.ts.map +1 -1
- package/dist/runtime/computations/StateMachine.d.ts +2 -2
- package/dist/runtime/computations/StateMachine.d.ts.map +1 -1
- package/dist/runtime/computations/Summation.d.ts +15 -49
- package/dist/runtime/computations/Summation.d.ts.map +1 -1
- package/dist/runtime/computations/Transform.d.ts.map +1 -1
- package/dist/runtime/computations/WeightedSummation.d.ts +18 -60
- package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
- package/dist/runtime/computations/aggregationTemplate.d.ts +163 -0
- package/dist/runtime/computations/aggregationTemplate.d.ts.map +1 -0
- package/dist/runtime/migration.d.ts.map +1 -1
- package/dist/storage/erstorage/AttributeQuery.d.ts.map +1 -1
- package/dist/storage/erstorage/CreationExecutor.d.ts.map +1 -1
- package/dist/storage/erstorage/DeletionExecutor.d.ts.map +1 -1
- package/dist/storage/erstorage/EntityQueryHandle.d.ts +13 -0
- package/dist/storage/erstorage/EntityQueryHandle.d.ts.map +1 -1
- package/dist/storage/erstorage/EntityToTableMap.d.ts +2 -0
- package/dist/storage/erstorage/EntityToTableMap.d.ts.map +1 -1
- package/dist/storage/erstorage/LinkInfo.d.ts +1 -0
- package/dist/storage/erstorage/LinkInfo.d.ts.map +1 -1
- package/dist/storage/erstorage/MatchExp.d.ts +3 -2
- package/dist/storage/erstorage/MatchExp.d.ts.map +1 -1
- package/dist/storage/erstorage/MergedItemProcessor.d.ts +12 -0
- package/dist/storage/erstorage/MergedItemProcessor.d.ts.map +1 -1
- package/dist/storage/erstorage/NewRecordData.d.ts +1 -0
- package/dist/storage/erstorage/NewRecordData.d.ts.map +1 -1
- package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
- package/dist/storage/erstorage/RecordQueryAgent.d.ts +11 -0
- package/dist/storage/erstorage/RecordQueryAgent.d.ts.map +1 -1
- package/dist/storage/erstorage/Setup.d.ts +25 -0
- package/dist/storage/erstorage/Setup.d.ts.map +1 -1
- package/dist/storage/erstorage/UpdateExecutor.d.ts.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -225,7 +225,7 @@ const deletionStateMachine = StateMachine.create({
|
|
|
225
225
|
current: NON_DELETED_STATE,
|
|
226
226
|
next: DELETED_STATE,
|
|
227
227
|
computeTarget: function(event) {
|
|
228
|
-
return { id: event.payload.userId }
|
|
228
|
+
return { id: event.record.payload.userId }
|
|
229
229
|
}
|
|
230
230
|
})
|
|
231
231
|
]
|
|
@@ -313,7 +313,7 @@ deletionProperty.computation = StateMachine.create({
|
|
|
313
313
|
current: NON_DELETED_STATE,
|
|
314
314
|
next: DELETED_STATE,
|
|
315
315
|
computeTarget: function(event) {
|
|
316
|
-
return { id: event.payload.articleId }
|
|
316
|
+
return { id: event.record.payload.articleId }
|
|
317
317
|
}
|
|
318
318
|
})
|
|
319
319
|
]
|
|
@@ -799,6 +799,7 @@ const userTotalScore = Property.create({
|
|
|
799
799
|
type: 'number',
|
|
800
800
|
computation: WeightedSummation.create({
|
|
801
801
|
property: 'scores', // Use property name from relation
|
|
802
|
+
attributeQuery: ['multiplier', 'points'], // Required when callback reads fields
|
|
802
803
|
callback: function(scoreRecord) {
|
|
803
804
|
return {
|
|
804
805
|
weight: scoreRecord.multiplier || 1,
|
|
@@ -811,6 +812,7 @@ const userTotalScore = Property.create({
|
|
|
811
812
|
// Global weighted summation
|
|
812
813
|
const globalWeightedScore = WeightedSummation.create({
|
|
813
814
|
record: ScoreRecord,
|
|
815
|
+
attributeQuery: ['difficulty', 'score'], // Required when callback reads fields
|
|
814
816
|
callback: function(record) {
|
|
815
817
|
return {
|
|
816
818
|
weight: record.difficulty,
|
|
@@ -1698,6 +1700,16 @@ Custom.create(config: CustomConfig): CustomInstance
|
|
|
1698
1700
|
|
|
1699
1701
|
If `incrementalCompute` or `incrementalPatchCompute` is provided without `incrementalDataDeps` or `planIncremental`, runtime setup fails with `ComputationProtocolError`. Incremental callbacks receive only the planned partial `dataDeps`, not all declared dependencies.
|
|
1700
1702
|
|
|
1703
|
+
**Mutation event snapshots**
|
|
1704
|
+
|
|
1705
|
+
Snapshot shapes differ by event type:
|
|
1706
|
+
|
|
1707
|
+
- `create`: `record` = the written fields (defaults + payload), `oldRecord` = `undefined`
|
|
1708
|
+
- `update`: `record` = **only the changed fields** (plus `id`), `oldRecord` = full pre-update snapshot
|
|
1709
|
+
- `delete`: `record` = full deleted snapshot, `oldRecord` = `undefined`
|
|
1710
|
+
|
|
1711
|
+
Never read a field straight off `mutationEvent.record` on update unless you know it was part of the update — it silently evaluates to `undefined` otherwise and corrupts the incremental result. Reconstruct the full new state with `{ ...mutationEvent.oldRecord, ...mutationEvent.record }`, or re-fetch the record via `this.controller.system.storage.findOne(...)` when computed columns or related data are needed (this is what the built-in aggregations do). The same applies to the third `record` parameter: it is only guaranteed to identify the dirty record, not to carry all of its fields.
|
|
1712
|
+
|
|
1701
1713
|
**Retry safety**
|
|
1702
1714
|
|
|
1703
1715
|
Custom `compute`, `incrementalCompute`, `incrementalPatchCompute`, and `asyncReturn` callbacks may be replayed after transaction retry. Keep them deterministic and avoid irreversible external IO. Put post-commit external work in `recordMutationSideEffects`.
|
|
@@ -1772,9 +1784,20 @@ const counterDict = Dictionary.create({
|
|
|
1772
1784
|
},
|
|
1773
1785
|
incrementalCompute: async function(lastValue, mutationEvent, record, dataDeps) {
|
|
1774
1786
|
console.log('Previous value:', lastValue);
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1787
|
+
// CAUTION mutation event snapshot shapes differ by type:
|
|
1788
|
+
// - create: record = written fields (defaults + payload), oldRecord = undefined
|
|
1789
|
+
// - update: record = ONLY the changed fields (+ id), oldRecord = full pre-update snapshot
|
|
1790
|
+
// - delete: record = full deleted snapshot, oldRecord = undefined
|
|
1791
|
+
// Reading a field straight off mutationEvent.record on update silently yields
|
|
1792
|
+
// undefined when that field was not part of the update. Reconstruct the full
|
|
1793
|
+
// new state by overlaying the changed fields on the old snapshot.
|
|
1794
|
+
const newRecord = mutationEvent.type === 'delete'
|
|
1795
|
+
? undefined
|
|
1796
|
+
: { ...(mutationEvent.oldRecord || {}), ...(mutationEvent.record || {}) };
|
|
1797
|
+
const previousRecord = mutationEvent.type === 'delete'
|
|
1798
|
+
? mutationEvent.record
|
|
1799
|
+
: mutationEvent.oldRecord;
|
|
1800
|
+
const delta = (newRecord?.value || 0) - (previousRecord?.value || 0);
|
|
1778
1801
|
const total = (lastValue?.total || 0) + delta;
|
|
1779
1802
|
|
|
1780
1803
|
return {
|
|
@@ -124,7 +124,11 @@ When using `InteractionEventEntity` as the Transform input source, understand th
|
|
|
124
124
|
states: [updatedState],
|
|
125
125
|
transfers: [
|
|
126
126
|
StateTransfer.create({
|
|
127
|
-
trigger:
|
|
127
|
+
trigger: {
|
|
128
|
+
recordName: InteractionEventEntity.name,
|
|
129
|
+
type: 'create',
|
|
130
|
+
record: { interactionName: UpdateStyleInteraction.name }
|
|
131
|
+
},
|
|
128
132
|
current: updatedState,
|
|
129
133
|
next: updatedState,
|
|
130
134
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
@@ -142,7 +146,11 @@ When using `InteractionEventEntity` as the Transform input source, understand th
|
|
|
142
146
|
initialState: activeState, // StateMachine controls initial value
|
|
143
147
|
transfers: [
|
|
144
148
|
StateTransfer.create({
|
|
145
|
-
trigger:
|
|
149
|
+
trigger: {
|
|
150
|
+
recordName: InteractionEventEntity.name,
|
|
151
|
+
type: 'create',
|
|
152
|
+
record: { interactionName: DeleteStyleInteraction.name }
|
|
153
|
+
},
|
|
146
154
|
current: activeState,
|
|
147
155
|
next: deletedState,
|
|
148
156
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
@@ -336,19 +344,30 @@ Used for status changes and field updates.
|
|
|
336
344
|
|
|
337
345
|
**🔴 IMPORTANT: StateTransfer Trigger Parameter**
|
|
338
346
|
|
|
339
|
-
The `trigger` parameter in `StateTransfer.create()`
|
|
347
|
+
The `trigger` parameter in `StateTransfer.create()` is a **RecordMutationEventPattern** — an object pattern matched against record mutation events (`{ recordName, type, keys?, record?, oldRecord? }`). It is NOT a string and NOT an Interaction instance. For interaction-triggered transitions, match the creation of the interaction's event record:
|
|
340
348
|
|
|
341
349
|
```typescript
|
|
342
|
-
// ❌ WRONG: Using string as trigger
|
|
350
|
+
// ❌ WRONG: Using a string as trigger
|
|
343
351
|
StateTransfer.create({
|
|
344
|
-
trigger: 'PublishStyle', // ERROR!
|
|
352
|
+
trigger: 'PublishStyle', // ERROR! Never matches any event
|
|
345
353
|
current: draftState,
|
|
346
354
|
next: activeState
|
|
347
355
|
})
|
|
348
356
|
|
|
349
|
-
//
|
|
357
|
+
// ❌ WRONG: Using an Interaction instance as trigger
|
|
350
358
|
StateTransfer.create({
|
|
351
|
-
trigger: PublishStyle, //
|
|
359
|
+
trigger: PublishStyle, // ERROR! Never matches any event
|
|
360
|
+
current: draftState,
|
|
361
|
+
next: activeState
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// ✅ CORRECT: Match the interaction's event record creation
|
|
365
|
+
StateTransfer.create({
|
|
366
|
+
trigger: {
|
|
367
|
+
recordName: InteractionEventEntity.name,
|
|
368
|
+
type: 'create',
|
|
369
|
+
record: { interactionName: PublishStyle.name }
|
|
370
|
+
},
|
|
352
371
|
current: draftState,
|
|
353
372
|
next: activeState
|
|
354
373
|
})
|
|
@@ -397,13 +416,21 @@ statusProperty.computation = StateMachine.create({
|
|
|
397
416
|
StateTransfer.create({
|
|
398
417
|
current: draftState,
|
|
399
418
|
next: activeState,
|
|
400
|
-
trigger:
|
|
419
|
+
trigger: {
|
|
420
|
+
recordName: InteractionEventEntity.name,
|
|
421
|
+
type: 'create',
|
|
422
|
+
record: { interactionName: PublishStyle.name }
|
|
423
|
+
},
|
|
401
424
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
402
425
|
}),
|
|
403
426
|
StateTransfer.create({
|
|
404
427
|
current: activeState,
|
|
405
428
|
next: offlineState,
|
|
406
|
-
trigger:
|
|
429
|
+
trigger: {
|
|
430
|
+
recordName: InteractionEventEntity.name,
|
|
431
|
+
type: 'create',
|
|
432
|
+
record: { interactionName: DeleteStyle.name }
|
|
433
|
+
},
|
|
407
434
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
408
435
|
})
|
|
409
436
|
]
|
|
@@ -443,7 +470,11 @@ updatedAtProperty.computation = StateMachine.create({
|
|
|
443
470
|
StateTransfer.create({
|
|
444
471
|
current: updatedState,
|
|
445
472
|
next: updatedState, // Self-loop to same state
|
|
446
|
-
trigger:
|
|
473
|
+
trigger: {
|
|
474
|
+
recordName: InteractionEventEntity.name,
|
|
475
|
+
type: 'create',
|
|
476
|
+
record: { interactionName: UpdateStyle.name }
|
|
477
|
+
},
|
|
447
478
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
448
479
|
})
|
|
449
480
|
]
|
|
@@ -506,7 +537,11 @@ modificationInfoProperty.computation = StateMachine.create({
|
|
|
506
537
|
StateTransfer.create({
|
|
507
538
|
current: modifiedState,
|
|
508
539
|
next: modifiedState,
|
|
509
|
-
trigger:
|
|
540
|
+
trigger: {
|
|
541
|
+
recordName: InteractionEventEntity.name,
|
|
542
|
+
type: 'create',
|
|
543
|
+
record: { interactionName: UpdateArticle.name }
|
|
544
|
+
},
|
|
510
545
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
511
546
|
})
|
|
512
547
|
]
|
|
@@ -869,7 +904,11 @@ updatedAtProperty.computation = StateMachine.create({
|
|
|
869
904
|
StateTransfer.create({
|
|
870
905
|
current: updatedState,
|
|
871
906
|
next: updatedState,
|
|
872
|
-
trigger:
|
|
907
|
+
trigger: {
|
|
908
|
+
recordName: InteractionEventEntity.name,
|
|
909
|
+
type: 'create',
|
|
910
|
+
record: { interactionName: UpdateStyle.name }
|
|
911
|
+
},
|
|
873
912
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
874
913
|
})
|
|
875
914
|
]
|
|
@@ -905,7 +944,11 @@ statusProperty.computation = StateMachine.create({
|
|
|
905
944
|
StateTransfer.create({
|
|
906
945
|
current: activeState,
|
|
907
946
|
next: offlineState,
|
|
908
|
-
trigger:
|
|
947
|
+
trigger: {
|
|
948
|
+
recordName: InteractionEventEntity.name,
|
|
949
|
+
type: 'create',
|
|
950
|
+
record: { interactionName: DeleteStyle.name }
|
|
951
|
+
},
|
|
909
952
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
910
953
|
})
|
|
911
954
|
]
|
|
@@ -977,7 +1020,11 @@ updatedAtProperty.computation = StateMachine.create({
|
|
|
977
1020
|
StateTransfer.create({
|
|
978
1021
|
current: updatedState,
|
|
979
1022
|
next: updatedState,
|
|
980
|
-
trigger:
|
|
1023
|
+
trigger: {
|
|
1024
|
+
recordName: InteractionEventEntity.name,
|
|
1025
|
+
type: 'create',
|
|
1026
|
+
record: { interactionName: UpdateInteraction.name }
|
|
1027
|
+
},
|
|
981
1028
|
computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
|
|
982
1029
|
})
|
|
983
1030
|
]
|
|
@@ -215,6 +215,7 @@ const Product = Entity.create({
|
|
|
215
215
|
name: 'currentStock',
|
|
216
216
|
computation: WeightedSummation.create({
|
|
217
217
|
record: OrderItemRelation,
|
|
218
|
+
attributeQuery: ['quantity'], // Required when callback reads fields
|
|
218
219
|
callback: (orderItem) => ({
|
|
219
220
|
weight: -1, // Reduce inventory
|
|
220
221
|
value: orderItem.quantity
|
|
@@ -225,6 +226,7 @@ const Product = Entity.create({
|
|
|
225
226
|
name: 'totalSales',
|
|
226
227
|
computation: WeightedSummation.create({
|
|
227
228
|
record: OrderItemRelation,
|
|
229
|
+
attributeQuery: ['quantity'], // Required when callback reads fields
|
|
228
230
|
callback: (orderItem) => ({
|
|
229
231
|
weight: 1,
|
|
230
232
|
value: orderItem.quantity
|
|
@@ -89,6 +89,11 @@ Custom computations are different because interaqt cannot inspect user callback
|
|
|
89
89
|
|
|
90
90
|
Custom incremental computations must declare an incremental plan. Use `incrementalDataDeps: [...]` for the common case, or `planIncremental(event, record, context)` when the computation needs to choose between incremental, full recompute, and skip per event. Incremental callbacks receive only the planned partial `dataDeps`; the runtime no longer resolves all declared dependencies before incremental execution.
|
|
91
91
|
|
|
92
|
+
Two contract details for Custom `dataDeps`:
|
|
93
|
+
|
|
94
|
+
- **`records` dataDeps must declare `attributeQuery`.** Setup fails fast otherwise, because an undeclared field set would resolve to id-only records and field updates would never re-trigger the computation. Declare the fields your `compute` reads (e.g. `attributeQuery: ['price']`), or pass an explicit `attributeQuery: []` when the computation only depends on record membership (create/delete).
|
|
95
|
+
- **`incrementalDataDeps` names the dependency values to resolve and pass into `incrementalCompute`; it is not an event filter.** Events from every declared `dataDep` reach `incrementalCompute` — distinguish sources by `event.recordName` inside the callback, or use `planIncremental` to return different plans per dependency. (Create/update events of unrelated global dicts are already filtered by key at the source-map level and never trigger the computation.)
|
|
96
|
+
|
|
92
97
|
Because retry replays the transaction attempt, `guard`, `mapEventData`, `resolve`, computation callbacks, `afterDispatch`, and `asyncReturn` must be deterministic and retry-safe. Put irreversible external IO in `postCommit` for interaction-specific effects or `recordMutationSideEffects` for mutation-driven effects; both run after the final commit.
|
|
93
98
|
|
|
94
99
|
### Scoped Atomic Sequences
|
|
@@ -490,6 +495,7 @@ const Student = Entity.create({
|
|
|
490
495
|
defaultValue: () => 0,
|
|
491
496
|
computation: WeightedSummation.create({
|
|
492
497
|
record: StudentGrades,
|
|
498
|
+
attributeQuery: [['target', { attributeQuery: ['credit', 'score'] }]], // Required when callback reads fields
|
|
493
499
|
callback: (relation) => ({
|
|
494
500
|
weight: relation.target.credit,
|
|
495
501
|
value: relation.target.score
|
|
@@ -503,6 +509,7 @@ const Student = Entity.create({
|
|
|
503
509
|
defaultValue: () => 0,
|
|
504
510
|
computation: WeightedSummation.create({
|
|
505
511
|
record: StudentGrades,
|
|
512
|
+
attributeQuery: [['target', { attributeQuery: ['credit'] }]], // Required when callback reads fields
|
|
506
513
|
callback: (relation) => ({
|
|
507
514
|
weight: 1,
|
|
508
515
|
value: relation.target.credit
|
|
@@ -529,6 +536,7 @@ const Student = Entity.create({
|
|
|
529
536
|
defaultValue: () => 0,
|
|
530
537
|
computation: WeightedSummation.create({
|
|
531
538
|
record: StudentGrades,
|
|
539
|
+
attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]], // Required when callback reads fields
|
|
532
540
|
callback: (relation) => {
|
|
533
541
|
// Only count subjects with score >= 60
|
|
534
542
|
if (relation.target.score >= 60) {
|
|
@@ -568,6 +576,7 @@ const Project = Entity.create({
|
|
|
568
576
|
defaultValue: () => false,
|
|
569
577
|
computation: Every.create({
|
|
570
578
|
record: ProjectTasks,
|
|
579
|
+
attributeQuery: [['target', { attributeQuery: ['status'] }]], // Required when callback reads fields
|
|
571
580
|
callback: (relation) => relation.target.status === 'completed'
|
|
572
581
|
})
|
|
573
582
|
})
|
|
@@ -605,6 +614,7 @@ const Project = Entity.create({
|
|
|
605
614
|
defaultValue: () => false,
|
|
606
615
|
computation: Any.create({
|
|
607
616
|
record: ProjectMember,
|
|
617
|
+
attributeQuery: ['role'], // Required when callback reads fields
|
|
608
618
|
callback: (relation) => relation.role === 'admin'
|
|
609
619
|
})
|
|
610
620
|
})
|
|
@@ -639,6 +649,7 @@ const Order = Entity.create({
|
|
|
639
649
|
defaultValue: () => false,
|
|
640
650
|
computation: Every.create({
|
|
641
651
|
record: OrderItems,
|
|
652
|
+
attributeQuery: [['target', { attributeQuery: ['quantity', 'stockQuantity'] }]], // Required when callback reads fields
|
|
642
653
|
callback: (relation) => {
|
|
643
654
|
const item = relation.target;
|
|
644
655
|
return item.quantity > 0 && item.stockQuantity >= item.quantity;
|
|
@@ -652,6 +663,7 @@ const Order = Entity.create({
|
|
|
652
663
|
defaultValue: () => false,
|
|
653
664
|
computation: Any.create({
|
|
654
665
|
record: OrderItems,
|
|
666
|
+
attributeQuery: [['target', { attributeQuery: ['quantity', 'price'] }]], // Required when callback reads fields
|
|
655
667
|
callback: (relation) => {
|
|
656
668
|
const item = relation.target;
|
|
657
669
|
return (item.quantity * item.price) > 1000;
|
|
@@ -1118,71 +1130,75 @@ StateMachine is used for state transition-based computations, particularly suita
|
|
|
1118
1130
|
|
|
1119
1131
|
### Basic State Machine
|
|
1120
1132
|
|
|
1121
|
-
|
|
1122
|
-
import { StateMachine } from 'interaqt';
|
|
1123
|
-
|
|
1124
|
-
const Order = Entity.create({
|
|
1125
|
-
name: 'Order',
|
|
1126
|
-
properties: [
|
|
1127
|
-
Property.create({ name: 'orderNumber', type: 'string' }),
|
|
1128
|
-
Property.create({
|
|
1129
|
-
name: 'status',
|
|
1130
|
-
type: 'string',
|
|
1131
|
-
computation: new StateMachine({
|
|
1132
|
-
states: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
|
|
1133
|
-
initialState: 'pending',
|
|
1134
|
-
transitions: [
|
|
1135
|
-
{ from: 'pending', to: 'paid', condition: 'payment_received' },
|
|
1136
|
-
{ from: 'paid', to: 'shipped', condition: 'order_shipped' },
|
|
1137
|
-
{ from: 'shipped', to: 'delivered', condition: 'delivery_confirmed' },
|
|
1138
|
-
{ from: 'pending', to: 'cancelled', condition: 'order_cancelled' },
|
|
1139
|
-
{ from: 'paid', to: 'cancelled', condition: 'order_cancelled' }
|
|
1140
|
-
]
|
|
1141
|
-
})
|
|
1142
|
-
})
|
|
1143
|
-
]
|
|
1144
|
-
});
|
|
1145
|
-
```
|
|
1133
|
+
A StateMachine is declared from three parts: `StateNode` instances (the states), `StateTransfer` instances (the transitions), and an `initialState`. States are always object references, never strings.
|
|
1146
1134
|
|
|
1147
|
-
|
|
1135
|
+
Each `StateTransfer` declares:
|
|
1136
|
+
- `current` / `next`: the state nodes the transfer connects
|
|
1137
|
+
- `trigger`: a `RecordMutationEventPattern` (`{ recordName, type, keys?, record?, oldRecord? }`) matched against record mutation events — for interaction-triggered transitions, match the creation of the interaction's event record
|
|
1138
|
+
- `computeTarget` (required for property-level state machines): maps the matched event to the record(s) whose property should transition
|
|
1148
1139
|
|
|
1149
1140
|
```javascript
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1141
|
+
import { StateMachine, StateNode, StateTransfer, InteractionEventEntity } from 'interaqt';
|
|
1142
|
+
|
|
1143
|
+
// 1. Declare state nodes
|
|
1144
|
+
const pendingState = StateNode.create({ name: 'pending' });
|
|
1145
|
+
const paidState = StateNode.create({ name: 'paid' });
|
|
1146
|
+
const shippedState = StateNode.create({ name: 'shipped' });
|
|
1147
|
+
const deliveredState = StateNode.create({ name: 'delivered' });
|
|
1148
|
+
const cancelledState = StateNode.create({ name: 'cancelled' });
|
|
1149
|
+
|
|
1150
|
+
// 2. Declare the interactions that drive the transitions
|
|
1151
|
+
const PayOrder = Interaction.create({
|
|
1152
|
+
name: 'PayOrder',
|
|
1153
|
+
action: Action.create({ name: 'payOrder' }),
|
|
1154
|
+
payload: Payload.create({
|
|
1155
|
+
items: [PayloadItem.create({ name: 'orderId', type: 'string', required: true })]
|
|
1162
1156
|
})
|
|
1163
1157
|
});
|
|
1158
|
+
// ... ShipOrder / ConfirmDelivery / CancelOrder declared the same way
|
|
1164
1159
|
|
|
1165
|
-
//
|
|
1160
|
+
// 3. Wire them together on the property
|
|
1166
1161
|
const Order = Entity.create({
|
|
1167
1162
|
name: 'Order',
|
|
1168
1163
|
properties: [
|
|
1164
|
+
Property.create({ name: 'orderNumber', type: 'string' }),
|
|
1169
1165
|
Property.create({
|
|
1170
1166
|
name: 'status',
|
|
1171
1167
|
type: 'string',
|
|
1172
|
-
computation:
|
|
1173
|
-
states: [
|
|
1174
|
-
initialState:
|
|
1175
|
-
|
|
1176
|
-
{
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1168
|
+
computation: StateMachine.create({
|
|
1169
|
+
states: [pendingState, paidState, shippedState, deliveredState, cancelledState],
|
|
1170
|
+
initialState: pendingState,
|
|
1171
|
+
transfers: [
|
|
1172
|
+
StateTransfer.create({
|
|
1173
|
+
current: pendingState,
|
|
1174
|
+
next: paidState,
|
|
1175
|
+
trigger: {
|
|
1176
|
+
recordName: InteractionEventEntity.name,
|
|
1177
|
+
type: 'create',
|
|
1178
|
+
record: { interactionName: PayOrder.name }
|
|
1179
|
+
},
|
|
1180
|
+
computeTarget: (event) => ({ id: event.record.payload.orderId })
|
|
1181
|
+
}),
|
|
1182
|
+
StateTransfer.create({
|
|
1183
|
+
current: paidState,
|
|
1184
|
+
next: shippedState,
|
|
1185
|
+
trigger: {
|
|
1186
|
+
recordName: InteractionEventEntity.name,
|
|
1187
|
+
type: 'create',
|
|
1188
|
+
record: { interactionName: ShipOrder.name }
|
|
1189
|
+
},
|
|
1190
|
+
computeTarget: (event) => ({ id: event.record.payload.orderId })
|
|
1191
|
+
}),
|
|
1192
|
+
StateTransfer.create({
|
|
1193
|
+
current: pendingState,
|
|
1194
|
+
next: cancelledState,
|
|
1195
|
+
trigger: {
|
|
1196
|
+
recordName: InteractionEventEntity.name,
|
|
1197
|
+
type: 'create',
|
|
1198
|
+
record: { interactionName: CancelOrder.name }
|
|
1199
|
+
},
|
|
1200
|
+
computeTarget: (event) => ({ id: event.record.payload.orderId })
|
|
1201
|
+
})
|
|
1186
1202
|
]
|
|
1187
1203
|
})
|
|
1188
1204
|
})
|
|
@@ -1190,48 +1206,62 @@ const Order = Entity.create({
|
|
|
1190
1206
|
});
|
|
1191
1207
|
```
|
|
1192
1208
|
|
|
1193
|
-
###
|
|
1209
|
+
### Data-Based State Transitions
|
|
1210
|
+
|
|
1211
|
+
`trigger` matches **any** record mutation event, not just interaction events. This means state transitions can also be driven by data changes — for example, transition when a specific field of the host record is updated (`keys` uses subset semantics: the transfer fires when the update touched all the listed fields):
|
|
1194
1212
|
|
|
1195
1213
|
```javascript
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
to: 'submitted',
|
|
1213
|
-
on: 'SubmitRequest',
|
|
1214
|
-
condition: (record) => record.reason && record.startDate && record.endDate
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
1217
|
-
from: 'submitted',
|
|
1218
|
-
to: 'approved',
|
|
1219
|
-
on: 'ApproveRequest',
|
|
1220
|
-
condition: (record, context) => context.user.role === 'manager'
|
|
1221
|
-
},
|
|
1222
|
-
{
|
|
1223
|
-
from: 'submitted',
|
|
1224
|
-
to: 'rejected',
|
|
1225
|
-
on: 'RejectRequest',
|
|
1226
|
-
condition: (record, context) => context.user.role === 'manager'
|
|
1227
|
-
}
|
|
1228
|
-
]
|
|
1214
|
+
const draftState = StateNode.create({ name: 'draft' });
|
|
1215
|
+
const publishedState = StateNode.create({ name: 'published' });
|
|
1216
|
+
|
|
1217
|
+
Property.create({
|
|
1218
|
+
name: 'status',
|
|
1219
|
+
type: 'string',
|
|
1220
|
+
computation: StateMachine.create({
|
|
1221
|
+
states: [draftState, publishedState],
|
|
1222
|
+
initialState: draftState,
|
|
1223
|
+
transfers: [
|
|
1224
|
+
StateTransfer.create({
|
|
1225
|
+
current: draftState,
|
|
1226
|
+
next: publishedState,
|
|
1227
|
+
// Fires when a Document update touches the `reviewed` field
|
|
1228
|
+
trigger: { recordName: 'Document', type: 'update', keys: ['reviewed'] },
|
|
1229
|
+
computeTarget: (event) => ({ id: event.oldRecord?.id ?? event.record?.id })
|
|
1229
1230
|
})
|
|
1230
|
-
|
|
1231
|
-
|
|
1231
|
+
]
|
|
1232
|
+
})
|
|
1232
1233
|
});
|
|
1233
1234
|
```
|
|
1234
1235
|
|
|
1236
|
+
Note: if several transfers share the same `current` state and their triggers match the same event but lead to different next states, the framework throws an "ambiguous transition" error — make the triggers mutually exclusive (e.g. via `record` / `keys` patterns).
|
|
1237
|
+
|
|
1238
|
+
### Guarding Transitions
|
|
1239
|
+
|
|
1240
|
+
There is no `condition` field on `StateTransfer`. Preconditions belong to the interaction layer (use `conditions` / `userAttributives` on the Interaction so unauthorized or invalid calls never produce an event), and fine-grained per-record filtering is expressed in `computeTarget` — return `null`/`undefined` (or an empty array) to skip the transition for records that should not transition:
|
|
1241
|
+
|
|
1242
|
+
```javascript
|
|
1243
|
+
StateTransfer.create({
|
|
1244
|
+
current: submittedState,
|
|
1245
|
+
next: approvedState,
|
|
1246
|
+
trigger: {
|
|
1247
|
+
recordName: InteractionEventEntity.name,
|
|
1248
|
+
type: 'create',
|
|
1249
|
+
record: { interactionName: ApproveRequest.name }
|
|
1250
|
+
},
|
|
1251
|
+
computeTarget: async function (event) {
|
|
1252
|
+
// `this` is the Controller: you can query before deciding the target
|
|
1253
|
+
const request = await this.system.storage.findOne(
|
|
1254
|
+
'LeaveRequest',
|
|
1255
|
+
MatchExp.atom({ key: 'id', value: ['=', event.record.payload.requestId] }),
|
|
1256
|
+
undefined,
|
|
1257
|
+
['id', 'reason', 'startDate', 'endDate']
|
|
1258
|
+
);
|
|
1259
|
+
if (!request?.reason || !request?.startDate || !request?.endDate) return null;
|
|
1260
|
+
return { id: request.id };
|
|
1261
|
+
}
|
|
1262
|
+
})
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1235
1265
|
### Dynamic Value Computation with StateNode
|
|
1236
1266
|
|
|
1237
1267
|
StateMachine supports dynamic value computation through the `computeValue` function in StateNode. This allows you to compute and update property values during state transitions.
|
|
@@ -1263,8 +1293,12 @@ const EventEntity = Entity.create({
|
|
|
1263
1293
|
// Self-transition: stays in the same state but triggers computeValue
|
|
1264
1294
|
current: triggeredState,
|
|
1265
1295
|
next: triggeredState,
|
|
1266
|
-
trigger:
|
|
1267
|
-
|
|
1296
|
+
trigger: {
|
|
1297
|
+
recordName: InteractionEventEntity.name,
|
|
1298
|
+
type: 'create',
|
|
1299
|
+
record: { interactionName: TriggerEventInteraction.name }
|
|
1300
|
+
},
|
|
1301
|
+
computeTarget: (event) => ({ id: event.record.payload.eventId })
|
|
1268
1302
|
})
|
|
1269
1303
|
],
|
|
1270
1304
|
initialState: triggeredState
|
|
@@ -1308,14 +1342,22 @@ const CounterEntity = Entity.create({
|
|
|
1308
1342
|
StateTransfer.create({
|
|
1309
1343
|
current: idleState,
|
|
1310
1344
|
next: incrementingState,
|
|
1311
|
-
trigger:
|
|
1312
|
-
|
|
1345
|
+
trigger: {
|
|
1346
|
+
recordName: InteractionEventEntity.name,
|
|
1347
|
+
type: 'create',
|
|
1348
|
+
record: { interactionName: IncrementInteraction.name }
|
|
1349
|
+
},
|
|
1350
|
+
computeTarget: (event) => ({ id: event.record.payload.counterId })
|
|
1313
1351
|
}),
|
|
1314
1352
|
StateTransfer.create({
|
|
1315
1353
|
current: incrementingState,
|
|
1316
1354
|
next: idleState,
|
|
1317
|
-
trigger:
|
|
1318
|
-
|
|
1355
|
+
trigger: {
|
|
1356
|
+
recordName: InteractionEventEntity.name,
|
|
1357
|
+
type: 'create',
|
|
1358
|
+
record: { interactionName: ResetInteraction.name }
|
|
1359
|
+
},
|
|
1360
|
+
computeTarget: (event) => ({ id: event.record.payload.counterId })
|
|
1319
1361
|
})
|
|
1320
1362
|
],
|
|
1321
1363
|
initialState: idleState
|
|
@@ -1369,20 +1411,32 @@ const TaskEntity = Entity.create({
|
|
|
1369
1411
|
StateTransfer.create({
|
|
1370
1412
|
current: newState,
|
|
1371
1413
|
next: inProgressState,
|
|
1372
|
-
trigger:
|
|
1373
|
-
|
|
1414
|
+
trigger: {
|
|
1415
|
+
recordName: InteractionEventEntity.name,
|
|
1416
|
+
type: 'create',
|
|
1417
|
+
record: { interactionName: StartTaskInteraction.name }
|
|
1418
|
+
},
|
|
1419
|
+
computeTarget: (event) => ({ id: event.record.payload.taskId })
|
|
1374
1420
|
}),
|
|
1375
1421
|
StateTransfer.create({
|
|
1376
1422
|
current: inProgressState,
|
|
1377
1423
|
next: completedState,
|
|
1378
|
-
trigger:
|
|
1379
|
-
|
|
1424
|
+
trigger: {
|
|
1425
|
+
recordName: InteractionEventEntity.name,
|
|
1426
|
+
type: 'create',
|
|
1427
|
+
record: { interactionName: CompleteTaskInteraction.name }
|
|
1428
|
+
},
|
|
1429
|
+
computeTarget: (event) => ({ id: event.record.payload.taskId })
|
|
1380
1430
|
}),
|
|
1381
1431
|
StateTransfer.create({
|
|
1382
1432
|
current: inProgressState,
|
|
1383
1433
|
next: cancelledState,
|
|
1384
|
-
trigger:
|
|
1385
|
-
|
|
1434
|
+
trigger: {
|
|
1435
|
+
recordName: InteractionEventEntity.name,
|
|
1436
|
+
type: 'create',
|
|
1437
|
+
record: { interactionName: CancelTaskInteraction.name }
|
|
1438
|
+
},
|
|
1439
|
+
computeTarget: (event) => ({ id: event.record.payload.taskId })
|
|
1386
1440
|
})
|
|
1387
1441
|
],
|
|
1388
1442
|
initialState: newState
|
|
@@ -1858,6 +1912,7 @@ const Post = Entity.create({
|
|
|
1858
1912
|
defaultValue: () => 0,
|
|
1859
1913
|
computation: WeightedSummation.create({
|
|
1860
1914
|
record: PostInteractions,
|
|
1915
|
+
attributeQuery: [['target', { attributeQuery: ['type'] }]], // Required when callback reads fields
|
|
1861
1916
|
callback: (relation) => {
|
|
1862
1917
|
const interaction = relation.target;
|
|
1863
1918
|
switch (interaction.type) {
|
|
@@ -1889,6 +1944,7 @@ const Post = Entity.create({
|
|
|
1889
1944
|
defaultValue: () => false,
|
|
1890
1945
|
computation: Every.create({
|
|
1891
1946
|
record: PostComments,
|
|
1947
|
+
attributeQuery: [['target', { attributeQuery: ['status'] }]], // Required when callback reads fields
|
|
1892
1948
|
callback: (relation) => relation.target.status === 'approved'
|
|
1893
1949
|
})
|
|
1894
1950
|
})
|
|
@@ -1933,6 +1989,7 @@ Property.create({
|
|
|
1933
1989
|
defaultValue: () => 0,
|
|
1934
1990
|
computation: Count.create({
|
|
1935
1991
|
record: User,
|
|
1992
|
+
attributeQuery: ['status'], // Required when callback reads fields
|
|
1936
1993
|
callback: (user) => user.status === 'active'
|
|
1937
1994
|
})
|
|
1938
1995
|
});
|