interaqt 1.1.2 → 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/agent/skill/interaqt-patterns.md +41 -36
- package/agent/skill/interaqt-recipes.md +164 -108
- package/agent/skill/interaqt-reference.md +264 -75
- package/package.json +1 -1
|
@@ -15,12 +15,12 @@ const User = Entity.create({
|
|
|
15
15
|
Property.create({ name: 'name', type: 'string' }),
|
|
16
16
|
Property.create({ name: 'email', type: 'string' }),
|
|
17
17
|
Property.create({ name: 'age', type: 'number' }),
|
|
18
|
-
Property.create({ name: 'status', type: 'string', defaultValue: 'active' }),
|
|
18
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'active' }),
|
|
19
19
|
Property.create({ name: 'createdAt', type: 'string', defaultValue: () => new Date().toISOString() }),
|
|
20
20
|
Property.create({
|
|
21
21
|
name: 'fullName',
|
|
22
22
|
type: 'string',
|
|
23
|
-
|
|
23
|
+
computed: (record) => `${record.firstName} ${record.lastName}`
|
|
24
24
|
})
|
|
25
25
|
]
|
|
26
26
|
})
|
|
@@ -52,7 +52,7 @@ The Klass pattern uses `generateUUID()` internally. Manual IDs risk collisions a
|
|
|
52
52
|
### Checklist
|
|
53
53
|
- [ ] Entity name is PascalCase and singular (`User` not `users`)
|
|
54
54
|
- [ ] No manual UUID assignment
|
|
55
|
-
- [ ] Computed properties that depend only on the same record use `
|
|
55
|
+
- [ ] Computed properties that depend only on the same record use `computed`, NOT Transform
|
|
56
56
|
- [ ] Properties with reactive computations (Count, etc.) include `defaultValue`
|
|
57
57
|
|
|
58
58
|
---
|
|
@@ -148,7 +148,7 @@ Without `type`, the framework cannot determine cardinality. ALWAYS explicitly se
|
|
|
148
148
|
| Check ANY related record matches condition | `Any` |
|
|
149
149
|
| Derive new entities from events or other entities | `Transform` (on Entity `computation`) |
|
|
150
150
|
| Update a property value based on state transitions | `StateMachine` (on Property `computation`) |
|
|
151
|
-
| Simple computation from same-record fields | `
|
|
151
|
+
| Simple computation from same-record fields | `computed` (on Property) |
|
|
152
152
|
|
|
153
153
|
```typescript
|
|
154
154
|
import { Entity, Property, Relation, Count, WeightedSummation, Transform, InteractionEventEntity } from 'interaqt'
|
|
@@ -184,12 +184,12 @@ Property.create({
|
|
|
184
184
|
Property.create({
|
|
185
185
|
name: 'formattedPrice',
|
|
186
186
|
type: 'string',
|
|
187
|
-
|
|
187
|
+
computed: (record) => `$${record.price}`
|
|
188
188
|
})
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
### WHY
|
|
192
|
-
Transform creates new records in a computed entity collection. It CANNOT update a single property. Use `
|
|
192
|
+
Transform creates new records in a computed entity collection. It CANNOT update a single property. Use `computed` for same-entity property computations.
|
|
193
193
|
|
|
194
194
|
### WRONG: Transform for counting
|
|
195
195
|
```typescript
|
|
@@ -220,8 +220,8 @@ Count uses incremental algorithms. Transform loads all records into memory, whic
|
|
|
220
220
|
```typescript
|
|
221
221
|
// DON'T — Controller does NOT accept a computations parameter
|
|
222
222
|
const controller = new Controller({
|
|
223
|
-
system, entities, relations,
|
|
224
|
-
|
|
223
|
+
system, entities, relations,
|
|
224
|
+
eventSources: [myComputation], // eventSources is for Interactions, not computations
|
|
225
225
|
})
|
|
226
226
|
```
|
|
227
227
|
|
|
@@ -243,7 +243,7 @@ All computations are declared within the `computation` field of Entity, Relation
|
|
|
243
243
|
- [ ] Transform is on `Entity.computation` or `Relation.computation`, NEVER on `Property.computation`
|
|
244
244
|
- [ ] Count, WeightedSummation, Every, Any are on `Property.computation`
|
|
245
245
|
- [ ] StateMachine is on `Property.computation`
|
|
246
|
-
- [ ] `
|
|
246
|
+
- [ ] `computed` is used for same-record-only property derivations
|
|
247
247
|
- [ ] Properties with computation ALWAYS have `defaultValue`
|
|
248
248
|
- [ ] NEVER pass computations to Controller constructor
|
|
249
249
|
|
|
@@ -261,9 +261,9 @@ const CreatePost = Interaction.create({
|
|
|
261
261
|
action: Action.create({ name: 'createPost' }),
|
|
262
262
|
payload: Payload.create({
|
|
263
263
|
items: [
|
|
264
|
-
PayloadItem.create({ name: 'title', required: true }),
|
|
265
|
-
PayloadItem.create({ name: 'content', required: true }),
|
|
266
|
-
PayloadItem.create({ name: 'postId', base: Post, isRef: true })
|
|
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
267
|
]
|
|
268
268
|
})
|
|
269
269
|
})
|
|
@@ -310,8 +310,7 @@ const controller = new Controller({
|
|
|
310
310
|
system,
|
|
311
311
|
entities: [User, Post],
|
|
312
312
|
relations: [UserPosts],
|
|
313
|
-
|
|
314
|
-
interactions: [CreatePost],
|
|
313
|
+
eventSources: [CreatePost],
|
|
315
314
|
dict: [],
|
|
316
315
|
recordMutationSideEffects: []
|
|
317
316
|
})
|
|
@@ -319,18 +318,18 @@ const controller = new Controller({
|
|
|
319
318
|
await controller.setup(true)
|
|
320
319
|
```
|
|
321
320
|
|
|
322
|
-
### WRONG: Calling
|
|
321
|
+
### WRONG: Calling dispatch before setup
|
|
323
322
|
```typescript
|
|
324
323
|
// DON'T — setup MUST come first
|
|
325
|
-
const controller = new Controller({ system, entities, relations,
|
|
326
|
-
await controller.
|
|
324
|
+
const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
|
|
325
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
|
|
327
326
|
```
|
|
328
327
|
|
|
329
328
|
### CORRECT:
|
|
330
329
|
```typescript
|
|
331
|
-
const controller = new Controller({ system, entities, relations,
|
|
330
|
+
const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
|
|
332
331
|
await controller.setup(true)
|
|
333
|
-
await controller.
|
|
332
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
|
|
334
333
|
```
|
|
335
334
|
|
|
336
335
|
### WHY
|
|
@@ -338,15 +337,15 @@ await controller.callInteraction('CreatePost', { user: { id: '1' }, payload: { t
|
|
|
338
337
|
|
|
339
338
|
### Checklist
|
|
340
339
|
- [ ] `system.conceptClass = KlassByName` is set before creating Controller
|
|
341
|
-
- [ ] `controller.setup(true)` is called BEFORE any `
|
|
340
|
+
- [ ] `controller.setup(true)` is called BEFORE any `dispatch`
|
|
342
341
|
- [ ] `dict` contains only Dictionary instances, not computations
|
|
343
342
|
|
|
344
343
|
---
|
|
345
344
|
|
|
346
|
-
## When
|
|
345
|
+
## When Dispatching Interactions
|
|
347
346
|
|
|
348
347
|
```typescript
|
|
349
|
-
const result = await controller.
|
|
348
|
+
const result = await controller.dispatch(CreatePost, {
|
|
350
349
|
user: { id: 'user-1', role: 'author' },
|
|
351
350
|
payload: {
|
|
352
351
|
title: 'My Post',
|
|
@@ -361,17 +360,17 @@ if (result.error) {
|
|
|
361
360
|
|
|
362
361
|
### WRONG: Using try-catch for error handling
|
|
363
362
|
```typescript
|
|
364
|
-
// DON'T — interaqt does NOT throw exceptions
|
|
363
|
+
// DON'T — interaqt does NOT throw exceptions by default
|
|
365
364
|
try {
|
|
366
|
-
await controller.
|
|
365
|
+
await controller.dispatch(CreatePost, { user: { id: '1' }, payload: {} })
|
|
367
366
|
} catch (e) {
|
|
368
|
-
// This code will NEVER execute
|
|
367
|
+
// This code will NEVER execute (unless forceThrowDispatchError is true)
|
|
369
368
|
}
|
|
370
369
|
```
|
|
371
370
|
|
|
372
371
|
### CORRECT:
|
|
373
372
|
```typescript
|
|
374
|
-
const result = await controller.
|
|
373
|
+
const result = await controller.dispatch(CreatePost, {
|
|
375
374
|
user: { id: '1' },
|
|
376
375
|
payload: {}
|
|
377
376
|
})
|
|
@@ -381,20 +380,26 @@ if (result.error) {
|
|
|
381
380
|
```
|
|
382
381
|
|
|
383
382
|
### WHY
|
|
384
|
-
The framework catches all errors internally and returns them via `result.error`. Exceptions are never thrown to callers.
|
|
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
|
+
```
|
|
385
390
|
|
|
386
391
|
### WRONG: Using non-existent API methods
|
|
387
392
|
```typescript
|
|
388
393
|
// DON'T — these methods do NOT exist
|
|
389
|
-
controller.
|
|
394
|
+
controller.callInteraction('CreatePost', payload)
|
|
390
395
|
controller.run()
|
|
391
396
|
controller.execute()
|
|
392
397
|
```
|
|
393
398
|
|
|
394
399
|
### CORRECT:
|
|
395
400
|
```typescript
|
|
396
|
-
// The ONLY method to trigger interactions
|
|
397
|
-
await controller.
|
|
401
|
+
// The ONLY method to trigger interactions — first arg is the instance reference
|
|
402
|
+
await controller.dispatch(CreatePost, {
|
|
398
403
|
user: { id: 'user-1' },
|
|
399
404
|
payload: { title: 'Hi' }
|
|
400
405
|
})
|
|
@@ -403,7 +408,7 @@ await controller.callInteraction('CreatePost', {
|
|
|
403
408
|
### Checklist
|
|
404
409
|
- [ ] ALWAYS pass a `user` object with at least `id`
|
|
405
410
|
- [ ] ALWAYS check `result.error` — NEVER use try-catch
|
|
406
|
-
- [ ] Use `controller.
|
|
411
|
+
- [ ] Use `controller.dispatch(eventSourceInstance, args)` — first arg is the instance, NOT a name string
|
|
407
412
|
|
|
408
413
|
---
|
|
409
414
|
|
|
@@ -486,13 +491,13 @@ describe('Feature', () => {
|
|
|
486
491
|
system = new MonoSystem(new PGLiteDB())
|
|
487
492
|
system.conceptClass = KlassByName
|
|
488
493
|
controller = new Controller({
|
|
489
|
-
system, entities, relations,
|
|
494
|
+
system, entities, relations, eventSources, dict: [], recordMutationSideEffects: []
|
|
490
495
|
})
|
|
491
496
|
await controller.setup(true)
|
|
492
497
|
})
|
|
493
498
|
|
|
494
499
|
test('creates a post via interaction', async () => {
|
|
495
|
-
const result = await controller.
|
|
500
|
+
const result = await controller.dispatch(CreatePost, {
|
|
496
501
|
user: { id: 'user-1' },
|
|
497
502
|
payload: { title: 'Test', content: 'Hello' }
|
|
498
503
|
})
|
|
@@ -518,8 +523,8 @@ const post = await system.storage.create('Post', { title: 'Test', content: 'Hell
|
|
|
518
523
|
|
|
519
524
|
### CORRECT:
|
|
520
525
|
```typescript
|
|
521
|
-
// Use
|
|
522
|
-
const result = await controller.
|
|
526
|
+
// Use dispatch to test business logic
|
|
527
|
+
const result = await controller.dispatch(CreatePost, {
|
|
523
528
|
user: { id: 'user-1' },
|
|
524
529
|
payload: { title: 'Test', content: 'Hello' }
|
|
525
530
|
})
|
|
@@ -531,6 +536,6 @@ const result = await controller.callInteraction('CreatePost', {
|
|
|
531
536
|
### Checklist
|
|
532
537
|
- [ ] Use `PGLiteDB` for test databases
|
|
533
538
|
- [ ] Call `controller.setup(true)` in `beforeEach`
|
|
534
|
-
- [ ] Test business logic through `
|
|
539
|
+
- [ ] Test business logic through `controller.dispatch`, not direct storage
|
|
535
540
|
- [ ] Check `result.error` — NEVER use try-catch
|
|
536
541
|
- [ ] ALWAYS pass `attributeQuery` when asserting on query results
|
|
@@ -30,7 +30,7 @@ const User = Entity.create({
|
|
|
30
30
|
name: 'postCount',
|
|
31
31
|
type: 'number',
|
|
32
32
|
defaultValue: () => 0,
|
|
33
|
-
computation: Count.create({
|
|
33
|
+
computation: Count.create({ property: 'posts' })
|
|
34
34
|
})
|
|
35
35
|
]
|
|
36
36
|
})
|
|
@@ -76,8 +76,8 @@ const CreatePost = Interaction.create({
|
|
|
76
76
|
action: Action.create({ name: 'createPost' }),
|
|
77
77
|
payload: Payload.create({
|
|
78
78
|
items: [
|
|
79
|
-
PayloadItem.create({ name: 'title', required: true }),
|
|
80
|
-
PayloadItem.create({ name: 'content', required: true })
|
|
79
|
+
PayloadItem.create({ name: 'title', type: 'string', required: true }),
|
|
80
|
+
PayloadItem.create({ name: 'content', type: 'string', required: true })
|
|
81
81
|
]
|
|
82
82
|
})
|
|
83
83
|
})
|
|
@@ -91,8 +91,7 @@ const controller = new Controller({
|
|
|
91
91
|
system,
|
|
92
92
|
entities: [User, Post],
|
|
93
93
|
relations: [UserPosts],
|
|
94
|
-
|
|
95
|
-
interactions: [CreatePost],
|
|
94
|
+
eventSources: [CreatePost],
|
|
96
95
|
dict: [],
|
|
97
96
|
recordMutationSideEffects: []
|
|
98
97
|
})
|
|
@@ -105,7 +104,7 @@ const adminUser = await system.storage.create('User', {
|
|
|
105
104
|
name: 'Alice', email: 'alice@example.com'
|
|
106
105
|
})
|
|
107
106
|
|
|
108
|
-
const result = await controller.
|
|
107
|
+
const result = await controller.dispatch(CreatePost, {
|
|
109
108
|
user: adminUser,
|
|
110
109
|
payload: { title: 'First Post', content: 'Hello World' }
|
|
111
110
|
})
|
|
@@ -121,7 +120,7 @@ const user = await system.storage.findOne(
|
|
|
121
120
|
```
|
|
122
121
|
|
|
123
122
|
## Design Decisions
|
|
124
|
-
- **Count on `postCount`**: Automatically maintained when UserPosts relations change
|
|
123
|
+
- **Count on `postCount`**: Uses `property: 'posts'` to count related Post records via the `posts` navigation property. Automatically maintained when UserPosts relations change — no manual update logic needed.
|
|
125
124
|
- **Transform on Post entity**: Posts are created reactively when `CreatePost` interaction fires. The Transform checks `interactionName` and returns entity data.
|
|
126
125
|
- **Relation direction**: `source: Post, target: User, type: 'n:1'` — many posts to one user. `sourceProperty: 'author'` lets you navigate from Post to User; `targetProperty: 'posts'` lets you navigate from User to Posts.
|
|
127
126
|
|
|
@@ -143,49 +142,6 @@ import {
|
|
|
143
142
|
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
144
143
|
} from 'interaqt'
|
|
145
144
|
|
|
146
|
-
// --- Interactions ---
|
|
147
|
-
|
|
148
|
-
const SubmitOrder = Interaction.create({
|
|
149
|
-
name: 'SubmitOrder',
|
|
150
|
-
action: Action.create({ name: 'submitOrder' }),
|
|
151
|
-
payload: Payload.create({
|
|
152
|
-
items: [
|
|
153
|
-
PayloadItem.create({ name: 'product', required: true }),
|
|
154
|
-
PayloadItem.create({ name: 'quantity', required: true })
|
|
155
|
-
]
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const PayOrder = Interaction.create({
|
|
160
|
-
name: 'PayOrder',
|
|
161
|
-
action: Action.create({ name: 'payOrder' }),
|
|
162
|
-
payload: Payload.create({
|
|
163
|
-
items: [
|
|
164
|
-
PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
|
|
165
|
-
]
|
|
166
|
-
})
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const ShipOrder = Interaction.create({
|
|
170
|
-
name: 'ShipOrder',
|
|
171
|
-
action: Action.create({ name: 'shipOrder' }),
|
|
172
|
-
payload: Payload.create({
|
|
173
|
-
items: [
|
|
174
|
-
PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
|
|
175
|
-
]
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
const CancelOrder = Interaction.create({
|
|
180
|
-
name: 'CancelOrder',
|
|
181
|
-
action: Action.create({ name: 'cancelOrder' }),
|
|
182
|
-
payload: Payload.create({
|
|
183
|
-
items: [
|
|
184
|
-
PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
|
|
185
|
-
]
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
|
|
189
145
|
// --- State Nodes ---
|
|
190
146
|
|
|
191
147
|
const pendingState = StateNode.create({ name: 'pending' })
|
|
@@ -211,18 +167,36 @@ const Order = Entity.create({
|
|
|
211
167
|
transfers: [
|
|
212
168
|
StateTransfer.create({
|
|
213
169
|
current: pendingState, next: paidState,
|
|
214
|
-
trigger:
|
|
215
|
-
|
|
170
|
+
trigger: {
|
|
171
|
+
recordName: InteractionEventEntity.name,
|
|
172
|
+
type: 'create',
|
|
173
|
+
record: { interactionName: 'PayOrder' }
|
|
174
|
+
},
|
|
175
|
+
computeTarget: function(mutationEvent) {
|
|
176
|
+
return { id: mutationEvent.record.payload.orderId }
|
|
177
|
+
}
|
|
216
178
|
}),
|
|
217
179
|
StateTransfer.create({
|
|
218
180
|
current: paidState, next: shippedState,
|
|
219
|
-
trigger:
|
|
220
|
-
|
|
181
|
+
trigger: {
|
|
182
|
+
recordName: InteractionEventEntity.name,
|
|
183
|
+
type: 'create',
|
|
184
|
+
record: { interactionName: 'ShipOrder' }
|
|
185
|
+
},
|
|
186
|
+
computeTarget: function(mutationEvent) {
|
|
187
|
+
return { id: mutationEvent.record.payload.orderId }
|
|
188
|
+
}
|
|
221
189
|
}),
|
|
222
190
|
StateTransfer.create({
|
|
223
191
|
current: pendingState, next: cancelledState,
|
|
224
|
-
trigger:
|
|
225
|
-
|
|
192
|
+
trigger: {
|
|
193
|
+
recordName: InteractionEventEntity.name,
|
|
194
|
+
type: 'create',
|
|
195
|
+
record: { interactionName: 'CancelOrder' }
|
|
196
|
+
},
|
|
197
|
+
computeTarget: function(mutationEvent) {
|
|
198
|
+
return { id: mutationEvent.record.payload.orderId }
|
|
199
|
+
}
|
|
226
200
|
})
|
|
227
201
|
],
|
|
228
202
|
initialState: pendingState
|
|
@@ -245,6 +219,49 @@ const Order = Entity.create({
|
|
|
245
219
|
})
|
|
246
220
|
})
|
|
247
221
|
|
|
222
|
+
// --- Interactions ---
|
|
223
|
+
|
|
224
|
+
const SubmitOrder = Interaction.create({
|
|
225
|
+
name: 'SubmitOrder',
|
|
226
|
+
action: Action.create({ name: 'submitOrder' }),
|
|
227
|
+
payload: Payload.create({
|
|
228
|
+
items: [
|
|
229
|
+
PayloadItem.create({ name: 'product', type: 'string', required: true }),
|
|
230
|
+
PayloadItem.create({ name: 'quantity', type: 'number', required: true })
|
|
231
|
+
]
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const PayOrder = Interaction.create({
|
|
236
|
+
name: 'PayOrder',
|
|
237
|
+
action: Action.create({ name: 'payOrder' }),
|
|
238
|
+
payload: Payload.create({
|
|
239
|
+
items: [
|
|
240
|
+
PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
|
|
241
|
+
]
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const ShipOrder = Interaction.create({
|
|
246
|
+
name: 'ShipOrder',
|
|
247
|
+
action: Action.create({ name: 'shipOrder' }),
|
|
248
|
+
payload: Payload.create({
|
|
249
|
+
items: [
|
|
250
|
+
PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
|
|
251
|
+
]
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const CancelOrder = Interaction.create({
|
|
256
|
+
name: 'CancelOrder',
|
|
257
|
+
action: Action.create({ name: 'cancelOrder' }),
|
|
258
|
+
payload: Payload.create({
|
|
259
|
+
items: [
|
|
260
|
+
PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
|
|
261
|
+
]
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
248
265
|
// --- Controller Setup & Usage ---
|
|
249
266
|
|
|
250
267
|
const system = new MonoSystem(new PGLiteDB())
|
|
@@ -254,8 +271,7 @@ const controller = new Controller({
|
|
|
254
271
|
system,
|
|
255
272
|
entities: [Order],
|
|
256
273
|
relations: [],
|
|
257
|
-
|
|
258
|
-
interactions: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
|
|
274
|
+
eventSources: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
|
|
259
275
|
dict: [],
|
|
260
276
|
recordMutationSideEffects: []
|
|
261
277
|
})
|
|
@@ -264,7 +280,7 @@ await controller.setup(true)
|
|
|
264
280
|
const user = { id: 'user-1' }
|
|
265
281
|
|
|
266
282
|
// Submit order
|
|
267
|
-
const submitResult = await controller.
|
|
283
|
+
const submitResult = await controller.dispatch(SubmitOrder, {
|
|
268
284
|
user,
|
|
269
285
|
payload: { product: 'Widget', quantity: 3 }
|
|
270
286
|
})
|
|
@@ -276,14 +292,14 @@ const order = await system.storage.findOne('Order',
|
|
|
276
292
|
// order.status === 'pending'
|
|
277
293
|
|
|
278
294
|
// Pay order
|
|
279
|
-
await controller.
|
|
295
|
+
await controller.dispatch(PayOrder, {
|
|
280
296
|
user,
|
|
281
297
|
payload: { orderId: order.id }
|
|
282
298
|
})
|
|
283
299
|
// order.status → 'paid'
|
|
284
300
|
|
|
285
301
|
// Ship order
|
|
286
|
-
await controller.
|
|
302
|
+
await controller.dispatch(ShipOrder, {
|
|
287
303
|
user,
|
|
288
304
|
payload: { orderId: order.id }
|
|
289
305
|
})
|
|
@@ -292,22 +308,27 @@ await controller.callInteraction('ShipOrder', {
|
|
|
292
308
|
|
|
293
309
|
## Design Decisions
|
|
294
310
|
- **StateMachine on `status` property**: Status transitions are declarative. The framework enforces valid transitions — you cannot jump from `pending` to `shipped` directly.
|
|
295
|
-
- **`
|
|
311
|
+
- **`trigger` is a pattern object**: Each StateTransfer `trigger` is a `RecordMutationEventPattern` that matches against InteractionEvent creation events — it is NOT an Interaction instance. The `record.interactionName` field matches the specific interaction by name string.
|
|
312
|
+
- **`computeTarget`**: Receives the `RecordMutationEvent` and returns which order the transition applies to. Access the InteractionEvent data via `mutationEvent.record` (e.g. `mutationEvent.record.payload.orderId`).
|
|
296
313
|
- **Transform on Entity `computation`**: Creates order records reactively when `SubmitOrder` fires.
|
|
297
314
|
- **Cancellation only from `pending`**: Only one `cancelledState` transfer is defined (from `pending`). Attempting to cancel a paid order will have no effect.
|
|
315
|
+
- **Declaration order**: Order entity is defined before the Interactions that reference it (via `base: Order`). The StateMachine triggers use interaction name strings (not variable references), avoiding circular dependencies.
|
|
298
316
|
|
|
299
317
|
---
|
|
300
318
|
|
|
301
319
|
# Recipe: Student GPA with Weighted Summation
|
|
302
320
|
|
|
303
321
|
## Scenario
|
|
304
|
-
A student grading system where each student has grades for multiple subjects, each with different credit weights. The student's GPA is automatically computed using WeightedSummation.
|
|
322
|
+
A student grading system where each student has grades for multiple subjects, each with different credit weights. The student's GPA is automatically computed using WeightedSummation. Grades are added via an Interaction to ensure computations trigger correctly.
|
|
305
323
|
|
|
306
324
|
## Complete Implementation
|
|
307
325
|
|
|
308
326
|
```typescript
|
|
309
327
|
import {
|
|
310
|
-
Entity, Property, Relation,
|
|
328
|
+
Entity, Property, Relation,
|
|
329
|
+
WeightedSummation, Summation, Count,
|
|
330
|
+
Interaction, Action, Payload, PayloadItem,
|
|
331
|
+
Transform, InteractionEventEntity,
|
|
311
332
|
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
312
333
|
} from 'interaqt'
|
|
313
334
|
|
|
@@ -322,7 +343,8 @@ const Student = Entity.create({
|
|
|
322
343
|
type: 'number',
|
|
323
344
|
defaultValue: () => 0,
|
|
324
345
|
computation: WeightedSummation.create({
|
|
325
|
-
|
|
346
|
+
property: 'grades',
|
|
347
|
+
direction: 'source',
|
|
326
348
|
attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]],
|
|
327
349
|
callback: (relation) => ({
|
|
328
350
|
weight: relation.target.credit,
|
|
@@ -334,20 +356,17 @@ const Student = Entity.create({
|
|
|
334
356
|
name: 'totalCredits',
|
|
335
357
|
type: 'number',
|
|
336
358
|
defaultValue: () => 0,
|
|
337
|
-
computation:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
weight: 1,
|
|
342
|
-
value: relation.target.credit
|
|
343
|
-
})
|
|
359
|
+
computation: Summation.create({
|
|
360
|
+
property: 'grades',
|
|
361
|
+
direction: 'source',
|
|
362
|
+
attributeQuery: [['target', { attributeQuery: ['credit'] }]]
|
|
344
363
|
})
|
|
345
364
|
}),
|
|
346
365
|
Property.create({
|
|
347
366
|
name: 'courseCount',
|
|
348
367
|
type: 'number',
|
|
349
368
|
defaultValue: () => 0,
|
|
350
|
-
computation: Count.create({
|
|
369
|
+
computation: Count.create({ property: 'grades' })
|
|
351
370
|
})
|
|
352
371
|
]
|
|
353
372
|
})
|
|
@@ -358,7 +377,22 @@ const Grade = Entity.create({
|
|
|
358
377
|
Property.create({ name: 'subject', type: 'string' }),
|
|
359
378
|
Property.create({ name: 'score', type: 'number' }),
|
|
360
379
|
Property.create({ name: 'credit', type: 'number' })
|
|
361
|
-
]
|
|
380
|
+
],
|
|
381
|
+
computation: Transform.create({
|
|
382
|
+
record: InteractionEventEntity,
|
|
383
|
+
attributeQuery: ['interactionName', 'payload'],
|
|
384
|
+
callback: function(event) {
|
|
385
|
+
if (event.interactionName === 'AddGrade') {
|
|
386
|
+
return {
|
|
387
|
+
subject: event.payload.subject,
|
|
388
|
+
score: event.payload.score,
|
|
389
|
+
credit: event.payload.credit,
|
|
390
|
+
student: { id: event.payload.studentId }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
})
|
|
362
396
|
})
|
|
363
397
|
|
|
364
398
|
// --- Relations ---
|
|
@@ -371,6 +405,21 @@ const StudentGrades = Relation.create({
|
|
|
371
405
|
type: '1:n'
|
|
372
406
|
})
|
|
373
407
|
|
|
408
|
+
// --- Interactions ---
|
|
409
|
+
|
|
410
|
+
const AddGrade = Interaction.create({
|
|
411
|
+
name: 'AddGrade',
|
|
412
|
+
action: Action.create({ name: 'addGrade' }),
|
|
413
|
+
payload: Payload.create({
|
|
414
|
+
items: [
|
|
415
|
+
PayloadItem.create({ name: 'studentId', type: 'string', required: true }),
|
|
416
|
+
PayloadItem.create({ name: 'subject', type: 'string', required: true }),
|
|
417
|
+
PayloadItem.create({ name: 'score', type: 'number', required: true }),
|
|
418
|
+
PayloadItem.create({ name: 'credit', type: 'number', required: true })
|
|
419
|
+
]
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
374
423
|
// --- Controller Setup & Usage ---
|
|
375
424
|
|
|
376
425
|
const system = new MonoSystem(new PGLiteDB())
|
|
@@ -380,8 +429,7 @@ const controller = new Controller({
|
|
|
380
429
|
system,
|
|
381
430
|
entities: [Student, Grade],
|
|
382
431
|
relations: [StudentGrades],
|
|
383
|
-
|
|
384
|
-
interactions: [],
|
|
432
|
+
eventSources: [AddGrade],
|
|
385
433
|
dict: [],
|
|
386
434
|
recordMutationSideEffects: []
|
|
387
435
|
})
|
|
@@ -389,8 +437,14 @@ await controller.setup(true)
|
|
|
389
437
|
|
|
390
438
|
const student = await system.storage.create('Student', { name: 'Alice' })
|
|
391
439
|
|
|
392
|
-
await
|
|
393
|
-
|
|
440
|
+
await controller.dispatch(AddGrade, {
|
|
441
|
+
user: { id: 'system' },
|
|
442
|
+
payload: { studentId: student.id, subject: 'Math', score: 90, credit: 4 }
|
|
443
|
+
})
|
|
444
|
+
await controller.dispatch(AddGrade, {
|
|
445
|
+
user: { id: 'system' },
|
|
446
|
+
payload: { studentId: student.id, subject: 'English', score: 80, credit: 3 }
|
|
447
|
+
})
|
|
394
448
|
|
|
395
449
|
const result = await system.storage.findOne('Student',
|
|
396
450
|
MatchExp.atom({ key: 'id', value: ['=', student.id] }),
|
|
@@ -403,16 +457,17 @@ const result = await system.storage.findOne('Student',
|
|
|
403
457
|
```
|
|
404
458
|
|
|
405
459
|
## Design Decisions
|
|
406
|
-
- **WeightedSummation for GPA**: The `weight` is the credit value, and the `value` is the score. The framework computes `sum(weight*value) / sum(weight)` automatically.
|
|
407
|
-
- **
|
|
408
|
-
-
|
|
460
|
+
- **WeightedSummation for GPA**: Uses `property: 'grades'` (property-level mode) to aggregate per-student. The `weight` is the credit value, and the `value` is the score. The framework computes `sum(weight*value) / sum(weight)` automatically.
|
|
461
|
+
- **Summation for totalCredits**: Uses `Summation` (not `WeightedSummation`) because `totalCredits` is a simple sum. `WeightedSummation` with `weight=1` would compute an average, not a sum.
|
|
462
|
+
- **Count for courseCount**: More efficient and semantically clear for counting than Summation or WeightedSummation.
|
|
463
|
+
- **Grades added via Interaction + Transform**: Using `controller.dispatch` ensures reactive computations (WeightedSummation, Summation, Count) are triggered. Direct `storage.create` bypasses reactive computations and should only be used for prerequisite data (like creating the Student record).
|
|
409
464
|
|
|
410
465
|
---
|
|
411
466
|
|
|
412
|
-
# Recipe: Interaction with
|
|
467
|
+
# Recipe: Interaction with Condition Validation
|
|
413
468
|
|
|
414
469
|
## Scenario
|
|
415
|
-
A content moderation system where only published posts can be shared. Demonstrates
|
|
470
|
+
A content moderation system where only published posts can be shared. Demonstrates Condition-based validation on Interactions: the framework checks the condition before allowing the interaction to proceed.
|
|
416
471
|
|
|
417
472
|
## Complete Implementation
|
|
418
473
|
|
|
@@ -420,7 +475,7 @@ A content moderation system where only published posts can be shared. Demonstrat
|
|
|
420
475
|
import {
|
|
421
476
|
Entity, Property,
|
|
422
477
|
Interaction, Action, Payload, PayloadItem,
|
|
423
|
-
|
|
478
|
+
Condition,
|
|
424
479
|
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
425
480
|
} from 'interaqt'
|
|
426
481
|
|
|
@@ -430,20 +485,11 @@ const Post = Entity.create({
|
|
|
430
485
|
name: 'Post',
|
|
431
486
|
properties: [
|
|
432
487
|
Property.create({ name: 'title', type: 'string' }),
|
|
433
|
-
Property.create({ name: 'status', type: 'string', defaultValue: 'draft' })
|
|
488
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' })
|
|
434
489
|
]
|
|
435
490
|
})
|
|
436
491
|
|
|
437
|
-
// ---
|
|
438
|
-
|
|
439
|
-
const PublishedPost = Attributive.create({
|
|
440
|
-
name: 'PublishedPost',
|
|
441
|
-
content: function(post, eventArgs) {
|
|
442
|
-
return post.status === 'published'
|
|
443
|
-
}
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
// --- Interaction with validation ---
|
|
492
|
+
// --- Interaction with condition ---
|
|
447
493
|
|
|
448
494
|
const SharePost = Interaction.create({
|
|
449
495
|
name: 'SharePost',
|
|
@@ -452,12 +498,23 @@ const SharePost = Interaction.create({
|
|
|
452
498
|
items: [
|
|
453
499
|
PayloadItem.create({
|
|
454
500
|
name: 'post',
|
|
501
|
+
type: 'string',
|
|
455
502
|
base: Post,
|
|
456
503
|
isRef: true,
|
|
457
|
-
required: true
|
|
458
|
-
attributives: PublishedPost
|
|
504
|
+
required: true
|
|
459
505
|
})
|
|
460
506
|
]
|
|
507
|
+
}),
|
|
508
|
+
conditions: Condition.create({
|
|
509
|
+
name: 'postMustBePublished',
|
|
510
|
+
content: async function(event) {
|
|
511
|
+
const post = await this.system.storage.findOne('Post',
|
|
512
|
+
MatchExp.atom({ key: 'id', value: ['=', event.payload.post] }),
|
|
513
|
+
undefined,
|
|
514
|
+
['id', 'status']
|
|
515
|
+
)
|
|
516
|
+
return post?.status === 'published'
|
|
517
|
+
}
|
|
461
518
|
})
|
|
462
519
|
})
|
|
463
520
|
|
|
@@ -470,8 +527,7 @@ const controller = new Controller({
|
|
|
470
527
|
system,
|
|
471
528
|
entities: [Post],
|
|
472
529
|
relations: [],
|
|
473
|
-
|
|
474
|
-
interactions: [SharePost],
|
|
530
|
+
eventSources: [SharePost],
|
|
475
531
|
dict: [],
|
|
476
532
|
recordMutationSideEffects: []
|
|
477
533
|
})
|
|
@@ -480,22 +536,22 @@ await controller.setup(true)
|
|
|
480
536
|
const draftPost = await system.storage.create('Post', { title: 'Draft', status: 'draft' })
|
|
481
537
|
const publishedPost = await system.storage.create('Post', { title: 'Published', status: 'published' })
|
|
482
538
|
|
|
483
|
-
// Sharing a draft post fails
|
|
484
|
-
const failResult = await controller.
|
|
539
|
+
// Sharing a draft post fails the condition check
|
|
540
|
+
const failResult = await controller.dispatch(SharePost, {
|
|
485
541
|
user: { id: 'user-1' },
|
|
486
|
-
payload: { post:
|
|
542
|
+
payload: { post: draftPost.id }
|
|
487
543
|
})
|
|
488
|
-
// failResult.error is defined —
|
|
544
|
+
// failResult.error is defined — condition rejected: post is not published
|
|
489
545
|
|
|
490
546
|
// Sharing a published post succeeds
|
|
491
|
-
const successResult = await controller.
|
|
547
|
+
const successResult = await controller.dispatch(SharePost, {
|
|
492
548
|
user: { id: 'user-1' },
|
|
493
|
-
payload: { post:
|
|
549
|
+
payload: { post: publishedPost.id }
|
|
494
550
|
})
|
|
495
551
|
// successResult.error is undefined — success
|
|
496
552
|
```
|
|
497
553
|
|
|
498
554
|
## Design Decisions
|
|
499
|
-
- **
|
|
500
|
-
- **`isRef: true`**: The payload contains only an ID reference.
|
|
501
|
-
- **Error in result, not exception**:
|
|
555
|
+
- **Condition on Interaction**: The `Condition.create` is attached to the Interaction's `conditions` field. The `content` function receives the event args and returns `true` to allow or `false` to reject. The `this` context is bound to the Controller, providing access to `this.system.storage` for database queries.
|
|
556
|
+
- **`isRef: true`**: The payload contains only an ID reference. With `isRef: true`, the payload value is the entity ID directly (e.g., `payload: { post: draftPost.id }`).
|
|
557
|
+
- **Error in result, not exception**: Condition failures return `{ error: { type: 'condition check failed' } }`, consistent with all interaqt error handling. Never use try-catch.
|
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
10
|
Entity.create(args: {
|
|
11
|
-
name: string // PascalCase, singular, unique
|
|
12
|
-
properties
|
|
11
|
+
name: string // PascalCase, singular, unique, must match /^[a-zA-Z0-9_]+$/
|
|
12
|
+
properties?: PropertyInstance[] // Array of Property.create() results (defaults to [])
|
|
13
13
|
computation?: ComputationInstance // Transform for derived entities
|
|
14
|
-
baseEntity?: EntityInstance
|
|
15
|
-
|
|
14
|
+
baseEntity?: EntityInstance | RelationInstance // For filtered entities
|
|
15
|
+
matchExpression?: MatchExp // Filter condition for filtered entities
|
|
16
|
+
inputEntities?: EntityInstance[] // For merged entities
|
|
17
|
+
commonProperties?: PropertyInstance[] // Shared attributes for merged entities
|
|
16
18
|
}): EntityInstance
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
Constraints:
|
|
20
22
|
- NEVER pass `uuid` — the framework generates it
|
|
21
|
-
- `name` must match `/^[a-zA-Z0-9_]+$/`
|
|
22
23
|
- `computation` accepts only Transform (for creating derived entity collections)
|
|
24
|
+
- **Filtered entity**: set `baseEntity` + `matchExpression`
|
|
25
|
+
- **Merged entity**: set `inputEntities` + `commonProperties` (cannot define own `properties`)
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
|
@@ -27,21 +30,21 @@ Constraints:
|
|
|
27
30
|
|
|
28
31
|
```typescript
|
|
29
32
|
Property.create(args: {
|
|
30
|
-
name: string //
|
|
31
|
-
type
|
|
33
|
+
name: string // Must match /^[a-zA-Z0-9_]+$/
|
|
34
|
+
type: string // Required: 'string' | 'number' | 'boolean' | 'object'
|
|
32
35
|
collection?: boolean // true for array types
|
|
33
|
-
defaultValue?:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
computation?: ComputationInstance // Reactive: Count, WeightedSummation, Every, Any, StateMachine
|
|
36
|
+
defaultValue?: Function // Factory function returning default value
|
|
37
|
+
computed?: (record: any) => any // Computed from same-record fields (not persisted)
|
|
38
|
+
computation?: ComputationInstance // Reactive: Count, Summation, WeightedSummation, Every, Any, StateMachine, Custom
|
|
37
39
|
}): PropertyInstance
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
Constraints:
|
|
41
|
-
- `
|
|
43
|
+
- `type` is REQUIRED — always specify it
|
|
44
|
+
- `computed` is for same-record derivations only — NOT persisted
|
|
42
45
|
- `computation` results ARE persisted and auto-updated
|
|
43
|
-
- When using `computation`,
|
|
44
|
-
- NEVER use Transform on Property `computation` — Transform belongs on Entity `computation`
|
|
46
|
+
- When using `computation`, provide `defaultValue`
|
|
47
|
+
- NEVER use Transform on Property `computation` — Transform belongs on Entity/Relation `computation`
|
|
45
48
|
|
|
46
49
|
---
|
|
47
50
|
|
|
@@ -49,20 +52,33 @@ Constraints:
|
|
|
49
52
|
|
|
50
53
|
```typescript
|
|
51
54
|
Relation.create(args: {
|
|
52
|
-
|
|
55
|
+
// Base relation (all required for normal relations):
|
|
56
|
+
name?: string // Optional — auto-generated if omitted
|
|
57
|
+
source: EntityInstance | RelationInstance
|
|
53
58
|
sourceProperty: string // Navigation property on source
|
|
54
|
-
target: EntityInstance
|
|
59
|
+
target: EntityInstance | RelationInstance
|
|
55
60
|
targetProperty: string // Navigation property on target
|
|
56
61
|
type: '1:1' | '1:n' | 'n:1' | 'n:n'
|
|
57
62
|
properties?: PropertyInstance[] // Relation's own properties
|
|
58
63
|
computation?: ComputationInstance // Transform for computed relations
|
|
64
|
+
isTargetReliance?: boolean // Defaults to false
|
|
65
|
+
|
|
66
|
+
// Filtered relation:
|
|
67
|
+
baseRelation?: RelationInstance // Base relation to filter from
|
|
68
|
+
matchExpression?: MatchExp // Filter condition
|
|
69
|
+
|
|
70
|
+
// Merged relation:
|
|
71
|
+
inputRelations?: RelationInstance[] // Relations to merge (must share same source/target)
|
|
72
|
+
commonProperties?: PropertyInstance[] // Shared attributes for merged relations
|
|
59
73
|
}): RelationInstance
|
|
60
74
|
```
|
|
61
75
|
|
|
62
76
|
Constraints:
|
|
63
|
-
-
|
|
64
|
-
- ALWAYS specify `type` explicitly
|
|
77
|
+
- `name` is optional — auto-generated as `${source.name}_${sourceProperty}_${targetProperty}_${target.name}`
|
|
78
|
+
- ALWAYS specify `type` explicitly for base relations
|
|
65
79
|
- Symmetric relations: set `source === target` AND `sourceProperty === targetProperty`
|
|
80
|
+
- **Filtered relation**: requires `baseRelation` + `matchExpression` + `sourceProperty` + `targetProperty`
|
|
81
|
+
- **Merged relation**: requires `inputRelations` + `sourceProperty` + `targetProperty` (cannot specify `source`/`target`/`properties`)
|
|
66
82
|
|
|
67
83
|
---
|
|
68
84
|
|
|
@@ -73,10 +89,16 @@ Interaction.create(args: {
|
|
|
73
89
|
name: string // Interaction identifier
|
|
74
90
|
action: ActionInstance // Action.create() result (identifier only)
|
|
75
91
|
payload?: PayloadInstance // Payload.create() result
|
|
76
|
-
conditions?: ConditionInstance
|
|
92
|
+
conditions?: ConditionsInstance | ConditionInstance // Execution conditions
|
|
93
|
+
data?: EntityInstance | RelationInstance // Entity/Relation to query (for data retrieval)
|
|
94
|
+
dataPolicy?: DataPolicyInstance // Fixed data access constraints
|
|
77
95
|
}): InteractionInstance
|
|
78
96
|
```
|
|
79
97
|
|
|
98
|
+
Constraints:
|
|
99
|
+
- For data retrieval, use `GetAction` as action and specify `data`
|
|
100
|
+
- `conditions` accepts either a single `Condition` or a `Conditions` (combined with BoolExp)
|
|
101
|
+
|
|
80
102
|
---
|
|
81
103
|
|
|
82
104
|
## Action.create
|
|
@@ -89,6 +111,7 @@ Action.create(args: {
|
|
|
89
111
|
|
|
90
112
|
Constraints:
|
|
91
113
|
- Action is ONLY an identifier — no `handler`, `execute`, or `callback`
|
|
114
|
+
- Use `GetAction` (pre-built) for data retrieval interactions
|
|
92
115
|
|
|
93
116
|
---
|
|
94
117
|
|
|
@@ -101,37 +124,34 @@ Payload.create(args: {
|
|
|
101
124
|
|
|
102
125
|
PayloadItem.create(args: {
|
|
103
126
|
name: string // Parameter name
|
|
127
|
+
type: string // Required: data type
|
|
104
128
|
base?: EntityInstance // Entity reference for validation
|
|
105
129
|
isRef?: boolean // true = reference by ID to existing entity
|
|
106
130
|
required?: boolean // true = mandatory parameter
|
|
107
131
|
isCollection?: boolean // true = array of items
|
|
108
|
-
|
|
132
|
+
itemRef?: AttributiveInstance | EntityInstance // Reference to entities defined in other interactions (for Activity)
|
|
109
133
|
}): PayloadItemInstance
|
|
110
134
|
```
|
|
111
135
|
|
|
112
|
-
Constraints:
|
|
113
|
-
- Without `base`: framework only checks required/collection, no concept validation
|
|
114
|
-
- With `base` + `isRef: true`: framework verifies entity exists by ID
|
|
115
|
-
- With `base` + `attributives`: framework validates data against attributive rules
|
|
116
|
-
- `attributives` are checked for EVERY item when `isCollection: true`
|
|
117
|
-
|
|
118
136
|
---
|
|
119
137
|
|
|
120
138
|
## Count.create
|
|
121
139
|
|
|
122
140
|
```typescript
|
|
123
141
|
Count.create(args: {
|
|
124
|
-
record
|
|
142
|
+
record?: EntityInstance | RelationInstance // What to count (for entity/global level)
|
|
143
|
+
property?: string // Relation property name (for property level)
|
|
125
144
|
direction?: 'source' | 'target' // For relation counting
|
|
126
145
|
callback?: (record: any) => boolean // Filter function
|
|
127
146
|
attributeQuery?: AttributeQueryData // Fields to load for callback
|
|
128
|
-
dataDeps?:
|
|
147
|
+
dataDeps?: DataDependencies // External data dependencies
|
|
129
148
|
}): CountInstance
|
|
130
149
|
```
|
|
131
150
|
|
|
132
151
|
Constraints:
|
|
133
|
-
-
|
|
134
|
-
-
|
|
152
|
+
- Use `record` for global/entity-level counting, `property` for property-level counting
|
|
153
|
+
- Place on Property `computation` or Dictionary `computation`
|
|
154
|
+
- Provide `defaultValue` on the Property when using as property computation
|
|
135
155
|
|
|
136
156
|
---
|
|
137
157
|
|
|
@@ -139,10 +159,12 @@ Constraints:
|
|
|
139
159
|
|
|
140
160
|
```typescript
|
|
141
161
|
WeightedSummation.create(args: {
|
|
142
|
-
record
|
|
143
|
-
|
|
162
|
+
record?: EntityInstance | RelationInstance // Entity/relation to aggregate (for global level)
|
|
163
|
+
property?: string // Relation property name (for property level)
|
|
164
|
+
direction?: 'source' | 'target' // For relation-based computation
|
|
165
|
+
callback: (record: any) => { weight: number, value: number }
|
|
144
166
|
attributeQuery?: AttributeQueryData
|
|
145
|
-
dataDeps?:
|
|
167
|
+
dataDeps?: DataDependencies
|
|
146
168
|
}): WeightedSummationInstance
|
|
147
169
|
```
|
|
148
170
|
|
|
@@ -150,19 +172,41 @@ Result: `sum(weight * value) / sum(weight)`
|
|
|
150
172
|
|
|
151
173
|
---
|
|
152
174
|
|
|
175
|
+
## Summation.create
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
Summation.create(args: {
|
|
179
|
+
record?: EntityInstance | RelationInstance // Entity/relation to sum (for global level)
|
|
180
|
+
property?: string // Relation property name (for property level)
|
|
181
|
+
direction?: 'source' | 'target' // For relation-based summation
|
|
182
|
+
attributeQuery: AttributeQueryData // Required: specifies field path to sum
|
|
183
|
+
}): SummationInstance
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Sums the field pointed to by the leftmost path in `attributeQuery`. Undefined/null/NaN/Infinity values are treated as 0.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
153
190
|
## Every.create / Any.create
|
|
154
191
|
|
|
155
192
|
```typescript
|
|
156
193
|
Every.create(args: {
|
|
157
|
-
record
|
|
158
|
-
|
|
194
|
+
record?: EntityInstance | RelationInstance // For global level
|
|
195
|
+
property?: string // Relation property name (for property level)
|
|
196
|
+
direction?: 'source' | 'target'
|
|
197
|
+
callback: (record: any) => boolean
|
|
159
198
|
attributeQuery?: AttributeQueryData
|
|
199
|
+
dataDeps?: DataDependencies
|
|
200
|
+
notEmpty?: boolean // Return value when collection is empty
|
|
160
201
|
}): EveryInstance
|
|
161
202
|
|
|
162
203
|
Any.create(args: {
|
|
163
|
-
record
|
|
164
|
-
|
|
204
|
+
record?: EntityInstance | RelationInstance
|
|
205
|
+
property?: string
|
|
206
|
+
direction?: 'source' | 'target'
|
|
207
|
+
callback: (record: any) => boolean
|
|
165
208
|
attributeQuery?: AttributeQueryData
|
|
209
|
+
dataDeps?: DataDependencies
|
|
166
210
|
}): AnyInstance
|
|
167
211
|
```
|
|
168
212
|
|
|
@@ -175,17 +219,33 @@ Any.create(args: {
|
|
|
175
219
|
|
|
176
220
|
```typescript
|
|
177
221
|
Transform.create(args: {
|
|
178
|
-
|
|
179
|
-
|
|
222
|
+
// Mode 1: Entity/Relation Transform
|
|
223
|
+
record?: EntityInstance | RelationInstance // Source data
|
|
180
224
|
attributeQuery?: AttributeQueryData
|
|
181
|
-
|
|
225
|
+
|
|
226
|
+
// Mode 2: Event-Driven Transform
|
|
227
|
+
eventDeps?: {
|
|
228
|
+
[key: string]: {
|
|
229
|
+
recordName: string
|
|
230
|
+
type: 'create' | 'update' | 'delete'
|
|
231
|
+
record?: Record<string, unknown>
|
|
232
|
+
oldRecord?: Record<string, unknown>
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Common
|
|
237
|
+
callback: Function // (this: Controller, record/mutationEvent) => any | any[] | null
|
|
238
|
+
dataDeps?: { [key: string]: DataDep }
|
|
182
239
|
}): TransformInstance
|
|
183
240
|
```
|
|
184
241
|
|
|
185
242
|
Constraints:
|
|
186
243
|
- Place on Entity `computation` or Relation `computation`, NEVER on Property
|
|
187
|
-
- Return `null` from callback to skip (conditional transformation)
|
|
244
|
+
- Return `null`/`undefined` from callback to skip (conditional transformation)
|
|
245
|
+
- Return array to create multiple records from one source
|
|
188
246
|
- NEVER reference the entity being defined as `record` (circular reference)
|
|
247
|
+
- Use `eventDeps` mode for interaction-based transformations (recommended)
|
|
248
|
+
- Use `record` mode for deriving entities from other entities
|
|
189
249
|
|
|
190
250
|
---
|
|
191
251
|
|
|
@@ -208,12 +268,16 @@ Place on Property `computation`. The property value equals the current state nod
|
|
|
208
268
|
```typescript
|
|
209
269
|
StateNode.create(args: {
|
|
210
270
|
name: string
|
|
211
|
-
computeValue?: (lastValue: any) => any
|
|
271
|
+
computeValue?: (this: Controller, lastValue: any, event?: any) => any
|
|
212
272
|
}): StateNodeInstance
|
|
213
273
|
```
|
|
214
274
|
|
|
215
275
|
- Without `computeValue`: property value is the state name string
|
|
216
276
|
- With `computeValue`: property value is the function's return value
|
|
277
|
+
- `lastValue`: previous property value before transition (undefined for initial state)
|
|
278
|
+
- `event`: the event record that triggered the transition (undefined during initialization)
|
|
279
|
+
- For interaction triggers: access `event.payload`, `event.user`, `event.interactionName`
|
|
280
|
+
- `this` is bound to the Controller instance — async functions can use `this.system.storage`
|
|
217
281
|
|
|
218
282
|
---
|
|
219
283
|
|
|
@@ -223,12 +287,28 @@ StateNode.create(args: {
|
|
|
223
287
|
StateTransfer.create(args: {
|
|
224
288
|
current: StateNodeInstance // From state
|
|
225
289
|
next: StateNodeInstance // To state
|
|
226
|
-
trigger:
|
|
227
|
-
computeTarget
|
|
290
|
+
trigger: RecordMutationEventPattern // Pattern to match against mutation events
|
|
291
|
+
computeTarget?: Function // Determines which records to transition
|
|
228
292
|
}): StateTransferInstance
|
|
229
293
|
```
|
|
230
294
|
|
|
231
|
-
|
|
295
|
+
**`trigger`** — a partial pattern object, NOT an Interaction instance:
|
|
296
|
+
```typescript
|
|
297
|
+
trigger: {
|
|
298
|
+
recordName: string // e.g. InteractionEventEntity.name
|
|
299
|
+
type: 'create' | 'update' | 'delete'
|
|
300
|
+
record?: Record<string, any> // deep partial match, e.g. { interactionName: myInteraction.name }
|
|
301
|
+
oldRecord?: Record<string, any>
|
|
302
|
+
keys?: string[]
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**`computeTarget`** — receives the mutation event, returns which record(s) to transition:
|
|
307
|
+
- Entity: `{ id: string }` or `{ id: string }[]`
|
|
308
|
+
- Relation: `{ source: { id: string }, target: { id: string } }`
|
|
309
|
+
- Return `undefined` to skip
|
|
310
|
+
- `this` is bound to Controller — async functions can use `this.system.storage`
|
|
311
|
+
- Required for property-level StateMachines; omit for global StateMachines
|
|
232
312
|
|
|
233
313
|
---
|
|
234
314
|
|
|
@@ -236,26 +316,34 @@ StateTransfer.create(args: {
|
|
|
236
316
|
|
|
237
317
|
```typescript
|
|
238
318
|
new Controller(args: {
|
|
239
|
-
system:
|
|
240
|
-
entities
|
|
241
|
-
relations
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
319
|
+
system: System
|
|
320
|
+
entities?: EntityInstance[]
|
|
321
|
+
relations?: RelationInstance[]
|
|
322
|
+
eventSources?: EventSourceInstance[] // Interactions, custom EventSources, etc.
|
|
323
|
+
dict?: DictionaryInstance[] // Global dictionaries
|
|
324
|
+
recordMutationSideEffects?: RecordMutationSideEffect[]
|
|
325
|
+
computations?: (new (...args: any[]) => Computation)[] // Additional computation handle classes
|
|
326
|
+
ignoreGuard?: boolean // Skip guard checks when true
|
|
327
|
+
forceThrowDispatchError?: boolean // Throw errors instead of returning them
|
|
246
328
|
}): Controller
|
|
247
329
|
|
|
248
|
-
controller.setup(install
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
330
|
+
controller.setup(install?: boolean): Promise<void>
|
|
331
|
+
|
|
332
|
+
controller.dispatch<TArgs, TResult>(
|
|
333
|
+
eventSource: EventSourceInstance<TArgs, TResult>,
|
|
334
|
+
args: TArgs
|
|
335
|
+
): Promise<DispatchResponse>
|
|
336
|
+
|
|
337
|
+
// DispatchResponse = { error?, data?, effects?, sideEffects?, context? }
|
|
253
338
|
```
|
|
254
339
|
|
|
255
340
|
Constraints:
|
|
256
|
-
- ALWAYS call `setup(true)` before any `
|
|
341
|
+
- ALWAYS call `setup(true)` before any `dispatch`
|
|
257
342
|
- `dict` is for Dictionary instances ONLY — never pass computations here
|
|
258
|
-
- `
|
|
343
|
+
- `dispatch` first parameter is the event source object reference, NOT a name string
|
|
344
|
+
- `dispatch` NEVER throws by default — errors are in `result.error`
|
|
345
|
+
- Set `forceThrowDispatchError: true` to make dispatch throw instead
|
|
346
|
+
- Controller automatically registers event source entities (e.g. `InteractionEventEntity`)
|
|
259
347
|
|
|
260
348
|
---
|
|
261
349
|
|
|
@@ -284,20 +372,29 @@ system.storage.find(
|
|
|
284
372
|
|
|
285
373
|
system.storage.findOne(
|
|
286
374
|
entityName: string,
|
|
287
|
-
matchExp
|
|
375
|
+
matchExp?: MatchExp,
|
|
288
376
|
modifier?: any,
|
|
289
377
|
attributeQuery?: AttributeQuery
|
|
290
378
|
): Promise<any>
|
|
291
379
|
|
|
292
380
|
system.storage.create(entityName: string, data: object): Promise<any>
|
|
293
|
-
system.storage.update(entityName: string, matchExp: MatchExp, data: object): Promise<
|
|
381
|
+
system.storage.update(entityName: string, matchExp: MatchExp, data: object): Promise<any>
|
|
294
382
|
system.storage.delete(entityName: string, matchExp: MatchExp): Promise<void>
|
|
383
|
+
|
|
384
|
+
// Dictionary-specific API
|
|
385
|
+
system.storage.dict.get(key: string): Promise<any>
|
|
386
|
+
system.storage.dict.set(key: string, value: any): Promise<void>
|
|
387
|
+
|
|
388
|
+
// General KV storage
|
|
389
|
+
system.storage.get(itemName: string, id: string, initialValue?: any): Promise<any>
|
|
390
|
+
system.storage.set(itemName: string, id: string, value: any): Promise<any>
|
|
295
391
|
```
|
|
296
392
|
|
|
297
393
|
Constraints:
|
|
298
394
|
- ALWAYS pass `attributeQuery` to `find`/`findOne` — without it, only `id` is returned
|
|
299
395
|
- Use `['*']` for all fields
|
|
300
396
|
- `create`/`update`/`delete` bypass all validation — use ONLY for test setup
|
|
397
|
+
- When querying relations, use dot notation for source/target: `{ key: 'source.id', value: ['=', id] }`
|
|
301
398
|
|
|
302
399
|
---
|
|
303
400
|
|
|
@@ -306,12 +403,15 @@ Constraints:
|
|
|
306
403
|
```typescript
|
|
307
404
|
MatchExp.atom(args: { key: string, value: [operator, value] }): MatchExp
|
|
308
405
|
|
|
309
|
-
// Operators: '=', '!=', '>', '>=', '<', '<=', 'like', 'in', 'between', 'not'
|
|
406
|
+
// Operators: '=', '!=', '>', '>=', '<', '<=', 'like', 'in', 'between', 'not'
|
|
310
407
|
|
|
311
408
|
// Chaining
|
|
312
409
|
matchExp.and(args: { key: string, value: [operator, value] }): MatchExp
|
|
313
410
|
matchExp.or(args: { key: string, value: [operator, value] }): MatchExp
|
|
314
411
|
|
|
412
|
+
// From object (all AND)
|
|
413
|
+
MatchExp.fromObject({ status: 'active', age: 25 }): MatchExp
|
|
414
|
+
|
|
315
415
|
// Nested field access
|
|
316
416
|
MatchExp.atom({ key: 'author.name', value: ['=', 'Alice'] })
|
|
317
417
|
```
|
|
@@ -346,24 +446,43 @@ MatchExp.atom({ key: 'author.name', value: ['=', 'Alice'] })
|
|
|
346
446
|
|
|
347
447
|
```typescript
|
|
348
448
|
Attributive.create(args: {
|
|
349
|
-
name
|
|
449
|
+
name?: string
|
|
350
450
|
content: (record: any, eventArgs: any) => boolean
|
|
351
451
|
}): AttributiveInstance
|
|
352
452
|
```
|
|
353
453
|
|
|
354
|
-
Used on
|
|
454
|
+
Used on Interaction `userAttributives` to validate user context.
|
|
355
455
|
|
|
356
456
|
---
|
|
357
457
|
|
|
358
458
|
## BoolExp
|
|
359
459
|
|
|
360
460
|
```typescript
|
|
361
|
-
BoolExp.atom(
|
|
362
|
-
boolExp.and(other: BoolExp): BoolExp
|
|
363
|
-
boolExp.or(other: BoolExp): BoolExp
|
|
461
|
+
BoolExp.atom(data: T): BoolExp
|
|
462
|
+
boolExp.and(other: BoolExp | T): BoolExp
|
|
463
|
+
boolExp.or(other: BoolExp | T): BoolExp
|
|
364
464
|
```
|
|
365
465
|
|
|
366
|
-
Combines multiple Attributives for complex
|
|
466
|
+
Combines multiple Attributives, Conditions, or other expressions for complex logic.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Condition.create / Conditions.create
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
Condition.create(args: {
|
|
474
|
+
name?: string
|
|
475
|
+
content: (this: Controller, event: InteractionEventArgs) => Promise<boolean>
|
|
476
|
+
}): ConditionInstance
|
|
477
|
+
|
|
478
|
+
Conditions.create(args: {
|
|
479
|
+
content: BoolExp<ConditionInstance> // Combined with AND/OR logic
|
|
480
|
+
}): ConditionsInstance
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
- `content` returns `true` to allow, `false` to reject
|
|
484
|
+
- `this` is bound to Controller — can access `this.system.storage`
|
|
485
|
+
- Failed conditions return `{ error: { type: 'condition check failed' } }`
|
|
367
486
|
|
|
368
487
|
---
|
|
369
488
|
|
|
@@ -374,12 +493,53 @@ Dictionary.create(args: {
|
|
|
374
493
|
name: string
|
|
375
494
|
type: 'string' | 'number' | 'boolean' | 'object'
|
|
376
495
|
collection?: boolean
|
|
377
|
-
defaultValue?:
|
|
496
|
+
defaultValue?: Function
|
|
378
497
|
computation?: ComputationInstance
|
|
379
498
|
}): DictionaryInstance
|
|
380
499
|
```
|
|
381
500
|
|
|
382
|
-
Global state values. Pass to Controller's `dict` parameter. Access via `system.storage.get(
|
|
501
|
+
Global state values. Pass to Controller's `dict` parameter. Access via `system.storage.dict.get(name)` / `system.storage.dict.set(name, value)`.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## EventSource.create
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
EventSource.create(args: {
|
|
509
|
+
name: string // Event source identifier
|
|
510
|
+
entity: EntityInstance // Entity to persist event records
|
|
511
|
+
guard?: (this: Controller, args: TArgs) => Promise<void>
|
|
512
|
+
mapEventData?: (args: TArgs) => Record<string, any>
|
|
513
|
+
resolve?: (this: Controller, args: TArgs) => Promise<TResult>
|
|
514
|
+
afterDispatch?: (this: Controller, args: TArgs, result: { data?: TResult }) => Promise<Record<string, unknown> | void>
|
|
515
|
+
}): EventSourceInstance
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Custom event source for scheduled tasks, webhooks, or any non-interaction trigger. Dispatch via `controller.dispatch(eventSource, args)`.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## HardDeletionProperty.create
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
HardDeletionProperty.create(): PropertyInstance
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Creates a property named `_isDeleted_`. When its value transitions to `true` (via StateMachine), the Controller physically deletes the record. Use with `DELETED_STATE` / `NON_DELETED_STATE`.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## RecordMutationSideEffect.create
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
RecordMutationSideEffect.create(args: {
|
|
536
|
+
name: string
|
|
537
|
+
record: { name: string } // Entity/relation name to monitor
|
|
538
|
+
content: (this: Controller, event: RecordMutationEvent) => Promise<any>
|
|
539
|
+
}): RecordMutationSideEffect
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Triggers custom logic on record mutations within dispatch context. Results available in `dispatchResult.sideEffects`.
|
|
383
543
|
|
|
384
544
|
---
|
|
385
545
|
|
|
@@ -387,23 +547,52 @@ Global state values. Pass to Controller's `dict` parameter. Access via `system.s
|
|
|
387
547
|
|
|
388
548
|
```typescript
|
|
389
549
|
import {
|
|
550
|
+
// Data model
|
|
390
551
|
Entity, Property, Relation,
|
|
391
|
-
|
|
552
|
+
|
|
553
|
+
// Event sources
|
|
554
|
+
EventSource, Interaction, Action, GetAction, Payload, PayloadItem,
|
|
392
555
|
Activity,
|
|
393
|
-
|
|
556
|
+
|
|
557
|
+
// Computations
|
|
558
|
+
Count, Every, Any, Summation, WeightedSummation, Average,
|
|
394
559
|
Transform, StateMachine, StateNode, StateTransfer,
|
|
395
|
-
RealTime,
|
|
396
|
-
|
|
560
|
+
RealTime, Custom,
|
|
561
|
+
|
|
562
|
+
// Math (for RealTime)
|
|
563
|
+
Expression, Inequality, Equation, MathResolver,
|
|
564
|
+
|
|
565
|
+
// Validation & conditions
|
|
566
|
+
Attributive, Attributives, DataAttributive, DataAttributives,
|
|
567
|
+
Condition, Conditions,
|
|
568
|
+
|
|
569
|
+
// Data policy
|
|
570
|
+
DataPolicy,
|
|
571
|
+
|
|
572
|
+
// Expressions
|
|
397
573
|
BoolExp, MatchExp,
|
|
574
|
+
|
|
575
|
+
// System
|
|
398
576
|
Controller, MonoSystem, Dictionary,
|
|
577
|
+
RecordMutationSideEffect,
|
|
578
|
+
HardDeletionProperty, HARD_DELETION_PROPERTY_NAME,
|
|
579
|
+
NON_DELETED_STATE, DELETED_STATE,
|
|
580
|
+
|
|
581
|
+
// Built-in entities
|
|
399
582
|
InteractionEventEntity,
|
|
583
|
+
|
|
584
|
+
// Drivers
|
|
400
585
|
PGLiteDB, SQLiteDB, PostgreSQLDB, MysqlDB,
|
|
586
|
+
|
|
587
|
+
// Utilities
|
|
401
588
|
KlassByName
|
|
402
589
|
} from 'interaqt'
|
|
403
590
|
```
|
|
404
591
|
|
|
405
592
|
Non-existent exports (commonly mistaken):
|
|
406
593
|
- `InteractionEvent` → use `InteractionEventEntity`
|
|
407
|
-
- `FilteredEntity` → use `Entity.create` with `baseEntity` + `
|
|
594
|
+
- `FilteredEntity` → use `Entity.create` with `baseEntity` + `matchExpression`
|
|
408
595
|
- `RelationBasedEvery` → use `Every`
|
|
596
|
+
- `Sum` → use `Summation`
|
|
597
|
+
- `callInteraction` → use `controller.dispatch(eventSource, args)`
|
|
409
598
|
- `User`, `Post`, etc. → no pre-built entities exist
|