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,959 @@
1
+ # Computation Implementation Guide
2
+
3
+ ## Overview
4
+ Computations are the reactive core of interaqt, connecting interactions to entities and enabling automatic data flow.
5
+
6
+ ## Types of Computations
7
+
8
+ ### 1. Transform - Creates Entities/Relations
9
+
10
+ **ONLY use in Entity/Relation computation, NEVER in Property!**
11
+
12
+ #### 🔴 CRITICAL: Transform Collection Conversion Concept
13
+
14
+ Transform converts one collection (source) into another collection (target). Understanding this is crucial:
15
+
16
+ 1. **Collection to Collection**: Transform maps items from source collection to target collection
17
+ - Source: InteractionEventEntity collection (all interaction events)
18
+ - Target: Entity/Relation collection being created
19
+
20
+ 2. **Callback Can Return One or Multiple Items**: The callback can return:
21
+ - A single object → creates one record of target type
22
+ - An array of objects → creates multiple records of target type from one source
23
+ - `null`/`undefined` → creates no records
24
+
25
+ ```typescript
26
+ callback: function(event) {
27
+ // event is ONE item from InteractionEventEntity collection
28
+
29
+ // Option 1: Return single item
30
+ return { /* single entity data */ };
31
+
32
+ // Option 2: Return multiple entities
33
+ return [];
34
+
35
+ // Option 3: Return nothing (filter out)
36
+ return null;
37
+ }
38
+ ```
39
+
40
+ 3. **System Auto-generates IDs**: NEVER include `id` in callback return value
41
+ ```typescript
42
+ // ❌ WRONG: Including id in new entity
43
+ callback: function(event) {
44
+ return {
45
+ id: uuid(), // NEVER DO THIS!
46
+ label: event.payload.label,
47
+ slug: event.payload.slug
48
+ };
49
+ }
50
+
51
+ // ✅ CORRECT: Let system generate id
52
+ callback: function(event) {
53
+ return {
54
+ label: event.payload.label,
55
+ slug: event.payload.slug
56
+ };
57
+ }
58
+ ```
59
+
60
+ 4. **Entity References Use ID**: When referencing existing entities in relations, use `{ id: ... }`
61
+ ```typescript
62
+ // ✅ CORRECT: Reference existing entity
63
+ callback: function(event) {
64
+ return {
65
+ title: event.payload.title,
66
+ author: event.user, // event.user already has id
67
+ category: { id: event.payload.categoryId } // Reference by id
68
+ };
69
+ }
70
+ ```
71
+
72
+ Remember: Transform is a **mapping function** that converts each matching source item into one or more target items (or none). The framework handles ID generation, storage, and relationship management.
73
+
74
+ #### 🔴 CRITICAL: InteractionEventEntity Transform Limitations
75
+
76
+ When using `InteractionEventEntity` as the Transform input source, understand these fundamental limitations:
77
+
78
+ 1. **ONLY Creates, Never Updates or Deletes**
79
+ - Transform with InteractionEventEntity can ONLY create new entities
80
+ - It CANNOT update existing entities
81
+ - It CANNOT delete entities
82
+
83
+ 2. **Why This Limitation Exists**
84
+ - InteractionEventEntity represents system interaction events
85
+ - Events are **immutable** - once occurred, they never change
86
+ - Events are **append-only** - the collection only grows, never shrinks
87
+ - Each interaction creates a NEW event, it doesn't modify old events
88
+
89
+ 3. **How to Handle Updates and Deletes**
90
+
91
+ ```typescript
92
+ // ❌ WRONG: Trying to update with Transform
93
+ const Style = Entity.create({
94
+ name: 'Style'
95
+ });
96
+ Style.computation = Transform.create({
97
+ record: InteractionEventEntity,
98
+ callback: function(event) {
99
+ if (event.interactionName === 'UpdateStyle') {
100
+ // This will CREATE a new Style, not update existing!
101
+ return { id: event.payload.id, ... } // WRONG!
102
+ }
103
+ }
104
+ });
105
+
106
+ // ✅ CORRECT: Use StateMachine for updates
107
+ const updatedAtProperty = Property.create({
108
+ name: 'updatedAt',
109
+ type: 'number'
110
+ });
111
+ updatedAtProperty.computation = StateMachine.create({
112
+ states: [updatedState],
113
+ transfers: [
114
+ StateTransfer.create({
115
+ trigger: UpdateStyleInteraction,
116
+ current: updatedState,
117
+ next: updatedState,
118
+ computeTarget: (event) => ({ id: event.payload.id })
119
+ })
120
+ ]
121
+ });
122
+
123
+ // ✅ CORRECT: Use soft delete with status
124
+ const statusProperty = Property.create({
125
+ name: 'status',
126
+ type: 'string'
127
+ });
128
+ statusProperty.computation = StateMachine.create({
129
+ states: [activeState, deletedState],
130
+ defaultState: activeState, // StateMachine controls initial value
131
+ transfers: [
132
+ StateTransfer.create({
133
+ trigger: DeleteStyleInteraction,
134
+ current: activeState,
135
+ next: deletedState,
136
+ computeTarget: (event) => ({ id: event.payload.id })
137
+ })
138
+ ]
139
+ });
140
+ ```
141
+
142
+ **Summary**: Think of InteractionEventEntity Transform as a "factory" that produces new entities from events. For any modifications to existing entities, use StateMachine. For deletions, use soft delete patterns with status fields.
143
+
144
+ #### Entity Creation from InteractionEventEntity via Transform
145
+ ```typescript
146
+ import { Transform, InteractionEventEntity, Entity, Property, Interaction, Action, Payload, PayloadItem } from 'interaqt';
147
+
148
+ // Define creation interaction
149
+ export const CreateStyle = Interaction.create({
150
+ name: 'CreateStyle',
151
+ action: Action.create({ name: 'createStyle' }),
152
+ payload: Payload.create({
153
+ items: [
154
+ PayloadItem.create({ name: 'label', required: true }),
155
+ PayloadItem.create({ name: 'slug', required: true }),
156
+ PayloadItem.create({ name: 'description' }),
157
+ PayloadItem.create({ name: 'type' }),
158
+ PayloadItem.create({ name: 'thumbKey' }),
159
+ PayloadItem.create({ name: 'priority' })
160
+ ]
161
+ })
162
+ });
163
+
164
+ // Entity with Transform
165
+ export const Style = Entity.create({
166
+ name: 'Style',
167
+ properties: [
168
+ Property.create({ name: 'label', type: 'string' }),
169
+ Property.create({ name: 'slug', type: 'string' }),
170
+ Property.create({ name: 'description', type: 'string' }),
171
+ Property.create({ name: 'type', type: 'string' }),
172
+ Property.create({ name: 'thumbKey', type: 'string' }),
173
+ Property.create({ name: 'priority', type: 'number', defaultValue: () => 0 }),
174
+ Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
175
+ // 🔴 CRITICAL: Always use seconds for timestamps, not milliseconds!
176
+ Property.create({ name: 'createdAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) }),
177
+ Property.create({ name: 'updatedAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) })
178
+ ]
179
+ });
180
+ // Transform in Entity's computation property
181
+ Style.computation = Transform.create({
182
+ record: InteractionEventEntity,
183
+ callback: function(event) {
184
+ if (event.interactionName === 'CreateStyle') {
185
+ return {
186
+ label: event.payload.label,
187
+ slug: event.payload.slug,
188
+ description: event.payload.description || '',
189
+ type: event.payload.type || 'default',
190
+ thumbKey: event.payload.thumbKey || '',
191
+ priority: event.payload.priority || 0,
192
+ status: 'draft',
193
+ createdAt: Math.floor(Date.now()/1000), // Always use seconds!
194
+ updatedAt: Math.floor(Date.now()/1000), // Always use seconds!
195
+ lastModifiedBy: event.user // Creates relation automatically
196
+ };
197
+ }
198
+ return null;
199
+ }
200
+ });
201
+ ```
202
+
203
+ #### Created With Parent - Child Entities in Parent Transform
204
+
205
+ **When to Use**:
206
+ - When child entities' lifecycle is completely dependent on parent entity
207
+ - **When computationDecision is expressed as `_parent:[Parent]`** - this indicates the child entity should be created through the parent's Transform computation
208
+
209
+ **Example**: Order with OrderItems
210
+
211
+ ```typescript
212
+ // Order creates OrderItems atomically
213
+ export const Order = Entity.create({
214
+ name: 'Order',
215
+ properties: [
216
+ Property.create({ name: 'orderNumber', type: 'string' }),
217
+ Property.create({ name: 'customerName', type: 'string' })
218
+ ]
219
+ });
220
+
221
+ export const OrderItem = Entity.create({
222
+ name: 'OrderItem',
223
+ properties: [
224
+ Property.create({ name: 'productName', type: 'string' }),
225
+ Property.create({ name: 'quantity', type: 'number' }),
226
+ Property.create({ name: 'price', type: 'number' })
227
+ ]
228
+ });
229
+
230
+ // Define relation
231
+ export const OrderItemRelation = Relation.create({
232
+ source: Order,
233
+ sourceProperty: 'items',
234
+ target: OrderItem,
235
+ targetProperty: 'order',
236
+ type: '1:n'
237
+ });
238
+
239
+
240
+ Order.computation = Transform.create({
241
+ record: InteractionEventEntity,
242
+ callback: function(event) {
243
+ if (event.interactionName === 'CreateOrder') {
244
+ return {
245
+ orderNumber: event.payload.orderNumber,
246
+ customerName: event.payload.customerName
247
+ items: event.payload.items // OrderItem and OrderItemRelation created with parent
248
+ };
249
+ }
250
+ return null;
251
+ }
252
+ });
253
+ ```
254
+
255
+
256
+ #### Derived from Other Entities/Relations (Non-InteractionEventEntity)
257
+
258
+ Transform can also use other entities as source, not just InteractionEventEntity. This is useful for creating derived entities based on existing data.
259
+
260
+ **🔴 IMPORTANT: Establishing Relations**
261
+ When transforming from one entity to another, if you want to establish a relation between the transformed entity and the source entity, you MUST explicitly include the source entity reference in the callback return value.
262
+
263
+ ```typescript
264
+ import { Transform, Entity, Property, Relation } from 'interaqt';
265
+
266
+ // Create snapshot entity from Style entity
267
+ export const StyleSnapshot = Entity.create({
268
+ name: 'StyleSnapshot',
269
+ properties: [
270
+ Property.create({ name: 'label', type: 'string' }),
271
+ Property.create({ name: 'slug', type: 'string' }),
272
+ Property.create({ name: 'description', type: 'string' }),
273
+ Property.create({ name: 'snapshotTakenAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) }),
274
+ Property.create({ name: 'version', type: 'number' })
275
+ ]
276
+ });
277
+ // Transform from Style entity (not InteractionEventEntity)
278
+ StyleSnapshot.computation = Transform.create({
279
+ record: Style, // ← Source is Style entity, not InteractionEventEntity
280
+ attributeQuery: ['id', 'label', 'slug', 'description', 'status'],
281
+ callback: function(style) {
282
+ // Only create snapshots for active styles
283
+ if (style.status === 'active') {
284
+ return {
285
+ label: style.label,
286
+ slug: style.slug,
287
+ description: style.description || '',
288
+ snapshotTakenAt: Math.floor(Date.now()/1000), // In seconds
289
+ version: Math.floor(Date.now()/1000), // Version number in seconds
290
+ // 🔴 CRITICAL: Must explicitly reference source entity to create relation
291
+ originalStyle: style // ← This creates the relation to source Style
292
+ };
293
+ }
294
+ return null; // Don't create snapshot for non-active styles
295
+ }
296
+ });
297
+
298
+ // Define the relation between Style and StyleSnapshot
299
+ export const StyleSnapshotRelation = Relation.create({
300
+ source: Style,
301
+ sourceProperty: 'snapshots',
302
+ target: StyleSnapshot,
303
+ targetProperty: 'originalStyle',
304
+ type: '1:n' // One style can have many snapshots
305
+ });
306
+ ```
307
+
308
+
309
+ ### 2. StateMachine - Updates Entities
310
+
311
+ Used for status changes and field updates.
312
+
313
+ **🔴 IMPORTANT: StateTransfer Trigger Parameter**
314
+
315
+ The `trigger` parameter in `StateTransfer.create()` must ALWAYS be an Interaction instance reference, NOT a string!
316
+
317
+ ```typescript
318
+ // ❌ WRONG: Using string as trigger
319
+ StateTransfer.create({
320
+ trigger: 'PublishStyle', // ERROR! Don't use string!
321
+ current: draftState,
322
+ next: activeState
323
+ })
324
+
325
+ // ✅ CORRECT: Using Interaction instance reference
326
+ StateTransfer.create({
327
+ trigger: PublishStyle, // Correct! Reference to Interaction instance
328
+ current: draftState,
329
+ next: activeState
330
+ })
331
+ ```
332
+
333
+ #### Basic StateMachine
334
+ ```typescript
335
+ import { StateMachine, StateNode, StateTransfer } from 'interaqt';
336
+
337
+ // Define states first (must be declared before use)
338
+ const draftState = StateNode.create({ name: 'draft' });
339
+ const activeState = StateNode.create({ name: 'active' });
340
+ const offlineState = StateNode.create({ name: 'offline' });
341
+
342
+ // Define interactions
343
+ const PublishStyle = Interaction.create({
344
+ name: 'PublishStyle',
345
+ action: Action.create({ name: 'publishStyle' }),
346
+ payload: Payload.create({
347
+ items: [
348
+ PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
349
+ ]
350
+ })
351
+ });
352
+
353
+ const DeleteStyle = Interaction.create({
354
+ name: 'DeleteStyle',
355
+ action: Action.create({ name: 'deleteStyle' }),
356
+ payload: Payload.create({
357
+ items: [
358
+ PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
359
+ ]
360
+ })
361
+ });
362
+
363
+ // Apply state machine to property
364
+ const statusProperty = Property.create({
365
+ name: 'status',
366
+ type: 'string'
367
+ });
368
+ statusProperty.computation = StateMachine.create({
369
+ name: 'StyleStatus',
370
+ states: [draftState, activeState, offlineState],
371
+ defaultState: draftState, // defaultState determines initial value
372
+ transfers: [
373
+ StateTransfer.create({
374
+ current: draftState,
375
+ next: activeState,
376
+ trigger: PublishStyle,
377
+ computeTarget: (event) => ({ id: event.payload.id })
378
+ }),
379
+ StateTransfer.create({
380
+ current: activeState,
381
+ next: offlineState,
382
+ trigger: DeleteStyle,
383
+ computeTarget: (event) => ({ id: event.payload.id })
384
+ })
385
+ ]
386
+ });
387
+ ```
388
+
389
+ #### StateMachine with Value Updates
390
+ ```typescript
391
+ // Track timestamps using single-state machine with computeValue
392
+ const UpdateStyle = Interaction.create({
393
+ name: 'UpdateStyle',
394
+ action: Action.create({ name: 'updateStyle' }),
395
+ payload: Payload.create({
396
+ items: [
397
+ PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true }),
398
+ PayloadItem.create({ name: 'label' }),
399
+ PayloadItem.create({ name: 'description' })
400
+ ]
401
+ })
402
+ });
403
+
404
+ // Define state node with computeValue
405
+ const updatedState = StateNode.create({
406
+ name: 'updated',
407
+ computeValue: () => Math.floor(Date.now()/1000) // Returns timestamp in seconds when state is entered
408
+ });
409
+
410
+ const updatedAtProperty = Property.create({
411
+ name: 'updatedAt',
412
+ type: 'number'
413
+ });
414
+ updatedAtProperty.computation = StateMachine.create({
415
+ name: 'UpdatedAt',
416
+ states: [updatedState],
417
+ defaultState: updatedState, // computeValue in updatedState provides initial value
418
+ transfers: [
419
+ StateTransfer.create({
420
+ current: updatedState,
421
+ next: updatedState, // Self-loop to same state
422
+ trigger: UpdateStyle,
423
+ computeTarget: (event) => ({ id: event.payload.id })
424
+ })
425
+ ]
426
+ });
427
+ ```
428
+
429
+ #### StateMachine with Event Context in computeValue
430
+
431
+ The `computeValue` function can access the interaction event as a second parameter, allowing you to use interaction context (user, payload) in value computation:
432
+
433
+ ```typescript
434
+ // Track who made changes and what was changed
435
+ const UpdateArticle = Interaction.create({
436
+ name: 'UpdateArticle',
437
+ action: Action.create({ name: 'updateArticle' }),
438
+ payload: Payload.create({
439
+ items: [
440
+ PayloadItem.create({ name: 'id', base: Article, isRef: true, required: true }),
441
+ PayloadItem.create({ name: 'title' }),
442
+ PayloadItem.create({ name: 'content' }),
443
+ PayloadItem.create({ name: 'updateReason' })
444
+ ]
445
+ })
446
+ });
447
+
448
+ // State node that captures user and payload information
449
+ const modifiedState = StateNode.create({
450
+ name: 'modified',
451
+ // computeValue receives (lastValue, event) parameters
452
+ computeValue: (lastValue, event) => {
453
+ // Access user who triggered the update
454
+ const modifier = event?.user?.name || event?.user?.id || 'anonymous';
455
+
456
+ // Access payload to see what was changed
457
+ const changes = [];
458
+ if (event?.payload?.title) changes.push('title');
459
+ if (event?.payload?.content) changes.push('content');
460
+
461
+ return {
462
+ modifiedAt: Math.floor(Date.now()/1000),
463
+ modifiedBy: modifier,
464
+ changedFields: changes,
465
+ updateReason: event?.payload?.updateReason || 'No reason provided',
466
+ // Preserve previous modification history
467
+ previousModifications: lastValue?.previousModifications || []
468
+ };
469
+ }
470
+ });
471
+
472
+ // Apply to property
473
+ const modificationInfoProperty = Property.create({
474
+ name: 'modificationInfo',
475
+ type: 'object'
476
+ });
477
+ modificationInfoProperty.computation = StateMachine.create({
478
+ name: 'ModificationTracker',
479
+ states: [modifiedState],
480
+ defaultState: modifiedState, // computeValue in modifiedState handles initial value
481
+ transfers: [
482
+ StateTransfer.create({
483
+ current: modifiedState,
484
+ next: modifiedState,
485
+ trigger: UpdateArticle,
486
+ computeTarget: (event) => ({ id: event.payload.id })
487
+ })
488
+ ]
489
+ });
490
+
491
+ // Another example: Approval workflow with approver tracking
492
+ const ApproveRequest = Interaction.create({
493
+ name: 'ApproveRequest',
494
+ action: Action.create({ name: 'approveRequest' }),
495
+ payload: Payload.create({
496
+ items: [
497
+ PayloadItem.create({ name: 'requestId', base: Request, isRef: true, required: true }),
498
+ PayloadItem.create({ name: 'comments' })
499
+ ]
500
+ })
501
+ });
502
+
503
+ const approvedState = StateNode.create({
504
+ name: 'approved',
505
+ computeValue: (lastValue, event) => {
506
+ // Capture complete approval context from event
507
+ return {
508
+ status: 'approved',
509
+ approvedAt: Math.floor(Date.now()/1000),
510
+ approvedBy: {
511
+ id: event?.user?.id,
512
+ name: event?.user?.name,
513
+ role: event?.user?.role
514
+ },
515
+ approvalComments: event?.payload?.comments,
516
+ // Keep approval history
517
+ approvalHistory: [
518
+ ...(lastValue?.approvalHistory || []),
519
+ {
520
+ action: 'approved',
521
+ timestamp: Math.floor(Date.now()/1000),
522
+ user: event?.user?.name || 'unknown',
523
+ comments: event?.payload?.comments
524
+ }
525
+ ]
526
+ };
527
+ }
528
+ });
529
+ ```
530
+
531
+ **Key Points about Event Parameter:**
532
+ - The `event` parameter is optional and may be `undefined` during initial state setup
533
+ - Contains the full interaction context: `user`, `payload`, `interactionName`, etc.
534
+ - Useful for audit trails, tracking who made changes, and capturing interaction-specific data
535
+ - Always use optional chaining (`?.`) when accessing event properties as it may be undefined
536
+
537
+
538
+ ### 3. Custom - Complete User Control (USE WITH CAUTION!)
539
+
540
+ ```typescript
541
+ import { Custom, Dictionary, GlobalBoundState, Entity, Property, Relation } from 'interaqt';
542
+ ```
543
+
544
+ **🔴 WARNING: Custom should be your LAST RESORT!**
545
+
546
+ Before using Custom computation, ask yourself:
547
+ 1. Can I use Transform for entity/relation creation? → Use Transform
548
+ 2. Can I use StateMachine for updates? → Use StateMachine
549
+ 3. Can I use Count/Summation/Every/Any for aggregations? → Use those
550
+ 4. Can I use computed/getValue for simple calculations? → Use those
551
+ 5. Can I combine existing computations? → Combine them
552
+
553
+ **Only use Custom when:**
554
+ - You need complex business logic that doesn't fit ANY existing computation type
555
+ - You need stateful calculations with custom persistence logic
556
+ - You need advanced data transformations that require full control
557
+
558
+ **Example of PROPER use:**
559
+ ```typescript
560
+ // ✅ CORRECT: Complex calculation using Custom computation
561
+ const totalProductValue = Dictionary.create({
562
+ name: 'totalProductValue',
563
+ type: 'number',
564
+ collection: false
565
+ });
566
+ totalProductValue.computation = Custom.create({
567
+ name: 'TotalValueCalculator',
568
+ dataDeps: {
569
+ products: {
570
+ type: 'records',
571
+ source: Product,
572
+ attributeQuery: ['price', 'quantity']
573
+ }
574
+ },
575
+ compute: async function(dataDeps) {
576
+ const products = dataDeps.products || [];
577
+ const total = products.reduce((sum, p) => {
578
+ return sum + (p.price || 0) * (p.quantity || 0);
579
+ }, 0);
580
+ return total;
581
+ },
582
+ getDefaultValue: function() {
583
+ return 0;
584
+ }
585
+ });
586
+
587
+ // ✅ CORRECT: Property-level Custom for computed field
588
+ const Product = Entity.create({
589
+ name: 'Product',
590
+ properties: [
591
+ Property.create({ name: 'name', type: 'string' }),
592
+ Property.create({ name: 'basePrice', type: 'number' }),
593
+ Property.create({ name: 'taxRate', type: 'number', defaultValue: () => 0.1 }),
594
+ Property.create({ name: 'discount', type: 'number', defaultValue: () => 0 })
595
+ ]
596
+ });
597
+
598
+ // Computed property based on other properties of same record
599
+ const finalPriceProperty = Property.create({
600
+ name: 'finalPrice',
601
+ type: 'number'
602
+ });
603
+ finalPriceProperty.computation = Custom.create({
604
+ name: 'FinalPriceCalculator',
605
+ dataDeps: {
606
+ _current: { // Special key for current record's properties
607
+ type: 'property',
608
+ attributeQuery: ['basePrice', 'taxRate', 'discount']
609
+ }
610
+ },
611
+ compute: async function(dataDeps, record) {
612
+ const basePrice = dataDeps._current?.basePrice || 0;
613
+ const taxRate = dataDeps._current?.taxRate || 0;
614
+ const discount = dataDeps._current?.discount || 0;
615
+
616
+ // Calculate: basePrice * (1 + taxRate) * (1 - discount)
617
+ const priceWithTax = basePrice * (1 + taxRate);
618
+ const finalPrice = priceWithTax * (1 - discount);
619
+
620
+ return Math.round(finalPrice * 100) / 100; // Round to 2 decimals
621
+ },
622
+ getDefaultValue: function() {
623
+ return 0;
624
+ }
625
+ });
626
+ Product.properties.push(finalPriceProperty);
627
+
628
+ // ✅ CORRECT: Accessing related entity properties through relations
629
+ const Department = Entity.create({
630
+ name: 'Department',
631
+ properties: [
632
+ Property.create({ name: 'name', type: 'string' }),
633
+ Property.create({ name: 'budget', type: 'number' })
634
+ ]
635
+ });
636
+
637
+ const Employee = Entity.create({
638
+ name: 'Employee',
639
+ properties: [
640
+ Property.create({ name: 'name', type: 'string' }),
641
+ Property.create({ name: 'salary', type: 'number' })
642
+ ]
643
+ });
644
+
645
+ // Define the relation between Employee and Department
646
+ const EmployeeDepartmentRelation = Relation.create({
647
+ source: Employee,
648
+ sourceProperty: 'department',
649
+ target: Department,
650
+ targetProperty: 'employees',
651
+ type: 'n:1' // Many employees to one department
652
+ });
653
+
654
+ // Property that accesses related department data
655
+ const EmployeeDepartmentInfoProperty = Property.create({
656
+ name: 'departmentInfo',
657
+ type: 'string'
658
+ });
659
+
660
+ Employee.properties.push(EmployeeDepartmentInfoProperty);
661
+
662
+ EmployeeDepartmentInfoProperty.computation = Custom.create({
663
+ name: 'DepartmentInfoGenerator',
664
+ dataDeps: {
665
+ _current: {
666
+ type: 'property',
667
+ // Access properties and related entities through nested attributeQuery
668
+ attributeQuery: [
669
+ 'name',
670
+ 'salary',
671
+ ['department', { // Access related entity through relation
672
+ attributeQuery: ['name', 'budget'] // Specify which properties of related entity
673
+ }]
674
+ ]
675
+ }
676
+ },
677
+ compute: async function(dataDeps, record) {
678
+ const employeeName = dataDeps._current?.name || 'Unknown';
679
+ const salary = dataDeps._current?.salary || 0;
680
+ const department = dataDeps._current?.department;
681
+
682
+ if (department) {
683
+ return `${employeeName} ($${salary}) works in ${department.name} with budget $${department.budget}`;
684
+ }
685
+ return `${employeeName} ($${salary}) - No department assigned`;
686
+ },
687
+ getDefaultValue: function() {
688
+ return 'No info available';
689
+ }
690
+ });
691
+ ```
692
+
693
+ **Custom Computation dataDeps Types:**
694
+ - `type: 'records'` - Access entity/relation records from storage
695
+ - `type: 'global'` - Access global dictionary values
696
+ - `type: 'property'` - Access current record's properties and related entities
697
+
698
+ **🔴 IMPORTANT: Property Type Custom Computation**
699
+
700
+ When using `type: 'property'` with Custom computation:
701
+ - Access same record properties: `attributeQuery: ['propertyName1', 'propertyName2']`
702
+ - Access related entities through relations: Use nested attributeQuery
703
+ ```typescript
704
+ attributeQuery: [
705
+ 'ownProperty', // Current record's property
706
+ ['relationName', { // Access related entity
707
+ attributeQuery: ['relatedProp1', 'relatedProp2'] // Properties of related entity
708
+ }]
709
+ ]
710
+ ```
711
+ - The framework automatically tracks dependencies and recomputes when related data changes
712
+
713
+ **Custom Computation Best Practices:**
714
+ 1. **Document WHY** you need Custom instead of other computations
715
+ 2. **Minimize dependencies** - only include data you absolutely need
716
+ 3. **Handle errors gracefully** - Custom computations can fail
717
+
718
+ **Remember:** The power of interaqt comes from its declarative computations. Custom computation breaks this paradigm and should only be used when absolutely necessary. Always try to express your logic using the standard computation types first!
719
+
720
+ ## Implementation Steps
721
+
722
+ ### Step 1: Entity Creation Pattern
723
+ ```typescript
724
+ // 1. Define interaction
725
+ export const CreateStyle = Interaction.create({
726
+ name: 'CreateStyle',
727
+ action: Action.create({ name: 'createStyle' }),
728
+ payload: Payload.create({
729
+ items: [
730
+ PayloadItem.create({ name: 'label', required: true }),
731
+ PayloadItem.create({ name: 'slug', required: true })
732
+ ]
733
+ })
734
+ });
735
+
736
+ // 2. Entity with Transform
737
+ export const Style = Entity.create({
738
+ name: 'Style',
739
+ properties: [
740
+ Property.create({ name: 'label', type: 'string' }),
741
+ Property.create({ name: 'slug', type: 'string' }),
742
+ Property.create({
743
+ name: 'status',
744
+ type: 'string',
745
+ defaultValue: () => 'draft'
746
+ })
747
+ ]
748
+ });
749
+ Style.computation = Transform.create({
750
+ record: InteractionEventEntity,
751
+ callback: (event) => {
752
+ if (event.interactionName === 'CreateStyle') {
753
+ return {
754
+ label: event.payload.label,
755
+ slug: event.payload.slug,
756
+ status: 'draft',
757
+ createdAt: Math.floor(Date.now()/1000)
758
+ };
759
+ }
760
+ return null;
761
+ }
762
+ });
763
+ ```
764
+
765
+ ### Step 2: Update Pattern with StateMachine
766
+ ```typescript
767
+ // Update interaction
768
+ export const UpdateStyle = Interaction.create({
769
+ name: 'UpdateStyle',
770
+ action: Action.create({ name: 'updateStyle' }),
771
+ payload: Payload.create({
772
+ items: [
773
+ PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true }),
774
+ PayloadItem.create({ name: 'label' }),
775
+ PayloadItem.create({ name: 'description' })
776
+ ]
777
+ })
778
+ });
779
+
780
+ // Property with update tracking
781
+ const updatedState = StateNode.create({
782
+ name: 'updated',
783
+ computeValue: () => Math.floor(Date.now()/1000)
784
+ });
785
+
786
+ const updatedAtProperty = Property.create({
787
+ name: 'updatedAt',
788
+ type: 'number'
789
+ });
790
+ updatedAtProperty.computation = StateMachine.create({
791
+ states: [updatedState],
792
+ defaultState: updatedState,
793
+ transfers: [
794
+ StateTransfer.create({
795
+ current: updatedState,
796
+ next: updatedState,
797
+ trigger: UpdateStyle,
798
+ computeTarget: (event) => ({ id: event.payload.id })
799
+ })
800
+ ]
801
+ });
802
+ ```
803
+
804
+ ### Step 3: Soft Delete Pattern
805
+ ```typescript
806
+ // Delete as state transition
807
+ const DeleteStyle = Interaction.create({
808
+ name: 'DeleteStyle',
809
+ action: Action.create({ name: 'deleteStyle' }),
810
+ payload: Payload.create({
811
+ items: [
812
+ PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
813
+ ]
814
+ })
815
+ });
816
+
817
+ // Declare states first
818
+ const activeState = StateNode.create({ name: 'active' });
819
+ const offlineState = StateNode.create({ name: 'offline' });
820
+
821
+ // Status property handles soft delete
822
+ const statusProperty = Property.create({
823
+ name: 'status',
824
+ type: 'string'
825
+ });
826
+ statusProperty.computation = StateMachine.create({
827
+ states: [activeState, offlineState],
828
+ defaultState: activeState,
829
+ transfers: [
830
+ StateTransfer.create({
831
+ current: activeState,
832
+ next: offlineState,
833
+ trigger: DeleteStyle,
834
+ computeTarget: (event) => ({ id: event.payload.id })
835
+ })
836
+ ]
837
+ });
838
+ ```
839
+
840
+ ## Critical Rules
841
+
842
+ ### ✅ DO
843
+ - Use Transform ONLY in Entity/Relation computation property
844
+ - Use StateMachine for updates and state management
845
+ - Use computed/getValue for simple property calculations
846
+ - Keep computations pure and side-effect free
847
+ - Declare StateNode variables before using them in StateMachine
848
+ - Use Interaction instance references (not strings) as trigger in StateTransfer
849
+
850
+ ### ❌ DON'T
851
+ - Never use Transform in Property computation
852
+ - **Don't use both defaultValue and computation on the same property** - they are mutually exclusive!
853
+ - Don't create circular dependencies
854
+ - Don't perform side effects in computations
855
+ - Don't access external services in computations
856
+ - Don't manually update computed values
857
+ - Don't create StateNode inside StateTransfer
858
+ - **Don't use strings as trigger in StateTransfer** - always use Interaction instance references
859
+
860
+ ## Common Patterns
861
+
862
+ ### 🔴 CRITICAL: Timestamp Tracking - Always Use Seconds!
863
+
864
+ **The database does NOT support millisecond precision. You MUST use `Math.floor(Date.now()/1000)` to convert milliseconds to seconds.**
865
+
866
+ ```typescript
867
+ // ❌ WRONG: Using milliseconds directly
868
+ Property.create({
869
+ name: 'createdAt',
870
+ type: 'number',
871
+ defaultValue: () => Date.now() // ERROR! Returns milliseconds, but database only supports seconds!
872
+ })
873
+
874
+ // ✅ CORRECT: Convert to seconds
875
+ Property.create({
876
+ name: 'createdAt',
877
+ type: 'number',
878
+ defaultValue: () => Math.floor(Date.now()/1000) // Correct! Unix timestamp in seconds
879
+ })
880
+
881
+ // ✅ CORRECT: Created at - set once (in seconds)
882
+ Property.create({
883
+ name: 'createdAt',
884
+ type: 'number',
885
+ defaultValue: () => Math.floor(Date.now()/1000)
886
+ })
887
+
888
+ // ✅ CORRECT: Updated at - updates on changes (in seconds)
889
+ const updatedState = StateNode.create({
890
+ name: 'updated',
891
+ computeValue: () => Math.floor(Date.now()/1000) // Always convert to seconds!
892
+ });
893
+
894
+ const updatedAtProperty = Property.create({
895
+ name: 'updatedAt',
896
+ type: 'number'
897
+ });
898
+ updatedAtProperty.computation = StateMachine.create({
899
+ states: [updatedState],
900
+ defaultState: updatedState,
901
+ transfers: [
902
+ StateTransfer.create({
903
+ current: updatedState,
904
+ next: updatedState,
905
+ trigger: UpdateInteraction,
906
+ computeTarget: (event) => ({ id: event.payload.id })
907
+ })
908
+ ]
909
+ });
910
+ ```
911
+
912
+ ### Version Management
913
+ ```typescript
914
+ // Create version from style
915
+ export const PublishStyle = Interaction.create({
916
+ name: 'PublishStyle',
917
+ action: Action.create({ name: 'publishStyle' }),
918
+ payload: Payload.create({
919
+ items: [
920
+ PayloadItem.create({ name: 'styleId', base: Style, isRef: true, required: true })
921
+ ]
922
+ })
923
+ });
924
+
925
+ export const Version = Entity.create({
926
+ name: 'Version',
927
+ properties: [
928
+ Property.create({ name: 'version', type: 'number' }),
929
+ Property.create({ name: 'publishedAt', type: 'bigint' }),
930
+ Property.create({ name: 'isActive', type: 'boolean', defaultValue: () => true })
931
+ ]
932
+ });
933
+ Version.computation = Transform.create({
934
+ record: InteractionEventEntity,
935
+ attributeQuery: ['interactionName', 'payload', 'user', 'createdAt'],
936
+ dataDeps: {
937
+ styles: {
938
+ type: 'records',
939
+ source: Style,
940
+ attributeQuery: ['*']
941
+ }
942
+ },
943
+ callback: function(event, dataDeps) {
944
+ if (event.interactionName === 'PublishStyle') {
945
+ const style = dataDeps.styles.find(s => s.id === event.payload.styleId);
946
+ if (style) {
947
+ return {
948
+ version: Math.floor(Date.now()/1000), // Version number in seconds
949
+ publishedAt: Math.floor(Date.now()/1000), // In seconds
950
+ isActive: true,
951
+ publishedBy: event.user,
952
+ style: { id: style.id }
953
+ };
954
+ }
955
+ }
956
+ return null;
957
+ }
958
+ });
959
+ ```