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