interaqt 1.1.1 → 1.1.3
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/README.md +1 -0
- package/agent/skill/interaqt-patterns.md +541 -0
- package/agent/skill/interaqt-recipes.md +557 -0
- package/agent/skill/interaqt-reference.md +598 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<a href="https://www.npmjs.com/package/interaqt"><img src="https://img.shields.io/npm/dm/interaqt.svg" alt="npm downloads"></a>
|
|
9
9
|
<a href="https://github.com/InteraqtDev/interaqt/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/interaqt.svg" alt="license"></a>
|
|
10
10
|
<a href="https://github.com/InteraqtDev/interaqt"><img src="https://img.shields.io/badge/TypeScript-strict-blue.svg" alt="TypeScript"></a>
|
|
11
|
+
<a href="#"><img src="https://img.shields.io/badge/coverage-92.41%25-brightgreen.svg" alt="coverage"></a>
|
|
11
12
|
</p>
|
|
12
13
|
</p>
|
|
13
14
|
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# interaqt Patterns
|
|
2
|
+
|
|
3
|
+
> Read this file BEFORE writing any interaqt code. Every section is self-contained.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When Defining Entities and Properties
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Entity, Property } from 'interaqt'
|
|
11
|
+
|
|
12
|
+
const User = Entity.create({
|
|
13
|
+
name: 'User',
|
|
14
|
+
properties: [
|
|
15
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
16
|
+
Property.create({ name: 'email', type: 'string' }),
|
|
17
|
+
Property.create({ name: 'age', type: 'number' }),
|
|
18
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'active' }),
|
|
19
|
+
Property.create({ name: 'createdAt', type: 'string', defaultValue: () => new Date().toISOString() }),
|
|
20
|
+
Property.create({
|
|
21
|
+
name: 'fullName',
|
|
22
|
+
type: 'string',
|
|
23
|
+
computed: (record) => `${record.firstName} ${record.lastName}`
|
|
24
|
+
})
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Property types: `'string'`, `'number'`, `'boolean'`. Use `collection: true` for arrays. Use `type: 'object'` for JSON.
|
|
30
|
+
|
|
31
|
+
### WRONG: Manual UUID assignment
|
|
32
|
+
```typescript
|
|
33
|
+
// DON'T — the Klass pattern generates IDs internally
|
|
34
|
+
const user = Entity.create({
|
|
35
|
+
name: 'User',
|
|
36
|
+
uuid: 'my-custom-id-123',
|
|
37
|
+
properties: [...]
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### CORRECT:
|
|
42
|
+
```typescript
|
|
43
|
+
const user = Entity.create({
|
|
44
|
+
name: 'User',
|
|
45
|
+
properties: [...]
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### WHY
|
|
50
|
+
The Klass pattern uses `generateUUID()` internally. Manual IDs risk collisions and break serialization.
|
|
51
|
+
|
|
52
|
+
### Checklist
|
|
53
|
+
- [ ] Entity name is PascalCase and singular (`User` not `users`)
|
|
54
|
+
- [ ] No manual UUID assignment
|
|
55
|
+
- [ ] Computed properties that depend only on the same record use `computed`, NOT Transform
|
|
56
|
+
- [ ] Properties with reactive computations (Count, etc.) include `defaultValue`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## When Defining Relations
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { Entity, Property, Relation } from 'interaqt'
|
|
64
|
+
|
|
65
|
+
const User = Entity.create({ name: 'User', properties: [Property.create({ name: 'name', type: 'string' })] })
|
|
66
|
+
const Post = Entity.create({ name: 'Post', properties: [Property.create({ name: 'title', type: 'string' })] })
|
|
67
|
+
|
|
68
|
+
const UserPosts = Relation.create({
|
|
69
|
+
source: User,
|
|
70
|
+
sourceProperty: 'posts',
|
|
71
|
+
target: Post,
|
|
72
|
+
targetProperty: 'author',
|
|
73
|
+
type: '1:n'
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Relation names are auto-generated from source+target entity names (e.g., `UserPost`).
|
|
78
|
+
|
|
79
|
+
### WRONG: Specifying relation name
|
|
80
|
+
```typescript
|
|
81
|
+
// DON'T — name is auto-generated
|
|
82
|
+
const UserPosts = Relation.create({
|
|
83
|
+
name: 'UserPost',
|
|
84
|
+
source: User,
|
|
85
|
+
sourceProperty: 'posts',
|
|
86
|
+
target: Post,
|
|
87
|
+
targetProperty: 'author',
|
|
88
|
+
type: '1:n'
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### CORRECT:
|
|
93
|
+
```typescript
|
|
94
|
+
const UserPosts = Relation.create({
|
|
95
|
+
source: User,
|
|
96
|
+
sourceProperty: 'posts',
|
|
97
|
+
target: Post,
|
|
98
|
+
targetProperty: 'author',
|
|
99
|
+
type: '1:n'
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### WHY
|
|
104
|
+
The framework generates relation names automatically from entity names. Specifying a name conflicts with this mechanism.
|
|
105
|
+
|
|
106
|
+
### WRONG: Omitting relation type
|
|
107
|
+
```typescript
|
|
108
|
+
// DON'T — type is mandatory
|
|
109
|
+
const UserPosts = Relation.create({
|
|
110
|
+
source: User,
|
|
111
|
+
sourceProperty: 'posts',
|
|
112
|
+
target: Post,
|
|
113
|
+
targetProperty: 'author'
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### CORRECT:
|
|
118
|
+
```typescript
|
|
119
|
+
const UserPosts = Relation.create({
|
|
120
|
+
source: User,
|
|
121
|
+
sourceProperty: 'posts',
|
|
122
|
+
target: Post,
|
|
123
|
+
targetProperty: 'author',
|
|
124
|
+
type: '1:n'
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### WHY
|
|
129
|
+
Without `type`, the framework cannot determine cardinality. ALWAYS explicitly set `'1:1'`, `'1:n'`, `'n:1'`, or `'n:n'`.
|
|
130
|
+
|
|
131
|
+
### Checklist
|
|
132
|
+
- [ ] NEVER specify `name` — it is auto-generated
|
|
133
|
+
- [ ] ALWAYS specify `type` explicitly
|
|
134
|
+
- [ ] Both `sourceProperty` and `targetProperty` are set with meaningful names
|
|
135
|
+
- [ ] Symmetric relations: `source === target` AND `sourceProperty === targetProperty`
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## When Adding Reactive Computations
|
|
140
|
+
|
|
141
|
+
**Decision tree — which computation type to use:**
|
|
142
|
+
|
|
143
|
+
| Need | Use |
|
|
144
|
+
|------|-----|
|
|
145
|
+
| Count related records | `Count` |
|
|
146
|
+
| Weighted sum of related records | `WeightedSummation` |
|
|
147
|
+
| Check ALL related records match condition | `Every` |
|
|
148
|
+
| Check ANY related record matches condition | `Any` |
|
|
149
|
+
| Derive new entities from events or other entities | `Transform` (on Entity `computation`) |
|
|
150
|
+
| Update a property value based on state transitions | `StateMachine` (on Property `computation`) |
|
|
151
|
+
| Simple computation from same-record fields | `computed` (on Property) |
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { Entity, Property, Relation, Count, WeightedSummation, Transform, InteractionEventEntity } from 'interaqt'
|
|
155
|
+
|
|
156
|
+
const Post = Entity.create({
|
|
157
|
+
name: 'Post',
|
|
158
|
+
properties: [
|
|
159
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
160
|
+
Property.create({
|
|
161
|
+
name: 'likeCount',
|
|
162
|
+
type: 'number',
|
|
163
|
+
defaultValue: () => 0,
|
|
164
|
+
computation: Count.create({ record: LikeRelation })
|
|
165
|
+
})
|
|
166
|
+
]
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### WRONG: Transform on a Property computation
|
|
171
|
+
```typescript
|
|
172
|
+
// DON'T — Transform belongs on Entity.computation, not Property.computation
|
|
173
|
+
Property.create({
|
|
174
|
+
name: 'formattedPrice',
|
|
175
|
+
computation: Transform.create({
|
|
176
|
+
record: Product,
|
|
177
|
+
callback: (product) => `$${product.price}`
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### CORRECT:
|
|
183
|
+
```typescript
|
|
184
|
+
Property.create({
|
|
185
|
+
name: 'formattedPrice',
|
|
186
|
+
type: 'string',
|
|
187
|
+
computed: (record) => `$${record.price}`
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### WHY
|
|
192
|
+
Transform creates new records in a computed entity collection. It CANNOT update a single property. Use `computed` for same-entity property computations.
|
|
193
|
+
|
|
194
|
+
### WRONG: Transform for counting
|
|
195
|
+
```typescript
|
|
196
|
+
// DON'T — use Count for counting
|
|
197
|
+
Property.create({
|
|
198
|
+
name: 'followerCount',
|
|
199
|
+
computation: Transform.create({
|
|
200
|
+
record: FollowRelation,
|
|
201
|
+
callback: (followers) => followers.length
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### CORRECT:
|
|
207
|
+
```typescript
|
|
208
|
+
Property.create({
|
|
209
|
+
name: 'followerCount',
|
|
210
|
+
type: 'number',
|
|
211
|
+
defaultValue: () => 0,
|
|
212
|
+
computation: Count.create({ record: FollowRelation })
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### WHY
|
|
217
|
+
Count uses incremental algorithms. Transform loads all records into memory, which is inefficient for counting.
|
|
218
|
+
|
|
219
|
+
### WRONG: Computations passed to Controller
|
|
220
|
+
```typescript
|
|
221
|
+
// DON'T — Controller does NOT accept a computations parameter
|
|
222
|
+
const controller = new Controller({
|
|
223
|
+
system, entities, relations,
|
|
224
|
+
eventSources: [myComputation], // eventSources is for Interactions, not computations
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### CORRECT:
|
|
229
|
+
```typescript
|
|
230
|
+
// Computations MUST be placed inside Entity/Relation/Property definitions
|
|
231
|
+
Property.create({
|
|
232
|
+
name: 'postCount',
|
|
233
|
+
type: 'number',
|
|
234
|
+
defaultValue: () => 0,
|
|
235
|
+
computation: Count.create({ record: UserPostRelation })
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### WHY
|
|
240
|
+
All computations are declared within the `computation` field of Entity, Relation, or Property. The `dict` parameter in Controller is for global Dictionary instances only.
|
|
241
|
+
|
|
242
|
+
### Checklist
|
|
243
|
+
- [ ] Transform is on `Entity.computation` or `Relation.computation`, NEVER on `Property.computation`
|
|
244
|
+
- [ ] Count, WeightedSummation, Every, Any are on `Property.computation`
|
|
245
|
+
- [ ] StateMachine is on `Property.computation`
|
|
246
|
+
- [ ] `computed` is used for same-record-only property derivations
|
|
247
|
+
- [ ] Properties with computation ALWAYS have `defaultValue`
|
|
248
|
+
- [ ] NEVER pass computations to Controller constructor
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## When Creating Interactions
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { Interaction, Action, Payload, PayloadItem, Entity, Property } from 'interaqt'
|
|
256
|
+
|
|
257
|
+
const Post = Entity.create({ name: 'Post', properties: [Property.create({ name: 'title', type: 'string' })] })
|
|
258
|
+
|
|
259
|
+
const CreatePost = Interaction.create({
|
|
260
|
+
name: 'CreatePost',
|
|
261
|
+
action: Action.create({ name: 'createPost' }),
|
|
262
|
+
payload: Payload.create({
|
|
263
|
+
items: [
|
|
264
|
+
PayloadItem.create({ name: 'title', type: 'string', required: true }),
|
|
265
|
+
PayloadItem.create({ name: 'content', type: 'string', required: true }),
|
|
266
|
+
PayloadItem.create({ name: 'postId', type: 'string', base: Post, isRef: true })
|
|
267
|
+
]
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Action is ONLY an identifier — it contains no operational logic. All data changes happen through reactive computations (Transform, StateMachine, Count, etc.).
|
|
273
|
+
|
|
274
|
+
### WRONG: Writing operational logic in Action
|
|
275
|
+
```typescript
|
|
276
|
+
// DON'T — Action has no handler/execute method
|
|
277
|
+
const CreatePost = Action.create({
|
|
278
|
+
name: 'createPost',
|
|
279
|
+
execute: async (payload) => {
|
|
280
|
+
await db.create('Post', payload)
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### CORRECT:
|
|
286
|
+
```typescript
|
|
287
|
+
const CreatePost = Action.create({ name: 'createPost' })
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### WHY
|
|
291
|
+
interaqt is declarative. Interactions declare "what users can do." Data changes are declared via computations (Transform, Count, StateMachine), not imperatively in handlers.
|
|
292
|
+
|
|
293
|
+
### Checklist
|
|
294
|
+
- [ ] Action has ONLY a `name` — no handler, no execute, no callback
|
|
295
|
+
- [ ] PayloadItem uses `isRef: true` when referencing an existing entity by ID
|
|
296
|
+
- [ ] PayloadItem uses `isCollection: true` for array parameters
|
|
297
|
+
- [ ] `base` is set when the payload item corresponds to an Entity
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## When Setting Up the Controller
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { Controller, MonoSystem, PGLiteDB, KlassByName } from 'interaqt'
|
|
305
|
+
|
|
306
|
+
const system = new MonoSystem(new PGLiteDB())
|
|
307
|
+
system.conceptClass = KlassByName
|
|
308
|
+
|
|
309
|
+
const controller = new Controller({
|
|
310
|
+
system,
|
|
311
|
+
entities: [User, Post],
|
|
312
|
+
relations: [UserPosts],
|
|
313
|
+
eventSources: [CreatePost],
|
|
314
|
+
dict: [],
|
|
315
|
+
recordMutationSideEffects: []
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
await controller.setup(true)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### WRONG: Calling dispatch before setup
|
|
322
|
+
```typescript
|
|
323
|
+
// DON'T — setup MUST come first
|
|
324
|
+
const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
|
|
325
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### CORRECT:
|
|
329
|
+
```typescript
|
|
330
|
+
const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
|
|
331
|
+
await controller.setup(true)
|
|
332
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### WHY
|
|
336
|
+
`setup(true)` installs database tables and initializes all computations. Without it, storage operations will fail.
|
|
337
|
+
|
|
338
|
+
### Checklist
|
|
339
|
+
- [ ] `system.conceptClass = KlassByName` is set before creating Controller
|
|
340
|
+
- [ ] `controller.setup(true)` is called BEFORE any `dispatch`
|
|
341
|
+
- [ ] `dict` contains only Dictionary instances, not computations
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## When Dispatching Interactions
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const result = await controller.dispatch(CreatePost, {
|
|
349
|
+
user: { id: 'user-1', role: 'author' },
|
|
350
|
+
payload: {
|
|
351
|
+
title: 'My Post',
|
|
352
|
+
content: 'Hello world'
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
if (result.error) {
|
|
357
|
+
console.log('Error:', result.error.message)
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### WRONG: Using try-catch for error handling
|
|
362
|
+
```typescript
|
|
363
|
+
// DON'T — interaqt does NOT throw exceptions by default
|
|
364
|
+
try {
|
|
365
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: {} })
|
|
366
|
+
} catch (e) {
|
|
367
|
+
// This code will NEVER execute (unless forceThrowDispatchError is true)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### CORRECT:
|
|
372
|
+
```typescript
|
|
373
|
+
const result = await controller.dispatch(CreatePost, {
|
|
374
|
+
user: { id: '1' },
|
|
375
|
+
payload: {}
|
|
376
|
+
})
|
|
377
|
+
if (result.error) {
|
|
378
|
+
console.log('Error:', result.error.message)
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### WHY
|
|
383
|
+
The framework catches all errors internally and returns them via `result.error`. Exceptions are never thrown to callers (unless `forceThrowDispatchError: true` is set on Controller).
|
|
384
|
+
|
|
385
|
+
### WRONG: Passing a name string instead of instance
|
|
386
|
+
```typescript
|
|
387
|
+
// DON'T — first argument must be the event source instance, not a string
|
|
388
|
+
controller.dispatch('CreatePost', payload)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### WRONG: Using non-existent API methods
|
|
392
|
+
```typescript
|
|
393
|
+
// DON'T — these methods do NOT exist
|
|
394
|
+
controller.callInteraction('CreatePost', payload)
|
|
395
|
+
controller.run()
|
|
396
|
+
controller.execute()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### CORRECT:
|
|
400
|
+
```typescript
|
|
401
|
+
// The ONLY method to trigger interactions — first arg is the instance reference
|
|
402
|
+
await controller.dispatch(CreatePost, {
|
|
403
|
+
user: { id: 'user-1' },
|
|
404
|
+
payload: { title: 'Hi' }
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Checklist
|
|
409
|
+
- [ ] ALWAYS pass a `user` object with at least `id`
|
|
410
|
+
- [ ] ALWAYS check `result.error` — NEVER use try-catch
|
|
411
|
+
- [ ] Use `controller.dispatch(eventSourceInstance, args)` — first arg is the instance, NOT a name string
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## When Querying Data
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { MatchExp } from 'interaqt'
|
|
419
|
+
|
|
420
|
+
const user = await system.storage.findOne(
|
|
421
|
+
'User',
|
|
422
|
+
MatchExp.atom({ key: 'email', value: ['=', 'alice@example.com'] }),
|
|
423
|
+
undefined,
|
|
424
|
+
['id', 'name', 'email', 'status']
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const activeUsers = await system.storage.find(
|
|
428
|
+
'User',
|
|
429
|
+
MatchExp.atom({ key: 'status', value: ['=', 'active'] }),
|
|
430
|
+
{ orderBy: { createdAt: 'DESC' }, limit: 10 },
|
|
431
|
+
['id', 'name', 'email']
|
|
432
|
+
)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### WRONG: Omitting attributeQuery
|
|
436
|
+
```typescript
|
|
437
|
+
// DON'T — without attributeQuery, only `id` is returned
|
|
438
|
+
const user = await system.storage.findOne(
|
|
439
|
+
'User',
|
|
440
|
+
MatchExp.atom({ key: 'id', value: ['=', 1] })
|
|
441
|
+
)
|
|
442
|
+
// user.name → undefined!
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### CORRECT:
|
|
446
|
+
```typescript
|
|
447
|
+
const user = await system.storage.findOne(
|
|
448
|
+
'User',
|
|
449
|
+
MatchExp.atom({ key: 'id', value: ['=', 1] }),
|
|
450
|
+
undefined,
|
|
451
|
+
['id', 'name', 'email', 'status']
|
|
452
|
+
)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### WHY
|
|
456
|
+
Without `attributeQuery`, the framework returns only the `id` field. This is the most common cause of "undefined" bugs.
|
|
457
|
+
|
|
458
|
+
Nested attributeQuery for relations:
|
|
459
|
+
```typescript
|
|
460
|
+
const usersWithPosts = await system.storage.find(
|
|
461
|
+
'User',
|
|
462
|
+
undefined,
|
|
463
|
+
{},
|
|
464
|
+
[
|
|
465
|
+
'id', 'name',
|
|
466
|
+
['posts', { attributeQuery: ['id', 'title', 'status'] }]
|
|
467
|
+
]
|
|
468
|
+
)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Relation properties use `['&', { attributeQuery: ['role', 'joinedAt'] }]` syntax.
|
|
472
|
+
|
|
473
|
+
### Checklist
|
|
474
|
+
- [ ] ALWAYS pass `attributeQuery` as the 4th argument to `find`/`findOne`
|
|
475
|
+
- [ ] Use `['*']` for all fields, or list specific field names
|
|
476
|
+
- [ ] Use nested arrays for relation data: `['relationName', { attributeQuery: [...] }]`
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## When Writing Tests
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { describe, test, expect, beforeEach } from 'vitest'
|
|
484
|
+
import { Controller, MonoSystem, KlassByName, PGLiteDB, MatchExp } from 'interaqt'
|
|
485
|
+
|
|
486
|
+
describe('Feature', () => {
|
|
487
|
+
let system: MonoSystem
|
|
488
|
+
let controller: Controller
|
|
489
|
+
|
|
490
|
+
beforeEach(async () => {
|
|
491
|
+
system = new MonoSystem(new PGLiteDB())
|
|
492
|
+
system.conceptClass = KlassByName
|
|
493
|
+
controller = new Controller({
|
|
494
|
+
system, entities, relations, eventSources, dict: [], recordMutationSideEffects: []
|
|
495
|
+
})
|
|
496
|
+
await controller.setup(true)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('creates a post via interaction', async () => {
|
|
500
|
+
const result = await controller.dispatch(CreatePost, {
|
|
501
|
+
user: { id: 'user-1' },
|
|
502
|
+
payload: { title: 'Test', content: 'Hello' }
|
|
503
|
+
})
|
|
504
|
+
expect(result.error).toBeUndefined()
|
|
505
|
+
|
|
506
|
+
const post = await system.storage.findOne(
|
|
507
|
+
'Post',
|
|
508
|
+
MatchExp.atom({ key: 'title', value: ['=', 'Test'] }),
|
|
509
|
+
undefined,
|
|
510
|
+
['id', 'title', 'content']
|
|
511
|
+
)
|
|
512
|
+
expect(post).toBeTruthy()
|
|
513
|
+
expect(post.title).toBe('Test')
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### WRONG: Direct storage mutation in tests
|
|
519
|
+
```typescript
|
|
520
|
+
// DON'T — bypasses all validation and business logic
|
|
521
|
+
const post = await system.storage.create('Post', { title: 'Test', content: 'Hello' })
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### CORRECT:
|
|
525
|
+
```typescript
|
|
526
|
+
// Use dispatch to test business logic
|
|
527
|
+
const result = await controller.dispatch(CreatePost, {
|
|
528
|
+
user: { id: 'user-1' },
|
|
529
|
+
payload: { title: 'Test', content: 'Hello' }
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### WHY
|
|
534
|
+
`storage.create` bypasses ALL validation, permissions, and reactive computations. It is acceptable ONLY for test data setup (creating prerequisite records), NEVER for testing business logic.
|
|
535
|
+
|
|
536
|
+
### Checklist
|
|
537
|
+
- [ ] Use `PGLiteDB` for test databases
|
|
538
|
+
- [ ] Call `controller.setup(true)` in `beforeEach`
|
|
539
|
+
- [ ] Test business logic through `controller.dispatch`, not direct storage
|
|
540
|
+
- [ ] Check `result.error` — NEVER use try-catch
|
|
541
|
+
- [ ] ALWAYS pass `attributeQuery` when asserting on query results
|