interaqt 0.3.0 → 0.3.1

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 (35) hide show
  1. package/agent/.claude/agents/code-generation-handler.md +2 -0
  2. package/agent/.claude/agents/computation-generation-handler.md +1 -0
  3. package/agent/.claude/agents/implement-design-handler.md +4 -13
  4. package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
  5. package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
  6. package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
  7. package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
  8. package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
  9. package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
  10. package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
  11. package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
  12. package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
  13. package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
  14. package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
  15. package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
  16. package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
  17. package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
  18. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
  19. package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
  20. package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
  21. package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
  22. package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
  23. package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
  24. package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
  25. package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
  26. package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
  27. package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
  28. package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
  29. package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
  30. package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
  31. package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
  32. package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
  33. package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
  34. package/agent/agentspace/knowledge/usage/README.md +148 -0
  35. package/package.json +1 -1
@@ -0,0 +1,1122 @@
1
+ # Reactive Patterns for Entity CRUD Operations
2
+
3
+ In interaqt, all data operations follow reactive design principles. This chapter will detail how to correctly handle entity creation, update, and deletion operations.
4
+
5
+ ## Core Principles
6
+
7
+ 1. **Creation**: Use Transform to listen to Interaction events to create entities
8
+ 2. **Deletion**:
9
+ - **Soft Delete (Recommended)**: Manage deletion state through StateMachine on status property
10
+ - **Hard Delete**: Use HardDeletionProperty + StateMachine for physical deletion
11
+ 3. **Update**: Reactively update entity state through StateMachine or Transform
12
+ 4. **Reference**: For entities that support deletion, use Filtered Entity to create "non-deleted" views
13
+ 5. **Transform Restriction**: Transform can ONLY be used in Entity or Relation computation, NEVER in Property computation
14
+
15
+ ## Transform Usage Guidelines
16
+
17
+ Understanding where to place Transform is crucial:
18
+
19
+ 1. **Entity's computation + Transform**: Use when you need to create new entities from interaction events
20
+ - The entity listens to InteractionEventEntity
21
+ - Returns entity data to be created
22
+ - Related entities can be referenced directly in the returned data
23
+
24
+ 2. **Relation's computation + Transform**: Use when you need to create relations between existing entities
25
+ - The relation listens to InteractionEventEntity
26
+ - Returns relation data (source, target, and any relation properties)
27
+ - Both source and target entities must already exist
28
+
29
+ ### ⚠️ CRITICAL: Transform CANNOT be Used in Property Computation
30
+
31
+ **Transform is ONLY for Entity or Relation computation, NEVER for Property computation!**
32
+
33
+ ```javascript
34
+ // ❌ WRONG: Never use Transform in Property computation
35
+ Property.create({
36
+ name: 'status',
37
+ type: 'string',
38
+ computation: Transform.create({ // ❌ ERROR!
39
+ record: InteractionEventEntity,
40
+ callback: function(event) {
41
+ // This is WRONG! Transform cannot be used at Property level
42
+ }
43
+ })
44
+ })
45
+
46
+ // ✅ CORRECT: Use appropriate computation for Properties
47
+ Property.create({
48
+ name: 'status',
49
+ type: 'string',
50
+ computation: StateMachine.create({ // ✅ Use StateMachine for state management
51
+ states: [activeState, inactiveState],
52
+ // ...
53
+ })
54
+ })
55
+
56
+ // ✅ CORRECT: Use computed for simple calculations
57
+ Property.create({
58
+ name: 'fullName',
59
+ type: 'string',
60
+ computed: function(user) { // ✅ Use computed for derived values
61
+ return `${user.firstName} ${user.lastName}`;
62
+ }
63
+ })
64
+
65
+ // ✅ CORRECT: Use getValue as an alternative
66
+ Property.create({
67
+ name: 'displayName',
68
+ type: 'string',
69
+ getValue: (record) => { // ✅ Use getValue for simple transformations
70
+ return record.name.toUpperCase();
71
+ }
72
+ })
73
+ ```
74
+
75
+ ### Why Transform Cannot Be Used in Properties
76
+
77
+ 1. **Transform is for collection-to-collection transformation**: It transforms sets of data (e.g., InteractionEventEntity → Entity/Relation)
78
+ 2. **Properties are record-level**: They belong to a single entity instance, not a collection
79
+ 3. **No `this` context in Transform**: Transform callbacks don't have access to the current entity instance
80
+ 4. **Circular reference issues**: Using Transform with the entity being defined creates circular dependencies
81
+
82
+ ### What to Use Instead for Property Computation
83
+
84
+ | Use Case | Correct Approach | Example |
85
+ |----------|-----------------|---------|
86
+ | State management | StateMachine | Status tracking, workflow states |
87
+ | Simple calculations | computed/getValue | Derived values, formatting |
88
+ | Timestamp recording | Single-node StateMachine with computeValue | lastActivityAt, updatedAt |
89
+ | Aggregations | Count, Summation, Every, Any | Counting relations, summing values |
90
+ | Time-based | RealTime | Time-sensitive calculations |
91
+
92
+ ## Creating Entities - Using Transform
93
+
94
+ ### Basic Pattern
95
+
96
+ By using Transform in an Entity's `computation`, you can listen to interaction events and create entities. When creating an entity that needs relations, include the related entity reference directly in the creation data:
97
+
98
+ ```javascript
99
+ import { Entity, Property, Relation, Transform, InteractionEventEntity, Interaction, Action, Payload, PayloadItem } from 'interaqt';
100
+
101
+ // 1. Define entities
102
+ const User = Entity.create({
103
+ name: 'User',
104
+ properties: [
105
+ Property.create({ name: 'username', type: 'string' }),
106
+ Property.create({ name: 'email', type: 'string' })
107
+ ]
108
+ });
109
+
110
+ const Article = Entity.create({
111
+ name: 'Article',
112
+ properties: [
113
+ Property.create({ name: 'title', type: 'string' }),
114
+ Property.create({ name: 'content', type: 'string' }),
115
+ Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
116
+ Property.create({ name: 'createdAt', type: 'string' }),
117
+ Property.create({ name: 'updatedAt', type: 'string' })
118
+ ],
119
+ // Transform in Entity's computation listens to interactions to create entities
120
+ computation: Transform.create({
121
+ record: InteractionEventEntity,
122
+ callback: function(event) {
123
+ if (event.interactionName === 'CreateArticle') {
124
+ // Return entity data with relation reference
125
+ // The relation will be created automatically
126
+ return {
127
+ title: event.payload.title,
128
+ content: event.payload.content,
129
+ status: 'draft',
130
+ createdAt: new Date().toISOString(),
131
+ updatedAt: new Date().toISOString(),
132
+ author: {id: event.payload.authorId} // Direct reference to User entity
133
+ };
134
+ }
135
+ return null;
136
+ }
137
+ })
138
+ });
139
+
140
+ // 2. Define creation interaction
141
+ const CreateArticle = Interaction.create({
142
+ name: 'CreateArticle',
143
+ action: Action.create({ name: 'createArticle' }),
144
+ payload: Payload.create({
145
+ items: [
146
+ PayloadItem.create({ name: 'title', required: true }),
147
+ PayloadItem.create({ name: 'content', required: true }),
148
+ PayloadItem.create({ name: 'authorId', base: User, isRef: true, required: true })
149
+ ]
150
+ })
151
+ });
152
+
153
+ // 3. Define relation - no computation needed for creation
154
+ const UserArticleRelation = Relation.create({
155
+ source: Article,
156
+ sourceProperty: 'author',
157
+ target: User,
158
+ targetProperty: 'articles',
159
+ type: 'n:1'
160
+ });
161
+ ```
162
+
163
+ ### Creating Relations Between Existing Entities
164
+
165
+ When you need to create relations between already existing entities, use Transform in Relation's `computation`:
166
+
167
+ ```javascript
168
+ // Define interaction to add article to favorites
169
+ const AddToFavorites = Interaction.create({
170
+ name: 'AddToFavorites',
171
+ action: Action.create({ name: 'addToFavorites' }),
172
+ payload: Payload.create({
173
+ items: [
174
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true, required: true })
175
+ ]
176
+ })
177
+ });
178
+
179
+ // Favorite relation with Transform in computation
180
+ const UserFavoriteRelation = Relation.create({
181
+ source: User,
182
+ sourceProperty: 'favorites',
183
+ target: Article,
184
+ targetProperty: 'favoritedBy',
185
+ type: 'n:n',
186
+ properties: [
187
+ Property.create({ name: 'addedAt', type: 'string' })
188
+ ],
189
+ // Transform creates relation between existing entities
190
+ computation: Transform.create({
191
+ record: InteractionEventEntity,
192
+ callback: function(event) {
193
+ if (event.interactionName === 'AddToFavorites') {
194
+ return {
195
+ source: event.user, // Current user
196
+ target: {id:event.payload.articleId}, // Article to favorite
197
+ addedAt: new Date().toISOString()
198
+ };
199
+ }
200
+ return null;
201
+ }
202
+ })
203
+ });
204
+ ```
205
+
206
+ ### Creating Entities with Complex Relations
207
+
208
+ For entities that need to create multiple relations simultaneously, you can reference multiple entities in the creation data:
209
+
210
+ ```javascript
211
+ // Define Tag entity
212
+ const Tag = Entity.create({
213
+ name: 'Tag',
214
+ properties: [
215
+ Property.create({ name: 'name', type: 'string' })
216
+ ]
217
+ });
218
+
219
+ // Update Article entity to handle creation with tags
220
+ const Article = Entity.create({
221
+ name: 'Article',
222
+ properties: [
223
+ Property.create({ name: 'title', type: 'string' }),
224
+ Property.create({ name: 'content', type: 'string' }),
225
+ Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
226
+ Property.create({ name: 'createdAt', type: 'string' }),
227
+ Property.create({ name: 'updatedAt', type: 'string' })
228
+ ],
229
+ // Transform creates article and its relations
230
+ computation: Transform.create({
231
+ record: InteractionEventEntity,
232
+ callback: function(event) {
233
+ if (event.interactionName === 'CreateArticle') {
234
+ return {
235
+ title: event.payload.title,
236
+ content: event.payload.content,
237
+ status: 'draft',
238
+ createdAt: new Date().toISOString(),
239
+ updatedAt: new Date().toISOString(),
240
+ author: {id:event.payload.authorId}
241
+ };
242
+ }
243
+ if (event.interactionName === 'CreateArticleWithTags') {
244
+ return {
245
+ title: event.payload.title,
246
+ content: event.payload.content,
247
+ status: 'draft',
248
+ createdAt: new Date().toISOString(),
249
+ updatedAt: new Date().toISOString(),
250
+ author: {id:event.payload.authorId},
251
+ tags: event.payload.tagIds.map(id=> {id}) // Multiple relations
252
+ };
253
+ }
254
+ return null;
255
+ }
256
+ })
257
+ });
258
+
259
+ // Create article with tags interaction
260
+ const CreateArticleWithTags = Interaction.create({
261
+ name: 'CreateArticleWithTags',
262
+ action: Action.create({ name: 'createArticleWithTags' }),
263
+ payload: Payload.create({
264
+ items: [
265
+ PayloadItem.create({ name: 'title', required: true }),
266
+ PayloadItem.create({ name: 'content', required: true }),
267
+ PayloadItem.create({ name: 'authorId', base: User, isRef: true }),
268
+ PayloadItem.create({ name: 'tagIds', base: Tag, isCollection: true, isRef: true })
269
+ ]
270
+ })
271
+ });
272
+
273
+ // Article-Tag relation - no computation needed for initial creation
274
+ const ArticleTagRelation = Relation.create({
275
+ source: Article,
276
+ sourceProperty: 'tags',
277
+ target: Tag,
278
+ targetProperty: 'articles',
279
+ type: 'n:n',
280
+ properties: [
281
+ Property.create({ name: 'addedAt', type: 'string', defaultValue: () => new Date().toISOString() })
282
+ ]
283
+ });
284
+
285
+ // If you need to add tags to existing articles later, use Transform in relation
286
+ const AddTagsToArticle = Interaction.create({
287
+ name: 'AddTagsToArticle',
288
+ action: Action.create({ name: 'addTagsToArticle' }),
289
+ payload: Payload.create({
290
+ items: [
291
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true, required: true }),
292
+ PayloadItem.create({ name: 'tagIds', base: Tag, isCollection: true, isRef: true })
293
+ ]
294
+ })
295
+ });
296
+
297
+ // Add Transform to handle adding tags to existing articles
298
+ ArticleTagRelation.computation = Transform.create({
299
+ record: InteractionEventEntity,
300
+ callback: function(event) {
301
+ if (event.interactionName === 'AddTagsToArticle') {
302
+ return event.payload.tagIds.map(tagId => ({
303
+ source: {id:event.payload.articleId},
304
+ target: {id:tagId},
305
+ addedAt: new Date().toISOString()
306
+ }));
307
+ }
308
+ return null;
309
+ }
310
+ });
311
+ ```
312
+
313
+ ## Deleting Entities - Soft Delete Pattern
314
+
315
+ ### Using StateMachine to Manage Deletion State
316
+
317
+ In reactive systems, soft delete is recommended over physical deletion:
318
+
319
+ ```javascript
320
+ import { StateMachine, StateNode, StateTransfer } from 'interaqt';
321
+
322
+ // 1. Define deletion-related interactions
323
+ const DeleteArticle = Interaction.create({
324
+ name: 'DeleteArticle',
325
+ action: Action.create({ name: 'deleteArticle' }),
326
+ payload: Payload.create({
327
+ items: [
328
+ PayloadItem.create({
329
+ name: 'articleId',
330
+ base: Article,
331
+ isRef: true,
332
+ required: true
333
+ })
334
+ ]
335
+ })
336
+ });
337
+
338
+ const RestoreArticle = Interaction.create({
339
+ name: 'RestoreArticle',
340
+ action: Action.create({ name: 'restoreArticle' }),
341
+ payload: Payload.create({
342
+ items: [
343
+ PayloadItem.create({
344
+ name: 'articleId',
345
+ base: Article,
346
+ isRef: true,
347
+ required: true
348
+ })
349
+ ]
350
+ })
351
+ });
352
+
353
+ // 2. Define state nodes
354
+ const ActiveState = StateNode.create({ name: 'active' });
355
+ const DeletedState = StateNode.create({ name: 'deleted' });
356
+
357
+ // 3. Create state machine
358
+ const ArticleStatusStateMachine = StateMachine.create({
359
+ name: 'ArticleStatus',
360
+ states: [ActiveState, DeletedState],
361
+ defaultState: ActiveState,
362
+ transfers: [
363
+ StateTransfer.create({
364
+ current: ActiveState,
365
+ next: DeletedState,
366
+ trigger: DeleteArticle,
367
+ computeTarget: (event) => ({ id: event.payload.articleId })
368
+ }),
369
+ StateTransfer.create({
370
+ current: DeletedState,
371
+ next: ActiveState,
372
+ trigger: RestoreArticle,
373
+ computeTarget: (event) => ({ id: event.payload.articleId })
374
+ })
375
+ ]
376
+ });
377
+
378
+ // 4. Apply state machine to entity
379
+ const Article = Entity.create({
380
+ name: 'Article',
381
+ properties: [
382
+ Property.create({ name: 'title', type: 'string' }),
383
+ Property.create({ name: 'content', type: 'string' }),
384
+ Property.create({
385
+ name: 'isDeleted',
386
+ type: 'boolean',
387
+ // Calculate deletion status based on state
388
+ computed: function(article) {
389
+ return article.status === 'deleted';
390
+ }
391
+ }),
392
+ Property.create({
393
+ name: 'status',
394
+ type: 'string',
395
+ computation: ArticleStatusStateMachine,
396
+ defaultValue: () => ArticleStatusStateMachine.defaultState.name
397
+ }),
398
+ Property.create({
399
+ name: 'deletedAt',
400
+ type: 'string',
401
+ defaultValue: () => null,
402
+ computation: (() => {
403
+ // First declare state nodes
404
+ const activeState = StateNode.create({
405
+ name: 'active',
406
+ computeValue: () => null // Active articles have no deletion time
407
+ });
408
+ const deletedState = StateNode.create({
409
+ name: 'deleted',
410
+ computeValue: () => new Date().toISOString() // Record deletion time
411
+ });
412
+
413
+ // Then create state machine with references
414
+ return StateMachine.create({
415
+ name: 'DeletionTimeTracker',
416
+ states: [activeState, deletedState],
417
+ transfers: [
418
+ StateTransfer.create({
419
+ current: activeState,
420
+ next: deletedState,
421
+ trigger: DeleteArticle,
422
+ computeTarget: (event) => ({ id: event.payload.articleId })
423
+ }),
424
+ StateTransfer.create({
425
+ current: deletedState,
426
+ next: activeState,
427
+ trigger: RestoreArticle,
428
+ computeTarget: (event) => ({ id: event.payload.articleId })
429
+ })
430
+ ],
431
+ defaultState: activeState
432
+ });
433
+ })()
434
+ })
435
+ ]
436
+ });
437
+ ```
438
+
439
+ ## Hard Delete Pattern with HardDeletionProperty
440
+
441
+ While soft delete is recommended for most cases, sometimes physical deletion is required (e.g., compliance requirements, storage optimization). Use HardDeletionProperty for this:
442
+
443
+ ```javascript
444
+ import { HardDeletionProperty, DELETED_STATE, NON_DELETED_STATE } from 'interaqt';
445
+
446
+ // Entity with HardDeletionProperty
447
+ const Article = Entity.create({
448
+ name: 'Article',
449
+ properties: [
450
+ Property.create({ name: 'title', type: 'string' }),
451
+ Property.create({ name: 'content', type: 'string' }),
452
+ Property.create({ name: 'createdAt', type: 'string' }),
453
+ // Add HardDeletionProperty for physical deletion
454
+ HardDeletionProperty.create()
455
+ ],
456
+ // Use Transform for creation
457
+ computation: Transform.create({
458
+ record: InteractionEventEntity,
459
+ callback: function(event) {
460
+ if (event.interactionName === 'CreateArticle') {
461
+ return {
462
+ title: event.payload.title,
463
+ content: event.payload.content,
464
+ createdAt: new Date().toISOString()
465
+ };
466
+ }
467
+ return null;
468
+ }
469
+ })
470
+ });
471
+
472
+ // Configure deletion StateMachine for HardDeletionProperty
473
+ const deletionProperty = Article.properties.find(p => p.name === '_isDeleted_');
474
+ deletionProperty.computation = StateMachine.create({
475
+ states: [NON_DELETED_STATE, DELETED_STATE],
476
+ defaultState: NON_DELETED_STATE,
477
+ transfers: [
478
+ StateTransfer.create({
479
+ trigger: DeleteArticle,
480
+ current: NON_DELETED_STATE,
481
+ next: DELETED_STATE,
482
+ computeTarget: function(event) {
483
+ return { id: event.payload.articleId };
484
+ }
485
+ })
486
+ ]
487
+ });
488
+ ```
489
+
490
+ ### Key Differences: Soft Delete vs Hard Delete
491
+
492
+ | Aspect | Soft Delete | Hard Delete |
493
+ |--------|------------|-------------|
494
+ | Data Persistence | Record remains in database | Record is physically removed |
495
+ | Recovery | Can be restored | Cannot be restored |
496
+ | Audit Trail | Full history preserved | History lost after deletion |
497
+ | Storage | Uses more storage | Frees storage immediately |
498
+ | Implementation | Status property + StateMachine | HardDeletionProperty + StateMachine |
499
+ | Use Cases | Most business scenarios | Compliance, privacy requirements |
500
+
501
+ ## Using Filtered Entity to Handle Non-Deleted Entities
502
+
503
+ For entities that support deletion, business logic usually only needs to reference "non-deleted" entities. Using Filtered Entity can create an automatically filtered view:
504
+
505
+ ```javascript
506
+ // Create Filtered Entity containing only non-deleted articles
507
+ const ActiveArticle = Entity.create({
508
+ name: 'ActiveArticle',
509
+ baseEntity: Article,
510
+ filterCondition: MatchExp.atom({
511
+ key: 'status',
512
+ value: ['!=', 'deleted']
513
+ })
514
+ // Or use isDeleted field
515
+ // filterCondition: MatchExp.atom({
516
+ // key: 'isDeleted',
517
+ // value: ['=', false]
518
+ // })
519
+ });
520
+
521
+ // Usage examples:
522
+ // 1. Query all active articles
523
+ const activeArticles = await controller.find('ActiveArticle', undefined, undefined, ['*']);
524
+
525
+ // 2. Query specific user's active articles
526
+ const userActiveArticles = await controller.find('ActiveArticle',
527
+ MatchExp.atom({ key: 'author.id', value: ['=', userId] }),
528
+ undefined,
529
+ ['title', 'content', 'createdAt']
530
+ );
531
+ ```
532
+
533
+ ### Important Notes
534
+
535
+ **Relations cannot directly reference Filtered Entities**. If you need to express "relations with non-deleted entities", you should:
536
+
537
+ 1. Use complete entities when defining relations
538
+ 2. Use StateMachine or conditional logic to control relation validity
539
+
540
+ ```javascript
541
+ // ❌ Wrong: Cannot do this
542
+ const UserActiveArticleRelation = Relation.create({
543
+ source: User,
544
+ target: ActiveArticle, // Wrong! Cannot use Filtered Entity
545
+ // ...
546
+ });
547
+
548
+ // ✅ Correct: Use complete entity, control through logic
549
+ const UserFavoriteRelation = Relation.create({
550
+ source: User,
551
+ sourceProperty: 'favorites',
552
+ target: Article, // Use complete entity
553
+ targetProperty: 'favoritedBy',
554
+ type: 'n:n',
555
+ properties: [
556
+ Property.create({
557
+ name: 'isActive',
558
+ type: 'boolean',
559
+ // Dynamically calculate based on article status
560
+ computed: function(relation) {
561
+ return relation.target.status !== 'deleted';
562
+ }
563
+ })
564
+ ]
565
+ });
566
+
567
+ // Filter when querying
568
+ const activeFavorites = await controller.find('UserFavoriteRelation',
569
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
570
+ .and({ key: 'target.status', value: ['!=', 'deleted'] }),
571
+ undefined,
572
+ [['target', { attributeQuery: ['title', 'content'] }]]
573
+ );
574
+ ```
575
+
576
+ ## Updating Entities - Reactive Updates
577
+
578
+ ### Using StateMachine to Manage State Changes
579
+
580
+ ```javascript
581
+ // Article publishing workflow
582
+ const PublishArticle = Interaction.create({
583
+ name: 'PublishArticle',
584
+ action: Action.create({ name: 'publishArticle' }),
585
+ payload: Payload.create({
586
+ items: [
587
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true })
588
+ ]
589
+ })
590
+ });
591
+
592
+ const UnpublishArticle = Interaction.create({
593
+ name: 'UnpublishArticle',
594
+ action: Action.create({ name: 'unpublishArticle' }),
595
+ payload: Payload.create({
596
+ items: [
597
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true })
598
+ ]
599
+ })
600
+ });
601
+
602
+ // Publishing state machine
603
+ const DraftState = StateNode.create({ name: 'draft' });
604
+ const PublishedState = StateNode.create({ name: 'published' });
605
+
606
+ const ArticlePublishStateMachine = StateMachine.create({
607
+ name: 'ArticlePublishStatus',
608
+ states: [DraftState, PublishedState],
609
+ defaultState: DraftState,
610
+ transfers: [
611
+ StateTransfer.create({
612
+ current: DraftState,
613
+ next: PublishedState,
614
+ trigger: PublishArticle,
615
+ computeTarget: (event) => ({ id: event.payload.articleId })
616
+ }),
617
+ StateTransfer.create({
618
+ current: PublishedState,
619
+ next: DraftState,
620
+ trigger: UnpublishArticle,
621
+ computeTarget: (event) => ({ id: event.payload.articleId })
622
+ })
623
+ ]
624
+ });
625
+ ```
626
+
627
+ ### Using Transform to Record Update History
628
+
629
+ **Note**: The following example uses Transform in Entity's computation to create new history records, NOT to update properties. This is a correct use of Transform.
630
+
631
+ ```javascript
632
+ const UpdateArticle = Interaction.create({
633
+ name: 'UpdateArticle',
634
+ action: Action.create({ name: 'updateArticle' }),
635
+ payload: Payload.create({
636
+ items: [
637
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true }),
638
+ PayloadItem.create({ name: 'title' }),
639
+ PayloadItem.create({ name: 'content' })
640
+ ]
641
+ })
642
+ });
643
+
644
+ // Article update history
645
+ const ArticleHistory = Entity.create({
646
+ name: 'ArticleHistory',
647
+ properties: [
648
+ Property.create({ name: 'articleId', type: 'string' }),
649
+ Property.create({ name: 'field', type: 'string' }),
650
+ Property.create({ name: 'oldValue', type: 'string' }),
651
+ Property.create({ name: 'newValue', type: 'string' }),
652
+ Property.create({ name: 'updatedAt', type: 'string' }),
653
+ Property.create({ name: 'updatedBy', type: 'string' })
654
+ ],
655
+ // ✅ CORRECT: Transform in Entity's computation creates new history records
656
+ computation: Transform.create({
657
+ record: InteractionEventEntity,
658
+ callback: function(event) {
659
+ if (event.interactionName === 'UpdateArticle') {
660
+ const changes = [];
661
+
662
+ if (event.payload.title !== undefined) {
663
+ changes.push({
664
+ articleId: event.payload.articleId,
665
+ field: 'title',
666
+ newValue: event.payload.title,
667
+ updatedAt: new Date().toISOString(),
668
+ updatedBy: event.user.id
669
+ });
670
+ }
671
+
672
+ if (event.payload.content !== undefined) {
673
+ changes.push({
674
+ articleId: event.payload.articleId,
675
+ field: 'content',
676
+ newValue: event.payload.content,
677
+ updatedAt: new Date().toISOString(),
678
+ updatedBy: event.user.id
679
+ });
680
+ }
681
+
682
+ return changes;
683
+ }
684
+ return null;
685
+ }
686
+ })
687
+ });
688
+ ```
689
+
690
+ ### Recording Timestamps with Single-Node StateMachine
691
+
692
+ For properties that need to record timestamps of specific events (like last activity time, last update time, etc.), you can use a single-node StateMachine with `computeValue` to dynamically compute timestamps:
693
+
694
+ ```javascript
695
+ // Define interaction to track activity
696
+ const RecordActivity = Interaction.create({
697
+ name: 'RecordActivity',
698
+ action: Action.create({ name: 'recordActivity' }),
699
+ payload: Payload.create({
700
+ items: [
701
+ PayloadItem.create({ name: 'entityId', base: SomeEntity, isRef: true })
702
+ ]
703
+ })
704
+ });
705
+
706
+ // Single-node StateMachine for timestamp recording
707
+ const TimestampState = StateNode.create({
708
+ name: 'active',
709
+ // computeValue is called each time the state is entered
710
+ computeValue: function(lastValue) {
711
+ // Always return current timestamp
712
+ return Date.now();
713
+ }
714
+ });
715
+
716
+ const TimestampStateMachine = StateMachine.create({
717
+ name: 'TimestampRecorder',
718
+ states: [TimestampState],
719
+ defaultState: TimestampState,
720
+ transfers: [
721
+ // Self-transition: stays in same state but triggers computeValue
722
+ StateTransfer.create({
723
+ current: TimestampState,
724
+ next: TimestampState,
725
+ trigger: RecordActivity,
726
+ computeTarget: (event) => ({ id: event.payload.entityId })
727
+ })
728
+ ]
729
+ });
730
+
731
+ // Apply to entity property
732
+ const SomeEntity = Entity.create({
733
+ name: 'SomeEntity',
734
+ properties: [
735
+ Property.create({ name: 'name', type: 'string' }),
736
+ Property.create({
737
+ name: 'lastActivityAt',
738
+ type: 'number',
739
+ defaultValue: () => 0,
740
+ computation: TimestampStateMachine
741
+ })
742
+ ]
743
+ });
744
+ ```
745
+
746
+ This pattern is particularly useful for:
747
+
748
+ 1. **User Activity Tracking**:
749
+ ```javascript
750
+ // First declare the state node
751
+ const activeState = StateNode.create({
752
+ name: 'active',
753
+ computeValue: () => Date.now()
754
+ });
755
+
756
+ const User = Entity.create({
757
+ name: 'User',
758
+ properties: [
759
+ Property.create({ name: 'username', type: 'string' }),
760
+ Property.create({
761
+ name: 'lastActiveAt',
762
+ type: 'number',
763
+ defaultValue: () => 0,
764
+ computation: StateMachine.create({
765
+ states: [activeState],
766
+ transfers: [
767
+ StateTransfer.create({
768
+ current: activeState,
769
+ next: activeState,
770
+ trigger: UserActivityInteraction,
771
+ computeTarget: (event) => ({ id: event.user.id })
772
+ })
773
+ ],
774
+ defaultState: activeState
775
+ })
776
+ })
777
+ ]
778
+ });
779
+ ```
780
+
781
+ 2. **Entity Update Tracking**:
782
+ ```javascript
783
+ // First declare the state node
784
+ const modifiedState = StateNode.create({
785
+ name: 'modified',
786
+ computeValue: () => Date.now()
787
+ });
788
+
789
+ const Article = Entity.create({
790
+ name: 'Article',
791
+ properties: [
792
+ Property.create({ name: 'title', type: 'string' }),
793
+ Property.create({ name: 'content', type: 'string' }),
794
+ Property.create({
795
+ name: 'lastModifiedAt',
796
+ type: 'number',
797
+ defaultValue: () => Date.now(),
798
+ computation: StateMachine.create({
799
+ states: [modifiedState],
800
+ transfers: [
801
+ StateTransfer.create({
802
+ current: modifiedState,
803
+ next: modifiedState,
804
+ trigger: UpdateArticleInteraction,
805
+ computeTarget: (event) => ({ id: event.payload.articleId })
806
+ })
807
+ ],
808
+ defaultState: modifiedState
809
+ })
810
+ })
811
+ ]
812
+ });
813
+ ```
814
+
815
+ 3. **Event Occurrence Tracking**:
816
+ ```javascript
817
+ // First declare the state node
818
+ const triggeredState = StateNode.create({
819
+ name: 'triggered',
820
+ computeValue: () => Date.now()
821
+ });
822
+
823
+ const Sensor = Entity.create({
824
+ name: 'Sensor',
825
+ properties: [
826
+ Property.create({ name: 'location', type: 'string' }),
827
+ Property.create({
828
+ name: 'lastTriggeredAt',
829
+ type: 'number',
830
+ defaultValue: () => 0,
831
+ computation: StateMachine.create({
832
+ states: [triggeredState],
833
+ transfers: [
834
+ StateTransfer.create({
835
+ current: triggeredState,
836
+ next: triggeredState,
837
+ trigger: SensorTriggerInteraction,
838
+ computeTarget: (event) => ({ id: event.payload.sensorId })
839
+ })
840
+ ],
841
+ defaultState: triggeredState
842
+ })
843
+ })
844
+ ]
845
+ });
846
+ ```
847
+
848
+ #### Advantages of This Pattern
849
+
850
+ 1. **Reactive**: Timestamps are automatically updated when specific interactions occur
851
+ 2. **Declarative**: No need to manually set timestamps in interaction handlers
852
+ 3. **Consistent**: Ensures timestamp recording logic is centralized and consistent
853
+ 4. **Efficient**: Only updates when the specific interaction is triggered
854
+ 5. **Flexible**: Can be combined with other state machines for complex workflows
855
+
856
+ #### When to Use This Pattern vs Transform
857
+
858
+ - **Use Single-Node StateMachine with computeValue** when:
859
+ - You need to record timestamps for specific entity instances
860
+ - The timestamp is a property of the entity itself
861
+ - You want the timestamp to update on specific interactions
862
+
863
+ - **Use Transform** when:
864
+ - You need to create new records (like history/audit logs)
865
+ - You need to record multiple fields or complex data
866
+ - You want to maintain a complete history of changes
867
+ - **REMEMBER**: Transform can ONLY be used at Entity or Relation level, NEVER at Property level!
868
+
869
+ **❌ Common Mistake to Avoid**:
870
+ ```javascript
871
+ // ❌ NEVER do this - Transform in Property computation
872
+ Property.create({
873
+ name: 'lastActivityAt',
874
+ computation: Transform.create({ // ❌ WRONG!
875
+ record: InteractionEventEntity,
876
+ callback: function(event) {
877
+ if (event.user.id === this.id) { // ❌ No 'this' context!
878
+ return new Date().toISOString();
879
+ }
880
+ }
881
+ })
882
+ })
883
+
884
+ // ✅ CORRECT approach - Use StateMachine with computeValue
885
+ const activeState = StateNode.create({
886
+ name: 'active',
887
+ computeValue: () => new Date().toISOString()
888
+ });
889
+
890
+ Property.create({
891
+ name: 'lastActivityAt',
892
+ computation: StateMachine.create({
893
+ states: [activeState],
894
+ defaultState: activeState,
895
+ transfers: [
896
+ StateTransfer.create({
897
+ current: activeState,
898
+ next: activeState,
899
+ trigger: UserActivityInteraction,
900
+ computeTarget: (event) => ({ id: event.user.id })
901
+ })
902
+ ]
903
+ })
904
+ })
905
+ ```
906
+
907
+ ## Complete Example: Blog System CRUD Operations
908
+
909
+ ```javascript
910
+ import {
911
+ Entity, Property, Relation, Interaction, Action, Payload, PayloadItem,
912
+ Transform, StateMachine, StateNode, StateTransfer, Count, MatchExp,
913
+ InteractionEventEntity
914
+ } from 'interaqt';
915
+
916
+ // === Entity Definitions ===
917
+ const User = Entity.create({
918
+ name: 'User',
919
+ properties: [
920
+ Property.create({ name: 'username', type: 'string' }),
921
+ Property.create({ name: 'email', type: 'string' }),
922
+ Property.create({
923
+ name: 'articleCount',
924
+ type: 'number',
925
+ computation: Count.create({
926
+ record: UserArticleRelation,
927
+ direction: 'target',
928
+ callback: (relation) => relation.source.status !== 'deleted'
929
+ })
930
+ })
931
+ ]
932
+ });
933
+
934
+ const Article = Entity.create({
935
+ name: 'Article',
936
+ properties: [
937
+ Property.create({ name: 'title', type: 'string' }),
938
+ Property.create({ name: 'content', type: 'string' }),
939
+ Property.create({ name: 'createdAt', type: 'string' }),
940
+ Property.create({ name: 'publishedAt', type: 'string' }),
941
+ Property.create({ name: 'deletedAt', type: 'string' }),
942
+ Property.create({
943
+ name: 'status',
944
+ type: 'string',
945
+ computation: ArticleLifecycleStateMachine,
946
+ defaultValue: () => 'draft'
947
+ }),
948
+ Property.create({
949
+ name: 'isDeleted',
950
+ type: 'boolean',
951
+ computed: (article) => article.status === 'deleted'
952
+ })
953
+ ],
954
+ // Transform to create articles from interactions
955
+ computation: Transform.create({
956
+ record: InteractionEventEntity,
957
+ callback: function(event) {
958
+ if (event.interactionName === 'CreateArticle') {
959
+ return {
960
+ title: event.payload.title,
961
+ content: event.payload.content,
962
+ createdAt: new Date().toISOString(),
963
+ author: {id:event.payload.authorId } // Relation created automatically
964
+ };
965
+ }
966
+ return null;
967
+ }
968
+ })
969
+ });
970
+
971
+ // === Filtered Entities ===
972
+ const ActiveArticle = Entity.create({
973
+ name: 'ActiveArticle',
974
+ baseEntity: Article,
975
+ filterCondition: MatchExp.atom({
976
+ key: 'status',
977
+ value: ['!=', 'deleted']
978
+ })
979
+ });
980
+
981
+ const PublishedArticle = Entity.create({
982
+ name: 'PublishedArticle',
983
+ baseEntity: Article,
984
+ filterCondition: MatchExp.atom({
985
+ key: 'status',
986
+ value: ['=', 'published']
987
+ })
988
+ });
989
+
990
+ // === Interaction Definitions ===
991
+ const CreateArticle = Interaction.create({
992
+ name: 'CreateArticle',
993
+ action: Action.create({ name: 'createArticle' }),
994
+ payload: Payload.create({
995
+ items: [
996
+ PayloadItem.create({ name: 'title', required: true }),
997
+ PayloadItem.create({ name: 'content', required: true }),
998
+ PayloadItem.create({ name: 'authorId', base: User, isRef: true })
999
+ ]
1000
+ })
1001
+ });
1002
+
1003
+ const PublishArticle = Interaction.create({
1004
+ name: 'PublishArticle',
1005
+ action: Action.create({ name: 'publishArticle' }),
1006
+ payload: Payload.create({
1007
+ items: [
1008
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true })
1009
+ ]
1010
+ })
1011
+ });
1012
+
1013
+ const DeleteArticle = Interaction.create({
1014
+ name: 'DeleteArticle',
1015
+ action: Action.create({ name: 'deleteArticle' }),
1016
+ payload: Payload.create({
1017
+ items: [
1018
+ PayloadItem.create({ name: 'articleId', base: Article, isRef: true })
1019
+ ]
1020
+ })
1021
+ });
1022
+
1023
+ // === State Machine Definition ===
1024
+ const DraftState = StateNode.create({ name: 'draft' });
1025
+ const PublishedState = StateNode.create({ name: 'published' });
1026
+ const DeletedState = StateNode.create({ name: 'deleted' });
1027
+
1028
+ const ArticleLifecycleStateMachine = StateMachine.create({
1029
+ name: 'ArticleLifecycle',
1030
+ states: [DraftState, PublishedState, DeletedState],
1031
+ defaultState: DraftState,
1032
+ transfers: [
1033
+ StateTransfer.create({
1034
+ current: DraftState,
1035
+ next: PublishedState,
1036
+ trigger: PublishArticle,
1037
+ computeTarget: (event) => ({ id: event.payload.articleId })
1038
+ }),
1039
+ StateTransfer.create({
1040
+ current: PublishedState,
1041
+ next: DeletedState,
1042
+ trigger: DeleteArticle,
1043
+ computeTarget: (event) => ({ id: event.payload.articleId })
1044
+ }),
1045
+ StateTransfer.create({
1046
+ current: DraftState,
1047
+ next: DeletedState,
1048
+ trigger: DeleteArticle,
1049
+ computeTarget: (event) => ({ id: event.payload.articleId })
1050
+ })
1051
+ ]
1052
+ });
1053
+
1054
+ // === Relation Definition ===
1055
+ const UserArticleRelation = Relation.create({
1056
+ source: Article,
1057
+ sourceProperty: 'author',
1058
+ target: User,
1059
+ targetProperty: 'articles',
1060
+ type: 'n:1'
1061
+ // No computation needed - relation is created automatically when Article is created with author reference
1062
+ });
1063
+
1064
+ // === Usage Examples ===
1065
+ // 1. Create article
1066
+ await controller.callInteraction('CreateArticle', {
1067
+ user: { id: 'user123' },
1068
+ payload: {
1069
+ title: 'My First Article',
1070
+ content: 'This is the content...',
1071
+ authorId: 'user123'
1072
+ }
1073
+ });
1074
+
1075
+ // 2. Publish article
1076
+ await controller.callInteraction('PublishArticle', {
1077
+ user: { id: 'user123' },
1078
+ payload: {
1079
+ articleId: 'article456'
1080
+ }
1081
+ });
1082
+
1083
+ // 3. Query active articles
1084
+ const activeArticles = await controller.find('ActiveArticle');
1085
+
1086
+ // 4. Query published articles
1087
+ const publishedArticles = await controller.find('PublishedArticle');
1088
+
1089
+ // 5. Delete article (soft delete)
1090
+ await controller.callInteraction('DeleteArticle', {
1091
+ user: { id: 'user123' },
1092
+ payload: {
1093
+ articleId: 'article456'
1094
+ }
1095
+ });
1096
+ ```
1097
+
1098
+ ## Best Practices
1099
+
1100
+ 1. **Prefer Soft Delete**: In reactive systems, soft delete preserves data integrity and historical traceability. Only use hard delete when absolutely necessary (compliance, privacy laws).
1101
+
1102
+ 2. **Reasonable Use of Filtered Entities**: For scenarios that frequently query non-deleted data, creating corresponding Filtered Entities can simplify queries.
1103
+
1104
+ 3. **StateMachine Over Direct Updates**: Using StateMachine to manage entity state is clearer and more controllable than direct field updates.
1105
+
1106
+ 4. **Record Operation History**: Use Transform to record important operation history for auditing and backtracking.
1107
+
1108
+ 5. **Consider Relation Validity**: When entities support deletion, related relations also need validity management.
1109
+
1110
+ 6. **Hard Delete Considerations**:
1111
+ - Use HardDeletionProperty + StateMachine pattern
1112
+ - Ensure no critical audit data is lost
1113
+ - Consider cascading effects on related entities
1114
+ - Document why hard delete is necessary
1115
+
1116
+ 7. **Never Use Transform in Property Computation**: Transform is designed for collection-to-collection transformation (Entity/Relation creation). For property-level computations, use:
1117
+ - **StateMachine**: For state management and interaction-driven updates
1118
+ - **computed/getValue**: For simple derived values
1119
+ - **Count/Summation/Every/Any**: For aggregations based on relations
1120
+ - **RealTime**: For time-based computations
1121
+
1122
+ By following these patterns, you can build a robust, traceable, and easily maintainable reactive data system.