interaqt 0.3.1 → 0.4.0

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.
Files changed (30) hide show
  1. package/agent/.claude/agents/computation-generation-handler.md +1 -3
  2. package/agent/.claude/agents/requirements-analysis-handler.md +43 -3
  3. package/agent/agentspace/knowledge/generator/api-reference.md +538 -101
  4. package/agent/agentspace/knowledge/generator/computation-analysis.md +22 -20
  5. package/agent/agentspace/knowledge/generator/computation-implementation.md +62 -38
  6. package/agent/agentspace/knowledge/generator/data-analysis.md +23 -2
  7. package/dist/index.js +2977 -2976
  8. package/dist/index.js.map +1 -1
  9. package/dist/runtime/ComputationSourceMap.d.ts +11 -21
  10. package/dist/runtime/ComputationSourceMap.d.ts.map +1 -1
  11. package/dist/runtime/Controller.d.ts +2 -2
  12. package/dist/runtime/MonoSystem.d.ts.map +1 -1
  13. package/dist/runtime/Scheduler.d.ts +6 -6
  14. package/dist/runtime/Scheduler.d.ts.map +1 -1
  15. package/dist/runtime/System.d.ts +5 -0
  16. package/dist/runtime/System.d.ts.map +1 -1
  17. package/dist/runtime/computations/Computation.d.ts +4 -9
  18. package/dist/runtime/computations/Computation.d.ts.map +1 -1
  19. package/dist/runtime/computations/StateMachine.d.ts +4 -7
  20. package/dist/runtime/computations/StateMachine.d.ts.map +1 -1
  21. package/dist/runtime/computations/Transform.d.ts +7 -1
  22. package/dist/runtime/computations/Transform.d.ts.map +1 -1
  23. package/dist/runtime/computations/TransitionFinder.d.ts +2 -2
  24. package/dist/runtime/computations/TransitionFinder.d.ts.map +1 -1
  25. package/dist/shared/StateTransfer.d.ts +15 -5
  26. package/dist/shared/StateTransfer.d.ts.map +1 -1
  27. package/dist/shared/Transform.d.ts +17 -3
  28. package/dist/shared/Transform.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/agent/.claude/agents/requirements-analysis-handler-bak.md +0 -530
@@ -163,14 +163,14 @@ HardDeletionProperty.create(config?: HardDeletionPropertyConfig): PropertyInstan
163
163
  ```
164
164
 
165
165
  **Parameters**
166
- - `config.name` (string, optional): Property name, defaults to `'_isDeleted_'`
166
+ - `config.name` (string, optional): Property name, defaults to `HARD_DELETION_PROPERTY_NAME`
167
167
 
168
168
  **Usage with StateMachine**
169
169
 
170
170
  HardDeletionProperty is typically used with StateMachine to manage record deletion:
171
171
 
172
172
  ```typescript
173
- import { DELETED_STATE, NON_DELETED_STATE } from 'interaqt'
173
+ import { DELETED_STATE, NON_DELETED_STATE, HARD_DELETION_PROPERTY_NAME } from 'interaqt'
174
174
 
175
175
  // Define deletion StateMachine for the HardDeletionProperty
176
176
  const deletionStateMachine = StateMachine.create({
@@ -178,7 +178,12 @@ const deletionStateMachine = StateMachine.create({
178
178
  defaultState: NON_DELETED_STATE,
179
179
  transfers: [
180
180
  StateTransfer.create({
181
- trigger: DeleteUserInteraction,
181
+ trigger: {
182
+ recordName: InteractionEventEntity.name,
183
+ record: {
184
+ interactionName: DeleteUserInteraction.name
185
+ }
186
+ },
182
187
  current: NON_DELETED_STATE,
183
188
  next: DELETED_STATE,
184
189
  computeTarget: function(event) {
@@ -208,6 +213,11 @@ const User = Entity.create({
208
213
  2. The deletion is physical - the record is completely removed from the database
209
214
  3. This is different from soft deletion where records remain but are marked as deleted
210
215
 
216
+ **Note**: When you need to find the HardDeletionProperty after entity creation, use:
217
+ ```typescript
218
+ const hardDeletionProp = entity.properties.find(p => p.name === HARD_DELETION_PROPERTY_NAME)
219
+ ```
220
+
211
221
  **Example: Entity Creation and Deletion Pattern**
212
222
 
213
223
  ```typescript
@@ -217,16 +227,31 @@ const Article = Entity.create({
217
227
  properties: [
218
228
  Property.create({ name: 'title', type: 'string' }),
219
229
  Property.create({ name: 'content', type: 'string' }),
230
+ Property.create({ name: 'slug', type: 'string' }),
220
231
  HardDeletionProperty.create()
221
232
  ],
222
233
  // Transform creates new articles from interaction events
223
234
  computation: Transform.create({
224
- record: InteractionEventEntity,
225
- callback: function(event) {
235
+ eventDeps: {
236
+ ArticleInteraction: {
237
+ recordName: InteractionEventEntity.name,
238
+ type: 'create'
239
+ }
240
+ },
241
+ callback: async function(this: Controller, mutationEvent) {
242
+ const event = mutationEvent.record;
226
243
  if (event.interactionName === 'CreateArticle') {
244
+ // Use Controller to generate article slug
245
+ const existingCount = await this.system.storage.find('Article',
246
+ undefined,
247
+ undefined,
248
+ ['id']
249
+ );
250
+
227
251
  return {
228
252
  title: event.payload.title,
229
- content: event.payload.content
253
+ content: event.payload.content,
254
+ slug: `article-${existingCount.length + 1}`
230
255
  }
231
256
  }
232
257
  return null
@@ -241,7 +266,12 @@ deletionProperty.computation = StateMachine.create({
241
266
  defaultState: NON_DELETED_STATE,
242
267
  transfers: [
243
268
  StateTransfer.create({
244
- trigger: DeleteArticleInteraction,
269
+ trigger: {
270
+ recordName: InteractionEventEntity.name,
271
+ record: {
272
+ interactionName: DeleteArticleInteraction.name
273
+ }
274
+ },
245
275
  current: NON_DELETED_STATE,
246
276
  next: DELETED_STATE,
247
277
  computeTarget: function(event) {
@@ -755,10 +785,211 @@ Transform.create(config: TransformConfig): TransformInstance
755
785
  ```
756
786
 
757
787
  **Parameters**
758
- - `config.record` (Entity|Relation, required): Entity or relation to transform from (source collection)
759
- - `config.callback` (function, required): Transformation function that converts source data to target data
760
- - `config.attributeQuery` (AttributeQueryData, required): Attribute query configuration
788
+ Transform supports two modes of operation:
789
+
790
+ 1. **Entity/Relation Transform Mode**:
791
+ - `config.record` (Entity|Relation, required): Entity or relation to transform from (source collection)
792
+ - `config.attributeQuery` (AttributeQueryData, optional): Attribute query configuration
793
+ - `config.callback` (function, required): Transformation function that converts source data to target data
794
+ - **Context**: `this` is bound to the Controller instance, providing access to system APIs via `this.system.storage`, `this.globals`, etc.
795
+ - **Signature**: `function(this: Controller, record: any): any | any[]`
796
+
797
+ 2. **Event-Driven Transform Mode** (Recommended for interaction-based transformations):
798
+ - `config.eventDeps` (EventDeps, required): Event dependencies that trigger the transformation
799
+ - `config.callback` (function, required): Transformation function that processes mutation events
800
+ - **Context**: `this` is bound to the Controller instance, providing access to system APIs via `this.system.storage`, `this.globals`, etc.
801
+ - **Signature**: `function(this: Controller, mutationEvent: MutationEvent): any | any[]`
802
+
803
+ **Event Dependencies (eventDeps)**
804
+
805
+ The `eventDeps` parameter allows Transform to react to specific mutation events (create, update, delete) on entities. This is the recommended approach for transforming data based on interactions or system events.
806
+
807
+ ```typescript
808
+ interface EventDep {
809
+ recordName: string; // Entity name to listen to
810
+ type: 'create' | 'update' | 'delete'; // Event type
811
+ }
812
+
813
+ // Example eventDeps structure
814
+ eventDeps: {
815
+ UserCreate: {
816
+ recordName: 'User',
817
+ type: 'create'
818
+ },
819
+ UserUpdate: {
820
+ recordName: 'User',
821
+ type: 'update'
822
+ }
823
+ }
824
+ ```
825
+
826
+ **Examples**
827
+
828
+ 1. **Entity-to-Entity Transform** (for deriving new entities from existing ones):
829
+ ```typescript
830
+ // Create discounted products from regular products
831
+ const DiscountedProduct = Entity.create({
832
+ name: 'DiscountedProduct',
833
+ properties: [
834
+ Property.create({ name: 'name', type: 'string' }),
835
+ Property.create({ name: 'originalPrice', type: 'number' }),
836
+ Property.create({ name: 'discountedPrice', type: 'number' })
837
+ ],
838
+ computation: Transform.create({
839
+ record: Product,
840
+ attributeQuery: ['name', 'price'],
841
+ callback: async function(this: Controller, product) {
842
+ // Access system configuration via Controller
843
+ const discountRate = await this.system.storage.get('config', 'globalDiscountRate', 0.1);
844
+
845
+ return {
846
+ name: product.name,
847
+ originalPrice: product.price,
848
+ discountedPrice: product.price * (1 - discountRate)
849
+ };
850
+ }
851
+ })
852
+ })
853
+ ```
854
+
855
+ 2. **Event-Driven Transform** (recommended for interaction-based transformations):
856
+ ```typescript
857
+ // Create audit logs from user mutations
858
+ const UserAudit = Entity.create({
859
+ name: 'UserAudit',
860
+ properties: [
861
+ Property.create({ name: 'action', type: 'string' }),
862
+ Property.create({ name: 'userId', type: 'string' }),
863
+ Property.create({ name: 'timestamp', type: 'string' }),
864
+ Property.create({ name: 'changes', type: 'object' })
865
+ ],
866
+ computation: Transform.create({
867
+ eventDeps: {
868
+ UserCreate: {
869
+ recordName: 'User',
870
+ type: 'create'
871
+ },
872
+ UserUpdate: {
873
+ recordName: 'User',
874
+ type: 'update'
875
+ },
876
+ UserDelete: {
877
+ recordName: 'User',
878
+ type: 'delete'
879
+ }
880
+ },
881
+ callback: function(this: Controller, mutationEvent) {
882
+ // Access Controller APIs via 'this'
883
+ console.log('Audit log created by:', this.name); // Controller name
884
+
885
+ return {
886
+ action: mutationEvent.type,
887
+ userId: (mutationEvent.record?.id || mutationEvent.oldRecord?.id),
888
+ timestamp: new Date().toISOString(),
889
+ changes: {
890
+ old: mutationEvent.oldRecord,
891
+ new: mutationEvent.record
892
+ }
893
+ }
894
+ }
895
+ })
896
+ })
897
+ ```
761
898
 
899
+ 3. **Transform from Interaction Events**:
900
+ ```typescript
901
+ // Create articles from interactions
902
+ const Article = Entity.create({
903
+ name: 'Article',
904
+ properties: [
905
+ Property.create({ name: 'title', type: 'string' }),
906
+ Property.create({ name: 'content', type: 'string' }),
907
+ Property.create({ name: 'authorId', type: 'string' }),
908
+ Property.create({ name: 'authorName', type: 'string' })
909
+ ],
910
+ computation: Transform.create({
911
+ eventDeps: {
912
+ ArticleInteraction: {
913
+ recordName: InteractionEventEntity.name,
914
+ type: 'create'
915
+ }
916
+ },
917
+ callback: async function(this: Controller, mutationEvent) {
918
+ const event = mutationEvent.record;
919
+ if (event.interactionName === 'CreateArticle') {
920
+ // Use Controller to fetch additional user data
921
+ const author = await this.system.storage.findOne('User',
922
+ this.globals.MatchExp.atom({ key: 'id', value: ['=', event.user.id] }),
923
+ undefined,
924
+ ['id', 'name']
925
+ );
926
+
927
+ return {
928
+ title: event.payload.title,
929
+ content: event.payload.content,
930
+ authorId: event.user.id,
931
+ authorName: author?.name || 'Unknown'
932
+ }
933
+ }
934
+ return null;
935
+ }
936
+ })
937
+ })
938
+ ```
939
+
940
+ 4. **One-to-Many Transform**:
941
+ ```typescript
942
+ // Create multiple notifications from one order
943
+ const Notification = Entity.create({
944
+ name: 'Notification',
945
+ properties: [
946
+ Property.create({ name: 'type', type: 'string' }),
947
+ Property.create({ name: 'recipient', type: 'string' }),
948
+ Property.create({ name: 'message', type: 'string' })
949
+ ],
950
+ computation: Transform.create({
951
+ eventDeps: {
952
+ OrderCreate: {
953
+ recordName: 'Order',
954
+ type: 'create'
955
+ }
956
+ },
957
+ callback: async function(this: Controller, mutationEvent) {
958
+ const order = mutationEvent.record;
959
+
960
+ // Use Controller to fetch warehouse email from configuration
961
+ const warehouseEmail = await this.system.storage.get('config', 'warehouseEmail', 'warehouse@company.com');
962
+
963
+ // Use Controller to check if customer wants notifications
964
+ const customer = await this.system.storage.findOne('User',
965
+ this.globals.MatchExp.atom({ key: 'email', value: ['=', order.customerEmail] }),
966
+ undefined,
967
+ ['id', 'notificationPreferences']
968
+ );
969
+
970
+ const notifications = [];
971
+
972
+ // Only send customer notification if they opted in
973
+ if (customer?.notificationPreferences?.orderUpdates !== false) {
974
+ notifications.push({
975
+ type: 'order_confirmation',
976
+ recipient: order.customerEmail,
977
+ message: `Order ${order.orderNumber} confirmed`
978
+ });
979
+ }
980
+
981
+ // Always notify warehouse
982
+ notifications.push({
983
+ type: 'warehouse_notification',
984
+ recipient: warehouseEmail,
985
+ message: `New order ${order.orderNumber} to process`
986
+ });
987
+
988
+ return notifications;
989
+ }
990
+ })
991
+ })
992
+ ```
762
993
 
763
994
  **Key Points**
764
995
 
@@ -767,7 +998,23 @@ Transform.create(config: TransformConfig): TransformInstance
767
998
  - Array of objects: Creates multiple target records from one source
768
999
  - `null`/`undefined`: Creates no records (useful for filtering)
769
1000
 
770
- 2. **Automatic Updates**: When source records are updated or deleted, the transformed records are automatically updated or removed
1001
+ 2. **Automatic Updates**:
1002
+ - For entity/relation transforms: When source records are updated or deleted, the transformed records are automatically updated or removed
1003
+ - For event-driven transforms: New records are created in response to mutation events
1004
+
1005
+ 3. **Choosing Between Modes**:
1006
+ - Use `record` mode when deriving entities from other entities (e.g., creating views or projections)
1007
+ - Use `eventDeps` mode when creating entities in response to system events or interactions
1008
+
1009
+ 4. **MutationEvent Structure** (for eventDeps callbacks):
1010
+ ```typescript
1011
+ {
1012
+ type: 'create' | 'update' | 'delete',
1013
+ recordName: string,
1014
+ record?: any, // New/current record (for create/update)
1015
+ oldRecord?: any // Previous record (for update/delete)
1016
+ }
1017
+ ```
771
1018
 
772
1019
  ### StateMachine.create()
773
1020
 
@@ -796,7 +1043,7 @@ const MyStateMachine = StateMachine.create({
796
1043
  transfers: [...],
797
1044
  defaultState: StateNode.create({
798
1045
  name: 'pending',
799
- computeValue: (lastValue) => {
1046
+ computeValue: (lastValue, mutationEvent) => {
800
1047
  // Explicitly handle initial value
801
1048
  return lastValue || 'initial_state_value';
802
1049
  }
@@ -818,7 +1065,13 @@ const OrderStateMachine = StateMachine.create({
818
1065
  StateTransfer.create({
819
1066
  current: pendingState,
820
1067
  next: confirmedState,
821
- trigger: ConfirmOrderInteraction
1068
+ trigger: {
1069
+ recordName: InteractionEventEntity.name,
1070
+ record: {
1071
+ interactionName: ConfirmOrderInteraction.name
1072
+ }
1073
+ },
1074
+ computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.orderId })
822
1075
  })
823
1076
  ],
824
1077
  defaultState: pendingState
@@ -837,9 +1090,27 @@ const UserTargetRelation = Relation.create({
837
1090
  ],
838
1091
  // Use Transform to create relations
839
1092
  computation: Transform.create({
840
- record: InteractionEventEntity,
841
- callback: function(event) {
1093
+ eventDeps: {
1094
+ RelationInteraction: {
1095
+ recordName: InteractionEventEntity.name,
1096
+ type: 'create'
1097
+ }
1098
+ },
1099
+ callback: async function(this: Controller, mutationEvent) {
1100
+ const event = mutationEvent.record;
842
1101
  if (event.interactionName === 'CreateRelation') {
1102
+ // Use Controller to validate the relation
1103
+ const sourceExists = await this.system.storage.findOne('User',
1104
+ MatchExp.atom({ key: 'id', value: ['=', event.payload.sourceUser.id] }),
1105
+ undefined,
1106
+ ['id']
1107
+ );
1108
+
1109
+ if (!sourceExists) {
1110
+ console.log('Source user not found, skipping relation creation');
1111
+ return null;
1112
+ }
1113
+
843
1114
  return {
844
1115
  source: event.payload.sourceUser,
845
1116
  target: event.payload.targetEntity,
@@ -860,19 +1131,24 @@ relationDeletionProperty.computation = StateMachine.create({
860
1131
  defaultState: NON_DELETED_STATE,
861
1132
  transfers: [
862
1133
  StateTransfer.create({
863
- trigger: DeleteRelationInteraction,
1134
+ trigger: {
1135
+ recordName: InteractionEventEntity.name,
1136
+ record: {
1137
+ interactionName: DeleteRelationInteraction.name
1138
+ }
1139
+ },
864
1140
  current: NON_DELETED_STATE,
865
1141
  next: DELETED_STATE,
866
- computeTarget: async function(this: Controller, event: any) {
1142
+ computeTarget: async function(this: Controller, mutationEvent: any) {
867
1143
  const MatchExp = this.globals.MatchExp;
868
1144
  const relation = await this.system.storage.findOne(
869
1145
  UserTargetRelation.name,
870
1146
  MatchExp.atom({
871
1147
  key: 'source.id',
872
- value: ['=', event.payload.sourceId]
1148
+ value: ['=', mutationEvent.record.payload.sourceId]
873
1149
  }).and({
874
1150
  key: 'target.id',
875
- value: ['=', event.payload.targetId]
1151
+ value: ['=', mutationEvent.record.payload.targetId]
876
1152
  }),
877
1153
  undefined,
878
1154
  ['id']
@@ -1052,8 +1328,8 @@ const nextRecomputeTimeKey = controller.scheduler.getBoundStateName(
1052
1328
  );
1053
1329
 
1054
1330
  // Read state values
1055
- const lastRecomputeTime = await system.storage.get(DICTIONARY_RECORD, lastRecomputeTimeKey);
1056
- const nextRecomputeTime = await system.storage.get(DICTIONARY_RECORD, nextRecomputeTimeKey);
1331
+ const lastRecomputeTime = await system.storage.dict.get(lastRecomputeTimeKey);
1332
+ const nextRecomputeTime = await system.storage.dict.get(nextRecomputeTimeKey);
1057
1333
  ```
1058
1334
 
1059
1335
  ### Custom.create()
@@ -1516,10 +1792,13 @@ const activeUsers = Dictionary.create({
1516
1792
  computation: Transform.create({
1517
1793
  record: User,
1518
1794
  attributeQuery: ['id', 'lastLoginTime'],
1519
- callback: (users) => {
1520
- const oneHourAgo = Date.now() - 3600000;
1795
+ callback: async function(this: Controller, users) {
1796
+ // Use Controller to get activity threshold from config
1797
+ const activityThreshold = await this.system.storage.get('config', 'userActivityThreshold', 3600000); // Default 1 hour
1798
+ const cutoffTime = Date.now() - activityThreshold;
1799
+
1521
1800
  return users
1522
- .filter(u => u.lastLoginTime > oneHourAgo)
1801
+ .filter(u => u.lastLoginTime > cutoffTime)
1523
1802
  .map(u => u.id);
1524
1803
  }
1525
1804
  })
@@ -1586,13 +1865,13 @@ const pendingState = StateNode.create({ name: 'pending' });
1586
1865
  // Store timestamp when entering state
1587
1866
  const processedState = StateNode.create({
1588
1867
  name: 'processed',
1589
- computeValue: () => new Date().toISOString()
1868
+ computeValue: (lastValue, mutationEvent) => new Date().toISOString()
1590
1869
  });
1591
1870
 
1592
1871
  // Store complex object
1593
1872
  const activeState = StateNode.create({
1594
1873
  name: 'active',
1595
- computeValue: () => ({
1874
+ computeValue: (lastValue, mutationEvent) => ({
1596
1875
  activatedAt: Math.floor(Date.now()/1000), // In seconds
1597
1876
  status: 'active',
1598
1877
  metadata: { source: 'manual' }
@@ -1602,7 +1881,7 @@ const activeState = StateNode.create({
1602
1881
  // Increment value based on previous state
1603
1882
  const incrementingState = StateNode.create({
1604
1883
  name: 'incrementing',
1605
- computeValue: (lastValue) => {
1884
+ computeValue: (lastValue, mutationEvent) => {
1606
1885
  const baseValue = typeof lastValue === 'number' ? lastValue : 0;
1607
1886
  return baseValue + 1;
1608
1887
  }
@@ -1611,7 +1890,7 @@ const incrementingState = StateNode.create({
1611
1890
  // Preserve previous value
1612
1891
  const idleState = StateNode.create({
1613
1892
  name: 'idle',
1614
- computeValue: (lastValue) => {
1893
+ computeValue: (lastValue, mutationEvent) => {
1615
1894
  return typeof lastValue === 'number' ? lastValue : 0;
1616
1895
  }
1617
1896
  });
@@ -1619,7 +1898,7 @@ const idleState = StateNode.create({
1619
1898
  // Return null to delete (for entity/relation state machines)
1620
1899
  const deletedState = StateNode.create({
1621
1900
  name: 'deleted',
1622
- computeValue: () => null
1901
+ computeValue: (lastValue, mutationEvent) => null
1623
1902
  });
1624
1903
 
1625
1904
  // Async computeValue - fetch data from database
@@ -1644,7 +1923,7 @@ const enrichedState = StateNode.create({
1644
1923
  // Async computeValue - external API call
1645
1924
  const verifiedState = StateNode.create({
1646
1925
  name: 'verified',
1647
- computeValue: async (lastValue) => {
1926
+ computeValue: async (lastValue, mutationEvent) => {
1648
1927
  // Simulate external verification
1649
1928
  const verificationResult = await someExternalAPI.verify(lastValue.data);
1650
1929
 
@@ -1660,9 +1939,9 @@ const verifiedState = StateNode.create({
1660
1939
  // Using event parameter - access user information
1661
1940
  const approvedState = StateNode.create({
1662
1941
  name: 'approved',
1663
- computeValue: (lastValue, event) => {
1942
+ computeValue: (lastValue, mutationEvent) => {
1664
1943
  // Access user who triggered the approval
1665
- const approver = event?.user?.name || 'system';
1944
+ const approver = mutationEvent?.record?.user?.name || 'system';
1666
1945
  return {
1667
1946
  status: 'approved',
1668
1947
  approvedAt: Math.floor(Date.now()/1000), // In seconds
@@ -1674,14 +1953,14 @@ const approvedState = StateNode.create({
1674
1953
  // Using event parameter - access payload data
1675
1954
  const updatedState = StateNode.create({
1676
1955
  name: 'updated',
1677
- computeValue: (lastValue, event) => {
1956
+ computeValue: (lastValue, mutationEvent) => {
1678
1957
  // Merge payload data with existing value
1679
- if (event?.payload) {
1958
+ if (mutationEvent?.record?.payload) {
1680
1959
  return {
1681
1960
  ...lastValue,
1682
- ...event.payload.updates,
1961
+ ...mutationEvent.record.payload.updates,
1683
1962
  lastModifiedAt: Math.floor(Date.now()/1000), // In seconds
1684
- lastModifiedBy: event.user?.id
1963
+ lastModifiedBy: mutationEvent.record.user?.id
1685
1964
  };
1686
1965
  }
1687
1966
  return lastValue;
@@ -1691,15 +1970,15 @@ const updatedState = StateNode.create({
1691
1970
  // Using event parameter - conditional logic based on interaction
1692
1971
  const reviewedState = StateNode.create({
1693
1972
  name: 'reviewed',
1694
- computeValue: (lastValue, event) => {
1973
+ computeValue: (lastValue, mutationEvent) => {
1695
1974
  // Different behavior based on which interaction triggered the transition
1696
- if (event?.interactionName === 'approve') {
1975
+ if (mutationEvent?.record?.interactionName === 'approve') {
1697
1976
  return { status: 'approved', reviewedAt: Math.floor(Date.now()/1000) }; // In seconds
1698
- } else if (event?.interactionName === 'reject') {
1977
+ } else if (mutationEvent?.record?.interactionName === 'reject') {
1699
1978
  return {
1700
1979
  status: 'rejected',
1701
1980
  reviewedAt: Math.floor(Date.now()/1000), // In seconds
1702
- reason: event.payload?.reason || 'No reason provided'
1981
+ reason: mutationEvent.record.payload?.reason || 'No reason provided'
1703
1982
  };
1704
1983
  }
1705
1984
  return { status: 'pending_review' };
@@ -1713,31 +1992,55 @@ StateNodes with `computeValue` are used in StateMachine to compute property valu
1713
1992
 
1714
1993
  ```typescript
1715
1994
  // Counter that increments on interaction
1995
+ const idleState = StateNode.create({
1996
+ name: 'idle',
1997
+ computeValue: (lastValue, mutationEvent) => lastValue || 0
1998
+ });
1999
+ const incrementingState = StateNode.create({
2000
+ name: 'incrementing',
2001
+ computeValue: (lastValue, mutationEvent) => (lastValue || 0) + 1
2002
+ });
2003
+
1716
2004
  const CounterStateMachine = StateMachine.create({
1717
- states: [
1718
- StateNode.create({
1719
- name: 'idle',
1720
- computeValue: (lastValue) => lastValue || 0
1721
- }),
1722
- StateNode.create({
1723
- name: 'incrementing',
1724
- computeValue: (lastValue) => (lastValue || 0) + 1
2005
+ states: [idleState, incrementingState],
2006
+ transfers: [
2007
+ StateTransfer.create({
2008
+ trigger: {
2009
+ recordName: InteractionEventEntity.name,
2010
+ record: {
2011
+ interactionName: IncrementInteraction.name
2012
+ }
2013
+ },
2014
+ current: idleState,
2015
+ next: incrementingState,
2016
+ computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.counterId })
1725
2017
  })
1726
2018
  ],
1727
- transfers: [/* ... */],
1728
2019
  defaultState: idleState
1729
2020
  });
1730
2021
 
1731
2022
  // Timestamp tracking
2023
+ const pendingState = StateNode.create({ name: 'pending' });
2024
+ const processedState = StateNode.create({
2025
+ name: 'processed',
2026
+ computeValue: (lastValue, mutationEvent) => new Date().toISOString()
2027
+ });
2028
+
1732
2029
  const ProcessingStateMachine = StateMachine.create({
1733
- states: [
1734
- StateNode.create({ name: 'pending' }),
1735
- StateNode.create({
1736
- name: 'processed',
1737
- computeValue: () => new Date().toISOString()
2030
+ states: [pendingState, processedState],
2031
+ transfers: [
2032
+ StateTransfer.create({
2033
+ trigger: {
2034
+ recordName: InteractionEventEntity.name,
2035
+ record: {
2036
+ interactionName: ProcessInteraction.name
2037
+ }
2038
+ },
2039
+ current: pendingState,
2040
+ next: processedState,
2041
+ computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.itemId })
1738
2042
  })
1739
2043
  ],
1740
- transfers: [/* ... */],
1741
2044
  defaultState: pendingState
1742
2045
  });
1743
2046
 
@@ -1764,7 +2067,7 @@ StateTransfer.create(config: StateTransferConfig): StateTransferInstance
1764
2067
  ```
1765
2068
 
1766
2069
  **Parameters**
1767
- - `config.trigger` (Interaction, required): The Interaction instance that triggers this state transfer. Must be a reference to an Interaction created with `Interaction.create()`
2070
+ - `config.trigger` (RecordMutationEventPattern, required): A partial pattern to match against RecordMutationEvent. Supports deep partial matching of event properties.
1768
2071
  - `config.current` (StateNode, required): Current state node
1769
2072
  - `config.next` (StateNode, required): Next state node
1770
2073
  - `config.computeTarget` (function, optional): Function to compute which records should undergo this state transition. Returns the target record(s) that should be affected by this state change.
@@ -1785,37 +2088,83 @@ The `computeTarget` function determines which specific records should transition
1785
2088
  - Async: `async function(this: Controller, event: InteractionEvent) => TargetRecord`
1786
2089
  - **Event Parameter**: Contains interaction details including `event.payload` with the interaction's payload data
1787
2090
 
2091
+ **Trigger Pattern**
2092
+
2093
+ The `trigger` parameter now accepts a RecordMutationEventPattern that allows deep partial matching:
2094
+
2095
+ ```typescript
2096
+ // Match interaction events by name
2097
+ trigger: {
2098
+ recordName: InteractionEventEntity.name, // Use the constant, not hardcoded string
2099
+ record: {
2100
+ interactionName: 'approve' // Or better: ApproveInteraction.name
2101
+ }
2102
+ }
2103
+
2104
+ // Match data mutation events
2105
+ trigger: {
2106
+ recordName: 'User',
2107
+ type: 'update' // Match only update events
2108
+ }
2109
+
2110
+ // Match with complex patterns
2111
+ trigger: {
2112
+ recordName: InteractionEventEntity.name,
2113
+ record: {
2114
+ interactionName: 'updateStatus',
2115
+ payload: {
2116
+ status: 'active' // Only trigger when status is set to 'active'
2117
+ }
2118
+ }
2119
+ }
2120
+ ```
2121
+
1788
2122
  **Examples**
1789
2123
  ```typescript
1790
2124
  // Simple state transfer (no computeTarget needed for global state)
1791
2125
  const approveTransfer = StateTransfer.create({
1792
- trigger: ApproveInteraction,
2126
+ trigger: {
2127
+ recordName: InteractionEventEntity.name,
2128
+ record: {
2129
+ interactionName: ApproveInteraction.name
2130
+ }
2131
+ },
1793
2132
  current: pendingState,
1794
2133
  next: approvedState
1795
2134
  });
1796
2135
 
1797
2136
  // Entity state transfer - specify which entity to update
1798
2137
  const incrementTransfer = StateTransfer.create({
1799
- trigger: IncrementInteraction,
2138
+ trigger: {
2139
+ recordName: InteractionEventEntity.name,
2140
+ record: {
2141
+ interactionName: IncrementInteraction.name
2142
+ }
2143
+ },
1800
2144
  current: idleState,
1801
2145
  next: incrementingState,
1802
- computeTarget: (event) => {
2146
+ computeTarget: (mutationEvent) => {
1803
2147
  // Return the counter entity that should transition states
1804
- return { id: event.payload.counter.id };
2148
+ return { id: mutationEvent.record.payload.counter.id };
1805
2149
  }
1806
2150
  });
1807
2151
 
1808
2152
  // Relation state transfer - create new relation
1809
2153
  const assignReviewerTransfer = StateTransfer.create({
1810
- trigger: AssignReviewerInteraction,
2154
+ trigger: {
2155
+ recordName: InteractionEventEntity.name,
2156
+ record: {
2157
+ interactionName: AssignReviewerInteraction.name
2158
+ }
2159
+ },
1811
2160
  current: unassignedState,
1812
2161
  next: assignedState,
1813
- computeTarget: async function(this: Controller, event) {
2162
+ computeTarget: async function(this: Controller, mutationEvent) {
1814
2163
  // Find the request entity
1815
2164
  const request = await this.system.storage.findOne('Request',
1816
2165
  this.globals.MatchExp.atom({
1817
2166
  key: 'id',
1818
- value: ['=', event.payload.requestId]
2167
+ value: ['=', mutationEvent.record.payload.requestId]
1819
2168
  }),
1820
2169
  undefined,
1821
2170
  ['id']
@@ -1824,53 +2173,51 @@ const assignReviewerTransfer = StateTransfer.create({
1824
2173
  // Return source and target to create/update relation
1825
2174
  return {
1826
2175
  source: request, // Can be {id: '...'} or full entity
1827
- target: event.payload.reviewer // From payload
2176
+ target: mutationEvent.record.payload.reviewer // From payload
1828
2177
  };
1829
2178
  }
1830
2179
  });
1831
2180
 
1832
- // Relation state transfer - simple source/target
1833
- const createFriendshipTransfer = StateTransfer.create({
1834
- trigger: AddFriendInteraction,
1835
- current: notFriendsState,
1836
- next: friendsState,
1837
- computeTarget: (event) => ({
1838
- source: event.user, // The user initiating the friendship
1839
- target: { id: event.payload.friendId } // The friend being added
1840
- })
2181
+ // Trigger on specific payload values
2182
+ const publishTransfer = StateTransfer.create({
2183
+ trigger: {
2184
+ recordName: InteractionEventEntity.name,
2185
+ record: {
2186
+ interactionName: UpdateStatusInteraction.name,
2187
+ payload: {
2188
+ newStatus: 'published' // Only trigger when status changes to 'published'
2189
+ }
2190
+ }
2191
+ },
2192
+ current: draftState,
2193
+ next: publishedState,
2194
+ computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.documentId })
1841
2195
  });
1842
2196
 
1843
- // Relation state transfer - existing relation
1844
- const updateRelationTransfer = StateTransfer.create({
1845
- trigger: UpdateStatusInteraction,
1846
- current: activeState,
2197
+ // Trigger on data mutations (non-interaction events)
2198
+ const dataUpdateTransfer = StateTransfer.create({
2199
+ trigger: {
2200
+ recordName: 'Order',
2201
+ type: 'update' // Only trigger on Order updates
2202
+ },
2203
+ current: processingState,
1847
2204
  next: updatedState,
1848
- computeTarget: async function(this: Controller, event) {
1849
- // Find existing relation
1850
- const relation = await this.system.storage.findOneRelationByName(UserProjectRelation.name,
1851
- this.globals.MatchExp.atom({
1852
- key: 'source.id',
1853
- value: ['=', event.user.id]
1854
- }).and({
1855
- key: 'target.id',
1856
- value: ['=', event.payload.projectId]
1857
- }),
1858
- undefined,
1859
- ['id']
1860
- );
1861
-
1862
- return relation; // Return existing relation by id
1863
- }
2205
+ computeTarget: (mutationEvent) => ({ id: mutationEvent.record.id })
1864
2206
  });
1865
2207
 
1866
2208
  // Multiple targets - return array
1867
2209
  const bulkApproveTransfer = StateTransfer.create({
1868
- trigger: BulkApproveInteraction,
2210
+ trigger: {
2211
+ recordName: InteractionEventEntity.name,
2212
+ record: {
2213
+ interactionName: BulkApproveInteraction.name
2214
+ }
2215
+ },
1869
2216
  current: pendingState,
1870
2217
  next: approvedState,
1871
- computeTarget: (event) => {
2218
+ computeTarget: (mutationEvent) => {
1872
2219
  // Return multiple entities to transition
1873
- return event.payload.items.map(item => ({ id: item.id }));
2220
+ return mutationEvent.record.payload.items.map(item => ({ id: item.id }));
1874
2221
  }
1875
2222
  });
1876
2223
  ```
@@ -2090,15 +2437,26 @@ const UserPostRelation = Relation.create({
2090
2437
  source: User,
2091
2438
  target: Post,
2092
2439
  computation: Transform.create({
2093
- record: InteractionEventEntity,
2094
- callback: (event) => {
2440
+ eventDeps: {
2441
+ PostInteraction: {
2442
+ recordName: InteractionEventEntity.name,
2443
+ type: 'create'
2444
+ }
2445
+ },
2446
+ callback: async function(this: Controller, mutationEvent) {
2447
+ const event = mutationEvent.record;
2095
2448
  if (event.interactionName === 'CreatePost') {
2449
+ // Use Controller to validate and enhance data
2450
+ const now = Math.floor(Date.now() / 1000);
2451
+
2096
2452
  // This is where the actual creation logic is
2097
2453
  return {
2098
2454
  source: event.user.id,
2099
2455
  target: {
2100
2456
  title: event.payload.title,
2101
- content: event.payload.content
2457
+ content: event.payload.content,
2458
+ createdAt: now,
2459
+ updatedAt: now
2102
2460
  }
2103
2461
  };
2104
2462
  }
@@ -2535,6 +2893,50 @@ interface System {
2535
2893
 
2536
2894
  Storage layer interface providing data persistence functionality.
2537
2895
 
2896
+ **Interface Definition**
2897
+ ```typescript
2898
+ interface Storage {
2899
+ // Entity mapping
2900
+ map: any
2901
+
2902
+ // Transaction operations
2903
+ beginTransaction: (transactionName?: string) => Promise<any>
2904
+ commitTransaction: (transactionName?: string) => Promise<any>
2905
+ rollbackTransaction: (transactionName?: string) => Promise<any>
2906
+
2907
+ // Dictionary-specific API
2908
+ dict: {
2909
+ get: (key: string) => Promise<any>
2910
+ set: (key: string, value: any) => Promise<void>
2911
+ }
2912
+
2913
+ // General KV storage
2914
+ get: (itemName: string, id: string, initialValue?: any) => Promise<any>
2915
+ set: (itemName: string, id: string, value: any, events?: RecordMutationEvent[]) => Promise<any>
2916
+
2917
+ // Entity/Relation operations
2918
+ setup: (entities: EntityInstance[], relations: RelationInstance[], createTables?: boolean) => any
2919
+ findOne: (entityName: string, ...args: any[]) => Promise<any>
2920
+ find: (entityName: string, ...args: any[]) => Promise<any[]>
2921
+ create: (entityName: string, data: any, events?: RecordMutationEvent[]) => Promise<any>
2922
+ update: (entityName: string, ...args: any[]) => Promise<any>
2923
+ delete: (entityName: string, data: any, events?: RecordMutationEvent[]) => Promise<any>
2924
+
2925
+ // Relation-specific operations
2926
+ findOneRelationByName: (relationName: string, ...args: any[]) => Promise<any>
2927
+ findRelationByName: (relationName: string, ...args: any[]) => Promise<any>
2928
+ updateRelationByName: (relationName: string, matchExpressionData: any, rawData: any, events?: RecordMutationEvent[]) => Promise<any>
2929
+ removeRelationByName: (relationName: string, matchExpressionData: any, events?: RecordMutationEvent[]) => Promise<any>
2930
+ addRelationByNameById: (relationName: string, sourceEntityId: string, targetEntityId: string, rawData?: any, events?: RecordMutationEvent[]) => Promise<any>
2931
+
2932
+ // Utility methods
2933
+ getRelationName: (entityName: string, attributeName: string) => string
2934
+ getEntityName: (entityName: string, attributeName: string) => string
2935
+ listen: (callback: RecordMutationCallback) => void
2936
+ destroy: () => void
2937
+ }
2938
+ ```
2939
+
2538
2940
  **Main Methods**
2539
2941
 
2540
2942
  #### Transaction Operations
@@ -2938,10 +3340,43 @@ await storage.removeRelationByName('UserPostRelation', // Don't hardcode!
2938
3340
  )
2939
3341
  ```
2940
3342
 
3343
+ #### Dictionary Operations
3344
+
3345
+ **dict.get(key: string)**
3346
+ Get value from Dictionary storage.
3347
+
3348
+ ```typescript
3349
+ // Get dictionary value
3350
+ const userCount = await storage.dict.get('userCount')
3351
+
3352
+ // Returns undefined if key doesn't exist
3353
+ const config = await storage.dict.get('systemConfig')
3354
+ if (config === undefined) {
3355
+ // Handle missing value
3356
+ }
3357
+ ```
3358
+
3359
+ **dict.set(key: string, value: any)**
3360
+ Set value in Dictionary storage.
3361
+
3362
+ ```typescript
3363
+ // Set dictionary value
3364
+ await storage.dict.set('userCount', 100)
3365
+
3366
+ // Store complex objects
3367
+ await storage.dict.set('systemConfig', {
3368
+ maxUsers: 1000,
3369
+ maintenanceMode: false,
3370
+ features: ['chat', 'notifications']
3371
+ })
3372
+ ```
3373
+
3374
+ **Note**: The Dictionary API is specifically designed for storing global state values that are defined as Dictionary instances in the system. It provides a cleaner, more focused API compared to the general KV storage operations.
3375
+
2941
3376
  #### KV Storage Operations
2942
3377
 
2943
3378
  **get(itemName: string, id: string, initialValue?: any)**
2944
- Get value from key-value storage.
3379
+ Get value from general key-value storage.
2945
3380
 
2946
3381
  ```typescript
2947
3382
  // Get value with default
@@ -2949,7 +3384,7 @@ const maxUsers = await storage.get('config', 'maxUsers', 100)
2949
3384
  ```
2950
3385
 
2951
3386
  **set(itemName: string, id: string, value: any, events?: RecordMutationEvent[])**
2952
- Set value in key-value storage.
3387
+ Set value in general key-value storage.
2953
3388
 
2954
3389
  ```typescript
2955
3390
  // Set configuration value
@@ -2963,6 +3398,8 @@ await storage.set('cache', 'userPreferences', {
2963
3398
  })
2964
3399
  ```
2965
3400
 
3401
+ **Note**: The general KV storage operations (`get`/`set`) are for arbitrary key-value pairs. For Dictionary entities specifically, prefer using the `dict` API.
3402
+
2966
3403
  #### Utility Methods
2967
3404
 
2968
3405
  **getRelationName(entityName: string, attributeName: string)**