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,784 @@
|
|
|
1
|
+
# How to Use Filtered Entities
|
|
2
|
+
|
|
3
|
+
Filtered Entity is an advanced feature in interaqt that allows you to create entity views based on specific conditions. A filtered entity is like a subset of the original entity, containing only records that meet certain conditions, while supporting reactive computations on this subset.
|
|
4
|
+
|
|
5
|
+
## Understanding Filtered Entities
|
|
6
|
+
|
|
7
|
+
### What is a Filtered Entity
|
|
8
|
+
|
|
9
|
+
A filtered entity is a virtual view created based on an existing entity, which:
|
|
10
|
+
- **Filters based on conditions**: Only contains records that meet specific conditions
|
|
11
|
+
- **Real-time updates**: Automatically updates when original data changes
|
|
12
|
+
- **Supports computations**: Can perform reactive computations on filtered data
|
|
13
|
+
- **Maintains references**: Records in filtered entities are still references to original entity records
|
|
14
|
+
|
|
15
|
+
### Use Cases
|
|
16
|
+
|
|
17
|
+
Filtered entities are particularly suitable for the following scenarios:
|
|
18
|
+
- **Status grouping**: Such as published posts, active users, pending orders
|
|
19
|
+
- **Permission control**: Such as users can only see their own data
|
|
20
|
+
- **Category statistics**: Such as counting products by category
|
|
21
|
+
- **Conditional aggregation**: Such as calculating sums or averages under specific conditions
|
|
22
|
+
|
|
23
|
+
### Filtered Entity vs Regular Queries
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// Regular query approach: Re-query every time
|
|
27
|
+
const getPublishedPosts = async () => {
|
|
28
|
+
return await controller.find('Post', { status: 'published' });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getPublishedPostCount = async () => {
|
|
32
|
+
return await controller.count('Post', { status: 'published' });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Filtered entity approach: Automatically maintained, reactive updates
|
|
36
|
+
const PublishedPost = Entity.create({
|
|
37
|
+
name: 'PublishedPost',
|
|
38
|
+
baseEntity: Post,
|
|
39
|
+
filterCondition: MatchExp.atom({
|
|
40
|
+
key: 'status',
|
|
41
|
+
value: ['=', 'published']
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Automatically maintained count
|
|
46
|
+
const GlobalStats = Entity.create({
|
|
47
|
+
name: 'GlobalStats',
|
|
48
|
+
properties: [
|
|
49
|
+
Property.create({
|
|
50
|
+
name: 'publishedPostCount',
|
|
51
|
+
type: 'number',
|
|
52
|
+
defaultValue: () => 0,
|
|
53
|
+
computation: Count.create({
|
|
54
|
+
record: PublishedPost
|
|
55
|
+
}) // Automatically updates
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Creating Filtered Entities
|
|
62
|
+
|
|
63
|
+
### Filtering Based on Properties
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
import { FilteredEntity, Entity, Property } from 'interaqt';
|
|
67
|
+
|
|
68
|
+
// Original entity
|
|
69
|
+
const Post = Entity.create({
|
|
70
|
+
name: 'Post',
|
|
71
|
+
properties: [
|
|
72
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
73
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
74
|
+
Property.create({ name: 'status', type: 'string' }),
|
|
75
|
+
Property.create({ name: 'category', type: 'string' }),
|
|
76
|
+
Property.create({ name: 'publishedAt', type: 'string' }),
|
|
77
|
+
Property.create({ name: 'viewCount', type: 'number', defaultValue: 0 })
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Create filtered entity for published posts
|
|
82
|
+
const PublishedPost = Entity.create({
|
|
83
|
+
name: 'PublishedPost',
|
|
84
|
+
baseEntity: Post,
|
|
85
|
+
filterCondition: MatchExp.atom({
|
|
86
|
+
key: 'status',
|
|
87
|
+
value: ['=', 'published']
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Create filtered entity for popular posts
|
|
92
|
+
const PopularPost = Entity.create({
|
|
93
|
+
name: 'PopularPost',
|
|
94
|
+
baseEntity: Post,
|
|
95
|
+
filterCondition: MatchExp.atom({
|
|
96
|
+
key: 'status',
|
|
97
|
+
value: ['=', 'published']
|
|
98
|
+
}).and({
|
|
99
|
+
key: 'viewCount',
|
|
100
|
+
value: ['>=', 1000]
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Create filtered entity for tech posts
|
|
105
|
+
const TechPost = Entity.create({
|
|
106
|
+
name: 'TechPost',
|
|
107
|
+
baseEntity: Post,
|
|
108
|
+
filterCondition: MatchExp.atom({
|
|
109
|
+
key: 'category',
|
|
110
|
+
value: ['=', 'technology']
|
|
111
|
+
}).and({
|
|
112
|
+
key: 'status',
|
|
113
|
+
value: ['=', 'published']
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Filtering Based on Relations
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const User = Entity.create({
|
|
122
|
+
name: 'User',
|
|
123
|
+
properties: [
|
|
124
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
125
|
+
Property.create({ name: 'email', type: 'string' }),
|
|
126
|
+
Property.create({ name: 'status', type: 'string' }),
|
|
127
|
+
Property.create({ name: 'lastLoginAt', type: 'string' })
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const UserPost = Relation.create({
|
|
132
|
+
source: User,
|
|
133
|
+
sourceProperty: 'posts',
|
|
134
|
+
target: Post,
|
|
135
|
+
targetProperty: 'author',
|
|
136
|
+
type: '1:n'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Create filtered entity for active users (logged in within the last 30 days)
|
|
140
|
+
const ActiveUser = Entity.create({
|
|
141
|
+
name: 'ActiveUser',
|
|
142
|
+
baseEntity: User,
|
|
143
|
+
filterCondition: MatchExp.atom({
|
|
144
|
+
key: 'status',
|
|
145
|
+
value: ['=', 'active']
|
|
146
|
+
}).and({
|
|
147
|
+
key: 'lastLoginAt',
|
|
148
|
+
value: ['>=', (() => {
|
|
149
|
+
const thirtyDaysAgo = new Date();
|
|
150
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
151
|
+
return thirtyDaysAgo.toISOString();
|
|
152
|
+
})()]
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Create filtered entity for users with posts (needs to be implemented through relation queries)
|
|
157
|
+
const AuthorUser = Entity.create({
|
|
158
|
+
name: 'AuthorUser',
|
|
159
|
+
baseEntity: User,
|
|
160
|
+
filterCondition: MatchExp.atom({
|
|
161
|
+
key: 'id',
|
|
162
|
+
value: ['in', 'SELECT DISTINCT author FROM Post WHERE author IS NOT NULL']
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Dynamic Filter Conditions
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
// Use functions as filter conditions, supporting dynamic computation
|
|
171
|
+
const RecentPost = FilteredEntity.create({
|
|
172
|
+
name: 'RecentPost',
|
|
173
|
+
baseEntity: Post,
|
|
174
|
+
filter: () => {
|
|
175
|
+
const sevenDaysAgo = new Date();
|
|
176
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
status: 'published',
|
|
180
|
+
publishedAt: { $gte: sevenDaysAgo.toISOString() }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Context-based filtering (such as user permissions)
|
|
186
|
+
const createUserVisiblePosts = (userId) => {
|
|
187
|
+
return FilteredEntity.create({
|
|
188
|
+
name: `UserVisiblePosts_${userId}`,
|
|
189
|
+
baseEntity: Post,
|
|
190
|
+
filter: async (context) => {
|
|
191
|
+
const user = await context.findOne('User', { id: userId });
|
|
192
|
+
|
|
193
|
+
if (user.role === 'admin') {
|
|
194
|
+
// Admins can see all posts
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (user.role === 'moderator') {
|
|
199
|
+
// Moderators can see published and pending review posts
|
|
200
|
+
return {
|
|
201
|
+
status: { $in: ['published', 'pending_review'] }
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Regular users can only see published posts
|
|
206
|
+
return {
|
|
207
|
+
status: 'published'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Complex Filter Conditions
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
// Use complex query conditions
|
|
218
|
+
const AdvancedFilteredPost = FilteredEntity.create({
|
|
219
|
+
name: 'AdvancedFilteredPost',
|
|
220
|
+
baseEntity: Post,
|
|
221
|
+
filter: {
|
|
222
|
+
$and: [
|
|
223
|
+
{ status: 'published' },
|
|
224
|
+
{
|
|
225
|
+
$or: [
|
|
226
|
+
{ category: 'technology' },
|
|
227
|
+
{ category: 'science' }
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
$expr: {
|
|
232
|
+
$gt: [
|
|
233
|
+
'$viewCount',
|
|
234
|
+
{ $multiply: ['$likeCount', 10] }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Date range-based filtering
|
|
243
|
+
const MonthlyPost = FilteredEntity.create({
|
|
244
|
+
name: 'MonthlyPost',
|
|
245
|
+
baseEntity: Post,
|
|
246
|
+
filter: (context) => {
|
|
247
|
+
const now = new Date();
|
|
248
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
249
|
+
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
status: 'published',
|
|
253
|
+
publishedAt: {
|
|
254
|
+
$gte: startOfMonth.toISOString(),
|
|
255
|
+
$lte: endOfMonth.toISOString()
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Operating on Filtered Entities
|
|
263
|
+
|
|
264
|
+
### Querying Filtered Data
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
// Query filtered entities just like regular entities
|
|
268
|
+
const publishedPosts = await controller.find('PublishedPost');
|
|
269
|
+
console.log('Published posts:', publishedPosts);
|
|
270
|
+
|
|
271
|
+
// Further filtering on filtered entities
|
|
272
|
+
const recentPublishedPosts = await controller.find('PublishedPost', {
|
|
273
|
+
publishedAt: {
|
|
274
|
+
$gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Query single record
|
|
279
|
+
const firstPublishedPost = await controller.findOne('PublishedPost', {
|
|
280
|
+
category: 'technology'
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Count query
|
|
284
|
+
const publishedPostCount = await controller.count('PublishedPost');
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Updating Filtered Data
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
// Update records in filtered entities
|
|
291
|
+
await controller.update('PublishedPost',
|
|
292
|
+
{ category: 'technology' },
|
|
293
|
+
{
|
|
294
|
+
updatedAt: new Date().toISOString(),
|
|
295
|
+
tags: ['tech', 'programming']
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Batch update
|
|
300
|
+
await controller.updateMany('PublishedPost',
|
|
301
|
+
{ viewCount: { $lt: 100 } },
|
|
302
|
+
{
|
|
303
|
+
$inc: { viewCount: 10 } // Add 10 views to low-view posts
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Update single record
|
|
308
|
+
const post = await controller.findOne('PublishedPost', { id: 'post123' });
|
|
309
|
+
if (post) {
|
|
310
|
+
await controller.update('PublishedPost',
|
|
311
|
+
{ id: post.id },
|
|
312
|
+
{
|
|
313
|
+
title: 'Updated Title',
|
|
314
|
+
content: 'Updated Content'
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Deleting Filtered Data
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
// Delete records in filtered entities
|
|
324
|
+
await controller.delete('PublishedPost', {
|
|
325
|
+
category: 'outdated',
|
|
326
|
+
publishedAt: {
|
|
327
|
+
$lt: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString() // Outdated content from a year ago
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Soft delete (update status instead of actual deletion)
|
|
332
|
+
await controller.update('PublishedPost',
|
|
333
|
+
{ id: 'post123' },
|
|
334
|
+
{ status: 'archived' } // This will cause the record to disappear from PublishedPost
|
|
335
|
+
);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Filtered Entities and Reactive Computations
|
|
339
|
+
|
|
340
|
+
### Using Filtered Entities in Computations
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
// Use filtered entities for computations
|
|
344
|
+
const User = Entity.create({
|
|
345
|
+
name: 'User',
|
|
346
|
+
properties: [
|
|
347
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
348
|
+
|
|
349
|
+
// Calculate the number of published posts by user
|
|
350
|
+
Property.create({
|
|
351
|
+
name: 'publishedPostCount',
|
|
352
|
+
type: 'number',
|
|
353
|
+
defaultValue: () => 0,
|
|
354
|
+
computation: Count.create({
|
|
355
|
+
record: UserPublishedPosts
|
|
356
|
+
})
|
|
357
|
+
}),
|
|
358
|
+
|
|
359
|
+
// Calculate the number of popular posts by user
|
|
360
|
+
Property.create({
|
|
361
|
+
name: 'popularPostCount',
|
|
362
|
+
type: 'number',
|
|
363
|
+
defaultValue: () => 0,
|
|
364
|
+
computation: Count.create({
|
|
365
|
+
record: UserPopularPosts
|
|
366
|
+
})
|
|
367
|
+
}),
|
|
368
|
+
|
|
369
|
+
// Calculate total views of user's posts
|
|
370
|
+
Property.create({
|
|
371
|
+
name: 'totalViews',
|
|
372
|
+
type: 'number',
|
|
373
|
+
defaultValue: () => 0,
|
|
374
|
+
computation: WeightedSummation.create({
|
|
375
|
+
record: UserPublishedPosts,
|
|
376
|
+
callback: (relation) => ({
|
|
377
|
+
weight: 1,
|
|
378
|
+
value: relation.target.viewCount
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
}),
|
|
382
|
+
|
|
383
|
+
// Calculate if user is an active author
|
|
384
|
+
Property.create({
|
|
385
|
+
name: 'isActiveAuthor',
|
|
386
|
+
type: 'boolean',
|
|
387
|
+
defaultValue: () => false,
|
|
388
|
+
computed: function(user) {
|
|
389
|
+
// Assuming user has a recentPosts property that's an array
|
|
390
|
+
const recentPosts = user.recentPosts || [];
|
|
391
|
+
return recentPosts.length >= 3; // Has 3 or more recent posts
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
]
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Global Statistics Computation
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
const GlobalStats = Entity.create({
|
|
402
|
+
name: 'GlobalStats',
|
|
403
|
+
properties: [
|
|
404
|
+
// Various post statistics
|
|
405
|
+
Property.create({
|
|
406
|
+
name: 'totalPublishedPosts',
|
|
407
|
+
type: 'number',
|
|
408
|
+
defaultValue: () => 0,
|
|
409
|
+
computation: Count.create({
|
|
410
|
+
record: PublishedPost
|
|
411
|
+
})
|
|
412
|
+
}),
|
|
413
|
+
|
|
414
|
+
Property.create({
|
|
415
|
+
name: 'totalPopularPosts',
|
|
416
|
+
type: 'number',
|
|
417
|
+
defaultValue: () => 0,
|
|
418
|
+
computation: Count.create({
|
|
419
|
+
record: PopularPost
|
|
420
|
+
})
|
|
421
|
+
}),
|
|
422
|
+
|
|
423
|
+
Property.create({
|
|
424
|
+
name: 'totalTechPosts',
|
|
425
|
+
type: 'number',
|
|
426
|
+
defaultValue: () => 0,
|
|
427
|
+
computation: Count.create({
|
|
428
|
+
record: TechPost
|
|
429
|
+
})
|
|
430
|
+
}),
|
|
431
|
+
|
|
432
|
+
// User statistics
|
|
433
|
+
Property.create({
|
|
434
|
+
name: 'activeUserCount',
|
|
435
|
+
type: 'number',
|
|
436
|
+
defaultValue: () => 0,
|
|
437
|
+
computation: Count.create({
|
|
438
|
+
record: ActiveUser
|
|
439
|
+
})
|
|
440
|
+
}),
|
|
441
|
+
|
|
442
|
+
Property.create({
|
|
443
|
+
name: 'authorCount',
|
|
444
|
+
type: 'number',
|
|
445
|
+
defaultValue: () => 0,
|
|
446
|
+
computation: Count.create({
|
|
447
|
+
record: AuthorUser
|
|
448
|
+
})
|
|
449
|
+
}),
|
|
450
|
+
|
|
451
|
+
// Check if all popular posts have tags
|
|
452
|
+
Property.create({
|
|
453
|
+
name: 'allPopularPostsTagged',
|
|
454
|
+
type: 'boolean',
|
|
455
|
+
computation: new Every(PopularPost, null, {
|
|
456
|
+
'tags.length': { $gt: 0 }
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
]
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Category Statistics
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
// Create filtered entities for each category
|
|
467
|
+
const categories = ['technology', 'science', 'business', 'lifestyle'];
|
|
468
|
+
|
|
469
|
+
const categoryEntities = categories.map(category =>
|
|
470
|
+
FilteredEntity.create({
|
|
471
|
+
name: `${category.charAt(0).toUpperCase() + category.slice(1)}Post`,
|
|
472
|
+
baseEntity: Post,
|
|
473
|
+
filter: {
|
|
474
|
+
category: category,
|
|
475
|
+
status: 'published'
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// Create category statistics entity
|
|
481
|
+
const CategoryStats = Entity.create({
|
|
482
|
+
name: 'CategoryStats',
|
|
483
|
+
properties: [
|
|
484
|
+
...categories.map(category =>
|
|
485
|
+
Property.create({
|
|
486
|
+
name: `${category}Count`,
|
|
487
|
+
type: 'number',
|
|
488
|
+
computation: new Count(
|
|
489
|
+
categoryEntities.find(e => e.name.toLowerCase().startsWith(category))
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
),
|
|
493
|
+
|
|
494
|
+
// Most popular category
|
|
495
|
+
Property.create({
|
|
496
|
+
name: 'mostPopularCategory',
|
|
497
|
+
type: 'string',
|
|
498
|
+
computation: new Transform(
|
|
499
|
+
categoryEntities,
|
|
500
|
+
null,
|
|
501
|
+
(allCategoryData) => {
|
|
502
|
+
let maxCount = 0;
|
|
503
|
+
let mostPopular = '';
|
|
504
|
+
|
|
505
|
+
categories.forEach(category => {
|
|
506
|
+
const categoryData = allCategoryData.find(data =>
|
|
507
|
+
data.entityName.toLowerCase().startsWith(category)
|
|
508
|
+
);
|
|
509
|
+
if (categoryData && categoryData.count > maxCount) {
|
|
510
|
+
maxCount = categoryData.count;
|
|
511
|
+
mostPopular = category;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return mostPopular;
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
})
|
|
519
|
+
]
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Real-time Updates and Event Handling
|
|
524
|
+
|
|
525
|
+
### Automatic Update Mechanism
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
// When original data changes, filtered entities automatically update
|
|
529
|
+
const createPost = async (postData) => {
|
|
530
|
+
// Create new post
|
|
531
|
+
const post = await controller.create('Post', {
|
|
532
|
+
...postData,
|
|
533
|
+
status: 'draft' // Initial status is draft
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// At this point, PublishedPost won't include this post
|
|
537
|
+
|
|
538
|
+
// Publish the post
|
|
539
|
+
await controller.update('Post',
|
|
540
|
+
{ id: post.id },
|
|
541
|
+
{ status: 'published', publishedAt: new Date().toISOString() }
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// Now PublishedPost will automatically include this post
|
|
545
|
+
// Related computations (like publishedPostCount) will also update automatically
|
|
546
|
+
};
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Listening to Filtered Entity Changes
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
// Listen to filtered entity change events
|
|
553
|
+
controller.on('filteredEntityChange', (event) => {
|
|
554
|
+
console.log('Filtered entity change:', {
|
|
555
|
+
entityName: event.entityName,
|
|
556
|
+
changeType: event.changeType, // 'added', 'removed', 'updated'
|
|
557
|
+
recordId: event.recordId,
|
|
558
|
+
oldData: event.oldData,
|
|
559
|
+
newData: event.newData
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Custom logic can be executed here
|
|
563
|
+
if (event.entityName === 'PublishedPost' && event.changeType === 'added') {
|
|
564
|
+
// Handle logic when new post is published
|
|
565
|
+
notifySubscribers(event.newData);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Listen to specific filtered entity changes
|
|
570
|
+
controller.on('PublishedPost:added', (post) => {
|
|
571
|
+
console.log('New published post:', post);
|
|
572
|
+
// Send notifications, update cache, etc.
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
controller.on('ActiveUser:removed', (user) => {
|
|
576
|
+
console.log('User became inactive:', user);
|
|
577
|
+
// Handle user inactivity logic
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Performance Optimization
|
|
582
|
+
|
|
583
|
+
### Index Optimization
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
// Add indexes for fields used in filter conditions
|
|
587
|
+
const OptimizedPost = Entity.create({
|
|
588
|
+
name: 'Post',
|
|
589
|
+
properties: [
|
|
590
|
+
Property.create({
|
|
591
|
+
name: 'status',
|
|
592
|
+
type: 'string',
|
|
593
|
+
index: true // Add index for status field
|
|
594
|
+
}),
|
|
595
|
+
Property.create({
|
|
596
|
+
name: 'category',
|
|
597
|
+
type: 'string',
|
|
598
|
+
index: true // Add index for category field
|
|
599
|
+
}),
|
|
600
|
+
Property.create({
|
|
601
|
+
name: 'publishedAt',
|
|
602
|
+
type: 'string',
|
|
603
|
+
index: true // Add index for published time
|
|
604
|
+
}),
|
|
605
|
+
Property.create({
|
|
606
|
+
name: 'viewCount',
|
|
607
|
+
type: 'number',
|
|
608
|
+
index: true // Add index for view count
|
|
609
|
+
})
|
|
610
|
+
]
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Composite indexes
|
|
614
|
+
const PostWithCompositeIndex = Entity.create({
|
|
615
|
+
name: 'Post',
|
|
616
|
+
properties: [
|
|
617
|
+
// ... other properties
|
|
618
|
+
],
|
|
619
|
+
indexes: [
|
|
620
|
+
{ fields: ['status', 'category'] }, // Composite index
|
|
621
|
+
{ fields: ['status', 'publishedAt'] },
|
|
622
|
+
{ fields: ['category', 'viewCount'] }
|
|
623
|
+
]
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Caching Strategy
|
|
628
|
+
|
|
629
|
+
```javascript
|
|
630
|
+
// Configure caching strategy for filtered entities
|
|
631
|
+
const CachedFilteredEntity = FilteredEntity.create({
|
|
632
|
+
name: 'CachedPublishedPost',
|
|
633
|
+
baseEntity: Post,
|
|
634
|
+
filter: { status: 'published' },
|
|
635
|
+
cache: {
|
|
636
|
+
enabled: true,
|
|
637
|
+
ttl: 300, // Cache for 5 minutes
|
|
638
|
+
invalidateOnChange: true // Automatically invalidate cache on data changes
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Pagination and Limits
|
|
644
|
+
|
|
645
|
+
```javascript
|
|
646
|
+
// Use pagination when querying filtered entities
|
|
647
|
+
const getPublishedPostsPaginated = async (page = 1, limit = 20) => {
|
|
648
|
+
return await controller.find('PublishedPost', {}, {
|
|
649
|
+
offset: (page - 1) * limit,
|
|
650
|
+
limit: limit,
|
|
651
|
+
orderBy: [{ field: 'publishedAt', direction: 'desc' }]
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Limit the amount of data used in computations
|
|
656
|
+
const TopPost = FilteredEntity.create({
|
|
657
|
+
name: 'TopPost',
|
|
658
|
+
baseEntity: Post,
|
|
659
|
+
filter: { status: 'published' },
|
|
660
|
+
options: {
|
|
661
|
+
orderBy: [{ field: 'viewCount', direction: 'desc' }],
|
|
662
|
+
limit: 100 // Only consider top 100 posts with highest view count
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Best Practices
|
|
668
|
+
|
|
669
|
+
### 1. Design Filter Conditions Properly
|
|
670
|
+
|
|
671
|
+
```javascript
|
|
672
|
+
// ✅ Efficient filter conditions
|
|
673
|
+
const EfficientFilter = FilteredEntity.create({
|
|
674
|
+
name: 'EfficientFilter',
|
|
675
|
+
baseEntity: Post,
|
|
676
|
+
filter: {
|
|
677
|
+
status: 'published', // Simple equality condition
|
|
678
|
+
category: { $in: ['tech', 'science'] } // Use index-friendly operations
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// ❌ Inefficient filter conditions
|
|
683
|
+
const InefficientFilter = FilteredEntity.create({
|
|
684
|
+
name: 'InefficientFilter',
|
|
685
|
+
baseEntity: Post,
|
|
686
|
+
filter: {
|
|
687
|
+
$where: function() {
|
|
688
|
+
// Avoid using $where, it cannot use indexes
|
|
689
|
+
return this.title.toLowerCase().includes('javascript');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### 2. Avoid Over-filtering
|
|
696
|
+
|
|
697
|
+
```javascript
|
|
698
|
+
// ✅ Reasonable filtering granularity
|
|
699
|
+
const PublishedPost = FilteredEntity.create({
|
|
700
|
+
name: 'PublishedPost',
|
|
701
|
+
baseEntity: Post,
|
|
702
|
+
filter: { status: 'published' }
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const TechPost = FilteredEntity.create({
|
|
706
|
+
name: 'TechPost',
|
|
707
|
+
baseEntity: PublishedPost, // Based on existing filtered entity
|
|
708
|
+
filter: { category: 'technology' }
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// ❌ Over-segmented filtered entities
|
|
712
|
+
const TechPostOnMonday = FilteredEntity.create({
|
|
713
|
+
name: 'TechPostOnMonday',
|
|
714
|
+
baseEntity: Post,
|
|
715
|
+
filter: {
|
|
716
|
+
category: 'technology',
|
|
717
|
+
status: 'published',
|
|
718
|
+
$expr: {
|
|
719
|
+
$eq: [{ $dayOfWeek: '$publishedAt' }, 2] // Too specific condition
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### 3. Use Computations Appropriately
|
|
726
|
+
|
|
727
|
+
```javascript
|
|
728
|
+
// ✅ Appropriate computations on filtered entities
|
|
729
|
+
const UserStats = Entity.create({
|
|
730
|
+
name: 'User',
|
|
731
|
+
properties: [
|
|
732
|
+
Property.create({
|
|
733
|
+
name: 'publishedPostCount',
|
|
734
|
+
type: 'number',
|
|
735
|
+
computation: new Count(PublishedPost, 'author') // Simple counting
|
|
736
|
+
})
|
|
737
|
+
]
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// ❌ Complex computations on filtered entities
|
|
741
|
+
const ComplexUserStats = Entity.create({
|
|
742
|
+
name: 'User',
|
|
743
|
+
properties: [
|
|
744
|
+
Property.create({
|
|
745
|
+
name: 'complexScore',
|
|
746
|
+
type: 'number',
|
|
747
|
+
computation: new Transform(
|
|
748
|
+
PublishedPost,
|
|
749
|
+
'author',
|
|
750
|
+
(posts) => {
|
|
751
|
+
// Avoid complex data processing in computations
|
|
752
|
+
return posts.reduce((score, post) => {
|
|
753
|
+
return score + (post.viewCount * 0.1 + post.likeCount * 0.5);
|
|
754
|
+
}, 0);
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
})
|
|
758
|
+
]
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### 4. Monitor Performance
|
|
763
|
+
|
|
764
|
+
```javascript
|
|
765
|
+
// Monitor filtered entity performance
|
|
766
|
+
const monitorFilteredEntity = (entityName) => {
|
|
767
|
+
controller.on(`${entityName}:query`, (event) => {
|
|
768
|
+
console.log(`Query ${entityName}:`, {
|
|
769
|
+
duration: event.duration,
|
|
770
|
+
resultCount: event.resultCount,
|
|
771
|
+
filter: event.filter
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (event.duration > 1000) { // Query takes more than 1 second
|
|
775
|
+
console.warn(`${entityName} query performance warning:`, event);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
monitorFilteredEntity('PublishedPost');
|
|
781
|
+
monitorFilteredEntity('ActiveUser');
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Filtered entities provide powerful data viewing and computation capabilities for interaqt. By using filtered entities appropriately, you can create efficient, real-time updating data statistics and analysis systems while maintaining code clarity and maintainability.
|