interaqt 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
# How to Define and Execute Interactions
|
|
2
|
+
|
|
3
|
+
Interactions are the only way users interact with the system in interaqt, and the source of all data changes in the system. By defining interactions, you can describe what operations users can perform and how these operations affect data in the system.
|
|
4
|
+
|
|
5
|
+
## Important Note: About User Identity
|
|
6
|
+
|
|
7
|
+
**interaqt focuses on reactive processing of business logic and does not include user authentication functionality.**
|
|
8
|
+
|
|
9
|
+
When using this framework, please note:
|
|
10
|
+
- The system assumes user identity has already been authenticated through other means (such as JWT, Session, etc.)
|
|
11
|
+
- All interactions start from a state where "user identity already exists"
|
|
12
|
+
- You don't need to define authentication-related interactions like user registration, login, logout
|
|
13
|
+
- User context should be provided to the framework by external systems
|
|
14
|
+
|
|
15
|
+
For example, when executing interactions, user information is passed as a parameter:
|
|
16
|
+
```javascript
|
|
17
|
+
// User identity provided by external system
|
|
18
|
+
const result = await controller.callInteraction('CreatePost', {
|
|
19
|
+
user: { id: 'user123', name: 'John', role: 'author' }, // Already authenticated user
|
|
20
|
+
payload: { /* ... */ }
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic Concepts of Interactions
|
|
25
|
+
|
|
26
|
+
### What is an Interaction
|
|
27
|
+
|
|
28
|
+
An interaction represents an operation that a user can perform, such as:
|
|
29
|
+
- Creating a blog post
|
|
30
|
+
- Liking a post
|
|
31
|
+
- Submitting an order
|
|
32
|
+
- Approving a request
|
|
33
|
+
|
|
34
|
+
Each interaction contains:
|
|
35
|
+
- **Name**: Identifier for the interaction
|
|
36
|
+
- **Action**: Identifier for the interaction type (⚠️ Note: Action is just an identifier and contains no operational logic)
|
|
37
|
+
- **Payload**: Parameters needed for the interaction
|
|
38
|
+
- **Permission control**: Who can execute this interaction
|
|
39
|
+
|
|
40
|
+
## ⚠️ Important Concept Clarification: Action is not "Operation"
|
|
41
|
+
|
|
42
|
+
Many developers misunderstand the concept of Action. **Action is just a name given to the interaction type, like an event type tag, and it contains no operational logic.**
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// ❌ Wrong understanding: Thinking Action contains operational logic
|
|
46
|
+
const CreatePost = Action.create({
|
|
47
|
+
name: 'createPost',
|
|
48
|
+
execute: async (payload) => { // ❌ Action has no execute method!
|
|
49
|
+
// Trying to write operational logic here...
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ✅ Correct understanding: Action is just an identifier
|
|
54
|
+
const CreatePost = Action.create({
|
|
55
|
+
name: 'createPost' // That's it! Just like naming an event
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**All data change logic is implemented through reactive computations (Transform, Count, Every, Any, etc.), not in Actions.**
|
|
60
|
+
|
|
61
|
+
### Interactions vs Traditional APIs
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// Traditional API approach
|
|
65
|
+
app.post('/api/posts', async (req, res) => {
|
|
66
|
+
const { title, content, authorId } = req.body;
|
|
67
|
+
|
|
68
|
+
// Manual data validation
|
|
69
|
+
if (!title || !content || !authorId) {
|
|
70
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Manual permission checking
|
|
74
|
+
if (!await checkPermission(req.user, 'create_post')) {
|
|
75
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Manual data operations
|
|
79
|
+
const post = await db.posts.create({ title, content, authorId });
|
|
80
|
+
|
|
81
|
+
// Manual related data updates
|
|
82
|
+
await db.users.update(authorId, {
|
|
83
|
+
postCount: { $inc: 1 }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
res.json(post);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// interaqt interaction approach
|
|
90
|
+
const CreatePost = Interaction.create({
|
|
91
|
+
name: 'CreatePost',
|
|
92
|
+
action: Action.create({
|
|
93
|
+
name: 'createPost'
|
|
94
|
+
// Action only contains name, no operational logic
|
|
95
|
+
}),
|
|
96
|
+
payload: Payload.create({
|
|
97
|
+
items: [
|
|
98
|
+
PayloadItem.create({ name: 'title', required: true }),
|
|
99
|
+
PayloadItem.create({ name: 'content', required: true }),
|
|
100
|
+
PayloadItem.create({ name: 'authorId', base: User, isRef: true })
|
|
101
|
+
]
|
|
102
|
+
})
|
|
103
|
+
// Data changes are declaratively defined through Relation or Property computation
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Creating Basic Interactions
|
|
108
|
+
|
|
109
|
+
### Simplest Interaction
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { Interaction, Action, Payload, PayloadItem } from 'interaqt';
|
|
113
|
+
|
|
114
|
+
const SayHello = Interaction.create({
|
|
115
|
+
name: 'SayHello',
|
|
116
|
+
action: Action.create({
|
|
117
|
+
name: 'sayHello'
|
|
118
|
+
// Action is just an identifier, contains no specific operations
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Interaction for Creating Entities
|
|
124
|
+
|
|
125
|
+
In interaqt, interactions don't directly operate on data. Data creation, updating, and deletion are all implemented through reactive computations.
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// 1. Define interaction
|
|
129
|
+
const CreateArticle = Interaction.create({
|
|
130
|
+
name: 'CreateArticle',
|
|
131
|
+
action: Action.create({
|
|
132
|
+
name: 'createArticle'
|
|
133
|
+
}),
|
|
134
|
+
payload: Payload.create({
|
|
135
|
+
items: [
|
|
136
|
+
PayloadItem.create({
|
|
137
|
+
name: 'title',
|
|
138
|
+
required: true
|
|
139
|
+
}),
|
|
140
|
+
PayloadItem.create({
|
|
141
|
+
name: 'content',
|
|
142
|
+
required: true
|
|
143
|
+
}),
|
|
144
|
+
PayloadItem.create({
|
|
145
|
+
name: 'categoryId',
|
|
146
|
+
base: Category,
|
|
147
|
+
isRef: true
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 2. Use Transform to listen to interaction events and create entities
|
|
154
|
+
import { Transform, InteractionEventEntity } from 'interaqt';
|
|
155
|
+
|
|
156
|
+
// When defining Article entity, use Transform in computation to create entities reactively
|
|
157
|
+
const Article = Entity.create({
|
|
158
|
+
name: 'Article',
|
|
159
|
+
properties: [
|
|
160
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
161
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
162
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
|
|
163
|
+
Property.create({ name: 'createdAt', type: 'string' })
|
|
164
|
+
],
|
|
165
|
+
// Transform in Entity's computation creates entities from interactions
|
|
166
|
+
computation: Transform.create({
|
|
167
|
+
record: InteractionEventEntity,
|
|
168
|
+
callback: function(event) {
|
|
169
|
+
if (event.interactionName === 'CreateArticle') {
|
|
170
|
+
// Return Article data to be created
|
|
171
|
+
return {
|
|
172
|
+
title: event.payload.title,
|
|
173
|
+
content: event.payload.content,
|
|
174
|
+
category: {id:event.payload.categoryId}, // Relation will be created automatically
|
|
175
|
+
status: 'draft',
|
|
176
|
+
createdAt: new Date().toISOString()
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Interaction for Updating Entities
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// 1. Define update interaction
|
|
189
|
+
// Note: This is for logged-in users updating their own profile, user identity passed through context
|
|
190
|
+
const UpdateUserProfile = Interaction.create({
|
|
191
|
+
name: 'UpdateUserProfile',
|
|
192
|
+
action: Action.create({
|
|
193
|
+
name: 'updateProfile'
|
|
194
|
+
}),
|
|
195
|
+
payload: Payload.create({
|
|
196
|
+
items: [
|
|
197
|
+
PayloadItem.create({
|
|
198
|
+
name: 'userId',
|
|
199
|
+
base: User,
|
|
200
|
+
isRef: true,
|
|
201
|
+
required: true
|
|
202
|
+
}),
|
|
203
|
+
PayloadItem.create({ name: 'name' }),
|
|
204
|
+
PayloadItem.create({ name: 'bio' }),
|
|
205
|
+
PayloadItem.create({ name: 'avatar' })
|
|
206
|
+
]
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 2. Use Transform or StateMachine to respond to interactions and update data
|
|
211
|
+
// This is usually defined in Property's computation
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Defining Interaction Parameters (Payload)
|
|
215
|
+
|
|
216
|
+
### Basic Parameter Types
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
const CreatePost = Interaction.create({
|
|
220
|
+
name: 'CreatePost',
|
|
221
|
+
payload: Payload.create({
|
|
222
|
+
items: [
|
|
223
|
+
// String parameter
|
|
224
|
+
PayloadItem.create({
|
|
225
|
+
name: 'title',
|
|
226
|
+
required: true
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
// Number parameter
|
|
230
|
+
PayloadItem.create({
|
|
231
|
+
name: 'priority'
|
|
232
|
+
}),
|
|
233
|
+
|
|
234
|
+
// Boolean parameter
|
|
235
|
+
PayloadItem.create({
|
|
236
|
+
name: 'isDraft'
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
// Object parameter
|
|
240
|
+
PayloadItem.create({
|
|
241
|
+
name: 'metadata',
|
|
242
|
+
required: false
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
// Array parameter
|
|
246
|
+
PayloadItem.create({
|
|
247
|
+
name: 'tags',
|
|
248
|
+
isCollection: true
|
|
249
|
+
})
|
|
250
|
+
]
|
|
251
|
+
})
|
|
252
|
+
// ... action definition
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Referencing Other Entities (isRef)
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
const CreateComment = Interaction.create({
|
|
260
|
+
name: 'CreateComment',
|
|
261
|
+
payload: Payload.create({
|
|
262
|
+
items: [
|
|
263
|
+
PayloadItem.create({
|
|
264
|
+
name: 'content',
|
|
265
|
+
required: true
|
|
266
|
+
}),
|
|
267
|
+
// Reference to post entity
|
|
268
|
+
PayloadItem.create({
|
|
269
|
+
name: 'postId',
|
|
270
|
+
base: Post,
|
|
271
|
+
isRef: true,
|
|
272
|
+
required: true
|
|
273
|
+
}),
|
|
274
|
+
// Reference to user entity
|
|
275
|
+
PayloadItem.create({
|
|
276
|
+
name: 'authorId',
|
|
277
|
+
base: User,
|
|
278
|
+
isRef: true,
|
|
279
|
+
required: true
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
}),
|
|
283
|
+
action: Action.create({
|
|
284
|
+
name: 'createComment'
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Comment entity with Transform in computation for reactive creation
|
|
289
|
+
const Comment = Entity.create({
|
|
290
|
+
name: 'Comment',
|
|
291
|
+
properties: [
|
|
292
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
293
|
+
Property.create({ name: 'createdAt', type: 'string' })
|
|
294
|
+
],
|
|
295
|
+
computation: Transform.create({
|
|
296
|
+
record: InteractionEventEntity,
|
|
297
|
+
callback: function(event) {
|
|
298
|
+
if (event.interactionName === 'CreateComment') {
|
|
299
|
+
return {
|
|
300
|
+
content: event.payload.content,
|
|
301
|
+
createdAt: new Date().toISOString(),
|
|
302
|
+
author: {id:event.payload.authorId}, // Relation created automatically
|
|
303
|
+
post: {id:event.payload.postId } // Relation created automatically
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Relations are defined normally without computation for creation
|
|
312
|
+
const CommentAuthorRelation = Relation.create({
|
|
313
|
+
source: Comment,
|
|
314
|
+
sourceProperty: 'author',
|
|
315
|
+
target: User,
|
|
316
|
+
targetProperty: 'comments',
|
|
317
|
+
type: 'n:1'
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const CommentPostRelation = Relation.create({
|
|
321
|
+
source: Comment,
|
|
322
|
+
sourceProperty: 'post',
|
|
323
|
+
target: Post,
|
|
324
|
+
targetProperty: 'comments',
|
|
325
|
+
type: 'n:1'
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Parameter Validation
|
|
330
|
+
|
|
331
|
+
The framework's PayloadItem supports basic required field validation, but doesn't support complex validation rules like length limits, regular expressions, etc. These validations should be implemented in business logic:
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
const CreateProduct = Interaction.create({
|
|
335
|
+
name: 'CreateProduct',
|
|
336
|
+
action: Action.create({ name: 'createProduct' }),
|
|
337
|
+
payload: Payload.create({
|
|
338
|
+
items: [
|
|
339
|
+
PayloadItem.create({
|
|
340
|
+
name: 'name',
|
|
341
|
+
type: 'string',
|
|
342
|
+
required: true
|
|
343
|
+
// Complex validation logic should be implemented in interaction handling
|
|
344
|
+
}),
|
|
345
|
+
PayloadItem.create({
|
|
346
|
+
name: 'price',
|
|
347
|
+
type: 'number',
|
|
348
|
+
required: true
|
|
349
|
+
// Price range validation should be handled in business logic
|
|
350
|
+
}),
|
|
351
|
+
PayloadItem.create({
|
|
352
|
+
name: 'email'
|
|
353
|
+
// Email format validation should be handled in business logic
|
|
354
|
+
}),
|
|
355
|
+
PayloadItem.create({
|
|
356
|
+
name: 'category'
|
|
357
|
+
// Enum validation should be handled in business logic
|
|
358
|
+
})
|
|
359
|
+
]
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Conditional Parameters
|
|
365
|
+
|
|
366
|
+
The framework itself doesn't support dynamic required conditions and complex validation functions. These logics should be implemented in interaction handling:
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
const CreateOrder = Interaction.create({
|
|
370
|
+
name: 'CreateOrder',
|
|
371
|
+
action: Action.create({ name: 'createOrder' }),
|
|
372
|
+
payload: Payload.create({
|
|
373
|
+
items: [
|
|
374
|
+
PayloadItem.create({
|
|
375
|
+
name: 'items',
|
|
376
|
+
isCollection: true,
|
|
377
|
+
required: true
|
|
378
|
+
}),
|
|
379
|
+
PayloadItem.create({
|
|
380
|
+
name: 'shippingAddress'
|
|
381
|
+
// Conditional required logic should be checked in interaction handling
|
|
382
|
+
}),
|
|
383
|
+
PayloadItem.create({
|
|
384
|
+
name: 'couponCode'
|
|
385
|
+
// Coupon validation should be implemented in business logic
|
|
386
|
+
})
|
|
387
|
+
]
|
|
388
|
+
})
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Validation logic should be implemented in Transform or Attributive
|
|
392
|
+
const orderValidation = Transform.create({
|
|
393
|
+
record: InteractionEventEntity,
|
|
394
|
+
callback: function(event) {
|
|
395
|
+
if (event.interactionName === 'CreateOrder') {
|
|
396
|
+
// Implement complex validation logic here
|
|
397
|
+
const { payload } = event;
|
|
398
|
+
if (payload.totalAmount < 100 && !payload.shippingAddress) {
|
|
399
|
+
throw new Error('Shipping address is required for orders under $100');
|
|
400
|
+
}
|
|
401
|
+
// Coupon validation etc.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Implementing Data Change Logic
|
|
408
|
+
|
|
409
|
+
⚠️ **Important: In interaqt, never try to "operate" data in interactions!**
|
|
410
|
+
|
|
411
|
+
Interactions only declare "what users can do" and contain no data operation logic. All data changes are **inherent properties** of data, automatically maintained through reactive computations.
|
|
412
|
+
|
|
413
|
+
### Mindset Shift: From "Operating Data" to "Declaring Data Essence"
|
|
414
|
+
|
|
415
|
+
❌ **Wrong mindset: Trying to operate data in interactions**
|
|
416
|
+
```javascript
|
|
417
|
+
// Wrong: Thinking you need to write "create post" logic somewhere
|
|
418
|
+
const CreatePost = Interaction.create({
|
|
419
|
+
name: 'CreatePost',
|
|
420
|
+
action: Action.create({
|
|
421
|
+
name: 'createPost',
|
|
422
|
+
// ❌ Wrong: Trying to write creation logic here
|
|
423
|
+
handler: async (payload) => {
|
|
424
|
+
const post = await db.create('Post', payload);
|
|
425
|
+
await updateUserPostCount(payload.authorId);
|
|
426
|
+
return post;
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
✅ **Correct mindset: Declare what data is**
|
|
433
|
+
```javascript
|
|
434
|
+
// 1. Interaction only declares that users can create posts
|
|
435
|
+
const CreatePost = Interaction.create({
|
|
436
|
+
name: 'CreatePost',
|
|
437
|
+
action: Action.create({ name: 'createPost' }), // Just an identifier
|
|
438
|
+
payload: Payload.create({
|
|
439
|
+
items: [
|
|
440
|
+
PayloadItem.create({ name: 'title', required: true }),
|
|
441
|
+
PayloadItem.create({ name: 'content', required: true })
|
|
442
|
+
]
|
|
443
|
+
})
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// 2. Post existence "is" a response to the create post interaction
|
|
447
|
+
const UserPostRelation = Relation.create({
|
|
448
|
+
source: User,
|
|
449
|
+
target: Post,
|
|
450
|
+
computation: Transform.create({
|
|
451
|
+
record: InteractionEventEntity, // Listen to all interaction events
|
|
452
|
+
callback: (event) => {
|
|
453
|
+
if (event.interactionName === 'CreatePost') {
|
|
454
|
+
// Return post data that should exist
|
|
455
|
+
return {
|
|
456
|
+
source: event.user.id,
|
|
457
|
+
target: {
|
|
458
|
+
title: event.payload.title,
|
|
459
|
+
content: event.payload.content,
|
|
460
|
+
createdAt: new Date().toISOString()
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// 3. User post count "is" the count of user-post relations
|
|
469
|
+
const User = Entity.create({
|
|
470
|
+
properties: [
|
|
471
|
+
Property.create({
|
|
472
|
+
name: 'postCount',
|
|
473
|
+
computation: Count.create({
|
|
474
|
+
record: UserPostRelation
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
]
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Correct Ways of Data Changes
|
|
482
|
+
|
|
483
|
+
Data changes are **declared** (not operated) through the following methods:
|
|
484
|
+
|
|
485
|
+
1. **Transform**: Declare "when a certain event occurs, certain data should exist"
|
|
486
|
+
2. **Count/Every/Any**: Declare "certain data is the computed result of other data"
|
|
487
|
+
3. **StateMachine**: Declare "how states transition based on events"
|
|
488
|
+
|
|
489
|
+
### Creating Entities - Reactive Way
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
// 1. Define blog creation interaction
|
|
493
|
+
const CreateBlogPost = Interaction.create({
|
|
494
|
+
name: 'CreateBlogPost',
|
|
495
|
+
action: Action.create({
|
|
496
|
+
name: 'createBlogPost'
|
|
497
|
+
}),
|
|
498
|
+
payload: Payload.create({
|
|
499
|
+
items: [
|
|
500
|
+
PayloadItem.create({ name: 'title', required: true }),
|
|
501
|
+
PayloadItem.create({ name: 'content', required: true }),
|
|
502
|
+
PayloadItem.create({ name: 'authorId', base: User, isRef: true })
|
|
503
|
+
]
|
|
504
|
+
})
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// 2. Create blog posts through Entity's computation
|
|
508
|
+
const Post = Entity.create({
|
|
509
|
+
name: 'Post',
|
|
510
|
+
properties: [
|
|
511
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
512
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
513
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
|
|
514
|
+
Property.create({ name: 'createdAt', type: 'string' }),
|
|
515
|
+
Property.create({ name: 'slug', type: 'string' })
|
|
516
|
+
],
|
|
517
|
+
computation: Transform.create({
|
|
518
|
+
record: InteractionEventEntity,
|
|
519
|
+
callback: function(event) {
|
|
520
|
+
if (event.interactionName === 'CreateBlogPost') {
|
|
521
|
+
// Return entity data with relation reference
|
|
522
|
+
return {
|
|
523
|
+
title: event.payload.title,
|
|
524
|
+
content: event.payload.content,
|
|
525
|
+
status: 'draft',
|
|
526
|
+
createdAt: new Date().toISOString(),
|
|
527
|
+
slug: event.payload.title.toLowerCase().replace(/\s+/g, '-'),
|
|
528
|
+
author: {id:event.payload.authorId} // Relation will be created automatically
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// 3. Define relation (no computation needed for creation)
|
|
537
|
+
const UserPostRelation = Relation.create({
|
|
538
|
+
source: Post,
|
|
539
|
+
sourceProperty: 'author',
|
|
540
|
+
target: User,
|
|
541
|
+
targetProperty: 'posts',
|
|
542
|
+
type: 'n:1'
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// 4. User's postCount property will automatically update
|
|
546
|
+
const User = Entity.create({
|
|
547
|
+
name: 'User',
|
|
548
|
+
properties: [
|
|
549
|
+
Property.create({
|
|
550
|
+
name: 'postCount',
|
|
551
|
+
type: 'number',
|
|
552
|
+
computation: Count.create({
|
|
553
|
+
record: UserPostRelation,
|
|
554
|
+
direction: 'target'
|
|
555
|
+
})
|
|
556
|
+
})
|
|
557
|
+
]
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Updating Entities - Reactive Way
|
|
562
|
+
|
|
563
|
+
```javascript
|
|
564
|
+
// 1. Define profile update interaction
|
|
565
|
+
const UpdateUserProfile = Interaction.create({
|
|
566
|
+
name: 'UpdateUserProfile',
|
|
567
|
+
action: Action.create({ name: 'updateProfile' }),
|
|
568
|
+
payload: Payload.create({
|
|
569
|
+
items: [
|
|
570
|
+
PayloadItem.create({ name: 'userId', base: User, isRef: true }),
|
|
571
|
+
PayloadItem.create({ name: 'name' }),
|
|
572
|
+
PayloadItem.create({ name: 'bio' }),
|
|
573
|
+
PayloadItem.create({ name: 'avatar' })
|
|
574
|
+
]
|
|
575
|
+
})
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// 2. User properties respond to update interactions
|
|
579
|
+
// First declare state nodes for tracking updates
|
|
580
|
+
const NameUpdatedState = StateNode.create({
|
|
581
|
+
name: 'nameUpdated',
|
|
582
|
+
computeValue: function(lastValue, context) {
|
|
583
|
+
// When entering this state due to UpdateUserProfile, return the new name
|
|
584
|
+
return context.event.payload.name || lastValue;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const BioUpdatedState = StateNode.create({
|
|
589
|
+
name: 'bioUpdated',
|
|
590
|
+
computeValue: function(lastValue, context) {
|
|
591
|
+
// When entering this state due to UpdateUserProfile, return the new bio
|
|
592
|
+
return context.event.payload.bio || lastValue;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const User = Entity.create({
|
|
597
|
+
name: 'User',
|
|
598
|
+
properties: [
|
|
599
|
+
Property.create({
|
|
600
|
+
name: 'name',
|
|
601
|
+
type: 'string',
|
|
602
|
+
computation: StateMachine.create({
|
|
603
|
+
states: [NameUpdatedState],
|
|
604
|
+
transfers: [
|
|
605
|
+
StateTransfer.create({
|
|
606
|
+
current: NameUpdatedState,
|
|
607
|
+
next: NameUpdatedState,
|
|
608
|
+
trigger: UpdateUserProfile,
|
|
609
|
+
computeTarget: (event) => ({ id: event.payload.userId })
|
|
610
|
+
})
|
|
611
|
+
],
|
|
612
|
+
defaultState: NameUpdatedState
|
|
613
|
+
})
|
|
614
|
+
}),
|
|
615
|
+
Property.create({
|
|
616
|
+
name: 'bio',
|
|
617
|
+
type: 'string',
|
|
618
|
+
computation: StateMachine.create({
|
|
619
|
+
states: [BioUpdatedState],
|
|
620
|
+
transfers: [
|
|
621
|
+
StateTransfer.create({
|
|
622
|
+
current: BioUpdatedState,
|
|
623
|
+
next: BioUpdatedState,
|
|
624
|
+
trigger: UpdateUserProfile,
|
|
625
|
+
computeTarget: (event) => ({ id: event.payload.userId })
|
|
626
|
+
})
|
|
627
|
+
],
|
|
628
|
+
defaultState: BioUpdatedState
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Deleting Entities - Through State Management
|
|
636
|
+
|
|
637
|
+
```javascript
|
|
638
|
+
// 1. Define soft delete interaction
|
|
639
|
+
const DeletePost = Interaction.create({
|
|
640
|
+
name: 'DeletePost',
|
|
641
|
+
action: Action.create({ name: 'deletePost' }),
|
|
642
|
+
payload: Payload.create({
|
|
643
|
+
items: [
|
|
644
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
645
|
+
]
|
|
646
|
+
})
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Define PublishPost interaction
|
|
650
|
+
const PublishPost = Interaction.create({
|
|
651
|
+
name: 'PublishPost',
|
|
652
|
+
action: Action.create({ name: 'publishPost' }),
|
|
653
|
+
payload: Payload.create({
|
|
654
|
+
items: [
|
|
655
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
656
|
+
]
|
|
657
|
+
})
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// 2. Declare state nodes for Post status
|
|
661
|
+
const draftState = StateNode.create({ name: 'draft' });
|
|
662
|
+
const publishedState = StateNode.create({ name: 'published' });
|
|
663
|
+
const deletedState = StateNode.create({ name: 'deleted' });
|
|
664
|
+
|
|
665
|
+
// 3. Use StateMachine to manage post status
|
|
666
|
+
const Post = Entity.create({
|
|
667
|
+
name: 'Post',
|
|
668
|
+
properties: [
|
|
669
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
670
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
671
|
+
Property.create({
|
|
672
|
+
name: 'status',
|
|
673
|
+
type: 'string',
|
|
674
|
+
defaultValue: () => 'draft',
|
|
675
|
+
computation: StateMachine.create({
|
|
676
|
+
name: 'PostStatus',
|
|
677
|
+
states: [draftState, publishedState, deletedState],
|
|
678
|
+
defaultState: draftState,
|
|
679
|
+
transfers: [
|
|
680
|
+
StateTransfer.create({
|
|
681
|
+
current: draftState,
|
|
682
|
+
next: publishedState,
|
|
683
|
+
trigger: PublishPost,
|
|
684
|
+
computeTarget: (event) => ({ id: event.payload.postId })
|
|
685
|
+
}),
|
|
686
|
+
StateTransfer.create({
|
|
687
|
+
current: publishedState,
|
|
688
|
+
next: deletedState,
|
|
689
|
+
trigger: DeletePost,
|
|
690
|
+
computeTarget: (event) => ({ id: event.payload.postId })
|
|
691
|
+
}),
|
|
692
|
+
StateTransfer.create({
|
|
693
|
+
current: draftState,
|
|
694
|
+
next: deletedState,
|
|
695
|
+
trigger: DeletePost,
|
|
696
|
+
computeTarget: (event) => ({ id: event.payload.postId })
|
|
697
|
+
})
|
|
698
|
+
]
|
|
699
|
+
})
|
|
700
|
+
})
|
|
701
|
+
]
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// 4. Filter active posts (exclude deleted ones)
|
|
705
|
+
const ActivePosts = FilteredEntity.create({
|
|
706
|
+
name: 'ActivePosts',
|
|
707
|
+
baseEntity: Post,
|
|
708
|
+
filter: function(record) {
|
|
709
|
+
return record.status !== 'deleted';
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Complex Interaction Examples
|
|
715
|
+
|
|
716
|
+
### Multi-Step Business Process
|
|
717
|
+
|
|
718
|
+
```javascript
|
|
719
|
+
// Define order status interactions
|
|
720
|
+
const ConfirmPayment = Interaction.create({
|
|
721
|
+
name: 'ConfirmPayment',
|
|
722
|
+
action: Action.create({ name: 'confirmPayment' }),
|
|
723
|
+
payload: Payload.create({
|
|
724
|
+
items: [
|
|
725
|
+
PayloadItem.create({ name: 'orderId', base: Order, isRef: true })
|
|
726
|
+
]
|
|
727
|
+
})
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const ShipOrder = Interaction.create({
|
|
731
|
+
name: 'ShipOrder',
|
|
732
|
+
action: Action.create({ name: 'shipOrder' }),
|
|
733
|
+
payload: Payload.create({
|
|
734
|
+
items: [
|
|
735
|
+
PayloadItem.create({ name: 'orderId', base: Order, isRef: true })
|
|
736
|
+
]
|
|
737
|
+
})
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const ConfirmDelivery = Interaction.create({
|
|
741
|
+
name: 'ConfirmDelivery',
|
|
742
|
+
action: Action.create({ name: 'confirmDelivery' }),
|
|
743
|
+
payload: Payload.create({
|
|
744
|
+
items: [
|
|
745
|
+
PayloadItem.create({ name: 'orderId', base: Order, isRef: true })
|
|
746
|
+
]
|
|
747
|
+
})
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const CancelOrder = Interaction.create({
|
|
751
|
+
name: 'CancelOrder',
|
|
752
|
+
action: Action.create({ name: 'cancelOrder' }),
|
|
753
|
+
payload: Payload.create({
|
|
754
|
+
items: [
|
|
755
|
+
PayloadItem.create({ name: 'orderId', base: Order, isRef: true })
|
|
756
|
+
]
|
|
757
|
+
})
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Order submission process with multiple steps
|
|
761
|
+
const SubmitOrder = Interaction.create({
|
|
762
|
+
name: 'SubmitOrder',
|
|
763
|
+
action: Action.create({ name: 'submitOrder' }),
|
|
764
|
+
payload: Payload.create({
|
|
765
|
+
items: [
|
|
766
|
+
PayloadItem.create({ name: 'items', isCollection: true, required: true }),
|
|
767
|
+
PayloadItem.create({ name: 'shippingAddress', required: true }),
|
|
768
|
+
PayloadItem.create({ name: 'paymentMethod', required: true }),
|
|
769
|
+
PayloadItem.create({ name: 'couponCode' })
|
|
770
|
+
]
|
|
771
|
+
})
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Declare order state nodes
|
|
775
|
+
const pendingState = StateNode.create({ name: 'pending' });
|
|
776
|
+
const confirmedState = StateNode.create({ name: 'confirmed' });
|
|
777
|
+
const shippedState = StateNode.create({ name: 'shipped' });
|
|
778
|
+
const deliveredState = StateNode.create({ name: 'delivered' });
|
|
779
|
+
const cancelledState = StateNode.create({ name: 'cancelled' });
|
|
780
|
+
|
|
781
|
+
// Order entity with computed properties responding to submission
|
|
782
|
+
const Order = Entity.create({
|
|
783
|
+
name: 'Order',
|
|
784
|
+
properties: [
|
|
785
|
+
Property.create({ name: 'items', type: 'object', collection: true }),
|
|
786
|
+
Property.create({ name: 'shippingAddress', type: 'object' }),
|
|
787
|
+
Property.create({ name: 'paymentMethod', type: 'string' }),
|
|
788
|
+
Property.create({ name: 'couponCode', type: 'string' }),
|
|
789
|
+
Property.create({
|
|
790
|
+
name: 'status',
|
|
791
|
+
type: 'string',
|
|
792
|
+
computation: StateMachine.create({
|
|
793
|
+
name: 'OrderStatus',
|
|
794
|
+
states: [pendingState, confirmedState, shippedState, deliveredState, cancelledState],
|
|
795
|
+
defaultState: pendingState,
|
|
796
|
+
transfers: [
|
|
797
|
+
StateTransfer.create({
|
|
798
|
+
current: pendingState,
|
|
799
|
+
next: confirmedState,
|
|
800
|
+
trigger: ConfirmPayment,
|
|
801
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
802
|
+
}),
|
|
803
|
+
StateTransfer.create({
|
|
804
|
+
current: confirmedState,
|
|
805
|
+
next: shippedState,
|
|
806
|
+
trigger: ShipOrder,
|
|
807
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
808
|
+
}),
|
|
809
|
+
StateTransfer.create({
|
|
810
|
+
current: shippedState,
|
|
811
|
+
next: deliveredState,
|
|
812
|
+
trigger: ConfirmDelivery,
|
|
813
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
814
|
+
}),
|
|
815
|
+
StateTransfer.create({
|
|
816
|
+
current: pendingState,
|
|
817
|
+
next: cancelledState,
|
|
818
|
+
trigger: CancelOrder,
|
|
819
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
820
|
+
})
|
|
821
|
+
]
|
|
822
|
+
})
|
|
823
|
+
}),
|
|
824
|
+
Property.create({
|
|
825
|
+
name: 'totalAmount',
|
|
826
|
+
type: 'number',
|
|
827
|
+
// Calculate from order items
|
|
828
|
+
computed: function(order) {
|
|
829
|
+
const items = order.items || [];
|
|
830
|
+
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
],
|
|
834
|
+
computation: Transform.create({
|
|
835
|
+
record: InteractionEventEntity,
|
|
836
|
+
callback: function(event) {
|
|
837
|
+
if (event.interactionName === 'SubmitOrder') {
|
|
838
|
+
return {
|
|
839
|
+
items: event.payload.items,
|
|
840
|
+
shippingAddress: event.payload.shippingAddress,
|
|
841
|
+
paymentMethod: event.payload.paymentMethod,
|
|
842
|
+
couponCode: event.payload.couponCode,
|
|
843
|
+
userId: event.user.id,
|
|
844
|
+
createdAt: new Date().toISOString()
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
});
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Conditional Business Logic
|
|
854
|
+
|
|
855
|
+
```javascript
|
|
856
|
+
// Product review with approval workflow
|
|
857
|
+
const SubmitReview = Interaction.create({
|
|
858
|
+
name: 'SubmitReview',
|
|
859
|
+
action: Action.create({ name: 'submitReview' }),
|
|
860
|
+
payload: Payload.create({
|
|
861
|
+
items: [
|
|
862
|
+
PayloadItem.create({ name: 'productId', base: Product, isRef: true }),
|
|
863
|
+
PayloadItem.create({ name: 'rating', required: true }),
|
|
864
|
+
PayloadItem.create({ name: 'content', required: true })
|
|
865
|
+
]
|
|
866
|
+
})
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const Review = Entity.create({
|
|
870
|
+
name: 'Review',
|
|
871
|
+
properties: [
|
|
872
|
+
Property.create({ name: 'productId', type: 'string' }),
|
|
873
|
+
Property.create({ name: 'rating', type: 'number' }),
|
|
874
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
875
|
+
Property.create({ name: 'userId', type: 'string' }),
|
|
876
|
+
Property.create({ name: 'createdAt', type: 'string' }),
|
|
877
|
+
Property.create({
|
|
878
|
+
name: 'status',
|
|
879
|
+
type: 'string',
|
|
880
|
+
defaultValue: () => 'pending'
|
|
881
|
+
})
|
|
882
|
+
],
|
|
883
|
+
computation: Transform.create({
|
|
884
|
+
record: InteractionEventEntity,
|
|
885
|
+
dataDeps: {
|
|
886
|
+
users: {
|
|
887
|
+
type: 'records',
|
|
888
|
+
source: User,
|
|
889
|
+
attributeQuery: ['id', 'trustLevel']
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
callback: function(event, dataDeps) {
|
|
893
|
+
if (event.interactionName === 'SubmitReview') {
|
|
894
|
+
const user = dataDeps.users?.find(u => u.id === event.user.id);
|
|
895
|
+
const userTrustLevel = user?.trustLevel || 0;
|
|
896
|
+
|
|
897
|
+
// Determine initial status based on trust level and rating
|
|
898
|
+
let initialStatus = 'pending';
|
|
899
|
+
|
|
900
|
+
// Auto-approve reviews from trusted users
|
|
901
|
+
if (userTrustLevel >= 80) {
|
|
902
|
+
initialStatus = 'approved';
|
|
903
|
+
}
|
|
904
|
+
// Reviews with low ratings require manual approval
|
|
905
|
+
else if (event.payload.rating <= 2) {
|
|
906
|
+
initialStatus = 'pending_review';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
productId: event.payload.productId,
|
|
911
|
+
rating: event.payload.rating,
|
|
912
|
+
content: event.payload.content,
|
|
913
|
+
userId: event.user.id,
|
|
914
|
+
createdAt: new Date().toISOString(),
|
|
915
|
+
status: initialStatus
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
## Permission Control and Security
|
|
925
|
+
|
|
926
|
+
### Basic Permission Checks
|
|
927
|
+
|
|
928
|
+
```javascript
|
|
929
|
+
// Interaction with permission requirements
|
|
930
|
+
const DeletePost = Interaction.create({
|
|
931
|
+
name: 'DeletePost',
|
|
932
|
+
action: Action.create({ name: 'deletePost' }),
|
|
933
|
+
payload: Payload.create({
|
|
934
|
+
items: [
|
|
935
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
936
|
+
]
|
|
937
|
+
}),
|
|
938
|
+
// Permission logic should be implemented through Attributive
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Use Attributive for permission control
|
|
942
|
+
const DeletePostPermission = Attributive.create({
|
|
943
|
+
name: 'canDeletePost',
|
|
944
|
+
type: 'boolean',
|
|
945
|
+
record: InteractionEventEntity,
|
|
946
|
+
computation: function(interactionEvent) {
|
|
947
|
+
if (interactionEvent.interactionName === 'DeletePost') {
|
|
948
|
+
const user = interactionEvent.user;
|
|
949
|
+
const postId = interactionEvent.payload.postId;
|
|
950
|
+
|
|
951
|
+
// Admin can delete any post
|
|
952
|
+
if (user.role === 'admin') {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Author can delete their own post
|
|
957
|
+
// This would need to be checked against the actual post data
|
|
958
|
+
return false; // Simplified for example
|
|
959
|
+
}
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### Role-Based Permission Control
|
|
966
|
+
|
|
967
|
+
```javascript
|
|
968
|
+
// Content moderation interaction
|
|
969
|
+
const ModerateContent = Interaction.create({
|
|
970
|
+
name: 'ModerateContent',
|
|
971
|
+
action: Action.create({ name: 'moderateContent' }),
|
|
972
|
+
payload: Payload.create({
|
|
973
|
+
items: [
|
|
974
|
+
PayloadItem.create({ name: 'contentId', required: true }),
|
|
975
|
+
PayloadItem.create({ name: 'action', required: true }), // approve, reject, flag
|
|
976
|
+
PayloadItem.create({ name: 'reason' })
|
|
977
|
+
]
|
|
978
|
+
})
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// Permission check through Attributive
|
|
982
|
+
const ModerationPermission = Attributive.create({
|
|
983
|
+
name: 'canModerateContent',
|
|
984
|
+
type: 'boolean',
|
|
985
|
+
record: InteractionEventEntity,
|
|
986
|
+
computation: function(interactionEvent) {
|
|
987
|
+
if (interactionEvent.interactionName === 'ModerateContent') {
|
|
988
|
+
const user = interactionEvent.user;
|
|
989
|
+
|
|
990
|
+
// Only moderators and admins can moderate content
|
|
991
|
+
return ['moderator', 'admin'].includes(user.role);
|
|
992
|
+
}
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
## Using Transform to Listen to Interactions and Create Data
|
|
999
|
+
|
|
1000
|
+
Transform is a core concept in interaqt, used to listen to events in the system (such as interaction events) and reactively create or update data.
|
|
1001
|
+
|
|
1002
|
+
### Listening to Interaction Events to Create Relations
|
|
1003
|
+
|
|
1004
|
+
```javascript
|
|
1005
|
+
// 1. Define like post interaction
|
|
1006
|
+
const LikePost = Interaction.create({
|
|
1007
|
+
name: 'LikePost',
|
|
1008
|
+
action: Action.create({ name: 'likePost' }),
|
|
1009
|
+
payload: Payload.create({
|
|
1010
|
+
items: [
|
|
1011
|
+
PayloadItem.create({ name: 'userId', base: User, isRef: true }),
|
|
1012
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
1013
|
+
]
|
|
1014
|
+
})
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// 2. Define like relation, using Transform to listen to interaction events
|
|
1018
|
+
const LikeRelation = Relation.create({
|
|
1019
|
+
source: User,
|
|
1020
|
+
sourceProperty: 'likedPosts',
|
|
1021
|
+
target: Post,
|
|
1022
|
+
targetProperty: 'likedBy',
|
|
1023
|
+
type: 'n:n',
|
|
1024
|
+
properties: [
|
|
1025
|
+
Property.create({
|
|
1026
|
+
name: 'likedAt',
|
|
1027
|
+
type: 'string'
|
|
1028
|
+
})
|
|
1029
|
+
],
|
|
1030
|
+
computation: Transform.create({
|
|
1031
|
+
record: InteractionEventEntity,
|
|
1032
|
+
callback: function(event) {
|
|
1033
|
+
if (event.interactionName === 'LikePost') {
|
|
1034
|
+
return {
|
|
1035
|
+
source: event.payload.userId,
|
|
1036
|
+
target: event.payload.postId,
|
|
1037
|
+
likedAt: new Date().toISOString()
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
})
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// 3. Post's like count will be automatically calculated
|
|
1046
|
+
const Post = Entity.create({
|
|
1047
|
+
name: 'Post',
|
|
1048
|
+
properties: [
|
|
1049
|
+
Property.create({
|
|
1050
|
+
name: 'likeCount',
|
|
1051
|
+
type: 'number',
|
|
1052
|
+
computation: Count.create({
|
|
1053
|
+
relation: LikeRelation,
|
|
1054
|
+
relationDirection: 'target'
|
|
1055
|
+
})
|
|
1056
|
+
})
|
|
1057
|
+
]
|
|
1058
|
+
});
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
## Using StateMachine for State Management
|
|
1062
|
+
|
|
1063
|
+
StateMachine is used to manage entity state changes and can automatically transition states based on interaction events.
|
|
1064
|
+
|
|
1065
|
+
### Basic State Machine Example
|
|
1066
|
+
|
|
1067
|
+
```javascript
|
|
1068
|
+
import { StateMachine, StateNode } from 'interaqt';
|
|
1069
|
+
|
|
1070
|
+
// 1. Define state-related interactions
|
|
1071
|
+
const PayOrder = Interaction.create({
|
|
1072
|
+
name: 'PayOrder',
|
|
1073
|
+
action: Action.create({ name: 'payOrder' }),
|
|
1074
|
+
payload: Payload.create({
|
|
1075
|
+
items: [
|
|
1076
|
+
PayloadItem.create({ name: 'orderId', type: 'string', isRef: true, base: Order }),
|
|
1077
|
+
PayloadItem.create({ name: 'paymentMethod', type: 'string' }),
|
|
1078
|
+
PayloadItem.create({ name: 'amount', type: 'number' })
|
|
1079
|
+
]
|
|
1080
|
+
})
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const ShipOrder = Interaction.create({
|
|
1084
|
+
name: 'ShipOrder',
|
|
1085
|
+
action: Action.create({ name: 'shipOrder' }),
|
|
1086
|
+
payload: Payload.create({
|
|
1087
|
+
items: [
|
|
1088
|
+
PayloadItem.create({ name: 'orderId', type: 'string', isRef: true, base: Order }),
|
|
1089
|
+
PayloadItem.create({ name: 'trackingNumber', type: 'string' })
|
|
1090
|
+
]
|
|
1091
|
+
})
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
// 2. Define state nodes
|
|
1095
|
+
const PendingState = StateNode.create({ name: 'pending' });
|
|
1096
|
+
const PaidState = StateNode.create({ name: 'paid' });
|
|
1097
|
+
const ShippedState = StateNode.create({ name: 'shipped' });
|
|
1098
|
+
const DeliveredState = StateNode.create({ name: 'delivered' });
|
|
1099
|
+
|
|
1100
|
+
// 3. Create order state machine
|
|
1101
|
+
const OrderStateMachine = StateMachine.create({
|
|
1102
|
+
name: 'OrderStatus',
|
|
1103
|
+
states: [PendingState, PaidState, ShippedState, DeliveredState],
|
|
1104
|
+
defaultState: PendingState,
|
|
1105
|
+
transfers: [
|
|
1106
|
+
StateTransfer.create({
|
|
1107
|
+
current: PendingState,
|
|
1108
|
+
next: PaidState,
|
|
1109
|
+
trigger: PayOrder,
|
|
1110
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
1111
|
+
}),
|
|
1112
|
+
StateTransfer.create({
|
|
1113
|
+
current: PaidState,
|
|
1114
|
+
next: ShippedState,
|
|
1115
|
+
trigger: ShipOrder,
|
|
1116
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
1117
|
+
})
|
|
1118
|
+
]
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// 4. Use state machine in order entity
|
|
1122
|
+
const Order = Entity.create({
|
|
1123
|
+
name: 'Order',
|
|
1124
|
+
properties: [
|
|
1125
|
+
Property.create({
|
|
1126
|
+
name: 'status',
|
|
1127
|
+
type: 'string',
|
|
1128
|
+
computation: OrderStateMachine
|
|
1129
|
+
}),
|
|
1130
|
+
// Calculate other properties based on state
|
|
1131
|
+
Property.create({
|
|
1132
|
+
name: 'canCancel',
|
|
1133
|
+
type: 'boolean',
|
|
1134
|
+
computed: function(order) {
|
|
1135
|
+
return order.status === 'pending' || order.status === 'paid';
|
|
1136
|
+
}
|
|
1137
|
+
}),
|
|
1138
|
+
// Payment info is stored in separate Payment entity
|
|
1139
|
+
]
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Create Payment entity to record payment history
|
|
1143
|
+
const Payment = Entity.create({
|
|
1144
|
+
name: 'Payment',
|
|
1145
|
+
properties: [
|
|
1146
|
+
Property.create({ name: 'orderId', type: 'string' }),
|
|
1147
|
+
Property.create({ name: 'method', type: 'string' }),
|
|
1148
|
+
Property.create({ name: 'amount', type: 'number' }),
|
|
1149
|
+
Property.create({ name: 'paidAt', type: 'string' })
|
|
1150
|
+
],
|
|
1151
|
+
computation: Transform.create({
|
|
1152
|
+
record: InteractionEventEntity,
|
|
1153
|
+
callback: function(event) {
|
|
1154
|
+
if (event.interactionName === 'PayOrder') {
|
|
1155
|
+
return {
|
|
1156
|
+
orderId: event.payload.orderId,
|
|
1157
|
+
method: event.payload.paymentMethod,
|
|
1158
|
+
amount: event.payload.amount,
|
|
1159
|
+
paidAt: new Date().toISOString()
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
})
|
|
1165
|
+
});
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
## Executing Interactions
|
|
1169
|
+
|
|
1170
|
+
### Basic Execution
|
|
1171
|
+
|
|
1172
|
+
```javascript
|
|
1173
|
+
// Use controller.callInteraction to execute interactions
|
|
1174
|
+
const result = await controller.callInteraction('CreatePost', {
|
|
1175
|
+
user: { id: 'user123', name: 'John' }, // User context
|
|
1176
|
+
payload: {
|
|
1177
|
+
title: 'My First Post',
|
|
1178
|
+
content: 'This is the content of my first post.',
|
|
1179
|
+
authorId: 'user123'
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
console.log('Interaction result:', result);
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
### Finding and Executing Interactions
|
|
1187
|
+
|
|
1188
|
+
```javascript
|
|
1189
|
+
// Find interaction by name
|
|
1190
|
+
const createPostInteraction = Interaction.instances.find(i => i.name === 'CreatePost');
|
|
1191
|
+
|
|
1192
|
+
if (createPostInteraction) {
|
|
1193
|
+
const result = await controller.callInteraction(createPostInteraction.name, {
|
|
1194
|
+
user: { id: 'user123' },
|
|
1195
|
+
payload: {
|
|
1196
|
+
title: 'Another Post',
|
|
1197
|
+
content: 'More content',
|
|
1198
|
+
authorId: 'user123'
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
### Executing Interactions in Activities
|
|
1205
|
+
|
|
1206
|
+
```javascript
|
|
1207
|
+
// Execute interaction as part of an activity
|
|
1208
|
+
const result = await controller.callActivityInteraction(
|
|
1209
|
+
'OrderProcess', // activity name
|
|
1210
|
+
'processPayment', // interaction name
|
|
1211
|
+
'activity-instance-id',// activity instance ID
|
|
1212
|
+
{
|
|
1213
|
+
user: { id: 'user123' },
|
|
1214
|
+
payload: { /* ... */ }
|
|
1215
|
+
}
|
|
1216
|
+
);
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
## Error Handling
|
|
1220
|
+
|
|
1221
|
+
> **Important**: The interaqt framework automatically catches and handles all errors, never throwing uncaught exceptions. All errors are returned through the `error` field in the return value of `callInteraction` or `callActivityInteraction`. Therefore, **DO NOT use try-catch to test error cases**, instead check the `error` field in the return value.
|
|
1222
|
+
|
|
1223
|
+
### Parameter Validation Errors
|
|
1224
|
+
|
|
1225
|
+
```javascript
|
|
1226
|
+
// ✅ Correct error handling approach
|
|
1227
|
+
const result = await controller.callInteraction('CreatePost', {
|
|
1228
|
+
user: { id: 'user123' },
|
|
1229
|
+
payload: {
|
|
1230
|
+
title: '', // Empty title will trigger validation error
|
|
1231
|
+
// content missing
|
|
1232
|
+
authorId: 'invalid-user-id'
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
if (result.error) {
|
|
1237
|
+
console.log('Error type:', result.error.type);
|
|
1238
|
+
console.log('Error message:', result.error.message);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ❌ Wrong approach: DO NOT use try-catch
|
|
1242
|
+
// try {
|
|
1243
|
+
// const result = await controller.callInteraction('CreatePost', {...});
|
|
1244
|
+
// } catch (e) {
|
|
1245
|
+
// // This code will never execute as the framework doesn't throw exceptions
|
|
1246
|
+
// }
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
### Permission Errors
|
|
1250
|
+
|
|
1251
|
+
```javascript
|
|
1252
|
+
const result = await controller.callInteraction('DeletePost', {
|
|
1253
|
+
user: { id: 'user456' }, // Not the author
|
|
1254
|
+
payload: {
|
|
1255
|
+
postId: 'post123'
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
if (result.error) {
|
|
1260
|
+
console.log('Permission denied:', result.error);
|
|
1261
|
+
}
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### Business Logic Errors
|
|
1265
|
+
|
|
1266
|
+
In reactive systems, business logic errors are usually prevented through computed properties and conditions:
|
|
1267
|
+
|
|
1268
|
+
```javascript
|
|
1269
|
+
// Use Every to ensure sufficient inventory
|
|
1270
|
+
const Order = Entity.create({
|
|
1271
|
+
name: 'Order',
|
|
1272
|
+
properties: [
|
|
1273
|
+
Property.create({
|
|
1274
|
+
name: 'isValid',
|
|
1275
|
+
type: 'boolean',
|
|
1276
|
+
computation: Every.create({
|
|
1277
|
+
record: OrderItemRelation,
|
|
1278
|
+
relationDirection: 'source',
|
|
1279
|
+
callback: function(orderItem) {
|
|
1280
|
+
// Check if each order item has sufficient product inventory
|
|
1281
|
+
return orderItem.product.stock >= orderItem.quantity;
|
|
1282
|
+
}
|
|
1283
|
+
})
|
|
1284
|
+
})
|
|
1285
|
+
]
|
|
1286
|
+
});
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
## Best Practices for Interactions
|
|
1290
|
+
|
|
1291
|
+
### 1. Design Appropriate Interaction Granularity
|
|
1292
|
+
|
|
1293
|
+
```javascript
|
|
1294
|
+
// ✅ Good design: Atomic operations
|
|
1295
|
+
const LikePost = Interaction.create({
|
|
1296
|
+
name: 'LikePost',
|
|
1297
|
+
action: Action.create({ name: 'likePost' }),
|
|
1298
|
+
payload: Payload.create({
|
|
1299
|
+
items: [
|
|
1300
|
+
PayloadItem.create({ name: 'userId', base: User, isRef: true }),
|
|
1301
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
1302
|
+
]
|
|
1303
|
+
})
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
const UnlikePost = Interaction.create({
|
|
1307
|
+
name: 'UnlikePost',
|
|
1308
|
+
action: Action.create({ name: 'unlikePost' }),
|
|
1309
|
+
payload: Payload.create({
|
|
1310
|
+
items: [
|
|
1311
|
+
PayloadItem.create({ name: 'userId', base: User, isRef: true }),
|
|
1312
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
1313
|
+
]
|
|
1314
|
+
})
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// ❌ Avoid: Overly complex interactions
|
|
1318
|
+
const ManagePostLike = Interaction.create({
|
|
1319
|
+
name: 'ManagePostLike',
|
|
1320
|
+
action: Action.create({ name: 'managePostLike' }),
|
|
1321
|
+
payload: Payload.create({
|
|
1322
|
+
items: [
|
|
1323
|
+
PayloadItem.create({ name: 'action' }),
|
|
1324
|
+
// One interaction handling multiple operations increases complexity
|
|
1325
|
+
PayloadItem.create({ name: 'userId', base: User, isRef: true }),
|
|
1326
|
+
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
1327
|
+
]
|
|
1328
|
+
})
|
|
1329
|
+
});
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
### 2. Use Meaningful Naming
|
|
1333
|
+
|
|
1334
|
+
```javascript
|
|
1335
|
+
// ✅ Clear naming
|
|
1336
|
+
const SubmitLeaveRequest = Interaction.create({
|
|
1337
|
+
name: 'SubmitLeaveRequest',
|
|
1338
|
+
action: Action.create({ name: 'submitLeaveRequest' })
|
|
1339
|
+
});
|
|
1340
|
+
const ApproveLeaveRequest = Interaction.create({
|
|
1341
|
+
name: 'ApproveLeaveRequest',
|
|
1342
|
+
action: Action.create({ name: 'approveLeaveRequest' })
|
|
1343
|
+
});
|
|
1344
|
+
const PublishBlogPost = Interaction.create({
|
|
1345
|
+
name: 'PublishBlogPost',
|
|
1346
|
+
action: Action.create({ name: 'publishBlogPost' })
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// ❌ Vague naming
|
|
1350
|
+
const DoAction = Interaction.create({
|
|
1351
|
+
name: 'DoAction',
|
|
1352
|
+
action: Action.create({ name: 'doAction' })
|
|
1353
|
+
});
|
|
1354
|
+
const ProcessData = Interaction.create({
|
|
1355
|
+
name: 'ProcessData',
|
|
1356
|
+
action: Action.create({ name: 'processData' })
|
|
1357
|
+
});
|
|
1358
|
+
const HandleRequest = Interaction.create({
|
|
1359
|
+
name: 'HandleRequest',
|
|
1360
|
+
action: Action.create({ name: 'handleRequest' })
|
|
1361
|
+
});
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
### 3. Leverage Reactive Features
|
|
1365
|
+
|
|
1366
|
+
```javascript
|
|
1367
|
+
// ✅ Fully leverage reactive computations
|
|
1368
|
+
// Define simple interactions
|
|
1369
|
+
const CreatePost = Interaction.create({
|
|
1370
|
+
name: 'CreatePost',
|
|
1371
|
+
action: Action.create({ name: 'createPost' }),
|
|
1372
|
+
payload: Payload.create({
|
|
1373
|
+
items: [
|
|
1374
|
+
PayloadItem.create({ name: 'title', required: true }),
|
|
1375
|
+
PayloadItem.create({ name: 'content', required: true }),
|
|
1376
|
+
PayloadItem.create({ name: 'authorId', base: User, isRef: true })
|
|
1377
|
+
]
|
|
1378
|
+
})
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
// Data changes automatically handled through reactive definitions
|
|
1382
|
+
const UserPostRelation = Relation.create({
|
|
1383
|
+
// ... relation definition
|
|
1384
|
+
computation: Transform.create({
|
|
1385
|
+
record: InteractionEventEntity,
|
|
1386
|
+
callback: function(event) {
|
|
1387
|
+
if (event.interactionName === 'CreatePost') {
|
|
1388
|
+
// Automatically create relations and entities
|
|
1389
|
+
return { /* ... */ };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
})
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// User's postCount automatically updates
|
|
1396
|
+
const User = Entity.create({
|
|
1397
|
+
name: 'User',
|
|
1398
|
+
properties: [
|
|
1399
|
+
Property.create({
|
|
1400
|
+
name: 'postCount',
|
|
1401
|
+
type: 'number',
|
|
1402
|
+
computation: Count.create({
|
|
1403
|
+
relation: UserPostRelation,
|
|
1404
|
+
relationDirection: 'target'
|
|
1405
|
+
})
|
|
1406
|
+
})
|
|
1407
|
+
]
|
|
1408
|
+
});
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
Interactions are the bridge connecting user operations and data changes in interaqt. By properly designing interactions and combining them with the framework's reactive features, you can create business logic systems that are both easy to understand and efficiently executed. Remember: interactions only define "what to do", while the specific "how to do it" is implemented through reactive computations.
|