interaqt 1.1.2 → 1.2.0
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/agentspace/knowledge/generator/api-reference.md +19 -21
- package/agent/agentspace/knowledge/generator/computation-implementation.md +6 -0
- package/agent/agentspace/knowledge/generator/integration-implementation-handler.md +2 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +8 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +13 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +13 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +12 -2
- package/agent/agentspace/knowledge/usage/14-api-reference.md +10 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +6 -1
- package/agent/agentspace/knowledge/usage/20-postgresql-concurrency-migration.md +105 -0
- package/agent/agentspace/knowledge/usage/README.md +1 -0
- 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/dist/core/Custom.d.ts +18 -0
- package/dist/core/Custom.d.ts.map +1 -1
- package/dist/core/EventSource.d.ts +17 -0
- package/dist/core/EventSource.d.ts.map +1 -1
- package/dist/drivers/PGLite.d.ts +2 -0
- package/dist/drivers/PGLite.d.ts.map +1 -1
- package/dist/drivers/PostgreSQL.d.ts +27 -5
- package/dist/drivers/PostgreSQL.d.ts.map +1 -1
- package/dist/drivers/SQLite.d.ts +2 -0
- package/dist/drivers/SQLite.d.ts.map +1 -1
- package/dist/index.js +3651 -3042
- package/dist/index.js.map +1 -1
- package/dist/runtime/ComputationSourceMap.d.ts.map +1 -1
- package/dist/runtime/Controller.d.ts +1 -0
- package/dist/runtime/Controller.d.ts.map +1 -1
- package/dist/runtime/MonoSystem.d.ts +2 -0
- package/dist/runtime/MonoSystem.d.ts.map +1 -1
- package/dist/runtime/Scheduler.d.ts +14 -1
- package/dist/runtime/Scheduler.d.ts.map +1 -1
- package/dist/runtime/System.d.ts +40 -6
- package/dist/runtime/System.d.ts.map +1 -1
- package/dist/runtime/computations/Any.d.ts.map +1 -1
- package/dist/runtime/computations/Average.d.ts +2 -2
- package/dist/runtime/computations/Average.d.ts.map +1 -1
- package/dist/runtime/computations/Computation.d.ts +17 -0
- package/dist/runtime/computations/Computation.d.ts.map +1 -1
- package/dist/runtime/computations/Count.d.ts +5 -1
- package/dist/runtime/computations/Count.d.ts.map +1 -1
- package/dist/runtime/computations/Every.d.ts +1 -2
- package/dist/runtime/computations/Every.d.ts.map +1 -1
- package/dist/runtime/computations/StateMachine.d.ts.map +1 -1
- package/dist/runtime/computations/Summation.d.ts +3 -1
- package/dist/runtime/computations/Summation.d.ts.map +1 -1
- package/dist/runtime/computations/Transform.d.ts.map +1 -1
- package/dist/runtime/computations/WeightedSummation.d.ts +3 -1
- package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/transaction.d.ts +15 -0
- package/dist/runtime/transaction.d.ts.map +1 -0
- package/dist/storage/erstorage/EntityQueryHandle.d.ts +1 -0
- package/dist/storage/erstorage/EntityQueryHandle.d.ts.map +1 -1
- package/dist/storage/erstorage/QueryExecutor.d.ts +1 -1
- package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
- package/dist/storage/erstorage/RecordQueryAgent.d.ts +1 -0
- package/dist/storage/erstorage/RecordQueryAgent.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -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.
|