interaqt 0.3.0 → 0.4.0

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