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,3378 @@
1
+ # Chapter 13: API Reference
2
+
3
+ This chapter provides detailed reference documentation for all core APIs in the interaqt framework, including complete parameter descriptions, type definitions, and usage examples.
4
+
5
+ ## 13.1 Entity-Related APIs
6
+
7
+ ### Entity.create()
8
+
9
+ Create entity definition. Entities are the basic units of data in the system.
10
+
11
+ **Syntax**
12
+ ```typescript
13
+ Entity.create(config: EntityConfig): EntityInstance
14
+ ```
15
+
16
+ **Parameters**
17
+ - `config.name` (string, required): Entity name, must match `/^[a-zA-Z0-9_]+$/` format
18
+ - `config.properties` (Property[], required): Entity property list, defaults to empty array
19
+ - `config.computation` (Computation[], optional): Entity-level computed data
20
+ - `config.baseEntity` (Entity|Relation, optional): Base entity for filtered entity (used to create filtered entities)
21
+ - `config.matchExpression` (MatchExp, optional): Match expression (used to create filtered entities)
22
+
23
+ **Examples**
24
+ ```typescript
25
+ // Create basic entity
26
+ const User = Entity.create({
27
+ name: 'User',
28
+ properties: [
29
+ Property.create({ name: 'username', type: 'string' }),
30
+ Property.create({ name: 'email', type: 'string' })
31
+ ]
32
+ })
33
+
34
+ // Create filtered entity
35
+ const ActiveUser = Entity.create({
36
+ name: 'ActiveUser',
37
+ baseEntity: User,
38
+ matchExpression: MatchExp.atom({
39
+ key: 'status',
40
+ value: ['=', 'active']
41
+ })
42
+ })
43
+ ```
44
+
45
+ **Filtered Entities**
46
+
47
+ Filtered entities are views of existing entities that automatically filter records based on specified conditions. They support:
48
+
49
+ 1. **Cascade Filtering**: Filtered entities can be used as `baseEntity` to create new filtered entities:
50
+ ```typescript
51
+ // First level filter
52
+ const ActiveUser = Entity.create({
53
+ name: 'ActiveUser',
54
+ baseEntity: User,
55
+ matchExpression: MatchExp.atom({
56
+ key: 'isActive',
57
+ value: ['=', true]
58
+ })
59
+ })
60
+
61
+ // Second level filter - based on ActiveUser
62
+ const TechActiveUser = Entity.create({
63
+ name: 'TechActiveUser',
64
+ baseEntity: ActiveUser, // Using filtered entity as base
65
+ matchExpression: MatchExp.atom({
66
+ key: 'department',
67
+ value: ['=', 'Tech']
68
+ })
69
+ })
70
+
71
+ // Third level filter - even more specific
72
+ const SeniorTechActiveUser = Entity.create({
73
+ name: 'SeniorTechActiveUser',
74
+ baseEntity: TechActiveUser,
75
+ matchExpression: MatchExp.atom({
76
+ key: 'role',
77
+ value: ['=', 'senior']
78
+ })
79
+ })
80
+ ```
81
+
82
+ 2. **Complex Conditions**: Use boolean expressions for complex filtering:
83
+ ```typescript
84
+ const TechYoungUser = Entity.create({
85
+ name: 'TechYoungUser',
86
+ baseEntity: User,
87
+ matchExpression: MatchExp.atom({
88
+ key: 'age',
89
+ value: ['<', 30]
90
+ }).and({
91
+ key: 'department',
92
+ value: ['=', 'Tech']
93
+ })
94
+ })
95
+ ```
96
+
97
+ 3. **Automatic Updates**: When source entity records are created, updated, or deleted, filtered entities automatically reflect these changes based on their match expressions.
98
+
99
+ ### Property.create()
100
+
101
+ Create entity property definition.
102
+
103
+ **Syntax**
104
+ ```typescript
105
+ Property.create(config: PropertyConfig): PropertyInstance
106
+ ```
107
+
108
+ **Parameters**
109
+ - `config.name` (string, required): Property name, must be 1-5 characters long
110
+ - `config.type` (string, required): Property type, options: 'string' | 'number' | 'boolean'
111
+ - `config.collection` (boolean, optional): Whether it's a collection type
112
+ - `config.defaultValue` (function, optional): Default value function
113
+ - `config.computed` (function, optional): Computed property function
114
+ - `config.computation` (Computation, optional): Property computed data
115
+
116
+ **⚠️ IMPORTANT: Timestamp Properties**
117
+ When creating timestamp properties with `defaultValue`, **always convert milliseconds to seconds** using `Math.floor(Date.now()/1000)`:
118
+ - ❌ WRONG: `defaultValue: () => Date.now()` - Returns milliseconds, database doesn't support this!
119
+ - ✅ CORRECT: `defaultValue: () => Math.floor(Date.now()/1000)` - Returns Unix timestamp in seconds
120
+
121
+ **Examples**
122
+ ```typescript
123
+ // Basic property
124
+ const username = Property.create({
125
+ name: 'username',
126
+ type: 'string'
127
+ })
128
+
129
+ // Property with default value - timestamp
130
+ // 🔴 CRITICAL: Always use seconds for timestamps, not milliseconds!
131
+ const createdAt = Property.create({
132
+ name: 'createdAt',
133
+ type: 'number',
134
+ defaultValue: () => Math.floor(Date.now()/1000) // Convert to seconds - database doesn't support milliseconds!
135
+ })
136
+
137
+ // Computed property
138
+ const fullName = Property.create({
139
+ name: 'fullName',
140
+ type: 'string',
141
+ computed: function(user) {
142
+ return `${user.firstName} ${user.lastName}`
143
+ }
144
+ })
145
+
146
+ // Property with reactive computation
147
+ const postCount = Property.create({
148
+ name: 'postCount',
149
+ type: 'number',
150
+ computation: Count.create({
151
+ property: 'posts' // Use property name from relation
152
+ })
153
+ })
154
+ ```
155
+
156
+ ### HardDeletionProperty.create()
157
+
158
+ Create a special property that triggers physical deletion of records when set to true.
159
+
160
+ **Syntax**
161
+ ```typescript
162
+ HardDeletionProperty.create(config?: HardDeletionPropertyConfig): PropertyInstance
163
+ ```
164
+
165
+ **Parameters**
166
+ - `config.name` (string, optional): Property name, defaults to `'_isDeleted_'`
167
+
168
+ **Usage with StateMachine**
169
+
170
+ HardDeletionProperty is typically used with StateMachine to manage record deletion:
171
+
172
+ ```typescript
173
+ import { DELETED_STATE, NON_DELETED_STATE } from 'interaqt'
174
+
175
+ // Define deletion StateMachine for the HardDeletionProperty
176
+ const deletionStateMachine = StateMachine.create({
177
+ states: [NON_DELETED_STATE, DELETED_STATE],
178
+ defaultState: NON_DELETED_STATE,
179
+ transfers: [
180
+ StateTransfer.create({
181
+ trigger: DeleteUserInteraction,
182
+ current: NON_DELETED_STATE,
183
+ next: DELETED_STATE,
184
+ computeTarget: function(event) {
185
+ return { id: event.payload.userId }
186
+ }
187
+ })
188
+ ]
189
+ })
190
+
191
+ const hardDeletionProperty = HardDeletionProperty.create()
192
+ hardDeletionProperty.computation = deletionStateMachine
193
+
194
+ // Add HardDeletionProperty to entity
195
+ const User = Entity.create({
196
+ name: 'User',
197
+ properties: [
198
+ Property.create({ name: 'name', type: 'string' }),
199
+ Property.create({ name: 'email', type: 'string' }),
200
+ // Add HardDeletionProperty with deletion StateMachine
201
+ hardDeletionProperty
202
+ ]
203
+ })
204
+ ```
205
+
206
+ **How It Works**
207
+ 1. When the property value transitions to `DELETED_STATE` (true), the Controller automatically deletes the record from storage
208
+ 2. The deletion is physical - the record is completely removed from the database
209
+ 3. This is different from soft deletion where records remain but are marked as deleted
210
+
211
+ **Example: Entity Creation and Deletion Pattern**
212
+
213
+ ```typescript
214
+ // Use Transform for entity creation
215
+ const Article = Entity.create({
216
+ name: 'Article',
217
+ properties: [
218
+ Property.create({ name: 'title', type: 'string' }),
219
+ Property.create({ name: 'content', type: 'string' }),
220
+ HardDeletionProperty.create()
221
+ ],
222
+ // Transform creates new articles from interaction events
223
+ computation: Transform.create({
224
+ record: InteractionEventEntity,
225
+ callback: function(event) {
226
+ if (event.interactionName === 'CreateArticle') {
227
+ return {
228
+ title: event.payload.title,
229
+ content: event.payload.content
230
+ }
231
+ }
232
+ return null
233
+ }
234
+ })
235
+ })
236
+
237
+ // Configure deletion for the HardDeletionProperty
238
+ const deletionProperty = HardDeletionProperty.create()
239
+ deletionProperty.computation = StateMachine.create({
240
+ states: [NON_DELETED_STATE, DELETED_STATE],
241
+ defaultState: NON_DELETED_STATE,
242
+ transfers: [
243
+ StateTransfer.create({
244
+ trigger: DeleteArticleInteraction,
245
+ current: NON_DELETED_STATE,
246
+ next: DELETED_STATE,
247
+ computeTarget: function(event) {
248
+ return { id: event.payload.articleId }
249
+ }
250
+ })
251
+ ]
252
+ })
253
+
254
+ Article.properties.push(deletionProperty)
255
+ ```
256
+
257
+ ### Relation.create()
258
+
259
+ Create relationship definition between entities or create filtered views of existing relations.
260
+
261
+ **Syntax**
262
+ ```typescript
263
+ Relation.create(config: RelationConfig): RelationInstance
264
+ ```
265
+
266
+ **Two Types of Relations**
267
+ 1. **Base Relations**: Define direct relationships between entities (requires `source`, `target`, `type`)
268
+ 2. **Filtered Relations**: Create filtered views of existing relations (requires `baseRelation`, `matchExpression`)
269
+
270
+ **Important: Auto-Generated Relation Names**
271
+
272
+ If you did not specify a `name` property when creating relations, The framework will automatically generates the relation name based on the source and target entities. For example:
273
+ - A relation between `User` and `Post` → automatically named `UserPost`
274
+ - A relation between `Post` and `Comment` → automatically named `PostComment`
275
+
276
+ **Parameters**
277
+ - `config.name` (string, optional): Relation name. If not specified, will be auto-generated from source and target entity names
278
+ - `config.source` (Entity|Relation, required for base relations): Source entity of the relationship
279
+ - `config.sourceProperty` (string, required): Relationship property name in source entity
280
+ - `config.target` (Entity|Relation, required for base relations): Target entity of the relationship
281
+ - `config.targetProperty` (string, required): Relationship property name in target entity
282
+ - `config.type` (string, required for base relations): Relationship type, options: '1:1' | '1:n' | 'n:1' | 'n:n'
283
+ - `config.properties` (Property[], optional): Properties of the relationship itself
284
+ - `config.computation` (Computation, optional): Relationship-level computed data
285
+ - `config.baseRelation` (Relation, required for filtered relations): Base relation to filter from
286
+ - `config.matchExpression` (MatchExp, required for filtered relations): Filter condition for the relation records
287
+
288
+
289
+ **Note on Symmetric Relations**: The system automatically detects symmetric relations when `source === target` AND `sourceProperty === targetProperty`. There is no need to specify a `symmetric` parameter.
290
+
291
+ **Examples**
292
+ ```typescript
293
+ // One-to-many relationship
294
+ const UserPostRelation = Relation.create({
295
+ source: User,
296
+ sourceProperty: 'posts',
297
+ target: Post,
298
+ targetProperty: 'author',
299
+ type: '1:n'
300
+ })
301
+
302
+ // Many-to-many relationship
303
+ const UserTagRelation = Relation.create({
304
+ source: User,
305
+ sourceProperty: 'tags',
306
+ target: Tag,
307
+ targetProperty: 'users',
308
+ type: 'n:n'
309
+ })
310
+
311
+ // Symmetric relationship (friendship)
312
+ // Note: System detects symmetric relations automatically when source === target AND sourceProperty === targetProperty
313
+ const FriendRelation = Relation.create({
314
+ source: User,
315
+ sourceProperty: 'friends',
316
+ target: User,
317
+ targetProperty: 'friends', // Same as sourceProperty - automatically symmetric
318
+ type: 'n:n'
319
+ })
320
+
321
+ // Relationship with properties
322
+ const UserRoleRelation = Relation.create({
323
+ source: User,
324
+ sourceProperty: 'roles',
325
+ target: Role,
326
+ targetProperty: 'users',
327
+ type: 'n:n',
328
+ properties: [
329
+ Property.create({ name: 'assignedAt', type: 'string' }),
330
+ Property.create({ name: 'isActive', type: 'boolean' })
331
+ ]
332
+ })
333
+ ```
334
+
335
+ **Filtered Relations**
336
+
337
+ Filtered relations are views of existing relations that automatically filter relationship records based on specified conditions. They support:
338
+
339
+ 1. **Basic Filtered Relations**: Create filtered views based on relation properties:
340
+ ```typescript
341
+ // Base relation with properties
342
+ const UserPostRelation = Relation.create({
343
+ source: User,
344
+ sourceProperty: 'posts',
345
+ target: Post,
346
+ targetProperty: 'author',
347
+ type: '1:n',
348
+ properties: [
349
+ Property.create({ name: 'isPublished', type: 'boolean' }),
350
+ Property.create({ name: 'priority', type: 'string' })
351
+ ]
352
+ })
353
+
354
+ // Filtered relation - only published posts
355
+ const PublishedUserPostRelation = Relation.create({
356
+ name: 'PublishedUserPostRelation',
357
+ baseRelation: UserPostRelation,
358
+ sourceProperty: 'publishedPosts',
359
+ targetProperty: 'publishedAuthor',
360
+ matchExpression: MatchExp.atom({
361
+ key: 'isPublished',
362
+ value: ['=', true]
363
+ })
364
+ })
365
+ ```
366
+
367
+ 2. **Cascade Filtering**: Filtered relations can be used as `baseRelation` to create new filtered relations:
368
+ ```typescript
369
+ // First level filter - active assignments
370
+ const ActiveUserProjectRelation = Relation.create({
371
+ name: 'ActiveUserProjectRelation',
372
+ baseRelation: UserProjectRelation,
373
+ sourceProperty: 'activeProjects',
374
+ targetProperty: 'activeUsers',
375
+ matchExpression: MatchExp.atom({
376
+ key: 'isActive',
377
+ value: ['=', true]
378
+ })
379
+ })
380
+
381
+ // Second level filter - only lead roles from active assignments
382
+ const LeadUserProjectRelation = Relation.create({
383
+ name: 'LeadUserProjectRelation',
384
+ baseRelation: ActiveUserProjectRelation, // Using filtered relation as base
385
+ sourceProperty: 'leadProjects',
386
+ targetProperty: 'leadUsers',
387
+ matchExpression: MatchExp.atom({
388
+ key: 'role',
389
+ value: ['=', 'lead']
390
+ })
391
+ })
392
+ ```
393
+
394
+ 3. **Complex Filter Conditions**: Combine multiple conditions:
395
+ ```typescript
396
+ const ImportantActiveRelation = Relation.create({
397
+ name: 'ImportantActiveRelation',
398
+ baseRelation: UserTaskRelation,
399
+ sourceProperty: 'importantActiveTasks',
400
+ targetProperty: 'assignedToImportant',
401
+ matchExpression: MatchExp.atom({
402
+ key: 'priority',
403
+ value: ['=', 'high']
404
+ }).and({
405
+ key: 'status',
406
+ value: ['=', 'active']
407
+ })
408
+ })
409
+ ```
410
+
411
+ 4. **Automatic Updates**: When source relation records are created, updated, or deleted, filtered relations automatically reflect these changes based on their match expressions. For example, if a relation's `isActive` property is updated from `false` to `true`, it will automatically appear in the corresponding filtered relation.
412
+
413
+ ## 13.2 Computation-Related APIs
414
+
415
+ ### Count.create()
416
+
417
+ Create count computation for counting records.
418
+
419
+ **Syntax**
420
+ ```typescript
421
+ Count.create(config: CountConfig): CountInstance
422
+ ```
423
+
424
+ **Parameters**
425
+ - `config.record` (Entity|Relation, optional): Entity or relation to count (for entity/global level computation)
426
+ - `config.property` (string, optional): Property name from relation (for property level computation)
427
+ - `config.direction` (string, optional): Relationship direction, options: 'source' | 'target', only for relation counting
428
+ - `config.callback` (function, optional): Filter callback function, returns boolean to decide if included in count
429
+ - `config.attributeQuery` (AttributeQueryData, optional): Attribute query configuration to optimize data fetching
430
+ - `config.dataDeps` (object, optional): Data dependency configuration, format: `{[key: string]: DataDep}`
431
+
432
+ **Examples**
433
+ ```typescript
434
+ // Basic global count
435
+ const totalUsers = Count.create({
436
+ record: User
437
+ })
438
+
439
+ // Basic property count (user's post count)
440
+ const userPostCount = Property.create({
441
+ name: 'postCount',
442
+ type: 'number',
443
+ computation: Count.create({
444
+ property: 'posts' // Use property name from relation
445
+ })
446
+ })
447
+
448
+ // Count with filter condition (only count published posts)
449
+ const publishedPostCount = Property.create({
450
+ name: 'publishedPostCount',
451
+ type: 'number',
452
+ computation: Count.create({
453
+ property: 'posts', // Use property name from relation
454
+ attributeQuery: ['status'], // Query properties on related entity
455
+ callback: function(post) {
456
+ return post.status === 'published'
457
+ }
458
+ })
459
+ })
460
+
461
+ // Count with data dependencies (filter based on global minimum score setting)
462
+ const minScoreThreshold = Dictionary.create({
463
+ name: 'minScoreThreshold',
464
+ type: 'number',
465
+ collection: false
466
+ })
467
+ const highScorePostCount = Property.create({
468
+ name: 'highScorePostCount',
469
+ type: 'number',
470
+ computation: Count.create({
471
+ property: 'posts', // Use property name from relation
472
+ attributeQuery: ['score'], // Query properties on related entity
473
+ dataDeps: {
474
+ minScore: {
475
+ type: 'global',
476
+ source: minScoreThreshold
477
+ }
478
+ },
479
+ callback: function(post, dataDeps) {
480
+ return post.score >= dataDeps.minScore
481
+ }
482
+ })
483
+ })
484
+
485
+ // Global count with filter and data dependencies
486
+ const userActiveDays = Dictionary.create({
487
+ name: 'userActiveDays',
488
+ type: 'number',
489
+ collection: false
490
+ })
491
+ const activeUsersCount = Dictionary.create({
492
+ name: 'activeUsersCount',
493
+ type: 'number',
494
+ collection: false,
495
+ computation: Count.create({
496
+ record: User,
497
+ attributeQuery: ['lastLoginDate'],
498
+ dataDeps: {
499
+ activeDays: {
500
+ type: 'global',
501
+ source: userActiveDays
502
+ }
503
+ },
504
+ callback: function(user, dataDeps) {
505
+ const daysSinceLogin = (Date.now() - new Date(user.lastLoginDate).getTime()) / (1000 * 60 * 60 * 24)
506
+ return daysSinceLogin <= dataDeps.activeDays
507
+ }
508
+ })
509
+ })
510
+
511
+ // Relation count with direction parameter
512
+ const authorPostCount = Property.create({
513
+ name: 'authoredPostCount',
514
+ type: 'number',
515
+ computation: Count.create({
516
+ property: 'posts', // Use property name from relation
517
+ })
518
+ })
519
+ ```
520
+
521
+ ### WeightedSummation.create()
522
+
523
+ Create weighted summation computation.
524
+
525
+ **Syntax**
526
+ ```typescript
527
+ WeightedSummation.create(config: WeightedSummationConfig): WeightedSummationInstance
528
+ ```
529
+
530
+ **Parameters**
531
+ - `config.record` (Entity|Relation, optional): Entity or relation to compute (for entity/global level computation)
532
+ - `config.property` (string, optional): Property name from relation (for property level computation)
533
+ - `config.callback` (function, required): Callback function to calculate weight and value, returns `{weight: number, value: number}`
534
+ - `config.attributeQuery` (AttributeQueryData, required): Attribute query configuration
535
+
536
+ **Examples**
537
+ ```typescript
538
+ // Calculate user total score
539
+ const userTotalScore = Property.create({
540
+ name: 'totalScore',
541
+ type: 'number',
542
+ computation: WeightedSummation.create({
543
+ property: 'scores', // Use property name from relation
544
+ callback: function(scoreRecord) {
545
+ return {
546
+ weight: scoreRecord.multiplier || 1,
547
+ value: scoreRecord.points
548
+ }
549
+ }
550
+ })
551
+ })
552
+
553
+ // Global weighted summation
554
+ const globalWeightedScore = WeightedSummation.create({
555
+ record: ScoreRecord,
556
+ callback: function(record) {
557
+ return {
558
+ weight: record.difficulty,
559
+ value: record.score
560
+ }
561
+ }
562
+ })
563
+ ```
564
+
565
+ ### Summation.create()
566
+
567
+ Create summation computation for summing specified fields.
568
+
569
+ **Syntax**
570
+ ```typescript
571
+ Summation.create(config: SummationConfig): SummationInstance
572
+ ```
573
+
574
+ **Parameters**
575
+ - `config.record` (Entity|Relation, optional): Entity or relation to compute (for entity/global level computation)
576
+ - `config.property` (string, optional): Property name from relation (for property level computation)
577
+ - `config.attributeQuery` (AttributeQueryData, required): Attribute query configuration, specifies field path to sum
578
+ - `config.direction` (string, optional): Relationship direction, options: 'source' | 'target', only for relation summation
579
+
580
+ **How it works**
581
+
582
+ Summation sums the field pointed to by the leftmost path in `attributeQuery`. If any value in the path is `undefined`, `null`, `NaN`, or `Infinity`, that value will be treated as 0.
583
+
584
+ **Examples**
585
+ ```typescript
586
+ // Basic global summation (sum all transaction amounts)
587
+ const totalRevenue = Dictionary.create({
588
+ name: 'totalRevenue',
589
+ type: 'number',
590
+ collection: false,
591
+ computation: Summation.create({
592
+ record: Transaction,
593
+ attributeQuery: ['amount']
594
+ })
595
+ })
596
+
597
+ // Property-level summation (calculate user's total order amount)
598
+ const userTotalSpent = Property.create({
599
+ name: 'totalSpent',
600
+ type: 'number',
601
+ computation: Summation.create({
602
+ property: 'orders', // Use property name from relation
603
+ attributeQuery: ['totalAmount'] // Query properties on related entity
604
+ })
605
+ })
606
+
607
+ // Nested path summation (sum nested fields of related entities)
608
+ const departmentBudget = Property.create({
609
+ name: 'totalBudget',
610
+ type: 'number',
611
+ computation: Summation.create({
612
+ property: 'projects', // Use property name from relation
613
+ attributeQuery: [['budget', {
614
+ attributeQuery: ['allocatedAmount']
615
+ }]] // Query nested properties on related entity
616
+ })
617
+ })
618
+
619
+ // Direct summation of relation properties
620
+ const totalShippingCost = Property.create({
621
+ name: 'totalShippingCost',
622
+ type: 'number',
623
+ computation: Summation.create({
624
+ property: 'shipments', // Use property name from relation
625
+ attributeQuery: [['&', {attributeQuery: ['shippingFee']}]] // Access relation's own property with '&'
626
+ })
627
+ })
628
+
629
+ // Global summation handling missing values
630
+ const totalBalance = Dictionary.create({
631
+ name: 'totalBalance',
632
+ type: 'number',
633
+ collection: false,
634
+ computation: Summation.create({
635
+ record: Account,
636
+ attributeQuery: ['balance'] // null or undefined values treated as 0
637
+ })
638
+ })
639
+ ```
640
+
641
+ **Working with Other Computations**
642
+
643
+ If you need complex summation logic (like conditional filtering, data transformation etc.), you can first use other computations (like Transform) to calculate the needed values on records, then use Summation for simple summing:
644
+
645
+ ```typescript
646
+ // First use Transform to calculate discounted price
647
+ const OrderItem = Entity.create({
648
+ name: 'OrderItem',
649
+ properties: [
650
+ Property.create({ name: 'price', type: 'number' }),
651
+ Property.create({ name: 'quantity', type: 'number' }),
652
+ Property.create({ name: 'discountRate', type: 'number' }),
653
+ Property.create({
654
+ name: 'finalPrice',
655
+ type: 'number',
656
+ computed: function(item) {
657
+ const subtotal = (item.price || 0) * (item.quantity || 0);
658
+ const discount = subtotal * (item.discountRate || 0);
659
+ return subtotal - discount;
660
+ }
661
+ })
662
+ ]
663
+ });
664
+
665
+ // Then use Summation to sum computed values
666
+ const orderTotal = Property.create({
667
+ name: 'total',
668
+ type: 'number',
669
+ computation: Summation.create({
670
+ property: 'items', // Use property name from relation
671
+ attributeQuery: ['finalPrice'] // Query properties on related entity
672
+ })
673
+ });
674
+ ```
675
+
676
+ ### Every.create()
677
+
678
+ Create boolean computation that checks if all records meet a condition.
679
+
680
+ **Syntax**
681
+ ```typescript
682
+ Every.create(config: EveryConfig): EveryInstance
683
+ ```
684
+
685
+ **Parameters**
686
+ - `config.record` (Entity|Relation, optional): Entity or relation to check (for entity/global level computation)
687
+ - `config.property` (string, optional): Property name from relation (for property level computation)
688
+ - `config.callback` (function, required): Condition check function, returns boolean
689
+ - `config.attributeQuery` (AttributeQueryData, required): Attribute query configuration
690
+ - `config.notEmpty` (boolean, optional): Return value when collection is empty
691
+
692
+ **Examples**
693
+ ```typescript
694
+ // Check if user completed all required courses
695
+ const completedAllRequired = Property.create({
696
+ name: 'completedAllRequired',
697
+ type: 'boolean',
698
+ computation: Every.create({
699
+ property: 'courses', // Use property name from relation
700
+ attributeQuery: [['&', {attributeQuery: ['status']}]], // Access relation's own property with '&'
701
+ callback: function(courseRelation) {
702
+ return courseRelation['&'].status === 'completed'
703
+ },
704
+ notEmpty: false
705
+ })
706
+ })
707
+ ```
708
+
709
+ ### Any.create()
710
+
711
+ Create boolean computation that checks if any record meets a condition.
712
+
713
+ **Syntax**
714
+ ```typescript
715
+ Any.create(config: AnyConfig): AnyInstance
716
+ ```
717
+
718
+ **Parameters**
719
+ - `config.record` (Entity|Relation, optional): Entity or relation to check (for entity/global level computation)
720
+ - `config.property` (string, optional): Property name from relation (for property level computation)
721
+ - `config.callback` (function, required): Condition check function, returns boolean
722
+ - `config.attributeQuery` (AttributeQueryData, required): Attribute query configuration
723
+
724
+ **Examples**
725
+ ```typescript
726
+ // Check if user has any pending tasks
727
+ const hasPendingTasks = Property.create({
728
+ name: 'hasPendingTasks',
729
+ type: 'boolean',
730
+ computation: Any.create({
731
+ property: 'tasks', // Use property name from relation
732
+ attributeQuery: [['&', {attributeQuery: ['status']}]], // Access relation's own property with '&'
733
+ callback: function(taskRelation) {
734
+ return taskRelation['&'].status === 'pending'
735
+ }
736
+ })
737
+ })
738
+ ```
739
+
740
+ ### Transform.create()
741
+
742
+ Create custom transformation computation.
743
+
744
+ Transform is fundamentally about **transforming data from one collection to another collection**. It transforms sets of data (e.g., InteractionEventEntity → Entity/Relation, Entity → different Entity). Transform **cannot** be used for property computations within the same entity - use `getValue` for that purpose.
745
+
746
+ **Important: One-to-Many Transformations**
747
+ Transform callbacks can return either:
748
+ - A single object → creates one record
749
+ - An array of objects → creates multiple records from a single source
750
+ - `null` or `undefined` → creates no records
751
+
752
+ **Syntax**
753
+ ```typescript
754
+ Transform.create(config: TransformConfig): TransformInstance
755
+ ```
756
+
757
+ **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
761
+
762
+
763
+ **Key Points**
764
+
765
+ 1. **Return Types**:
766
+ - Single object: Creates one target record
767
+ - Array of objects: Creates multiple target records from one source
768
+ - `null`/`undefined`: Creates no records (useful for filtering)
769
+
770
+ 2. **Automatic Updates**: When source records are updated or deleted, the transformed records are automatically updated or removed
771
+
772
+ ### StateMachine.create()
773
+
774
+ Create state machine computation that manages state transitions and computes values based on those transitions. Can be used for entity properties, relation properties, or global dictionaries.
775
+
776
+ **Syntax**
777
+ ```typescript
778
+ StateMachine.create(config: StateMachineConfig): StateMachineInstance
779
+ ```
780
+
781
+ **Parameters**
782
+ - `config.states` (StateNode[], required): List of state nodes
783
+ - `config.transfers` (StateTransfer[], required): List of state transfers
784
+ - `config.defaultState` (StateNode, required): Default state
785
+
786
+ **Important: Initial Value Handling**
787
+
788
+ When using StateMachine for entity/relation properties:
789
+ - If the property's initial value is set when the entity/relation is created, handle it in the entity/relation's computation
790
+ - If the StateMachine needs to save or modify this initial value, explicitly define `computeValue` on the `defaultState` to handle it
791
+
792
+ ```typescript
793
+ // Example: StateMachine handling initial value
794
+ const MyStateMachine = StateMachine.create({
795
+ states: [pendingState, activeState],
796
+ transfers: [...],
797
+ defaultState: StateNode.create({
798
+ name: 'pending',
799
+ computeValue: (lastValue) => {
800
+ // Explicitly handle initial value
801
+ return lastValue || 'initial_state_value';
802
+ }
803
+ })
804
+ });
805
+ ```
806
+
807
+ **Examples**
808
+ ```typescript
809
+ // First declare state nodes
810
+ const pendingState = StateNode.create({ name: 'pending' });
811
+ const confirmedState = StateNode.create({ name: 'confirmed' });
812
+ const shippedState = StateNode.create({ name: 'shipped' });
813
+ const deliveredState = StateNode.create({ name: 'delivered' });
814
+
815
+ const OrderStateMachine = StateMachine.create({
816
+ states: [pendingState, confirmedState, shippedState, deliveredState],
817
+ transfers: [
818
+ StateTransfer.create({
819
+ current: pendingState,
820
+ next: confirmedState,
821
+ trigger: ConfirmOrderInteraction
822
+ })
823
+ ],
824
+ defaultState: pendingState
825
+ });
826
+
827
+ // Relation with HardDeletionProperty for deletion management
828
+ const UserTargetRelation = Relation.create({
829
+ source: User,
830
+ sourceProperty: 'targets',
831
+ target: TargetEntity,
832
+ targetProperty: 'users',
833
+ type: 'n:n',
834
+ properties: [
835
+ Property.create({ name: 'createdAt', type: 'string' }),
836
+ Property.create({ name: 'isActive', type: 'boolean' }),
837
+ ],
838
+ // Use Transform to create relations
839
+ computation: Transform.create({
840
+ record: InteractionEventEntity,
841
+ callback: function(event) {
842
+ if (event.interactionName === 'CreateRelation') {
843
+ return {
844
+ source: event.payload.sourceUser,
845
+ target: event.payload.targetEntity,
846
+ createdAt: new Date().toISOString(),
847
+ isActive: true
848
+ }
849
+ }
850
+ return null
851
+ }
852
+ })
853
+ })
854
+
855
+ const relationDeletionProperty = HardDeletionProperty.create()
856
+
857
+ // Configure deletion for relation's HardDeletionProperty
858
+ relationDeletionProperty.computation = StateMachine.create({
859
+ states: [NON_DELETED_STATE, DELETED_STATE],
860
+ defaultState: NON_DELETED_STATE,
861
+ transfers: [
862
+ StateTransfer.create({
863
+ trigger: DeleteRelationInteraction,
864
+ current: NON_DELETED_STATE,
865
+ next: DELETED_STATE,
866
+ computeTarget: async function(this: Controller, event: any) {
867
+ const MatchExp = this.globals.MatchExp;
868
+ const relation = await this.system.storage.findOne(
869
+ UserTargetRelation.name,
870
+ MatchExp.atom({
871
+ key: 'source.id',
872
+ value: ['=', event.payload.sourceId]
873
+ }).and({
874
+ key: 'target.id',
875
+ value: ['=', event.payload.targetId]
876
+ }),
877
+ undefined,
878
+ ['id']
879
+ );
880
+ return relation ? { id: relation.id } : undefined;
881
+ }
882
+ })
883
+ ]
884
+ })
885
+
886
+ UserTargetRelation.properties.push(relationDeletionProperty)
887
+
888
+ ```
889
+
890
+ ### RealTime.create()
891
+
892
+ Create real-time computation for handling time-based reactive computations. Real-time computations automatically manage state (lastRecomputeTime and nextRecomputeTime) and adopt different scheduling strategies based on return type.
893
+
894
+ **Syntax**
895
+ ```typescript
896
+ RealTime.create(config: RealTimeConfig): RealTimeInstance
897
+ ```
898
+
899
+ **Parameters**
900
+ - `config.callback` (function, required): Real-time computation callback function, accepts `(now: Expression, dataDeps: any) => Expression | Inequality | Equation`
901
+ - `config.nextRecomputeTime` (function, optional): Recomputation interval function, accepts `(now: number, dataDeps: any) => number`, only valid for Expression type
902
+ - `config.dataDeps` (object, optional): Data dependency configuration, format: `{[key: string]: DataDep}`
903
+ - `config.attributeQuery` (AttributeQueryData, optional): Attribute query configuration
904
+
905
+ **Return Types and Scheduling Behavior**
906
+ - **Expression**: Returns numeric computation result, nextRecomputeTime = lastRecomputeTime + nextRecomputeTime function return value
907
+ - **Inequality**: Returns boolean comparison result, nextRecomputeTime = solve() result (critical time point for state change)
908
+ - **Equation**: Returns boolean equation result, nextRecomputeTime = solve() result (critical time point for state change)
909
+
910
+ **State Management**
911
+
912
+ RealTime computations automatically create and manage two state fields:
913
+ - `lastRecomputeTime`: Timestamp of last computation
914
+ - `nextRecomputeTime`: Timestamp of next computation
915
+
916
+ State field naming convention:
917
+ - Global computations: `_global_boundState_{computationName}_{stateName}`
918
+ - Property computations: `_record_boundState_{entityName}_{propertyName}_{stateName}`
919
+
920
+ **Examples**
921
+
922
+ ```typescript
923
+ // Expression type: manually specify recomputation interval
924
+ const currentTimestamp = Dictionary.create({
925
+ name: 'currentTimestamp',
926
+ type: 'number',
927
+ computation: RealTime.create({
928
+ nextRecomputeTime: (now: number, dataDeps: any) => 1000, // Update every second
929
+ callback: async (now: Expression, dataDeps: any) => {
930
+ return now.divide(1000); // Convert to seconds
931
+ }
932
+ })
933
+ });
934
+
935
+ // Inequality type: system automatically calculates critical time points
936
+ const isAfterDeadline = Dictionary.create({
937
+ name: 'isAfterDeadline',
938
+ type: 'boolean',
939
+ computation: RealTime.create({
940
+ dataDeps: {
941
+ project: {
942
+ type: 'records',
943
+ source: projectEntity,
944
+ attributeQuery: ['deadline']
945
+ }
946
+ },
947
+ callback: async (now: Expression, dataDeps: any) => {
948
+ const deadline = dataDeps.project?.[0]?.deadline || Date.now() + 86400000;
949
+ // System will automatically recompute at deadline time
950
+ return now.gt(deadline);
951
+ }
952
+ })
953
+ });
954
+
955
+ // Equation type: check time equations
956
+ const isExactHour = Dictionary.create({
957
+ name: 'isExactHour',
958
+ type: 'boolean',
959
+ computation: RealTime.create({
960
+ callback: async (now: Expression, dataDeps: any) => {
961
+ const millisecondsInHour = 3600000;
962
+ // System will automatically recompute at next exact hour
963
+ return now.modulo(millisecondsInHour).eq(0);
964
+ }
965
+ })
966
+ });
967
+
968
+ // Property-level real-time computation
969
+ const userEntity = Entity.create({
970
+ name: 'User',
971
+ properties: [
972
+ Property.create({ name: 'lastLoginAt', type: 'number' }),
973
+ Property.create({
974
+ name: 'isRecentlyActive',
975
+ type: 'boolean',
976
+ computation: RealTime.create({
977
+ dataDeps: {
978
+ _current: {
979
+ type: 'property',
980
+ attributeQuery: ['lastLoginAt']
981
+ }
982
+ },
983
+ callback: async (now: Expression, dataDeps: any) => {
984
+ const lastLogin = dataDeps._current?.lastLoginAt || 0;
985
+ const oneHourAgo = now.subtract(3600000);
986
+ return Expression.number(lastLogin).gt(oneHourAgo);
987
+ }
988
+ })
989
+ })
990
+ ]
991
+ });
992
+
993
+ // Complex data dependencies real-time computation
994
+ const businessMetrics = Dictionary.create({
995
+ name: 'businessMetrics',
996
+ type: 'object',
997
+ computation: RealTime.create({
998
+ nextRecomputeTime: (now: number, dataDeps: any) => 300000, // Update every 5 minutes
999
+ dataDeps: {
1000
+ config: {
1001
+ type: 'records',
1002
+ source: configEntity,
1003
+ attributeQuery: ['businessHourStart', 'businessHourEnd']
1004
+ },
1005
+ metrics: {
1006
+ type: 'records',
1007
+ source: metricsEntity,
1008
+ attributeQuery: ['dailyTarget', 'currentValue']
1009
+ }
1010
+ },
1011
+ callback: async (now: Expression, dataDeps: any) => {
1012
+ const config = dataDeps.config?.[0] || {};
1013
+ const metrics = dataDeps.metrics?.[0] || {};
1014
+
1015
+ const startHour = config.businessHourStart || 9;
1016
+ const endHour = config.businessHourEnd || 17;
1017
+ const currentHour = now.divide(3600000).modulo(24);
1018
+
1019
+ const isBusinessTime = currentHour.gt(startHour).and(currentHour.lt(endHour));
1020
+ const progressRate = Expression.number(metrics.currentValue || 0).divide(metrics.dailyTarget || 1);
1021
+
1022
+ return {
1023
+ isBusinessTime: isBusinessTime.evaluate({now: Date.now()}),
1024
+ progressRate: progressRate.evaluate({now: Date.now()}),
1025
+ timestamp: now.evaluate({now: Date.now()})
1026
+ };
1027
+ }
1028
+ })
1029
+ });
1030
+ ```
1031
+
1032
+ **State Access Example**
1033
+
1034
+ ```typescript
1035
+ // Get computation instance
1036
+ const realTimeComputation = Array.from(controller.scheduler.computations.values()).find(
1037
+ computation => computation.dataContext.type === 'global' &&
1038
+ computation.dataContext.id === 'currentTimestamp'
1039
+ );
1040
+
1041
+ // Get state key names
1042
+ const lastRecomputeTimeKey = controller.scheduler.getBoundStateName(
1043
+ realTimeComputation.dataContext,
1044
+ 'lastRecomputeTime',
1045
+ realTimeComputation.state.lastRecomputeTime
1046
+ );
1047
+
1048
+ const nextRecomputeTimeKey = controller.scheduler.getBoundStateName(
1049
+ realTimeComputation.dataContext,
1050
+ 'nextRecomputeTime',
1051
+ realTimeComputation.state.nextRecomputeTime
1052
+ );
1053
+
1054
+ // Read state values
1055
+ const lastRecomputeTime = await system.storage.get(DICTIONARY_RECORD, lastRecomputeTimeKey);
1056
+ const nextRecomputeTime = await system.storage.get(DICTIONARY_RECORD, nextRecomputeTimeKey);
1057
+ ```
1058
+
1059
+ ### Custom.create()
1060
+
1061
+ Create custom computation with completely user-defined calculation logic.
1062
+
1063
+ Custom computation provides maximum flexibility, allowing developers to implement any business logic that doesn't fit into other computation types. It supports custom compute functions, incremental computation, state management, and flexible data dependencies.
1064
+
1065
+ **Syntax**
1066
+ ```typescript
1067
+ Custom.create(config: CustomConfig): CustomInstance
1068
+ ```
1069
+
1070
+ **Parameters**
1071
+ - `config.name` (string, required): Computation name for identification
1072
+ - `config.compute` (function, required): Main computation function with signature:
1073
+ ```typescript
1074
+ // For Dictionary/Global context:
1075
+ async function(
1076
+ this: { controller: Controller, state: any },
1077
+ dataDeps: any
1078
+ ): Promise<any>
1079
+
1080
+ // For Property context:
1081
+ async function(
1082
+ this: { controller: Controller, state: any },
1083
+ dataDeps: any,
1084
+ record: any
1085
+ ): Promise<any>
1086
+ ```
1087
+ - `config.incrementalCompute` (function, optional): Incremental computation function for optimized updates:
1088
+ ```typescript
1089
+ async function(
1090
+ this: { controller: Controller, state: any },
1091
+ lastValue: any,
1092
+ mutationEvent: any,
1093
+ record: any,
1094
+ dataDeps: any
1095
+ ): Promise<any>
1096
+ ```
1097
+ - `config.incrementalPatchCompute` (function, optional): Patch-based incremental computation:
1098
+ ```typescript
1099
+ async function(
1100
+ this: { controller: Controller, state: any },
1101
+ lastValue: any,
1102
+ mutationEvent: any,
1103
+ record: any,
1104
+ dataDeps: any
1105
+ ): Promise<ComputationResultPatch[]>
1106
+ ```
1107
+ - `config.createState` (function, optional): Create custom state for the computation:
1108
+ ```typescript
1109
+ function(): any
1110
+ ```
1111
+ - `config.getDefaultValue` (function, optional): Provide default value when computation hasn't run:
1112
+ ```typescript
1113
+ function(): any
1114
+ ```
1115
+ - `config.asyncReturn` (function, optional): Handle async task results:
1116
+ ```typescript
1117
+ async function(
1118
+ this: { controller: Controller, state: any },
1119
+ asyncResult: any,
1120
+ dataDeps: any,
1121
+ record?: any
1122
+ ): Promise<any>
1123
+ ```
1124
+ - `config.dataDeps` (object, optional): Data dependency configuration, format: `{[key: string]: DataDep}`.
1125
+ - For Property computation: use `type: 'property'` with `attributeQuery` to access current record and its relations
1126
+ - For Dictionary computation: use `type: 'records'` with `source: EntityName`
1127
+ - For accessing dictionaries: use `type: 'global'` with `source: DictionaryInstance`
1128
+ - `config.useLastValue` (boolean, optional): Whether to use last computed value in incremental computation
1129
+ - `config.attributeQuery` (AttributeQueryData, optional): Attribute query configuration
1130
+
1131
+ **Examples**
1132
+
1133
+ ```typescript
1134
+ // Basic custom computation for global dictionary
1135
+ const userStats = Dictionary.create({
1136
+ name: 'userStats',
1137
+ type: 'object',
1138
+ collection: false,
1139
+ computation: Custom.create({
1140
+ name: 'UserStatsCalculator',
1141
+ dataDeps: {
1142
+ users: {
1143
+ type: 'records',
1144
+ source: User,
1145
+ attributeQuery: ['id', 'status', 'createdAt']
1146
+ },
1147
+ posts: {
1148
+ type: 'records',
1149
+ source: Post,
1150
+ attributeQuery: ['id', 'authorId', 'status']
1151
+ }
1152
+ },
1153
+ compute: async function(this: Controller, dataContext, args, state, dataDeps) {
1154
+ const activeUsers = dataDeps.users.filter(u => u.status === 'active');
1155
+ const publishedPosts = dataDeps.posts.filter(p => p.status === 'published');
1156
+
1157
+ return {
1158
+ totalUsers: dataDeps.users.length,
1159
+ activeUsers: activeUsers.length,
1160
+ totalPosts: dataDeps.posts.length,
1161
+ publishedPosts: publishedPosts.length,
1162
+ avgPostsPerUser: dataDeps.posts.length / dataDeps.users.length || 0
1163
+ };
1164
+ },
1165
+ getDefaultValue: function() {
1166
+ return {
1167
+ totalUsers: 0,
1168
+ activeUsers: 0,
1169
+ totalPosts: 0,
1170
+ publishedPosts: 0,
1171
+ avgPostsPerUser: 0
1172
+ };
1173
+ }
1174
+ })
1175
+ });
1176
+
1177
+ // Incremental computation with state management
1178
+ const counterDict = Dictionary.create({
1179
+ name: 'globalCounter',
1180
+ type: 'object',
1181
+ collection: false,
1182
+ computation: Custom.create({
1183
+ name: 'IncrementalCounter',
1184
+ useLastValue: true,
1185
+ dataDeps: {
1186
+ counters: {
1187
+ type: 'records',
1188
+ source: Counter,
1189
+ attributeQuery: ['value']
1190
+ }
1191
+ },
1192
+ compute: async function(dataDeps) {
1193
+ const total = dataDeps.counters.reduce((sum, c) => sum + (c.value || 0), 0);
1194
+ return { total, count: dataDeps.counters.length };
1195
+ },
1196
+ incrementalCompute: async function(lastValue, mutationEvent, record, dataDeps) {
1197
+ console.log('Previous value:', lastValue);
1198
+ const total = dataDeps.counters.reduce((sum, c) => sum + (c.value || 0), 0);
1199
+ const diff = total - (lastValue?.total || 0);
1200
+
1201
+ return {
1202
+ total,
1203
+ count: dataDeps.counters.length,
1204
+ lastChange: diff,
1205
+ timestamp: Math.floor(Date.now()/1000) // In seconds
1206
+ };
1207
+ },
1208
+ getDefaultValue: function() {
1209
+ return { total: 0, count: 0, lastChange: 0 };
1210
+ }
1211
+ })
1212
+ });
1213
+
1214
+ // Custom computation with persistent state
1215
+ const stateManager = Dictionary.create({
1216
+ name: 'stateManager',
1217
+ type: 'object',
1218
+ defaultValue: () => ({ value: 0 }),
1219
+ computation: Custom.create({
1220
+ name: 'StateManager',
1221
+ dataDeps: {
1222
+ trigger: {
1223
+ type: 'global',
1224
+ source: triggerDict
1225
+ }
1226
+ },
1227
+ createState: function() {
1228
+ return {
1229
+ persistentCounter: new GlobalBoundState({ count: 0 })
1230
+ };
1231
+ },
1232
+ compute: async function(dataDeps) {
1233
+ if (!this.state || !this.state.persistentCounter) {
1234
+ return { value: 0, error: 'no state' };
1235
+ }
1236
+
1237
+ // Read current state
1238
+ const current = await this.state.persistentCounter.get() || { count: 0 };
1239
+
1240
+ // Update state based on trigger
1241
+ const increment = dataDeps.trigger || 0;
1242
+ const newState = { count: current.count + increment };
1243
+ await this.state.persistentCounter.set(newState);
1244
+
1245
+ return {
1246
+ value: newState.count,
1247
+ triggerValue: dataDeps.trigger
1248
+ };
1249
+ }
1250
+ })
1251
+ });
1252
+
1253
+ // Async custom computation
1254
+ const apiDataProcessor = Dictionary.create({
1255
+ name: 'apiData',
1256
+ type: 'object',
1257
+ collection: false,
1258
+ computation: Custom.create({
1259
+ name: 'APIDataProcessor',
1260
+ asyncReturn: true,
1261
+ dataDeps: {
1262
+ endpoints: {
1263
+ type: 'records',
1264
+ source: APIEndpoint,
1265
+ attributeQuery: ['url', 'method', 'headers']
1266
+ }
1267
+ },
1268
+ compute: async function(dataDeps) {
1269
+ // Return async task definition
1270
+ return {
1271
+ taskType: 'fetchAPI',
1272
+ endpoints: dataDeps.endpoints,
1273
+ processAt: Date.now()
1274
+ };
1275
+ },
1276
+ getDefaultValue: function() {
1277
+ return { status: 'pending', data: null };
1278
+ }
1279
+ })
1280
+ });
1281
+
1282
+ // Complex relation-based computation (Dictionary level)
1283
+ const relationAnalyzer = Dictionary.create({
1284
+ name: 'relationStats',
1285
+ type: 'object',
1286
+ collection: true,
1287
+ computation: Custom.create({
1288
+ name: 'RelationAnalyzer',
1289
+ dataDeps: {
1290
+ posts: {
1291
+ type: 'records', // Global query for all posts
1292
+ source: Post,
1293
+ attributeQuery: ['id', 'title']
1294
+ },
1295
+ relations: {
1296
+ type: 'records', // Global query for all relations
1297
+ source: PostAuthorRelation,
1298
+ attributeQuery: ['source', 'target']
1299
+ }
1300
+ },
1301
+ compute: async function(dataDeps) {
1302
+ const result: any = {};
1303
+
1304
+ for (const post of dataDeps.posts || []) {
1305
+ const authorCount = (dataDeps.relations || []).filter(r =>
1306
+ r.source && r.source.id === post.id
1307
+ ).length;
1308
+
1309
+ result[post.id] = {
1310
+ title: post.title,
1311
+ authorCount: authorCount
1312
+ };
1313
+ }
1314
+
1315
+ return result;
1316
+ }
1317
+ })
1318
+ });
1319
+
1320
+ // Property-level custom computation accessing own properties
1321
+ const User = Entity.create({
1322
+ name: 'User',
1323
+ properties: [
1324
+ Property.create({ name: 'score', type: 'number' }),
1325
+ Property.create({
1326
+ name: 'level',
1327
+ type: 'string',
1328
+ defaultValue: () => 'beginner',
1329
+ computation: Custom.create({
1330
+ name: 'UserLevelCalculator',
1331
+ dataDeps: {
1332
+ self: {
1333
+ type: 'property',
1334
+ attributeQuery: ['score'] // Access current record's score property
1335
+ },
1336
+ levelConfig: {
1337
+ type: 'global',
1338
+ source: levelConfigDict // Global trigger required
1339
+ }
1340
+ },
1341
+ compute: async function(dataDeps, record) {
1342
+ const score = dataDeps.self?.score || 0;
1343
+ const config = dataDeps.levelConfig || {
1344
+ beginner: 0,
1345
+ intermediate: 100,
1346
+ expert: 500
1347
+ };
1348
+
1349
+ if (score >= config.expert) return 'expert';
1350
+ if (score >= config.intermediate) return 'intermediate';
1351
+ return 'beginner';
1352
+ }
1353
+ })
1354
+ })
1355
+ ]
1356
+ });
1357
+
1358
+ // Property-level custom computation accessing related entities
1359
+ const Order = Entity.create({
1360
+ name: 'Order',
1361
+ properties: [
1362
+ Property.create({ name: 'orderId', type: 'string' }),
1363
+ Property.create({
1364
+ name: 'totalValue',
1365
+ type: 'number',
1366
+ defaultValue: () => 0,
1367
+ computation: Custom.create({
1368
+ name: 'OrderTotalCalculator',
1369
+ dataDeps: {
1370
+ orderData: {
1371
+ type: 'property',
1372
+ attributeQuery: [
1373
+ ['items', { // Access related items through nested query
1374
+ attributeQuery: ['price', 'quantity', 'discount']
1375
+ }]
1376
+ ]
1377
+ }
1378
+ },
1379
+ compute: async function(dataDeps, record) {
1380
+ const items = dataDeps.orderData?.items || [];
1381
+ return items.reduce((total, item) => {
1382
+ const price = item.price || 0;
1383
+ const quantity = item.quantity || 1;
1384
+ const discount = item.discount || 0;
1385
+ return total + (price * quantity * (1 - discount));
1386
+ }, 0);
1387
+ }
1388
+ })
1389
+ })
1390
+ ]
1391
+ });
1392
+ ```
1393
+
1394
+ **Advanced Features**
1395
+
1396
+ 1. **State Management**: Use `createState()` to create persistent state that survives across computation cycles. State can be stored using `GlobalBoundState`, `EntityBoundState`, or `RelationBoundState`.
1397
+
1398
+ 2. **Incremental Computation**: Implement `incrementalCompute` for optimized updates that can access the previous computed value.
1399
+
1400
+ 3. **Patch-based Updates**: Use `incrementalPatchCompute` for fine-grained updates that return specific changes rather than full recomputation.
1401
+
1402
+ 4. **Flexible Data Dependencies**: Configure complex data dependencies including:
1403
+ - `type: 'records'`: Fetch all entity/relation records globally (for Dictionary/global computations)
1404
+ - `type: 'property'`: Access current record's data including relations via nested attributeQuery (for Property computations)
1405
+ - `type: 'global'`: Access global dictionary values
1406
+
1407
+ **🔴 CRITICAL: dataDeps type Configuration**
1408
+
1409
+ **For Property-level Custom computation:**
1410
+ - Use `type: 'property'` to access the current record's data
1411
+ - Use nested attributeQuery to access related entities through relations
1412
+ - Example: If you have a UserPostRelation with `sourceProperty: 'posts'`, use:
1413
+ ```typescript
1414
+ dataDeps: {
1415
+ currentRecord: {
1416
+ type: 'property',
1417
+ attributeQuery: [
1418
+ 'id', 'name', // Current record's own properties
1419
+ ['posts', { // Access related entities through nested query
1420
+ attributeQuery: ['title', 'status']
1421
+ }]
1422
+ ]
1423
+ }
1424
+ }
1425
+ // Then access in compute: dataDeps.currentRecord.posts
1426
+ ```
1427
+
1428
+ **For Dictionary-level Custom computation:**
1429
+ - Use `type: 'records'` for global entity queries
1430
+ - Specify `source: EntityName` to query all records of that entity
1431
+ - Example:
1432
+ ```typescript
1433
+ dataDeps: {
1434
+ users: {
1435
+ type: 'records', // Global query
1436
+ source: User,
1437
+ attributeQuery: ['id', 'status']
1438
+ }
1439
+ }
1440
+ ```
1441
+
1442
+ **Common mistake**: Using `type: 'records'` in Property computation won't work correctly - it won't access the related entities for the specific record!
1443
+
1444
+ 5. **Async Support**: Set `asyncReturn: true` to return async task definitions that will be processed by the system's task queue.
1445
+
1446
+ **Best Practices**
1447
+
1448
+ 1. **Always provide `getDefaultValue`**: This ensures the computation has a valid initial value
1449
+ 2. **Use appropriate context type**: Global computations for system-wide values, property computations for entity-specific values
1450
+ 3. **Use correct dataDeps type**:
1451
+ - `type: 'property'` for accessing current record data in Property computations (use nested attributeQuery for relations)
1452
+ - `type: 'records'` for global queries in Dictionary computations
1453
+ - Never use `type: 'records'` in Property computations (won't access the specific record's relations)
1454
+ 4. **Handle missing data gracefully**: Check for null/undefined in dataDeps
1455
+ 5. **Optimize with incremental computation**: Use `incrementalCompute` for expensive calculations
1456
+ 6. **Property computations need triggers**: Property-level computations require global data dependencies to trigger on entity creation
1457
+
1458
+ ### Dictionary.create()
1459
+
1460
+ Create global dictionary for storing system-wide state and values.
1461
+
1462
+ **Syntax**
1463
+ ```typescript
1464
+ Dictionary.create(config: DictionaryConfig): DictionaryInstance
1465
+ ```
1466
+
1467
+ **Parameters**
1468
+ - `config.name` (string, required): Dictionary name
1469
+ - `config.type` (string, required): Value type, must be one of PropertyTypes (e.g., 'string', 'number', 'boolean', 'object', etc.)
1470
+ - `config.collection` (boolean, required): Whether it's a collection type, defaults to false
1471
+ - `config.args` (object, optional): Type-specific arguments (e.g., string length, number range)
1472
+ - `config.defaultValue` (function, optional): Default value generator function
1473
+ - `config.computation` (Computation, optional): Reactive computation for the dictionary value
1474
+
1475
+ **Examples**
1476
+ ```typescript
1477
+ // Simple global counter
1478
+ const userCountDict = Dictionary.create({
1479
+ name: 'userCount',
1480
+ type: 'number',
1481
+ collection: false,
1482
+ computation: Count.create({
1483
+ record: User
1484
+ })
1485
+ })
1486
+
1487
+ // System configuration
1488
+ const systemConfig = Dictionary.create({
1489
+ name: 'config',
1490
+ type: 'object',
1491
+ collection: false,
1492
+ defaultValue: () => ({
1493
+ maxUsers: 1000,
1494
+ maintenanceMode: false
1495
+ })
1496
+ })
1497
+
1498
+ // Real-time values
1499
+ const currentTime = Dictionary.create({
1500
+ name: 'currentTime',
1501
+ type: 'number',
1502
+ collection: false,
1503
+ computation: RealTime.create({
1504
+ nextRecomputeTime: () => 1000, // Update every second
1505
+ callback: async (now) => {
1506
+ return now.divide(1000);
1507
+ }
1508
+ })
1509
+ })
1510
+
1511
+ // Collection type dictionary
1512
+ const activeUsers = Dictionary.create({
1513
+ name: 'activeUsers',
1514
+ type: 'string',
1515
+ collection: true,
1516
+ computation: Transform.create({
1517
+ record: User,
1518
+ attributeQuery: ['id', 'lastLoginTime'],
1519
+ callback: (users) => {
1520
+ const oneHourAgo = Date.now() - 3600000;
1521
+ return users
1522
+ .filter(u => u.lastLoginTime > oneHourAgo)
1523
+ .map(u => u.id);
1524
+ }
1525
+ })
1526
+ })
1527
+ ```
1528
+
1529
+ **Usage in Controller**
1530
+
1531
+ Dictionaries are passed as the 6th parameter to Controller:
1532
+
1533
+ ```typescript
1534
+ const controller = new Controller({
1535
+ system: system,
1536
+ entities: entities,
1537
+ relations: relations,
1538
+ activities: activities,
1539
+ interactions: interactions,
1540
+ dict: [userCountDict, systemConfig, currentTime, activeUsers],, // Dictionaries
1541
+ recordMutationSideEffects: []
1542
+ });
1543
+ ```
1544
+
1545
+ ### StateNode.create()
1546
+
1547
+ Create state node for state machine computation.
1548
+
1549
+ **Syntax**
1550
+ ```typescript
1551
+ StateNode.create(config: StateNodeConfig): StateNodeInstance
1552
+ ```
1553
+
1554
+ **Parameters**
1555
+ - `config.name` (string, required): State name identifier
1556
+ - `config.computeValue` (function, optional): Function to compute the value when transitioning to this state
1557
+
1558
+ **computeValue Function**
1559
+
1560
+ The `computeValue` function determines what value should be stored when the state machine transitions to this state:
1561
+
1562
+ - **Function Signature**:
1563
+ - Sync: `(lastValue?: any, event?: InteractionEvent) => any`
1564
+ - Async: `async (lastValue?: any, event?: InteractionEvent) => Promise<any>`
1565
+ - `lastValue`: The previous value before the state transition (may be undefined for initial state)
1566
+ - `event`: The interaction event that triggered the state transition (optional)
1567
+ - Contains `user`, `payload`, `interactionName`, and other interaction metadata
1568
+ - Only available when state transition is triggered by an interaction
1569
+ - May be undefined for default state initialization
1570
+ - Returns: The new value to be stored (can be a Promise)
1571
+ - **Context**: Called with `this` bound to the Controller instance
1572
+ - **Async Support**: Yes, `computeValue` can be an async function
1573
+ - **Purpose**:
1574
+ - Store state-specific data (e.g., timestamps, user IDs)
1575
+ - Transform or calculate values based on the previous state
1576
+ - Access interaction context (user, payload) during state transitions
1577
+ - Return complex objects with multiple properties
1578
+ - Return `null` to indicate deletion (for entity/relation state machines)
1579
+ - Perform async operations like database queries or API calls
1580
+
1581
+ **Examples**
1582
+ ```typescript
1583
+ // Simple state node (no value computation)
1584
+ const pendingState = StateNode.create({ name: 'pending' });
1585
+
1586
+ // Store timestamp when entering state
1587
+ const processedState = StateNode.create({
1588
+ name: 'processed',
1589
+ computeValue: () => new Date().toISOString()
1590
+ });
1591
+
1592
+ // Store complex object
1593
+ const activeState = StateNode.create({
1594
+ name: 'active',
1595
+ computeValue: () => ({
1596
+ activatedAt: Math.floor(Date.now()/1000), // In seconds
1597
+ status: 'active',
1598
+ metadata: { source: 'manual' }
1599
+ })
1600
+ });
1601
+
1602
+ // Increment value based on previous state
1603
+ const incrementingState = StateNode.create({
1604
+ name: 'incrementing',
1605
+ computeValue: (lastValue) => {
1606
+ const baseValue = typeof lastValue === 'number' ? lastValue : 0;
1607
+ return baseValue + 1;
1608
+ }
1609
+ });
1610
+
1611
+ // Preserve previous value
1612
+ const idleState = StateNode.create({
1613
+ name: 'idle',
1614
+ computeValue: (lastValue) => {
1615
+ return typeof lastValue === 'number' ? lastValue : 0;
1616
+ }
1617
+ });
1618
+
1619
+ // Return null to delete (for entity/relation state machines)
1620
+ const deletedState = StateNode.create({
1621
+ name: 'deleted',
1622
+ computeValue: () => null
1623
+ });
1624
+
1625
+ // Async computeValue - fetch data from database
1626
+ const enrichedState = StateNode.create({
1627
+ name: 'enriched',
1628
+ computeValue: async function(lastValue) {
1629
+ // Access controller to query database
1630
+ const relatedData = await this.system.storage.findOne('RelatedEntity',
1631
+ this.globals.MatchExp.atom({ key: 'id', value: ['=', lastValue.relatedId] }),
1632
+ undefined,
1633
+ ['name', 'metadata']
1634
+ );
1635
+
1636
+ return {
1637
+ ...lastValue,
1638
+ enrichedAt: new Date().toISOString(),
1639
+ relatedInfo: relatedData
1640
+ };
1641
+ }
1642
+ });
1643
+
1644
+ // Async computeValue - external API call
1645
+ const verifiedState = StateNode.create({
1646
+ name: 'verified',
1647
+ computeValue: async (lastValue) => {
1648
+ // Simulate external verification
1649
+ const verificationResult = await someExternalAPI.verify(lastValue.data);
1650
+
1651
+ return {
1652
+ status: 'verified',
1653
+ verifiedAt: new Date().toISOString(),
1654
+ verificationId: verificationResult.id,
1655
+ confidence: verificationResult.confidence
1656
+ };
1657
+ }
1658
+ });
1659
+
1660
+ // Using event parameter - access user information
1661
+ const approvedState = StateNode.create({
1662
+ name: 'approved',
1663
+ computeValue: (lastValue, event) => {
1664
+ // Access user who triggered the approval
1665
+ const approver = event?.user?.name || 'system';
1666
+ return {
1667
+ status: 'approved',
1668
+ approvedAt: Math.floor(Date.now()/1000), // In seconds
1669
+ approvedBy: approver
1670
+ };
1671
+ }
1672
+ });
1673
+
1674
+ // Using event parameter - access payload data
1675
+ const updatedState = StateNode.create({
1676
+ name: 'updated',
1677
+ computeValue: (lastValue, event) => {
1678
+ // Merge payload data with existing value
1679
+ if (event?.payload) {
1680
+ return {
1681
+ ...lastValue,
1682
+ ...event.payload.updates,
1683
+ lastModifiedAt: Math.floor(Date.now()/1000), // In seconds
1684
+ lastModifiedBy: event.user?.id
1685
+ };
1686
+ }
1687
+ return lastValue;
1688
+ }
1689
+ });
1690
+
1691
+ // Using event parameter - conditional logic based on interaction
1692
+ const reviewedState = StateNode.create({
1693
+ name: 'reviewed',
1694
+ computeValue: (lastValue, event) => {
1695
+ // Different behavior based on which interaction triggered the transition
1696
+ if (event?.interactionName === 'approve') {
1697
+ return { status: 'approved', reviewedAt: Math.floor(Date.now()/1000) }; // In seconds
1698
+ } else if (event?.interactionName === 'reject') {
1699
+ return {
1700
+ status: 'rejected',
1701
+ reviewedAt: Math.floor(Date.now()/1000), // In seconds
1702
+ reason: event.payload?.reason || 'No reason provided'
1703
+ };
1704
+ }
1705
+ return { status: 'pending_review' };
1706
+ }
1707
+ });
1708
+ ```
1709
+
1710
+ **Usage in StateMachine**
1711
+
1712
+ StateNodes with `computeValue` are used in StateMachine to compute property values or entity states:
1713
+
1714
+ ```typescript
1715
+ // Counter that increments on interaction
1716
+ 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
1725
+ })
1726
+ ],
1727
+ transfers: [/* ... */],
1728
+ defaultState: idleState
1729
+ });
1730
+
1731
+ // Timestamp tracking
1732
+ const ProcessingStateMachine = StateMachine.create({
1733
+ states: [
1734
+ StateNode.create({ name: 'pending' }),
1735
+ StateNode.create({
1736
+ name: 'processed',
1737
+ computeValue: () => new Date().toISOString()
1738
+ })
1739
+ ],
1740
+ transfers: [/* ... */],
1741
+ defaultState: pendingState
1742
+ });
1743
+
1744
+ // Apply to property
1745
+ const SomeEntity = Entity.create({
1746
+ name: 'SomeEntity',
1747
+ properties: [
1748
+ Property.create({
1749
+ name: 'processedAt',
1750
+ type: 'string',
1751
+ computation: ProcessingStateMachine
1752
+ })
1753
+ ]
1754
+ });
1755
+ ```
1756
+
1757
+ ### StateTransfer.create()
1758
+
1759
+ Create state transfer for state machine computation.
1760
+
1761
+ **Syntax**
1762
+ ```typescript
1763
+ StateTransfer.create(config: StateTransferConfig): StateTransferInstance
1764
+ ```
1765
+
1766
+ **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()`
1768
+ - `config.current` (StateNode, required): Current state node
1769
+ - `config.next` (StateNode, required): Next state node
1770
+ - `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.
1771
+
1772
+ **computeTarget Function**
1773
+
1774
+ The `computeTarget` function determines which specific records should transition states when the trigger occurs:
1775
+
1776
+ - **For Entity StateMachines**: Return entity record(s) with `id` field: `{id: string}` or array of such objects
1777
+ - **For Relation StateMachines**:
1778
+ - Return new relation specification: `{source: EntityRef, target: EntityRef}` where EntityRef can be:
1779
+ - An object with `id`: `{id: string}`
1780
+ - A full entity record: `{id: string, ...otherFields}`
1781
+ - The source/target can also be arrays for batch operations
1782
+ - Or return existing relation record(s) with `id`
1783
+ - **Function Signature**:
1784
+ - Sync: `(event: InteractionEvent) => TargetRecord`
1785
+ - Async: `async function(this: Controller, event: InteractionEvent) => TargetRecord`
1786
+ - **Event Parameter**: Contains interaction details including `event.payload` with the interaction's payload data
1787
+
1788
+ **Examples**
1789
+ ```typescript
1790
+ // Simple state transfer (no computeTarget needed for global state)
1791
+ const approveTransfer = StateTransfer.create({
1792
+ trigger: ApproveInteraction,
1793
+ current: pendingState,
1794
+ next: approvedState
1795
+ });
1796
+
1797
+ // Entity state transfer - specify which entity to update
1798
+ const incrementTransfer = StateTransfer.create({
1799
+ trigger: IncrementInteraction,
1800
+ current: idleState,
1801
+ next: incrementingState,
1802
+ computeTarget: (event) => {
1803
+ // Return the counter entity that should transition states
1804
+ return { id: event.payload.counter.id };
1805
+ }
1806
+ });
1807
+
1808
+ // Relation state transfer - create new relation
1809
+ const assignReviewerTransfer = StateTransfer.create({
1810
+ trigger: AssignReviewerInteraction,
1811
+ current: unassignedState,
1812
+ next: assignedState,
1813
+ computeTarget: async function(this: Controller, event) {
1814
+ // Find the request entity
1815
+ const request = await this.system.storage.findOne('Request',
1816
+ this.globals.MatchExp.atom({
1817
+ key: 'id',
1818
+ value: ['=', event.payload.requestId]
1819
+ }),
1820
+ undefined,
1821
+ ['id']
1822
+ );
1823
+
1824
+ // Return source and target to create/update relation
1825
+ return {
1826
+ source: request, // Can be {id: '...'} or full entity
1827
+ target: event.payload.reviewer // From payload
1828
+ };
1829
+ }
1830
+ });
1831
+
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
+ })
1841
+ });
1842
+
1843
+ // Relation state transfer - existing relation
1844
+ const updateRelationTransfer = StateTransfer.create({
1845
+ trigger: UpdateStatusInteraction,
1846
+ current: activeState,
1847
+ 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
+ }
1864
+ });
1865
+
1866
+ // Multiple targets - return array
1867
+ const bulkApproveTransfer = StateTransfer.create({
1868
+ trigger: BulkApproveInteraction,
1869
+ current: pendingState,
1870
+ next: approvedState,
1871
+ computeTarget: (event) => {
1872
+ // Return multiple entities to transition
1873
+ return event.payload.items.map(item => ({ id: item.id }));
1874
+ }
1875
+ });
1876
+ ```
1877
+
1878
+ ## 13.3 Interaction-Related APIs
1879
+
1880
+ ### Interaction.create()
1881
+
1882
+ Create user interaction definition.
1883
+
1884
+ **Syntax**
1885
+ ```typescript
1886
+ Interaction.create(config: InteractionConfig): InteractionInstance
1887
+ ```
1888
+
1889
+ **Parameters**
1890
+ - `config.name` (string, required): Interaction name
1891
+ - `config.action` (Action, required): Interaction action
1892
+ - `config.payload` (Payload, optional): Interaction parameters
1893
+ - `config.conditions` (Condition|Conditions, optional): Execution conditions
1894
+ - `config.sideEffects` (SideEffect[], optional): Side effect handlers
1895
+ - `config.data` (Entity|Relation, optional): Associated data entity
1896
+ - `config.query` (Query, optional): Query definition for data fetching
1897
+
1898
+ **Examples**
1899
+
1900
+ 1. **Create Data Interaction**
1901
+ ```typescript
1902
+ // Create post interaction
1903
+ const CreatePostInteraction = Interaction.create({
1904
+ name: 'createPost',
1905
+ action: Action.create({ name: 'create' }),
1906
+ payload: Payload.create({
1907
+ items: [
1908
+ PayloadItem.create({
1909
+ name: 'postData',
1910
+ base: Post,
1911
+ required: true
1912
+ })
1913
+ ]
1914
+ }),
1915
+ conditions: Condition.create({
1916
+ name: 'AuthenticatedUser',
1917
+ content: async function(event) {
1918
+ return event.user.id !== undefined
1919
+ }
1920
+ })
1921
+ })
1922
+ ```
1923
+
1924
+ 2. **Get Data Interactions**
1925
+
1926
+ To retrieve data, use `GetAction` and specify the `data` field:
1927
+
1928
+ ```typescript
1929
+ // Get all users
1930
+ const GetAllUsers = Interaction.create({
1931
+ name: 'getAllUsers',
1932
+ action: GetAction, // Special action for data retrieval
1933
+ data: User // Entity to query
1934
+ })
1935
+
1936
+ // Get filtered data with query configuration
1937
+ const GetActiveUsers = Interaction.create({
1938
+ name: 'getActiveUsers',
1939
+ action: GetAction,
1940
+ data: User,
1941
+ query: Query.create({
1942
+ items: [
1943
+ QueryItem.create({
1944
+ name: 'attributeQuery',
1945
+ value: ['id', 'name', 'email', 'status'] // Fields to retrieve
1946
+ })
1947
+ ]
1948
+ })
1949
+ })
1950
+
1951
+ // Get data with complex conditions
1952
+ const GetUserPosts = Interaction.create({
1953
+ name: 'getUserPosts',
1954
+ action: GetAction,
1955
+ data: Post,
1956
+ conditions: Condition.create({
1957
+ name: 'canViewPosts',
1958
+ content: async function(this: Controller, event) {
1959
+ // Check if user can view posts based on query
1960
+ const targetUserId = event.query?.match?.key === 'author.id'
1961
+ ? event.query.match.value[1]
1962
+ : null;
1963
+
1964
+ // Allow users to see their own posts or public posts
1965
+ return targetUserId === event.user.id || event.user.role === 'admin';
1966
+ }
1967
+ })
1968
+ })
1969
+
1970
+ // Get paginated data
1971
+ const GetPostsPaginated = Interaction.create({
1972
+ name: 'getPostsPaginated',
1973
+ action: GetAction,
1974
+ data: Post,
1975
+ query: Query.create({
1976
+ items: [
1977
+ QueryItem.create({
1978
+ name: 'attributeQuery',
1979
+ value: ['id', 'title', 'content', 'createdAt', 'author']
1980
+ }),
1981
+ QueryItem.create({
1982
+ name: 'modifier',
1983
+ value: { limit: 10, offset: 0 } // Pagination
1984
+ })
1985
+ ]
1986
+ })
1987
+ })
1988
+ ```
1989
+
1990
+ **Usage Examples for Get Data Interactions**
1991
+
1992
+ ```typescript
1993
+ // Get all users
1994
+ const result = await controller.callInteraction('getAllUsers', {
1995
+ user: { id: 'admin-user' }
1996
+ })
1997
+ // result.data contains array of users
1998
+
1999
+ // Get filtered users with runtime query
2000
+ const activeUsersResult = await controller.callInteraction('getActiveUsers', {
2001
+ user: { id: 'admin-user' },
2002
+ query: {
2003
+ match: MatchExp.atom({ key: 'status', value: ['=', 'active'] }),
2004
+ attributeQuery: ['id', 'name', 'email']
2005
+ }
2006
+ })
2007
+ // result.data contains filtered users with specified fields
2008
+
2009
+ // Get user's posts
2010
+ const userPostsResult = await controller.callInteraction('getUserPosts', {
2011
+ user: { id: 'user123' },
2012
+ query: {
2013
+ match: MatchExp.atom({ key: 'author.id', value: ['=', 'user123'] }),
2014
+ attributeQuery: ['id', 'title', 'content', 'createdAt']
2015
+ }
2016
+ })
2017
+ // result.data contains posts authored by user123
2018
+
2019
+ // Get paginated posts
2020
+ const paginatedResult = await controller.callInteraction('getPostsPaginated', {
2021
+ user: { id: 'user123' },
2022
+ query: {
2023
+ match: MatchExp.atom({ key: 'status', value: ['=', 'published'] }),
2024
+ modifier: { limit: 10, offset: 20 }, // Get page 3 (assuming 10 per page)
2025
+ attributeQuery: ['id', 'title', 'createdAt']
2026
+ }
2027
+ })
2028
+ // result.data contains 10 posts starting from offset 20
2029
+ ```
2030
+
2031
+ **Key Points for Data Retrieval**
2032
+
2033
+ 1. **Required**: Must use `GetAction` as the action
2034
+ 2. **Required**: Must specify `data` field with the Entity or Relation to query
2035
+ 3. **Optional**: Use `query` in Interaction definition for fixed query configuration
2036
+ 4. **Runtime Query**: Pass `query` object in `callInteraction` to dynamically filter data
2037
+ 5. **Conditions**: Can use conditions to control access based on query parameters
2038
+ 6. **Response**: Retrieved data is returned in `result.data` field
2039
+
2040
+ ### Action.create()
2041
+
2042
+ Create interaction action identifier.
2043
+
2044
+ ⚠️ **Important: Action is not an "operation" but an identifier**
2045
+
2046
+ Action is just a name for interaction types, like event type labels. It contains no operation logic or execution code.
2047
+
2048
+ **Syntax**
2049
+ ```typescript
2050
+ Action.create(config: ActionConfig): ActionInstance
2051
+ ```
2052
+
2053
+ **Parameters**
2054
+ - `config.name` (string, required): Action type identifier name
2055
+
2056
+ **Examples**
2057
+ ```typescript
2058
+ // These are just identifiers, containing no operation logic
2059
+ const CreateAction = Action.create({ name: 'create' })
2060
+ const UpdateAction = Action.create({ name: 'update' })
2061
+ const DeleteAction = Action.create({ name: 'delete' })
2062
+ const LikeAction = Action.create({ name: 'like' })
2063
+
2064
+ // ❌ Wrong understanding: thinking Action contains operation logic
2065
+ const WrongAction = Action.create({
2066
+ name: 'create',
2067
+ execute: () => { /* ... */ } // ❌ Action has no execute method!
2068
+ })
2069
+
2070
+ // ✅ Correct understanding: Action is just an identifier
2071
+ const CorrectAction = Action.create({
2072
+ name: 'create' // That's it!
2073
+ })
2074
+ ```
2075
+
2076
+ **Where is the operation logic?**
2077
+
2078
+ All operation logic is implemented through reactive computations (Transform, Count, etc.):
2079
+
2080
+ ```typescript
2081
+ // Interaction just declares that users can create posts
2082
+ const CreatePost = Interaction.create({
2083
+ name: 'CreatePost',
2084
+ action: Action.create({ name: 'create' }), // Just an identifier
2085
+ payload: Payload.create({ /* ... */ })
2086
+ });
2087
+
2088
+ // The actual "create" logic is in Transform
2089
+ const UserPostRelation = Relation.create({
2090
+ source: User,
2091
+ target: Post,
2092
+ computation: Transform.create({
2093
+ record: InteractionEventEntity,
2094
+ callback: (event) => {
2095
+ if (event.interactionName === 'CreatePost') {
2096
+ // This is where the actual creation logic is
2097
+ return {
2098
+ source: event.user.id,
2099
+ target: {
2100
+ title: event.payload.title,
2101
+ content: event.payload.content
2102
+ }
2103
+ };
2104
+ }
2105
+ }
2106
+ })
2107
+ });
2108
+ ```
2109
+
2110
+ ### Payload.create()
2111
+
2112
+ Create interaction parameter definition.
2113
+
2114
+ **Syntax**
2115
+ ```typescript
2116
+ Payload.create(config: PayloadConfig): PayloadInstance
2117
+ ```
2118
+
2119
+ **Parameters**
2120
+ - `config.items` (PayloadItem[], required): Parameter item list, defaults to empty array
2121
+
2122
+ **Examples**
2123
+ ```typescript
2124
+ const CreateUserPayload = Payload.create({
2125
+ items: [
2126
+ PayloadItem.create({
2127
+ name: 'userData',
2128
+ required: true
2129
+ }),
2130
+ PayloadItem.create({
2131
+ name: 'profileData',
2132
+ required: false
2133
+ })
2134
+ ]
2135
+ })
2136
+ ```
2137
+
2138
+ ### PayloadItem.create()
2139
+
2140
+ Create interaction parameter item.
2141
+
2142
+ **Syntax**
2143
+ ```typescript
2144
+ PayloadItem.create(config: PayloadItemConfig): PayloadItemInstance
2145
+ ```
2146
+
2147
+ **Parameters**
2148
+ - `config.name` (string, required): Parameter name
2149
+ - `config.required` (boolean, optional): Whether it's required, defaults to false
2150
+ - `config.isCollection` (boolean, optional): Whether it's a collection type, defaults to false
2151
+ - `config.itemRef` (Attributive|Entity, optional): Used to reference entities defined in other interactions within Activity
2152
+
2153
+ **Examples**
2154
+ ```typescript
2155
+ // Reference existing user
2156
+ const userRef = PayloadItem.create({
2157
+ name: 'user',
2158
+ required: true
2159
+ })
2160
+
2161
+ // Create new post data
2162
+ const postData = PayloadItem.create({
2163
+ name: 'postData',
2164
+ required: true
2165
+ })
2166
+
2167
+ // Collection type reference
2168
+ const reviewersItem = PayloadItem.create({
2169
+ name: 'reviewers',
2170
+ isCollection: true
2171
+ })
2172
+
2173
+ // Activity item reference
2174
+ const activityItem = PayloadItem.create({
2175
+ name: 'to',
2176
+ itemRef: userRefB
2177
+ })
2178
+ ```
2179
+
2180
+ ## 13.4 Activity-Related APIs
2181
+
2182
+ ### Activity.create()
2183
+
2184
+ Create business activity definition.
2185
+
2186
+ **Syntax**
2187
+ ```typescript
2188
+ Activity.create(config: ActivityConfig): ActivityInstance
2189
+ ```
2190
+
2191
+ **Parameters**
2192
+ - `config.name` (string, required): Activity name
2193
+ - `config.interactions` (Interaction[], optional): List of interactions in the activity
2194
+ - `config.transfers` (Transfer[], optional): List of state transfers
2195
+ - `config.groups` (ActivityGroup[], optional): List of activity groups
2196
+ - `config.gateways` (Gateway[], optional): List of gateways
2197
+ - `config.events` (Event[], optional): List of events
2198
+
2199
+ **Examples**
2200
+ ```typescript
2201
+ const OrderProcessActivity = Activity.create({
2202
+ name: 'OrderProcess',
2203
+ interactions: [
2204
+ CreateOrderInteraction,
2205
+ ConfirmOrderInteraction,
2206
+ PayOrderInteraction,
2207
+ ShipOrderInteraction
2208
+ ],
2209
+ transfers: [
2210
+ Transfer.create({
2211
+ name: 'createToConfirm',
2212
+ source: CreateOrderInteraction,
2213
+ target: ConfirmOrderInteraction
2214
+ }),
2215
+ Transfer.create({
2216
+ name: 'confirmToPay',
2217
+ source: ConfirmOrderInteraction,
2218
+ target: PayOrderInteraction
2219
+ })
2220
+ ]
2221
+ })
2222
+ ```
2223
+
2224
+ ### Transfer.create()
2225
+
2226
+ Create activity state transfer.
2227
+
2228
+ **Syntax**
2229
+ ```typescript
2230
+ Transfer.create(config: TransferConfig): TransferInstance
2231
+ ```
2232
+
2233
+ **Parameters**
2234
+ - `config.name` (string, required): Transfer name
2235
+ - `config.source` (Interaction|ActivityGroup|Gateway, required): Source node
2236
+ - `config.target` (Interaction|ActivityGroup|Gateway, required): Target node
2237
+
2238
+ **Examples**
2239
+ ```typescript
2240
+ const ApprovalTransfer = Transfer.create({
2241
+ name: 'submitToApprove',
2242
+ source: SubmitApplicationInteraction,
2243
+ target: ApproveApplicationInteraction
2244
+ })
2245
+ ```
2246
+
2247
+ ### Condition.create()
2248
+
2249
+ Create interaction execution condition. Conditions are used to determine whether an interaction can be executed based on dynamic runtime checks.
2250
+
2251
+ **Syntax**
2252
+ ```typescript
2253
+ Condition.create(config: ConditionConfig): ConditionInstance
2254
+ ```
2255
+
2256
+ **Parameters**
2257
+ - `config.name` (string, optional): Condition name for debugging and error messages
2258
+ - `config.content` (function, required): Async condition check function with signature:
2259
+ ```typescript
2260
+ async function(this: Controller, event: InteractionEventArgs): Promise<boolean>
2261
+ ```
2262
+
2263
+ **Function Context**
2264
+ The `content` function is called with:
2265
+ - `this`: The Controller instance, providing access to system storage and other services
2266
+ - `event`: The interaction event containing:
2267
+ - `event.user`: The user executing the interaction
2268
+ - `event.payload`: The interaction payload data
2269
+ - Other event properties based on the interaction context
2270
+
2271
+ **Return Values**
2272
+ - `true`: Condition passes, interaction can proceed
2273
+ - `false`: Condition fails, interaction is rejected with "condition check failed" error
2274
+ - `undefined`: Treated as `true` with a warning (condition might not be implemented)
2275
+ - Thrown error: Caught and treated as `false`
2276
+
2277
+ **Examples**
2278
+
2279
+ ```typescript
2280
+ // Basic condition - check user has enough credits
2281
+ const hasEnoughCredits = Condition.create({
2282
+ name: 'hasEnoughCredits',
2283
+ content: async function(this: Controller, event: any) {
2284
+ const user = await this.system.storage.findOne('User',
2285
+ MatchExp.atom({ key: 'id', value: ['=', event.user.id] }),
2286
+ undefined,
2287
+ ['id', 'credits']
2288
+ )
2289
+ return user.credits >= 10
2290
+ }
2291
+ })
2292
+
2293
+ // Complex condition - check based on payload data
2294
+ const canAccessPremiumContent = Condition.create({
2295
+ name: 'canAccessPremiumContent',
2296
+ content: async function(this: Controller, event: any) {
2297
+ const user = await this.system.storage.findOne('User',
2298
+ MatchExp.atom({ key: 'id', value: ['=', event.user.id] }),
2299
+ undefined,
2300
+ ['id', 'credits', 'subscriptionLevel']
2301
+ )
2302
+ const content = event.payload?.content
2303
+
2304
+ // Regular content is always accessible
2305
+ if (!content?.isPremium) return true
2306
+
2307
+ // Premium content requires subscription or credits
2308
+ return user.subscriptionLevel === 'premium' || user.credits >= content.creditCost
2309
+ }
2310
+ })
2311
+
2312
+ // System state condition
2313
+ const systemNotInMaintenance = Condition.create({
2314
+ name: 'systemNotInMaintenance',
2315
+ content: async function(this: Controller, event: any) {
2316
+ const system = await this.system.storage.findOne('System',
2317
+ undefined,
2318
+ undefined,
2319
+ ['maintenanceMode']
2320
+ )
2321
+ return !system?.maintenanceMode
2322
+ }
2323
+ })
2324
+
2325
+ // Using condition in interaction
2326
+ const ViewContent = Interaction.create({
2327
+ name: 'viewContent',
2328
+ action: Action.create({ name: 'view' }),
2329
+ payload: Payload.create({
2330
+ items: [
2331
+ PayloadItem.create({ name: 'content', base: Content })
2332
+ ]
2333
+ }),
2334
+ conditions: canAccessPremiumContent // Single condition
2335
+ })
2336
+ ```
2337
+
2338
+ **Error Handling**
2339
+ - Conditions that return `false` trigger a `'condition check failed'` error
2340
+ - Throwing an error in the content function also results in permission denial
2341
+ - Use `event.error` to provide custom error messages
2342
+
2343
+ **Combining Conditions**
2344
+ Use `Conditions.create()` to combine multiple conditions with AND/OR logic:
2345
+
2346
+ ```typescript
2347
+ import { Conditions, BoolExp } from 'interaqt'
2348
+
2349
+ const condition1 = Condition.create({
2350
+ name: 'hasCredits',
2351
+ content: async function(this: Controller, event) {
2352
+ return event.user.credits > 0
2353
+ }
2354
+ })
2355
+
2356
+ const condition2 = Condition.create({
2357
+ name: 'isVerified',
2358
+ content: async function(this: Controller, event) {
2359
+ return event.user.isVerified === true
2360
+ }
2361
+ })
2362
+
2363
+ // Combine with BoolExp
2364
+ const combinedConditions = Conditions.create({
2365
+ content: BoolExp.atom(condition1).and(BoolExp.atom(condition2))
2366
+ })
2367
+
2368
+ // Use in Interaction
2369
+ const PremiumAction = Interaction.create({
2370
+ name: 'premiumAction',
2371
+ action: Action.create({ name: 'premium' }),
2372
+ conditions: combinedConditions
2373
+ })
2374
+ ```
2375
+
2376
+ **Usage Patterns**
2377
+ ```typescript
2378
+ // Single condition - can be used directly
2379
+ conditions: AdminRole
2380
+
2381
+ // Combined conditions with AND
2382
+ conditions: Conditions.create({
2383
+ content: BoolExp.atom(AdminRole).and(BoolExp.atom(ActiveUser))
2384
+ })
2385
+
2386
+ // Combined conditions with OR
2387
+ conditions: Conditions.create({
2388
+ content: BoolExp.atom(AdminRole).or(BoolExp.atom(ModeratorRole))
2389
+ })
2390
+
2391
+ // Complex combinations
2392
+ conditions: Conditions.create({
2393
+ content: BoolExp.atom(AuthenticatedUser)
2394
+ .and(BoolExp.atom(ActiveUser))
2395
+ .and(
2396
+ BoolExp.atom(AdminRole).or(BoolExp.atom(ModeratorRole))
2397
+ )
2398
+ })
2399
+ ```
2400
+
2401
+ **Best Practices**
2402
+
2403
+ 1. **Always handle async operations properly**: Use await for all storage queries
2404
+ 2. **Return explicit boolean values**: Avoid implicit conversions
2405
+ 3. **Provide meaningful condition names**: Helps with debugging when conditions fail
2406
+ 4. **Handle missing data gracefully**: Check for null/undefined before accessing properties
2407
+ 5. **Keep conditions focused**: Each condition should check one specific rule
2408
+ 6. **Use storage attributeQuery**: Only fetch the fields you need for performance
2409
+
2410
+ **Error Handling**
2411
+
2412
+ When a condition fails or throws an error, the interaction call returns:
2413
+ ```typescript
2414
+ {
2415
+ error: {
2416
+ type: 'condition check failed',
2417
+ message: 'condition check failed'
2418
+ }
2419
+ }
2420
+ ```
2421
+
2422
+ ## 13.5 System-Related APIs
2423
+
2424
+ ### Controller
2425
+
2426
+ System controller that coordinates the work of various components.
2427
+
2428
+ **Constructor**
2429
+ ```typescript
2430
+ new Controller({
2431
+ system: System,
2432
+ entities: EntityInstance[],
2433
+ relations: RelationInstance[],
2434
+ activities: ActivityInstance[],
2435
+ interactions: InteractionInstance[],
2436
+ dict?: DictionaryInstance[], // Note: This is for global dictionaries, NOT computations
2437
+ recordMutationSideEffects?: RecordMutationSideEffect[],
2438
+ ignorePermission?: boolean // Skip condition checks when true
2439
+ })
2440
+ ```
2441
+
2442
+ ⚠️ **IMPORTANT**: Controller does NOT accept a computations parameter. All computations should be defined within the `computation` field of Entity/Relation/Property definitions. The 6th parameter `dict` is for global dictionary definitions (Dictionary.create), not for computation definitions.
2443
+
2444
+ **Main Methods**
2445
+
2446
+ #### setup(install?: boolean)
2447
+ Initialize system.
2448
+ ```typescript
2449
+ await controller.setup(true) // Create database tables
2450
+ ```
2451
+
2452
+ #### callInteraction(interactionName: string, args: InteractionEventArgs)
2453
+ Call interaction.
2454
+
2455
+ **Note about ignorePermission**: When `controller.ignorePermission` is set to `true`, this method will bypass all condition checks, user validation, and payload validation defined in the interaction.
2456
+
2457
+ **Return Type**
2458
+ ```typescript
2459
+ type InteractionCallResponse = {
2460
+ // Contains error information if the interaction failed
2461
+ error?: unknown
2462
+
2463
+ // For GET interactions: contains the retrieved data
2464
+ data?: unknown
2465
+
2466
+ // The interaction event that was processed
2467
+ event?: InteractionEvent
2468
+
2469
+ // Record mutations (create/update/delete) that occurred
2470
+ effects?: RecordMutationEvent[]
2471
+
2472
+ // Results from side effects defined in the interaction
2473
+ sideEffects?: {
2474
+ [effectName: string]: {
2475
+ result?: unknown
2476
+ error?: unknown
2477
+ }
2478
+ }
2479
+
2480
+ // Additional context (e.g., activityId for activity interactions)
2481
+ context?: {
2482
+ [key: string]: unknown
2483
+ }
2484
+ }
2485
+ ```
2486
+
2487
+ **Example**
2488
+ ```typescript
2489
+ const result = await controller.callInteraction('createPost', {
2490
+ user: { id: 'user1' },
2491
+ payload: { postData: { title: 'Hello', content: 'World' } }
2492
+ })
2493
+
2494
+ // Check for errors
2495
+ if (result.error) {
2496
+ console.error('Interaction failed:', result.error)
2497
+ return
2498
+ }
2499
+
2500
+ // Access created record ID from effects
2501
+ const createdPostId = result.effects?.[0]?.record?.id
2502
+
2503
+ // Check side effects
2504
+ if (result.sideEffects?.emailNotification?.error) {
2505
+ console.warn('Email notification failed')
2506
+ }
2507
+ ```
2508
+
2509
+ #### callActivityInteraction(activityName: string, interactionName: string, activityId: string, args: InteractionEventArgs)
2510
+ Call interaction within activity.
2511
+ ```typescript
2512
+ const result = await controller.callActivityInteraction(
2513
+ 'OrderProcess',
2514
+ 'confirmOrder',
2515
+ 'activity-instance-1',
2516
+ { user: { id: 'user1' }, payload: { orderData: {...} } }
2517
+ )
2518
+ ```
2519
+
2520
+ ### System
2521
+
2522
+ System abstract interface that defines basic services like storage and logging.
2523
+
2524
+ **Interface Definition**
2525
+ ```typescript
2526
+ interface System {
2527
+ conceptClass: Map<string, ReturnType<typeof createClass>>
2528
+ storage: Storage
2529
+ logger: SystemLogger
2530
+ setup: (entities: Entity[], relations: Relation[], states: ComputationState[], install?: boolean) => Promise<any>
2531
+ }
2532
+ ```
2533
+
2534
+ ### Storage
2535
+
2536
+ Storage layer interface providing data persistence functionality.
2537
+
2538
+ **Main Methods**
2539
+
2540
+ #### Transaction Operations
2541
+
2542
+ **beginTransaction(transactionName?: string)**
2543
+ Begin a database transaction.
2544
+ ```typescript
2545
+ await storage.beginTransaction('updateOrder')
2546
+ ```
2547
+
2548
+ **commitTransaction(transactionName?: string)**
2549
+ Commit a database transaction.
2550
+ ```typescript
2551
+ await storage.commitTransaction('updateOrder')
2552
+ ```
2553
+
2554
+ **rollbackTransaction(transactionName?: string)**
2555
+ Rollback a database transaction.
2556
+ ```typescript
2557
+ await storage.rollbackTransaction('updateOrder')
2558
+ ```
2559
+
2560
+ #### Entity/Relation Operations
2561
+
2562
+ 🔴 **CRITICAL: Always specify attributeQuery parameter!**
2563
+ - Without `attributeQuery`, only the `id` field is returned
2564
+ - This is a common source of bugs in tests and applications
2565
+ - Always explicitly list all fields you need
2566
+
2567
+ 🔴 **CRITICAL: Relations have source and target as nested entities!**
2568
+ When working with relations (not regular entities), remember that:
2569
+ - **Relations** have `source` and `target` fields that reference entities
2570
+ - These fields should be treated as **nested entities**, not simple values
2571
+ - In `matchExpression`: Use dot notation (`source.id`, `target.name`)
2572
+ - In `attributeQuery`: Use nested query syntax (`['source', { attributeQuery: [...] }]`)
2573
+ - This applies to ALL storage methods when querying relations: `find()`, `findOne()`, `findRelationByName()`, `findOneRelationByName()`
2574
+
2575
+ 🔴 **CRITICAL: Always use Relation instance name when querying!**
2576
+ When using `storage.find()` or `storage.findOne()` to query relations:
2577
+ - **ALWAYS** use the name from the Relation instance: `storage.find(UserPostRelation.name, ...)`
2578
+ - **NEVER** hardcode the relation name: `storage.find('UserPostRelation', ...)` ❌
2579
+ - This is crucial because:
2580
+ 1. Relation names can be manually specified in `Relation.create({ name: 'CustomName', ... })`
2581
+ 2. If no name is specified, the framework auto-generates one (e.g., `UserPost` for User→Post relation)
2582
+ 3. Using the instance name ensures you always get the correct name regardless of how it was defined
2583
+
2584
+ **Example:**
2585
+ ```typescript
2586
+ // Define relation - name might be auto-generated or manually specified
2587
+ const UserPostRelation = Relation.create({
2588
+ source: User,
2589
+ sourceProperty: 'posts',
2590
+ target: Post,
2591
+ targetProperty: 'author',
2592
+ type: '1:n'
2593
+ // Note: no 'name' property, so it will be auto-generated as 'UserPost'
2594
+ })
2595
+
2596
+ // ✅ CORRECT: Always use the relation instance's name property
2597
+ const relations = await storage.find(UserPostRelation.name, ...) // Will use 'UserPost'
2598
+
2599
+ // ❌ WRONG: Never hardcode the relation name
2600
+ const relations = await storage.find('UserPost', ...) // Might break if name changes
2601
+ const relations = await storage.find('UserPostRelation', ...) // Wrong assumption
2602
+ ```
2603
+
2604
+ **find(entityName: string, matchExpression?: MatchExpressionData, modifier?: ModifierData, attributeQuery?: AttributeQueryData)**
2605
+ Find multiple records matching the criteria.
2606
+
2607
+ **Parameters**
2608
+ - `entityName` (string): Name of the entity to query
2609
+ - `matchExpression` (MatchExpressionData, optional): Query conditions
2610
+ - `modifier` (ModifierData, optional): Query modifiers (limit, offset, orderBy, etc.)
2611
+ - `attributeQuery` (AttributeQueryData, optional but critical): Fields to retrieve
2612
+
2613
+ ```typescript
2614
+ // ✅ CORRECT: Returns all specified fields
2615
+ const users = await storage.find('User',
2616
+ MatchExp.atom({ key: 'status', value: ['=', 'active'] }),
2617
+ { limit: 10, orderBy: { createdAt: 'desc' } },
2618
+ ['id', 'username', 'email', 'lastLoginDate']
2619
+ )
2620
+ ```
2621
+
2622
+ **🔴 CRITICAL: Querying Relations with source/target Fields**
2623
+
2624
+ When querying relations that have `source` and `target` fields, these fields should be treated as related entities:
2625
+
2626
+ 1. **In matchExpression - Use dot notation for nested properties:**
2627
+ ```typescript
2628
+ // ✅ CORRECT: Use relation instance name and dot notation for nested properties
2629
+ const relations = await storage.find(UserPostRelation.name,
2630
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2631
+ .and({ key: 'target.status', value: ['=', 'published'] }),
2632
+ undefined,
2633
+ ['id', 'createdAt']
2634
+ )
2635
+
2636
+ // ❌ WRONG: Don't hardcode relation names or compare source/target directly
2637
+ const relations = await storage.find('UserPostRelation', // Don't hardcode!
2638
+ MatchExp.atom({ key: 'source', value: ['=', userId] }), // This won't work!
2639
+ undefined,
2640
+ ['id']
2641
+ )
2642
+ ```
2643
+
2644
+ 2. **In attributeQuery - Use nested query syntax for source/target:**
2645
+ ```typescript
2646
+ // ✅ CORRECT: Use relation instance name and nested attributeQuery
2647
+ const relations = await storage.find(UserPostRelation.name,
2648
+ undefined,
2649
+ undefined,
2650
+ [
2651
+ 'id',
2652
+ 'createdAt', // Relation's own property
2653
+ ['source', { attributeQuery: ['id', 'name', 'email'] }], // Source entity fields
2654
+ ['target', { attributeQuery: ['id', 'title', 'status'] }] // Target entity fields
2655
+ ]
2656
+ )
2657
+
2658
+ // ❌ WRONG: Hardcoded name and incorrect attributeQuery
2659
+ const relations = await storage.find('UserPostRelation', // Don't hardcode!
2660
+ undefined,
2661
+ undefined,
2662
+ ['id', 'source', 'target'] // This only returns entity references, not data!
2663
+ )
2664
+ ```
2665
+
2666
+ **🔴 CRITICAL: Querying and Retrieving Related Entities**
2667
+
2668
+ When working with related entities, you must use the correct syntax:
2669
+
2670
+ 1. **In matchExpression - Query by nested property path:**
2671
+ ```typescript
2672
+ // ✅ CORRECT: Use dot notation to access related entity's properties
2673
+ const students = await storage.find('Student',
2674
+ MatchExp.atom({ key: 'teacher.id', value: ['=', teacherId] }),
2675
+ undefined,
2676
+ ['id', 'name']
2677
+ )
2678
+
2679
+ // ❌ WRONG: Cannot compare entity object directly
2680
+ const students = await storage.find('Student',
2681
+ MatchExp.atom({ key: 'teacher', value: ['=', teacherId] }), // This won't work!
2682
+ undefined,
2683
+ ['id', 'name']
2684
+ )
2685
+ ```
2686
+
2687
+ 2. **In attributeQuery - Retrieve related entity fields:**
2688
+ ```typescript
2689
+ // ✅ CORRECT: Use nested attributeQuery to fetch related entity fields
2690
+ const students = await storage.find('Student',
2691
+ undefined,
2692
+ undefined,
2693
+ [
2694
+ 'id',
2695
+ 'name',
2696
+ ['teacher', { attributeQuery: ['id', 'name', 'email'] }] // Nested query for related entity
2697
+ ]
2698
+ )
2699
+
2700
+ // ❌ WRONG: This only returns the reference, not the actual data
2701
+ const students = await storage.find('Student',
2702
+ undefined,
2703
+ undefined,
2704
+ ['id', 'name', 'teacher'] // This won't fetch teacher's data!
2705
+ )
2706
+ ```
2707
+
2708
+ **Complete Example:**
2709
+ ```typescript
2710
+ // Find all students of a specific teacher with teacher details
2711
+ const students = await storage.find('Student',
2712
+ MatchExp.atom({ key: 'teacher.id', value: ['=', 'teacher123'] }),
2713
+ { limit: 20 },
2714
+ [
2715
+ 'id',
2716
+ 'name',
2717
+ 'grade',
2718
+ ['teacher', {
2719
+ attributeQuery: ['id', 'name', 'department']
2720
+ }],
2721
+ ['courses', {
2722
+ attributeQuery: ['id', 'title', 'credits']
2723
+ }]
2724
+ ]
2725
+ )
2726
+ ```
2727
+
2728
+ **findOne(entityName: string, matchExpression?: MatchExpressionData, modifier?: ModifierData, attributeQuery?: AttributeQueryData)**
2729
+ Find a single record matching the criteria.
2730
+
2731
+ **Parameters**
2732
+ - Same as `find()` but returns only the first result
2733
+
2734
+ ```typescript
2735
+ // ✅ CORRECT: Returns all specified fields
2736
+ const user = await storage.findOne('User',
2737
+ MatchExp.atom({ key: 'email', value: ['=', 'user@example.com'] }),
2738
+ undefined,
2739
+ ['id', 'name', 'email', 'role', 'createdAt']
2740
+ )
2741
+ ```
2742
+
2743
+ **🔴 The same rules for related entities apply to findOne:**
2744
+ ```typescript
2745
+ // ✅ CORRECT: Query and retrieve related entity
2746
+ const student = await storage.findOne('Student',
2747
+ MatchExp.atom({ key: 'teacher.id', value: ['=', teacherId] }), // Use dot notation in query
2748
+ undefined,
2749
+ [
2750
+ 'id',
2751
+ 'name',
2752
+ ['teacher', { attributeQuery: ['id', 'name'] }] // Use nested attributeQuery for retrieval
2753
+ ]
2754
+ )
2755
+ ```
2756
+
2757
+ **🔴 When using findOne with Relations:**
2758
+ ```typescript
2759
+ // ✅ CORRECT: Use relation instance name and dot notation
2760
+ const relation = await storage.findOne(UserPostRelation.name,
2761
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2762
+ .and({ key: 'target.id', value: ['=', postId] }),
2763
+ undefined,
2764
+ [
2765
+ 'id',
2766
+ ['source', { attributeQuery: ['id', 'name'] }],
2767
+ ['target', { attributeQuery: ['id', 'title'] }]
2768
+ ]
2769
+ )
2770
+
2771
+ // ❌ WRONG: Don't hardcode names or query source/target directly
2772
+ const relation = await storage.findOne('UserPostRelation', // Don't hardcode!
2773
+ MatchExp.atom({ key: 'source', value: ['=', userId] }), // Won't work!
2774
+ undefined,
2775
+ ['id', 'source', 'target'] // Won't fetch entity data!
2776
+ )
2777
+ ```
2778
+
2779
+ **create(entityName: string, data: any, events?: RecordMutationEvent[])**
2780
+ Create a new record.
2781
+
2782
+ **Parameters**
2783
+ - `entityName` (string): Name of the entity
2784
+ - `data` (any): Entity data (do NOT include id field)
2785
+ - `events` (RecordMutationEvent[], optional): Mutation events array
2786
+
2787
+ ```typescript
2788
+ const user = await storage.create('User', {
2789
+ username: 'john',
2790
+ email: 'john@example.com',
2791
+ role: 'user'
2792
+ })
2793
+ // Returns created record with generated id
2794
+ ```
2795
+
2796
+ **update(entityName: string, matchExpression: MatchExpressionData, data: any, events?: RecordMutationEvent[])**
2797
+ Update existing records.
2798
+
2799
+ **Parameters**
2800
+ - `entityName` (string): Name of the entity
2801
+ - `matchExpression` (MatchExpressionData): Which records to update
2802
+ - `data` (any): Fields to update
2803
+ - `events` (RecordMutationEvent[], optional): Mutation events array
2804
+
2805
+ ```typescript
2806
+ await storage.update('User',
2807
+ MatchExp.atom({ key: 'id', value: ['=', userId] }),
2808
+ { status: 'inactive', lastModified: Math.floor(Date.now()/1000) } // In seconds
2809
+ )
2810
+ ```
2811
+
2812
+ **delete(entityName: string, matchExpression: MatchExpressionData, events?: RecordMutationEvent[])**
2813
+ Delete records.
2814
+
2815
+ **Parameters**
2816
+ - `entityName` (string): Name of the entity
2817
+ - `matchExpression` (MatchExpressionData): Which records to delete
2818
+ - `events` (RecordMutationEvent[], optional): Mutation events array
2819
+
2820
+ ```typescript
2821
+ await storage.delete('User',
2822
+ MatchExp.atom({ key: 'id', value: ['=', userId] })
2823
+ )
2824
+ ```
2825
+
2826
+ #### Relation-Specific Operations
2827
+
2828
+ **🔴 IMPORTANT: Relations have source and target fields that should be treated as related entities**
2829
+
2830
+ When using relation-specific operations, the same rules apply for source/target fields:
2831
+ - In `matchExpression`: Use dot notation like `source.id` or `target.status`
2832
+ - In `attributeQuery`: Use nested query syntax like `['source', { attributeQuery: [...] }]`
2833
+
2834
+ **findRelationByName(relationName: string, matchExpression?: MatchExpressionData, modifier?: ModifierData, attributeQuery?: AttributeQueryData)**
2835
+ Find relation records by relation name.
2836
+
2837
+ ```typescript
2838
+ // ✅ CORRECT: Use relation instance name, dot notation in matchExpression and nested query in attributeQuery
2839
+ const userPosts = await storage.findRelationByName(UserPostRelation.name,
2840
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2841
+ .and({ key: 'target.status', value: ['=', 'published'] }),
2842
+ { limit: 10 },
2843
+ [
2844
+ 'id',
2845
+ 'createdAt',
2846
+ ['source', { attributeQuery: ['id', 'name'] }],
2847
+ ['target', { attributeQuery: ['id', 'title', 'status'] }]
2848
+ ]
2849
+ )
2850
+
2851
+ // ❌ WRONG: Don't hardcode relation names or use source/target directly in matchExpression
2852
+ const userPosts = await storage.findRelationByName('UserPostRelation', // Don't hardcode!
2853
+ MatchExp.atom({ key: 'source', value: ['=', userId] }), // Won't work!
2854
+ undefined,
2855
+ ['id', 'source', 'target'] // Won't fetch entity data!
2856
+ )
2857
+ ```
2858
+
2859
+ **findOneRelationByName(relationName: string, matchExpression: MatchExpressionData, modifier?: ModifierData, attributeQuery?: AttributeQueryData)**
2860
+ Find a single relation record by relation name.
2861
+
2862
+ ```typescript
2863
+ // ✅ CORRECT: Use relation instance name to query and fetch relation with entity data
2864
+ const relation = await storage.findOneRelationByName(UserPostRelation.name,
2865
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2866
+ .and({ key: 'target.id', value: ['=', postId] }),
2867
+ undefined,
2868
+ [
2869
+ 'id',
2870
+ 'createdAt',
2871
+ ['source', { attributeQuery: ['id', 'name', 'email'] }],
2872
+ ['target', { attributeQuery: ['id', 'title', 'content'] }]
2873
+ ]
2874
+ )
2875
+
2876
+ // Simple case - just fetch by relation ID (still use instance name)
2877
+ const relation = await storage.findOneRelationByName(UserPostRelation.name,
2878
+ MatchExp.atom({ key: 'id', value: ['=', relationId] }),
2879
+ undefined,
2880
+ ['*'] // This is OK for fetching all relation properties, but won't expand source/target
2881
+ )
2882
+ ```
2883
+
2884
+ **addRelationByNameById(relationName: string, sourceEntityId: string, targetEntityId: string, data?: any, events?: RecordMutationEvent[])**
2885
+ Create a relation between two entities by their IDs.
2886
+
2887
+ ```typescript
2888
+ // Create relation between user and post
2889
+ await storage.addRelationByNameById(UserPostRelation.name,
2890
+ userId,
2891
+ postId,
2892
+ { createdAt: Math.floor(Date.now()/1000) } // Optional relation properties - in seconds
2893
+ )
2894
+ ```
2895
+
2896
+ **updateRelationByName(relationName: string, matchExpression: MatchExpressionData, data: any, events?: RecordMutationEvent[])**
2897
+ Update relation properties (cannot update source/target).
2898
+
2899
+ ```typescript
2900
+ // Update by relation ID
2901
+ await storage.updateRelationByName(UserPostRelation.name,
2902
+ MatchExp.atom({ key: 'id', value: ['=', relationId] }),
2903
+ { priority: 'high' } // Only update relation properties
2904
+ )
2905
+
2906
+ // ✅ CORRECT: Use relation instance name and source/target properties
2907
+ await storage.updateRelationByName(UserPostRelation.name,
2908
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2909
+ .and({ key: 'target.status', value: ['=', 'draft'] }),
2910
+ { reviewed: true }
2911
+ )
2912
+
2913
+ // ❌ WRONG: Don't hardcode relation names or use source/target directly
2914
+ await storage.updateRelationByName('UserPostRelation', // Don't hardcode!
2915
+ MatchExp.atom({ key: 'source', value: ['=', userId] }), // Won't work!
2916
+ { reviewed: true }
2917
+ )
2918
+ ```
2919
+
2920
+ **removeRelationByName(relationName: string, matchExpression: MatchExpressionData, events?: RecordMutationEvent[])**
2921
+ Remove relations.
2922
+
2923
+ ```typescript
2924
+ // Remove by relation ID
2925
+ await storage.removeRelationByName(UserPostRelation.name,
2926
+ MatchExp.atom({ key: 'id', value: ['=', relationId] })
2927
+ )
2928
+
2929
+ // ✅ CORRECT: Use relation instance name with source/target properties
2930
+ await storage.removeRelationByName(UserPostRelation.name,
2931
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
2932
+ .and({ key: 'target.id', value: ['=', postId] })
2933
+ )
2934
+
2935
+ // ❌ WRONG: Don't hardcode relation names or use source/target directly
2936
+ await storage.removeRelationByName('UserPostRelation', // Don't hardcode!
2937
+ MatchExp.atom({ key: 'target', value: ['=', postId] }) // Won't work!
2938
+ )
2939
+ ```
2940
+
2941
+ #### KV Storage Operations
2942
+
2943
+ **get(itemName: string, id: string, initialValue?: any)**
2944
+ Get value from key-value storage.
2945
+
2946
+ ```typescript
2947
+ // Get value with default
2948
+ const maxUsers = await storage.get('config', 'maxUsers', 100)
2949
+ ```
2950
+
2951
+ **set(itemName: string, id: string, value: any, events?: RecordMutationEvent[])**
2952
+ Set value in key-value storage.
2953
+
2954
+ ```typescript
2955
+ // Set configuration value
2956
+ await storage.set('config', 'maxUsers', 1000)
2957
+
2958
+ // Store complex objects
2959
+ await storage.set('cache', 'userPreferences', {
2960
+ theme: 'dark',
2961
+ language: 'en',
2962
+ notifications: true
2963
+ })
2964
+ ```
2965
+
2966
+ #### Utility Methods
2967
+
2968
+ **getRelationName(entityName: string, attributeName: string)**
2969
+ Get the internal relation name for an entity's relation property.
2970
+
2971
+ ```typescript
2972
+ const relationName = storage.getRelationName('User', 'posts')
2973
+ // Returns something like 'User_posts_author_Post'
2974
+ ```
2975
+
2976
+ **getEntityName(entityName: string, attributeName: string)**
2977
+ Get the target entity name for a relation property.
2978
+
2979
+ ```typescript
2980
+ const targetEntity = storage.getEntityName('User', 'posts')
2981
+ // Returns 'Post'
2982
+ ```
2983
+
2984
+ **listen(callback: RecordMutationCallback)**
2985
+ Register a callback to listen for record mutations.
2986
+
2987
+ ```typescript
2988
+ storage.listen(async (events) => {
2989
+ for (const event of events) {
2990
+ console.log(`${event.type} on ${event.recordName}`, event.record)
2991
+ }
2992
+ })
2993
+ ```
2994
+
2995
+ #### AttributeQueryData Format
2996
+
2997
+ AttributeQuery specifies which fields to retrieve and supports nested queries for relations:
2998
+
2999
+ ```typescript
3000
+ type AttributeQueryData = (string | [string, { attributeQuery?: AttributeQueryData }])[]
3001
+
3002
+ // Examples:
3003
+ // Simple fields
3004
+ ['id', 'name', 'email']
3005
+
3006
+ // All fields
3007
+ ['*']
3008
+
3009
+ // Nested relation query
3010
+ [
3011
+ 'id',
3012
+ 'name',
3013
+ ['posts', {
3014
+ attributeQuery: ['title', 'status', 'createdAt']
3015
+ }]
3016
+ ]
3017
+
3018
+ // Multi-level nesting
3019
+ [
3020
+ 'id',
3021
+ ['posts', {
3022
+ attributeQuery: [
3023
+ 'title',
3024
+ ['comments', {
3025
+ attributeQuery: ['content', 'author']
3026
+ }]
3027
+ ]
3028
+ }]
3029
+ ]
3030
+ ```
3031
+
3032
+ #### ModifierData Format
3033
+
3034
+ Modifiers control query behavior:
3035
+
3036
+ ```typescript
3037
+ type ModifierData = {
3038
+ limit?: number
3039
+ offset?: number
3040
+ orderBy?: {
3041
+ [field: string]: 'asc' | 'desc'
3042
+ }
3043
+ }
3044
+
3045
+ // Example
3046
+ {
3047
+ limit: 20,
3048
+ offset: 40,
3049
+ orderBy: {
3050
+ createdAt: 'desc',
3051
+ priority: 'asc'
3052
+ }
3053
+ }
3054
+ ```
3055
+
3056
+ ## 13.6 Utility Function APIs
3057
+
3058
+ ### MatchExp
3059
+
3060
+ Query expression builder for constructing complex query conditions.
3061
+
3062
+ #### MatchExp.atom(condition: MatchAtom)
3063
+ Create atomic query condition.
3064
+
3065
+ **Parameters**
3066
+ - `condition.key` (string): Field name, supports dot notation like 'user.profile.name'
3067
+ - `condition.value` ([string, any]): Array of operator and value
3068
+ - `condition.isReferenceValue` (boolean, optional): Whether it's a reference value
3069
+
3070
+ **Supported Operators**
3071
+ - `['=', value]`: Equals
3072
+ - `['!=', value]`: Not equals
3073
+ - `['>', value]`: Greater than
3074
+ - `['<', value]`: Less than
3075
+ - `['>=', value]`: Greater than or equal
3076
+ - `['<=', value]`: Less than or equal
3077
+ - `['like', pattern]`: Pattern matching
3078
+ - `['in', array]`: In array
3079
+ - `['between', [min, max]]`: In range
3080
+ - `['not', null]`: Not null
3081
+
3082
+ **Examples**
3083
+ ```typescript
3084
+ // Basic condition
3085
+ const condition1 = MatchExp.atom({
3086
+ key: 'status',
3087
+ value: ['=', 'active']
3088
+ })
3089
+
3090
+ // Range query
3091
+ const condition2 = MatchExp.atom({
3092
+ key: 'age',
3093
+ value: ['between', [18, 65]]
3094
+ })
3095
+
3096
+ // Relational query
3097
+ const condition3 = MatchExp.atom({
3098
+ key: 'user.profile.city',
3099
+ value: ['=', 'Beijing']
3100
+ })
3101
+
3102
+ // Combined conditions
3103
+ const complexCondition = MatchExp.atom({
3104
+ key: 'status',
3105
+ value: ['=', 'active']
3106
+ }).and({
3107
+ key: 'age',
3108
+ value: ['>', 18]
3109
+ }).or({
3110
+ key: 'vip',
3111
+ value: ['=', true]
3112
+ })
3113
+ ```
3114
+
3115
+ #### MatchExp.fromObject(condition: Object)
3116
+ Create query condition from object (all conditions connected with AND).
3117
+
3118
+ ```typescript
3119
+ const condition = MatchExp.fromObject({
3120
+ status: 'active',
3121
+ age: 25,
3122
+ city: 'Beijing'
3123
+ })
3124
+ // Equivalent to: status='active' AND age=25 AND city='Beijing'
3125
+ ```
3126
+
3127
+ ### BoolExp
3128
+
3129
+ Boolean expression builder for constructing complex logical expressions.
3130
+
3131
+ #### BoolExp.atom(data: T)
3132
+ Create atomic expression.
3133
+
3134
+ ```typescript
3135
+ const expr1 = BoolExp.atom({ condition: 'isActive' })
3136
+ const expr2 = BoolExp.atom({ condition: 'isAdmin' })
3137
+
3138
+ // Combined expression
3139
+ const combined = expr1.and(expr2).or({ condition: 'isOwner' })
3140
+ ```
3141
+
3142
+ ## 13.5 Query APIs
3143
+
3144
+ ### Conditions
3145
+
3146
+ Conditions are used to determine whether an interaction can be executed based on dynamic runtime checks.
3147
+
3148
+ **Syntax**
3149
+ ```typescript
3150
+ Conditions.create(config: ConditionsConfig): ConditionsInstance
3151
+ ```
3152
+
3153
+ **Parameters**
3154
+ - `config.content` (BoolExp, required): Boolean expression containing conditions combined with AND/OR logic
3155
+
3156
+ **Examples**
3157
+ ```typescript
3158
+ import { Conditions, BoolExp } from 'interaqt'
3159
+
3160
+ const condition1 = Condition.create({
3161
+ name: 'hasCredits',
3162
+ content: async function(this: Controller, event) {
3163
+ return event.user.credits > 0
3164
+ }
3165
+ })
3166
+
3167
+ const condition2 = Condition.create({
3168
+ name: 'isVerified',
3169
+ content: async function(this: Controller, event) {
3170
+ return event.user.isVerified === true
3171
+ }
3172
+ })
3173
+
3174
+ // Combine with BoolExp
3175
+ const combinedConditions = Conditions.create({
3176
+ content: BoolExp.atom(condition1).and(BoolExp.atom(condition2))
3177
+ })
3178
+
3179
+ const MyInteraction = Interaction.create({
3180
+ name: 'myInteraction',
3181
+ action: Action.create({ name: 'execute' }),
3182
+ conditions: combinedConditions
3183
+ })
3184
+ ```
3185
+
3186
+ ### BoolExp
3187
+
3188
+ Boolean expression builder for constructing complex logical expressions.
3189
+
3190
+ #### BoolExp.atom(data: T)
3191
+ Create atomic expression.
3192
+
3193
+ ```typescript
3194
+ const expr1 = BoolExp.atom({ condition: 'isActive' })
3195
+ const expr2 = BoolExp.atom({ condition: 'isAdmin' })
3196
+
3197
+ // Combined expression
3198
+ const combined = expr1.and(expr2).or({ condition: 'isOwner' })
3199
+ ```
3200
+
3201
+ ## Type Definitions
3202
+
3203
+ ### Core Types
3204
+
3205
+ ```typescript
3206
+ // Interaction event arguments
3207
+ type InteractionEventArgs = {
3208
+ user: { id: string, [key: string]: any }
3209
+ payload?: { [key: string]: any }
3210
+ [key: string]: any
3211
+ }
3212
+
3213
+ // Record mutation event
3214
+ type RecordMutationEvent = {
3215
+ recordName: string
3216
+ type: 'create' | 'update' | 'delete'
3217
+ record?: EntityIdRef & { [key: string]: any }
3218
+ oldRecord?: EntityIdRef & { [key: string]: any }
3219
+ }
3220
+
3221
+ // Entity reference
3222
+ type EntityIdRef = {
3223
+ id: string
3224
+ _rowId?: string
3225
+ [key: string]: any
3226
+ }
3227
+
3228
+ // Attribute query data
3229
+ type AttributeQueryData = (string | [string, { attributeQuery?: AttributeQueryData }])[]
3230
+ ```
3231
+
3232
+ ### Computation-Related Types
3233
+
3234
+ ```typescript
3235
+ // Computation context
3236
+ type DataContext = {
3237
+ type: 'global' | 'entity' | 'relation' | 'property'
3238
+ id: string | Entity | Relation
3239
+ host?: Entity | Relation
3240
+ }
3241
+
3242
+ // Computation dependency
3243
+ type DataDep = {
3244
+ type: 'records' | 'property' | 'global'
3245
+ // For type: 'records' - the entity/relation to query globally
3246
+ source?: Entity | Relation | Dictionary
3247
+ // For type: 'property' - the relation property name to access
3248
+ property?: string
3249
+ attributeQuery?: AttributeQueryData
3250
+ }
3251
+
3252
+ // Computation result
3253
+ type ComputationResult = any
3254
+ type ComputationResultPatch = {
3255
+ type: 'insert' | 'update' | 'delete'
3256
+ data?: any
3257
+ affectedId?: string
3258
+ }
3259
+ ```
3260
+
3261
+ ## Usage Examples
3262
+
3263
+ ### Complete Blog System Example
3264
+
3265
+ ```typescript
3266
+ import { Entity, Property, Relation, Interaction, Activity, Controller } from 'interaqt'
3267
+
3268
+ // 1. Define entities
3269
+ const User = Entity.create({
3270
+ name: 'User',
3271
+ properties: [
3272
+ Property.create({ name: 'username', type: 'string' }),
3273
+ Property.create({ name: 'email', type: 'string' }),
3274
+ Property.create({
3275
+ name: 'postCount',
3276
+ type: 'number',
3277
+ computation: Count.create({ property: 'posts' }) // Use property name from relation
3278
+ })
3279
+ ]
3280
+ })
3281
+
3282
+ const Post = Entity.create({
3283
+ name: 'Post',
3284
+ properties: [
3285
+ Property.create({ name: 'title', type: 'string' }),
3286
+ Property.create({ name: 'content', type: 'string' }),
3287
+ Property.create({
3288
+ name: 'likeCount',
3289
+ type: 'number',
3290
+ computation: Count.create({ property: 'likes' }) // Use property name from relation
3291
+ })
3292
+ ]
3293
+ })
3294
+
3295
+ // 2. Define relations
3296
+ const UserPostRelation = Relation.create({
3297
+ source: User,
3298
+ sourceProperty: 'posts',
3299
+ target: Post,
3300
+ targetProperty: 'author',
3301
+ type: '1:n'
3302
+ })
3303
+
3304
+ const PostLikeRelation = Relation.create({
3305
+ source: Post,
3306
+ sourceProperty: 'likes',
3307
+ target: User,
3308
+ targetProperty: 'likedPosts',
3309
+ type: 'n:n'
3310
+ })
3311
+
3312
+ // 3. Define interactions
3313
+ const CreatePostInteraction = Interaction.create({
3314
+ name: 'createPost',
3315
+ action: Action.create({ name: 'create' }),
3316
+ payload: Payload.create({
3317
+ items: [
3318
+ PayloadItem.create({
3319
+ name: 'postData',
3320
+ base: Post,
3321
+ required: true
3322
+ })
3323
+ ]
3324
+ }),
3325
+ conditions: Condition.create({
3326
+ name: 'AuthenticatedUser',
3327
+ content: async function(event) {
3328
+ return event.user.id !== undefined
3329
+ }
3330
+ })
3331
+ })
3332
+
3333
+ const LikePostInteraction = Interaction.create({
3334
+ name: 'likePost',
3335
+ action: Action.create({ name: 'create' }),
3336
+ payload: Payload.create({
3337
+ items: [
3338
+ PayloadItem.create({
3339
+ name: 'post',
3340
+ required: true
3341
+ })
3342
+ ]
3343
+ })
3344
+ })
3345
+
3346
+ // 4. Create controller and initialize system
3347
+ const controller = new Controller({
3348
+ system, // System implementation
3349
+ entities: [User, Post], // Entities
3350
+ relations: [UserPostRelation, PostLikeRelation], // Relations
3351
+ activities: [], // Activities
3352
+ interactions: [CreatePostInteraction, LikePostInteraction] // Interactions
3353
+ })
3354
+
3355
+ await controller.setup(true)
3356
+
3357
+ // 5. Use APIs
3358
+ // Create post
3359
+ const result = await controller.callInteraction('createPost', {
3360
+ user: { id: 'user1' },
3361
+ payload: {
3362
+ postData: {
3363
+ title: 'Hello World',
3364
+ content: 'This is my first post!'
3365
+ }
3366
+ }
3367
+ })
3368
+
3369
+ // Like post
3370
+ await controller.callInteraction('likePost', {
3371
+ user: { id: 'user2' },
3372
+ payload: {
3373
+ post: { id: result.recordId }
3374
+ }
3375
+ })
3376
+ ```
3377
+
3378
+ This API reference documentation covers all core APIs of the interaqt framework, providing complete parameter descriptions and practical usage examples. Developers can quickly get started and deeply use various framework features based on this documentation.