interaqt 1.1.0 → 1.1.2
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 +329 -0
- package/agent/skill/interaqt-patterns.md +536 -0
- package/agent/skill/interaqt-recipes.md +501 -0
- package/agent/skill/interaqt-reference.md +409 -0
- package/dist/builtins/interaction/Interaction.d.ts +16 -10
- package/dist/builtins/interaction/Interaction.d.ts.map +1 -1
- package/dist/builtins/interaction/activity/ActivityManager.d.ts.map +1 -1
- package/dist/builtins/interaction/errors/ActivityErrors.d.ts +2 -2
- package/dist/builtins/interaction/errors/ActivityErrors.d.ts.map +1 -1
- package/dist/builtins/interaction/errors/InteractionErrors.d.ts +1 -1
- package/dist/builtins/interaction/errors/InteractionErrors.d.ts.map +1 -1
- package/dist/core/BoolExp.d.ts +2 -1
- package/dist/core/BoolExp.d.ts.map +1 -1
- package/dist/core/Computation.d.ts +3 -8
- package/dist/core/Computation.d.ts.map +1 -1
- package/dist/core/Custom.d.ts +2 -2
- package/dist/core/Custom.d.ts.map +1 -1
- package/dist/core/EventSource.d.ts +18 -16
- package/dist/core/EventSource.d.ts.map +1 -1
- package/dist/core/Property.d.ts +0 -3
- package/dist/core/Property.d.ts.map +1 -1
- package/dist/core/RealDictionary.d.ts +0 -3
- package/dist/core/RealDictionary.d.ts.map +1 -1
- package/dist/core/Relation.d.ts.map +1 -1
- package/dist/core/StateNode.d.ts +3 -3
- package/dist/core/StateNode.d.ts.map +1 -1
- package/dist/core/StateTransfer.d.ts +2 -6
- package/dist/core/StateTransfer.d.ts.map +1 -1
- package/dist/core/Transform.d.ts +2 -2
- package/dist/core/Transform.d.ts.map +1 -1
- package/dist/core/interfaces.d.ts +0 -9
- package/dist/core/interfaces.d.ts.map +1 -1
- package/dist/core/types.d.ts +0 -29
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils.d.ts +22 -6
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/drivers/Mysql.d.ts +5 -5
- package/dist/drivers/Mysql.d.ts.map +1 -1
- package/dist/drivers/PGLite.d.ts +5 -5
- package/dist/drivers/PGLite.d.ts.map +1 -1
- package/dist/drivers/PostgreSQL.d.ts +5 -5
- package/dist/drivers/PostgreSQL.d.ts.map +1 -1
- package/dist/drivers/SQLite.d.ts +5 -5
- package/dist/drivers/SQLite.d.ts.map +1 -1
- package/dist/index.js +194 -228
- package/dist/index.js.map +1 -1
- package/dist/runtime/Controller.d.ts +14 -14
- package/dist/runtime/Controller.d.ts.map +1 -1
- package/dist/runtime/MonoSystem.d.ts +4 -4
- package/dist/runtime/MonoSystem.d.ts.map +1 -1
- package/dist/runtime/Scheduler.d.ts +3 -3
- package/dist/runtime/Scheduler.d.ts.map +1 -1
- package/dist/runtime/System.d.ts +50 -51
- package/dist/runtime/System.d.ts.map +1 -1
- package/dist/runtime/computations/Any.d.ts +4 -4
- 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 +41 -47
- package/dist/runtime/computations/Computation.d.ts.map +1 -1
- package/dist/runtime/computations/Count.d.ts +4 -4
- package/dist/runtime/computations/Count.d.ts.map +1 -1
- package/dist/runtime/computations/Every.d.ts +4 -4
- package/dist/runtime/computations/Every.d.ts.map +1 -1
- package/dist/runtime/computations/RealTime.d.ts +9 -17
- package/dist/runtime/computations/RealTime.d.ts.map +1 -1
- package/dist/runtime/computations/StateMachine.d.ts +2 -2
- package/dist/runtime/computations/Summation.d.ts +2 -2
- package/dist/runtime/computations/Summation.d.ts.map +1 -1
- package/dist/runtime/computations/TransitionFinder.d.ts +9 -4
- package/dist/runtime/computations/TransitionFinder.d.ts.map +1 -1
- package/dist/runtime/computations/WeightedSummation.d.ts +2 -2
- package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
- package/dist/runtime/errors/ComputationErrors.d.ts +3 -3
- package/dist/runtime/errors/ComputationErrors.d.ts.map +1 -1
- package/dist/runtime/errors/ConditionErrors.d.ts +6 -6
- package/dist/runtime/errors/ConditionErrors.d.ts.map +1 -1
- package/dist/runtime/errors/FrameworkError.d.ts +4 -4
- package/dist/runtime/errors/FrameworkError.d.ts.map +1 -1
- package/dist/runtime/errors/SideEffectError.d.ts +1 -1
- package/dist/runtime/errors/SideEffectError.d.ts.map +1 -1
- package/dist/runtime/errors/SystemErrors.d.ts +1 -1
- package/dist/runtime/errors/SystemErrors.d.ts.map +1 -1
- package/dist/runtime/errors/index.d.ts +5 -5
- package/dist/runtime/errors/index.d.ts.map +1 -1
- package/dist/runtime/types/computation.d.ts +11 -0
- package/dist/runtime/types/computation.d.ts.map +1 -0
- package/dist/runtime/util.d.ts +6 -6
- package/dist/runtime/util.d.ts.map +1 -1
- package/dist/storage/erstorage/MatchExp.d.ts +4 -4
- package/dist/storage/erstorage/MatchExp.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/RecordQuery.d.ts +5 -5
- package/dist/storage/erstorage/RecordQuery.d.ts.map +1 -1
- package/dist/storage/erstorage/SQLBuilder.d.ts +11 -11
- package/dist/storage/erstorage/SQLBuilder.d.ts.map +1 -1
- package/dist/storage/erstorage/Setup.d.ts +1 -1
- package/dist/storage/erstorage/util/RecursiveContext.d.ts +4 -10
- package/dist/storage/erstorage/util/RecursiveContext.d.ts.map +1 -1
- package/dist/storage/erstorage/util.d.ts +3 -3
- package/dist/storage/erstorage/util.d.ts.map +1 -1
- package/dist/storage/utils.d.ts +2 -2
- package/dist/storage/utils.d.ts.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# interaqt Recipes
|
|
2
|
+
|
|
3
|
+
> Complete runnable scenarios. Read this when building a feature from scratch.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Recipe: Blog with Author Stats
|
|
8
|
+
|
|
9
|
+
## Scenario
|
|
10
|
+
A blog system where users author posts. Each user has an auto-maintained `postCount` property. Demonstrates Entity, Relation (1:n), Count computation, Interaction-driven entity creation via Transform, and querying with nested attributeQuery.
|
|
11
|
+
|
|
12
|
+
## Complete Implementation
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import {
|
|
16
|
+
Entity, Property, Relation, Count,
|
|
17
|
+
Interaction, Action, Payload, PayloadItem,
|
|
18
|
+
Transform, InteractionEventEntity,
|
|
19
|
+
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
20
|
+
} from 'interaqt'
|
|
21
|
+
|
|
22
|
+
// --- Entities ---
|
|
23
|
+
|
|
24
|
+
const User = Entity.create({
|
|
25
|
+
name: 'User',
|
|
26
|
+
properties: [
|
|
27
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
28
|
+
Property.create({ name: 'email', type: 'string' }),
|
|
29
|
+
Property.create({
|
|
30
|
+
name: 'postCount',
|
|
31
|
+
type: 'number',
|
|
32
|
+
defaultValue: () => 0,
|
|
33
|
+
computation: Count.create({ record: UserPosts })
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const Post = Entity.create({
|
|
39
|
+
name: 'Post',
|
|
40
|
+
properties: [
|
|
41
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
42
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
43
|
+
Property.create({ name: 'createdAt', type: 'string' })
|
|
44
|
+
],
|
|
45
|
+
computation: Transform.create({
|
|
46
|
+
record: InteractionEventEntity,
|
|
47
|
+
attributeQuery: ['interactionName', 'user', 'payload'],
|
|
48
|
+
callback: function(event) {
|
|
49
|
+
if (event.interactionName === 'CreatePost') {
|
|
50
|
+
return {
|
|
51
|
+
title: event.payload.title,
|
|
52
|
+
content: event.payload.content,
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
author: { id: event.user.id }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// --- Relations ---
|
|
63
|
+
|
|
64
|
+
const UserPosts = Relation.create({
|
|
65
|
+
source: Post,
|
|
66
|
+
sourceProperty: 'author',
|
|
67
|
+
target: User,
|
|
68
|
+
targetProperty: 'posts',
|
|
69
|
+
type: 'n:1'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// --- Interactions ---
|
|
73
|
+
|
|
74
|
+
const CreatePost = Interaction.create({
|
|
75
|
+
name: 'CreatePost',
|
|
76
|
+
action: Action.create({ name: 'createPost' }),
|
|
77
|
+
payload: Payload.create({
|
|
78
|
+
items: [
|
|
79
|
+
PayloadItem.create({ name: 'title', required: true }),
|
|
80
|
+
PayloadItem.create({ name: 'content', required: true })
|
|
81
|
+
]
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// --- Controller Setup ---
|
|
86
|
+
|
|
87
|
+
const system = new MonoSystem(new PGLiteDB())
|
|
88
|
+
system.conceptClass = KlassByName
|
|
89
|
+
|
|
90
|
+
const controller = new Controller({
|
|
91
|
+
system,
|
|
92
|
+
entities: [User, Post],
|
|
93
|
+
relations: [UserPosts],
|
|
94
|
+
activities: [],
|
|
95
|
+
interactions: [CreatePost],
|
|
96
|
+
dict: [],
|
|
97
|
+
recordMutationSideEffects: []
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await controller.setup(true)
|
|
101
|
+
|
|
102
|
+
// --- Usage ---
|
|
103
|
+
|
|
104
|
+
const adminUser = await system.storage.create('User', {
|
|
105
|
+
name: 'Alice', email: 'alice@example.com'
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const result = await controller.callInteraction('CreatePost', {
|
|
109
|
+
user: adminUser,
|
|
110
|
+
payload: { title: 'First Post', content: 'Hello World' }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const user = await system.storage.findOne(
|
|
114
|
+
'User',
|
|
115
|
+
MatchExp.atom({ key: 'id', value: ['=', adminUser.id] }),
|
|
116
|
+
undefined,
|
|
117
|
+
['id', 'name', 'postCount', ['posts', { attributeQuery: ['id', 'title'] }]]
|
|
118
|
+
)
|
|
119
|
+
// user.postCount === 1
|
|
120
|
+
// user.posts[0].title === 'First Post'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Design Decisions
|
|
124
|
+
- **Count on `postCount`**: Automatically maintained when UserPosts relations change. No manual update logic needed.
|
|
125
|
+
- **Transform on Post entity**: Posts are created reactively when `CreatePost` interaction fires. The Transform checks `interactionName` and returns entity data.
|
|
126
|
+
- **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
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Recipe: Order Workflow with State Machine
|
|
131
|
+
|
|
132
|
+
## Scenario
|
|
133
|
+
An order system with status transitions: pending → paid → shipped → delivered, plus cancellation. Demonstrates StateMachine, StateNode, StateTransfer, and multiple Interactions triggering state changes.
|
|
134
|
+
|
|
135
|
+
## Complete Implementation
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import {
|
|
139
|
+
Entity, Property,
|
|
140
|
+
Interaction, Action, Payload, PayloadItem,
|
|
141
|
+
Transform, InteractionEventEntity,
|
|
142
|
+
StateMachine, StateNode, StateTransfer,
|
|
143
|
+
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
144
|
+
} from 'interaqt'
|
|
145
|
+
|
|
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
|
+
// --- State Nodes ---
|
|
190
|
+
|
|
191
|
+
const pendingState = StateNode.create({ name: 'pending' })
|
|
192
|
+
const paidState = StateNode.create({ name: 'paid' })
|
|
193
|
+
const shippedState = StateNode.create({ name: 'shipped' })
|
|
194
|
+
const deliveredState = StateNode.create({ name: 'delivered' })
|
|
195
|
+
const cancelledState = StateNode.create({ name: 'cancelled' })
|
|
196
|
+
|
|
197
|
+
// --- Entity ---
|
|
198
|
+
|
|
199
|
+
const Order = Entity.create({
|
|
200
|
+
name: 'Order',
|
|
201
|
+
properties: [
|
|
202
|
+
Property.create({ name: 'product', type: 'string' }),
|
|
203
|
+
Property.create({ name: 'quantity', type: 'number' }),
|
|
204
|
+
Property.create({ name: 'createdAt', type: 'string' }),
|
|
205
|
+
Property.create({
|
|
206
|
+
name: 'status',
|
|
207
|
+
type: 'string',
|
|
208
|
+
defaultValue: () => 'pending',
|
|
209
|
+
computation: StateMachine.create({
|
|
210
|
+
states: [pendingState, paidState, shippedState, deliveredState, cancelledState],
|
|
211
|
+
transfers: [
|
|
212
|
+
StateTransfer.create({
|
|
213
|
+
current: pendingState, next: paidState,
|
|
214
|
+
trigger: PayOrder,
|
|
215
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
216
|
+
}),
|
|
217
|
+
StateTransfer.create({
|
|
218
|
+
current: paidState, next: shippedState,
|
|
219
|
+
trigger: ShipOrder,
|
|
220
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
221
|
+
}),
|
|
222
|
+
StateTransfer.create({
|
|
223
|
+
current: pendingState, next: cancelledState,
|
|
224
|
+
trigger: CancelOrder,
|
|
225
|
+
computeTarget: (event) => ({ id: event.payload.orderId })
|
|
226
|
+
})
|
|
227
|
+
],
|
|
228
|
+
initialState: pendingState
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
],
|
|
232
|
+
computation: Transform.create({
|
|
233
|
+
record: InteractionEventEntity,
|
|
234
|
+
attributeQuery: ['interactionName', 'user', 'payload'],
|
|
235
|
+
callback: function(event) {
|
|
236
|
+
if (event.interactionName === 'SubmitOrder') {
|
|
237
|
+
return {
|
|
238
|
+
product: event.payload.product,
|
|
239
|
+
quantity: event.payload.quantity,
|
|
240
|
+
createdAt: new Date().toISOString()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// --- Controller Setup & Usage ---
|
|
249
|
+
|
|
250
|
+
const system = new MonoSystem(new PGLiteDB())
|
|
251
|
+
system.conceptClass = KlassByName
|
|
252
|
+
|
|
253
|
+
const controller = new Controller({
|
|
254
|
+
system,
|
|
255
|
+
entities: [Order],
|
|
256
|
+
relations: [],
|
|
257
|
+
activities: [],
|
|
258
|
+
interactions: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
|
|
259
|
+
dict: [],
|
|
260
|
+
recordMutationSideEffects: []
|
|
261
|
+
})
|
|
262
|
+
await controller.setup(true)
|
|
263
|
+
|
|
264
|
+
const user = { id: 'user-1' }
|
|
265
|
+
|
|
266
|
+
// Submit order
|
|
267
|
+
const submitResult = await controller.callInteraction('SubmitOrder', {
|
|
268
|
+
user,
|
|
269
|
+
payload: { product: 'Widget', quantity: 3 }
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const order = await system.storage.findOne('Order',
|
|
273
|
+
MatchExp.atom({ key: 'product', value: ['=', 'Widget'] }),
|
|
274
|
+
undefined, ['id', 'status', 'product', 'quantity']
|
|
275
|
+
)
|
|
276
|
+
// order.status === 'pending'
|
|
277
|
+
|
|
278
|
+
// Pay order
|
|
279
|
+
await controller.callInteraction('PayOrder', {
|
|
280
|
+
user,
|
|
281
|
+
payload: { orderId: order.id }
|
|
282
|
+
})
|
|
283
|
+
// order.status → 'paid'
|
|
284
|
+
|
|
285
|
+
// Ship order
|
|
286
|
+
await controller.callInteraction('ShipOrder', {
|
|
287
|
+
user,
|
|
288
|
+
payload: { orderId: order.id }
|
|
289
|
+
})
|
|
290
|
+
// order.status → 'shipped'
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Design Decisions
|
|
294
|
+
- **StateMachine on `status` property**: Status transitions are declarative. The framework enforces valid transitions — you cannot jump from `pending` to `shipped` directly.
|
|
295
|
+
- **`computeTarget`**: Each StateTransfer uses `computeTarget` to identify WHICH order the transition applies to, using the orderId from the interaction payload.
|
|
296
|
+
- **Transform on Entity `computation`**: Creates order records reactively when `SubmitOrder` fires.
|
|
297
|
+
- **Cancellation only from `pending`**: Only one `cancelledState` transfer is defined (from `pending`). Attempting to cancel a paid order will have no effect.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
# Recipe: Student GPA with Weighted Summation
|
|
302
|
+
|
|
303
|
+
## 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.
|
|
305
|
+
|
|
306
|
+
## Complete Implementation
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import {
|
|
310
|
+
Entity, Property, Relation, WeightedSummation, Count,
|
|
311
|
+
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
312
|
+
} from 'interaqt'
|
|
313
|
+
|
|
314
|
+
// --- Entities ---
|
|
315
|
+
|
|
316
|
+
const Student = Entity.create({
|
|
317
|
+
name: 'Student',
|
|
318
|
+
properties: [
|
|
319
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
320
|
+
Property.create({
|
|
321
|
+
name: 'gpa',
|
|
322
|
+
type: 'number',
|
|
323
|
+
defaultValue: () => 0,
|
|
324
|
+
computation: WeightedSummation.create({
|
|
325
|
+
record: StudentGrades,
|
|
326
|
+
attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]],
|
|
327
|
+
callback: (relation) => ({
|
|
328
|
+
weight: relation.target.credit,
|
|
329
|
+
value: relation.target.score
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
}),
|
|
333
|
+
Property.create({
|
|
334
|
+
name: 'totalCredits',
|
|
335
|
+
type: 'number',
|
|
336
|
+
defaultValue: () => 0,
|
|
337
|
+
computation: WeightedSummation.create({
|
|
338
|
+
record: StudentGrades,
|
|
339
|
+
attributeQuery: [['target', { attributeQuery: ['credit'] }]],
|
|
340
|
+
callback: (relation) => ({
|
|
341
|
+
weight: 1,
|
|
342
|
+
value: relation.target.credit
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
}),
|
|
346
|
+
Property.create({
|
|
347
|
+
name: 'courseCount',
|
|
348
|
+
type: 'number',
|
|
349
|
+
defaultValue: () => 0,
|
|
350
|
+
computation: Count.create({ record: StudentGrades })
|
|
351
|
+
})
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const Grade = Entity.create({
|
|
356
|
+
name: 'Grade',
|
|
357
|
+
properties: [
|
|
358
|
+
Property.create({ name: 'subject', type: 'string' }),
|
|
359
|
+
Property.create({ name: 'score', type: 'number' }),
|
|
360
|
+
Property.create({ name: 'credit', type: 'number' })
|
|
361
|
+
]
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// --- Relations ---
|
|
365
|
+
|
|
366
|
+
const StudentGrades = Relation.create({
|
|
367
|
+
source: Student,
|
|
368
|
+
sourceProperty: 'grades',
|
|
369
|
+
target: Grade,
|
|
370
|
+
targetProperty: 'student',
|
|
371
|
+
type: '1:n'
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// --- Controller Setup & Usage ---
|
|
375
|
+
|
|
376
|
+
const system = new MonoSystem(new PGLiteDB())
|
|
377
|
+
system.conceptClass = KlassByName
|
|
378
|
+
|
|
379
|
+
const controller = new Controller({
|
|
380
|
+
system,
|
|
381
|
+
entities: [Student, Grade],
|
|
382
|
+
relations: [StudentGrades],
|
|
383
|
+
activities: [],
|
|
384
|
+
interactions: [],
|
|
385
|
+
dict: [],
|
|
386
|
+
recordMutationSideEffects: []
|
|
387
|
+
})
|
|
388
|
+
await controller.setup(true)
|
|
389
|
+
|
|
390
|
+
const student = await system.storage.create('Student', { name: 'Alice' })
|
|
391
|
+
|
|
392
|
+
await system.storage.create('Grade', { subject: 'Math', score: 90, credit: 4, student: student.id })
|
|
393
|
+
await system.storage.create('Grade', { subject: 'English', score: 80, credit: 3, student: student.id })
|
|
394
|
+
|
|
395
|
+
const result = await system.storage.findOne('Student',
|
|
396
|
+
MatchExp.atom({ key: 'id', value: ['=', student.id] }),
|
|
397
|
+
undefined,
|
|
398
|
+
['id', 'name', 'gpa', 'totalCredits', 'courseCount']
|
|
399
|
+
)
|
|
400
|
+
// result.gpa === (90*4 + 80*3) / (4+3) ≈ 85.7
|
|
401
|
+
// result.totalCredits === 7
|
|
402
|
+
// result.courseCount === 2
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## 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
|
+
- **Separate Count for courseCount**: Even though totalCredits could imply count, Count is more efficient and semantically clear for counting.
|
|
408
|
+
- **`attributeQuery` in computation**: Specifies which fields of related records to fetch, avoiding loading unnecessary data.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
# Recipe: Interaction with Payload Validation
|
|
413
|
+
|
|
414
|
+
## Scenario
|
|
415
|
+
A content moderation system where only published posts can be shared. Demonstrates Attributive-based payload validation on interactions.
|
|
416
|
+
|
|
417
|
+
## Complete Implementation
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import {
|
|
421
|
+
Entity, Property,
|
|
422
|
+
Interaction, Action, Payload, PayloadItem,
|
|
423
|
+
Attributive, BoolExp,
|
|
424
|
+
Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
|
|
425
|
+
} from 'interaqt'
|
|
426
|
+
|
|
427
|
+
// --- Entities ---
|
|
428
|
+
|
|
429
|
+
const Post = Entity.create({
|
|
430
|
+
name: 'Post',
|
|
431
|
+
properties: [
|
|
432
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
433
|
+
Property.create({ name: 'status', type: 'string', defaultValue: 'draft' })
|
|
434
|
+
]
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// --- Attributive (validation rule) ---
|
|
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 ---
|
|
447
|
+
|
|
448
|
+
const SharePost = Interaction.create({
|
|
449
|
+
name: 'SharePost',
|
|
450
|
+
action: Action.create({ name: 'sharePost' }),
|
|
451
|
+
payload: Payload.create({
|
|
452
|
+
items: [
|
|
453
|
+
PayloadItem.create({
|
|
454
|
+
name: 'post',
|
|
455
|
+
base: Post,
|
|
456
|
+
isRef: true,
|
|
457
|
+
required: true,
|
|
458
|
+
attributives: PublishedPost
|
|
459
|
+
})
|
|
460
|
+
]
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// --- Controller Setup & Usage ---
|
|
465
|
+
|
|
466
|
+
const system = new MonoSystem(new PGLiteDB())
|
|
467
|
+
system.conceptClass = KlassByName
|
|
468
|
+
|
|
469
|
+
const controller = new Controller({
|
|
470
|
+
system,
|
|
471
|
+
entities: [Post],
|
|
472
|
+
relations: [],
|
|
473
|
+
activities: [],
|
|
474
|
+
interactions: [SharePost],
|
|
475
|
+
dict: [],
|
|
476
|
+
recordMutationSideEffects: []
|
|
477
|
+
})
|
|
478
|
+
await controller.setup(true)
|
|
479
|
+
|
|
480
|
+
const draftPost = await system.storage.create('Post', { title: 'Draft', status: 'draft' })
|
|
481
|
+
const publishedPost = await system.storage.create('Post', { title: 'Published', status: 'published' })
|
|
482
|
+
|
|
483
|
+
// Sharing a draft post fails validation
|
|
484
|
+
const failResult = await controller.callInteraction('SharePost', {
|
|
485
|
+
user: { id: 'user-1' },
|
|
486
|
+
payload: { post: { id: draftPost.id } }
|
|
487
|
+
})
|
|
488
|
+
// failResult.error is defined — draft post cannot be shared
|
|
489
|
+
|
|
490
|
+
// Sharing a published post succeeds
|
|
491
|
+
const successResult = await controller.callInteraction('SharePost', {
|
|
492
|
+
user: { id: 'user-1' },
|
|
493
|
+
payload: { post: { id: publishedPost.id } }
|
|
494
|
+
})
|
|
495
|
+
// successResult.error is undefined — success
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Design Decisions
|
|
499
|
+
- **Attributive on PayloadItem**: The `PublishedPost` attributive is attached directly to the PayloadItem, so the framework validates the referenced entity's data before the interaction proceeds.
|
|
500
|
+
- **`isRef: true`**: The payload contains only an ID reference. The framework loads the full record and runs the attributive check against it.
|
|
501
|
+
- **Error in result, not exception**: Validation failures are returned in `result.error`, consistent with all interaqt error handling.
|