interaqt 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
# Computation Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Computations are the reactive core of interaqt, connecting interactions to entities and enabling automatic data flow.
|
|
5
|
+
|
|
6
|
+
## Types of Computations
|
|
7
|
+
|
|
8
|
+
### 1. Transform - Creates Entities/Relations
|
|
9
|
+
|
|
10
|
+
**ONLY use in Entity/Relation computation, NEVER in Property!**
|
|
11
|
+
|
|
12
|
+
#### 🔴 CRITICAL: Transform Collection Conversion Concept
|
|
13
|
+
|
|
14
|
+
Transform converts one collection (source) into another collection (target). Understanding this is crucial:
|
|
15
|
+
|
|
16
|
+
1. **Collection to Collection**: Transform maps items from source collection to target collection
|
|
17
|
+
- Source: InteractionEventEntity collection (all interaction events)
|
|
18
|
+
- Target: Entity/Relation collection being created
|
|
19
|
+
|
|
20
|
+
2. **Callback Can Return One or Multiple Items**: The callback can return:
|
|
21
|
+
- A single object → creates one record of target type
|
|
22
|
+
- An array of objects → creates multiple records of target type from one source
|
|
23
|
+
- `null`/`undefined` → creates no records
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
callback: function(event) {
|
|
27
|
+
// event is ONE item from InteractionEventEntity collection
|
|
28
|
+
|
|
29
|
+
// Option 1: Return single item
|
|
30
|
+
return { /* single entity data */ };
|
|
31
|
+
|
|
32
|
+
// Option 2: Return multiple entities
|
|
33
|
+
return [];
|
|
34
|
+
|
|
35
|
+
// Option 3: Return nothing (filter out)
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. **System Auto-generates IDs**: NEVER include `id` in callback return value
|
|
41
|
+
```typescript
|
|
42
|
+
// ❌ WRONG: Including id in new entity
|
|
43
|
+
callback: function(event) {
|
|
44
|
+
return {
|
|
45
|
+
id: uuid(), // NEVER DO THIS!
|
|
46
|
+
label: event.payload.label,
|
|
47
|
+
slug: event.payload.slug
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ✅ CORRECT: Let system generate id
|
|
52
|
+
callback: function(event) {
|
|
53
|
+
return {
|
|
54
|
+
label: event.payload.label,
|
|
55
|
+
slug: event.payload.slug
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. **Entity References Use ID**: When referencing existing entities in relations, use `{ id: ... }`
|
|
61
|
+
```typescript
|
|
62
|
+
// ✅ CORRECT: Reference existing entity
|
|
63
|
+
callback: function(event) {
|
|
64
|
+
return {
|
|
65
|
+
title: event.payload.title,
|
|
66
|
+
author: event.user, // event.user already has id
|
|
67
|
+
category: { id: event.payload.categoryId } // Reference by id
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Remember: Transform is a **mapping function** that converts each matching source item into one or more target items (or none). The framework handles ID generation, storage, and relationship management.
|
|
73
|
+
|
|
74
|
+
#### 🔴 CRITICAL: InteractionEventEntity Transform Limitations
|
|
75
|
+
|
|
76
|
+
When using `InteractionEventEntity` as the Transform input source, understand these fundamental limitations:
|
|
77
|
+
|
|
78
|
+
1. **ONLY Creates, Never Updates or Deletes**
|
|
79
|
+
- Transform with InteractionEventEntity can ONLY create new entities
|
|
80
|
+
- It CANNOT update existing entities
|
|
81
|
+
- It CANNOT delete entities
|
|
82
|
+
|
|
83
|
+
2. **Why This Limitation Exists**
|
|
84
|
+
- InteractionEventEntity represents system interaction events
|
|
85
|
+
- Events are **immutable** - once occurred, they never change
|
|
86
|
+
- Events are **append-only** - the collection only grows, never shrinks
|
|
87
|
+
- Each interaction creates a NEW event, it doesn't modify old events
|
|
88
|
+
|
|
89
|
+
3. **How to Handle Updates and Deletes**
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// ❌ WRONG: Trying to update with Transform
|
|
93
|
+
const Style = Entity.create({
|
|
94
|
+
name: 'Style'
|
|
95
|
+
});
|
|
96
|
+
Style.computation = Transform.create({
|
|
97
|
+
record: InteractionEventEntity,
|
|
98
|
+
callback: function(event) {
|
|
99
|
+
if (event.interactionName === 'UpdateStyle') {
|
|
100
|
+
// This will CREATE a new Style, not update existing!
|
|
101
|
+
return { id: event.payload.id, ... } // WRONG!
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ✅ CORRECT: Use StateMachine for updates
|
|
107
|
+
const updatedAtProperty = Property.create({
|
|
108
|
+
name: 'updatedAt',
|
|
109
|
+
type: 'number'
|
|
110
|
+
});
|
|
111
|
+
updatedAtProperty.computation = StateMachine.create({
|
|
112
|
+
states: [updatedState],
|
|
113
|
+
transfers: [
|
|
114
|
+
StateTransfer.create({
|
|
115
|
+
trigger: UpdateStyleInteraction,
|
|
116
|
+
current: updatedState,
|
|
117
|
+
next: updatedState,
|
|
118
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ✅ CORRECT: Use soft delete with status
|
|
124
|
+
const statusProperty = Property.create({
|
|
125
|
+
name: 'status',
|
|
126
|
+
type: 'string'
|
|
127
|
+
});
|
|
128
|
+
statusProperty.computation = StateMachine.create({
|
|
129
|
+
states: [activeState, deletedState],
|
|
130
|
+
defaultState: activeState, // StateMachine controls initial value
|
|
131
|
+
transfers: [
|
|
132
|
+
StateTransfer.create({
|
|
133
|
+
trigger: DeleteStyleInteraction,
|
|
134
|
+
current: activeState,
|
|
135
|
+
next: deletedState,
|
|
136
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Summary**: Think of InteractionEventEntity Transform as a "factory" that produces new entities from events. For any modifications to existing entities, use StateMachine. For deletions, use soft delete patterns with status fields.
|
|
143
|
+
|
|
144
|
+
#### Entity Creation from InteractionEventEntity via Transform
|
|
145
|
+
```typescript
|
|
146
|
+
import { Transform, InteractionEventEntity, Entity, Property, Interaction, Action, Payload, PayloadItem } from 'interaqt';
|
|
147
|
+
|
|
148
|
+
// Define creation interaction
|
|
149
|
+
export const CreateStyle = Interaction.create({
|
|
150
|
+
name: 'CreateStyle',
|
|
151
|
+
action: Action.create({ name: 'createStyle' }),
|
|
152
|
+
payload: Payload.create({
|
|
153
|
+
items: [
|
|
154
|
+
PayloadItem.create({ name: 'label', required: true }),
|
|
155
|
+
PayloadItem.create({ name: 'slug', required: true }),
|
|
156
|
+
PayloadItem.create({ name: 'description' }),
|
|
157
|
+
PayloadItem.create({ name: 'type' }),
|
|
158
|
+
PayloadItem.create({ name: 'thumbKey' }),
|
|
159
|
+
PayloadItem.create({ name: 'priority' })
|
|
160
|
+
]
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Entity with Transform
|
|
165
|
+
export const Style = Entity.create({
|
|
166
|
+
name: 'Style',
|
|
167
|
+
properties: [
|
|
168
|
+
Property.create({ name: 'label', type: 'string' }),
|
|
169
|
+
Property.create({ name: 'slug', type: 'string' }),
|
|
170
|
+
Property.create({ name: 'description', type: 'string' }),
|
|
171
|
+
Property.create({ name: 'type', type: 'string' }),
|
|
172
|
+
Property.create({ name: 'thumbKey', type: 'string' }),
|
|
173
|
+
Property.create({ name: 'priority', type: 'number', defaultValue: () => 0 }),
|
|
174
|
+
Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' }),
|
|
175
|
+
// 🔴 CRITICAL: Always use seconds for timestamps, not milliseconds!
|
|
176
|
+
Property.create({ name: 'createdAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) }),
|
|
177
|
+
Property.create({ name: 'updatedAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) })
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
// Transform in Entity's computation property
|
|
181
|
+
Style.computation = Transform.create({
|
|
182
|
+
record: InteractionEventEntity,
|
|
183
|
+
callback: function(event) {
|
|
184
|
+
if (event.interactionName === 'CreateStyle') {
|
|
185
|
+
return {
|
|
186
|
+
label: event.payload.label,
|
|
187
|
+
slug: event.payload.slug,
|
|
188
|
+
description: event.payload.description || '',
|
|
189
|
+
type: event.payload.type || 'default',
|
|
190
|
+
thumbKey: event.payload.thumbKey || '',
|
|
191
|
+
priority: event.payload.priority || 0,
|
|
192
|
+
status: 'draft',
|
|
193
|
+
createdAt: Math.floor(Date.now()/1000), // Always use seconds!
|
|
194
|
+
updatedAt: Math.floor(Date.now()/1000), // Always use seconds!
|
|
195
|
+
lastModifiedBy: event.user // Creates relation automatically
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Created With Parent - Child Entities in Parent Transform
|
|
204
|
+
|
|
205
|
+
**When to Use**:
|
|
206
|
+
- When child entities' lifecycle is completely dependent on parent entity
|
|
207
|
+
- **When computationDecision is expressed as `_parent:[Parent]`** - this indicates the child entity should be created through the parent's Transform computation
|
|
208
|
+
|
|
209
|
+
**Example**: Order with OrderItems
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Order creates OrderItems atomically
|
|
213
|
+
export const Order = Entity.create({
|
|
214
|
+
name: 'Order',
|
|
215
|
+
properties: [
|
|
216
|
+
Property.create({ name: 'orderNumber', type: 'string' }),
|
|
217
|
+
Property.create({ name: 'customerName', type: 'string' })
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const OrderItem = Entity.create({
|
|
222
|
+
name: 'OrderItem',
|
|
223
|
+
properties: [
|
|
224
|
+
Property.create({ name: 'productName', type: 'string' }),
|
|
225
|
+
Property.create({ name: 'quantity', type: 'number' }),
|
|
226
|
+
Property.create({ name: 'price', type: 'number' })
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Define relation
|
|
231
|
+
export const OrderItemRelation = Relation.create({
|
|
232
|
+
source: Order,
|
|
233
|
+
sourceProperty: 'items',
|
|
234
|
+
target: OrderItem,
|
|
235
|
+
targetProperty: 'order',
|
|
236
|
+
type: '1:n'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
Order.computation = Transform.create({
|
|
241
|
+
record: InteractionEventEntity,
|
|
242
|
+
callback: function(event) {
|
|
243
|
+
if (event.interactionName === 'CreateOrder') {
|
|
244
|
+
return {
|
|
245
|
+
orderNumber: event.payload.orderNumber,
|
|
246
|
+
customerName: event.payload.customerName
|
|
247
|
+
items: event.payload.items // OrderItem and OrderItemRelation created with parent
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
#### Derived from Other Entities/Relations (Non-InteractionEventEntity)
|
|
257
|
+
|
|
258
|
+
Transform can also use other entities as source, not just InteractionEventEntity. This is useful for creating derived entities based on existing data.
|
|
259
|
+
|
|
260
|
+
**🔴 IMPORTANT: Establishing Relations**
|
|
261
|
+
When transforming from one entity to another, if you want to establish a relation between the transformed entity and the source entity, you MUST explicitly include the source entity reference in the callback return value.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Transform, Entity, Property, Relation } from 'interaqt';
|
|
265
|
+
|
|
266
|
+
// Create snapshot entity from Style entity
|
|
267
|
+
export const StyleSnapshot = Entity.create({
|
|
268
|
+
name: 'StyleSnapshot',
|
|
269
|
+
properties: [
|
|
270
|
+
Property.create({ name: 'label', type: 'string' }),
|
|
271
|
+
Property.create({ name: 'slug', type: 'string' }),
|
|
272
|
+
Property.create({ name: 'description', type: 'string' }),
|
|
273
|
+
Property.create({ name: 'snapshotTakenAt', type: 'number', defaultValue: () => Math.floor(Date.now()/1000) }),
|
|
274
|
+
Property.create({ name: 'version', type: 'number' })
|
|
275
|
+
]
|
|
276
|
+
});
|
|
277
|
+
// Transform from Style entity (not InteractionEventEntity)
|
|
278
|
+
StyleSnapshot.computation = Transform.create({
|
|
279
|
+
record: Style, // ← Source is Style entity, not InteractionEventEntity
|
|
280
|
+
attributeQuery: ['id', 'label', 'slug', 'description', 'status'],
|
|
281
|
+
callback: function(style) {
|
|
282
|
+
// Only create snapshots for active styles
|
|
283
|
+
if (style.status === 'active') {
|
|
284
|
+
return {
|
|
285
|
+
label: style.label,
|
|
286
|
+
slug: style.slug,
|
|
287
|
+
description: style.description || '',
|
|
288
|
+
snapshotTakenAt: Math.floor(Date.now()/1000), // In seconds
|
|
289
|
+
version: Math.floor(Date.now()/1000), // Version number in seconds
|
|
290
|
+
// 🔴 CRITICAL: Must explicitly reference source entity to create relation
|
|
291
|
+
originalStyle: style // ← This creates the relation to source Style
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return null; // Don't create snapshot for non-active styles
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Define the relation between Style and StyleSnapshot
|
|
299
|
+
export const StyleSnapshotRelation = Relation.create({
|
|
300
|
+
source: Style,
|
|
301
|
+
sourceProperty: 'snapshots',
|
|
302
|
+
target: StyleSnapshot,
|
|
303
|
+
targetProperty: 'originalStyle',
|
|
304
|
+
type: '1:n' // One style can have many snapshots
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
### 2. StateMachine - Updates Entities
|
|
310
|
+
|
|
311
|
+
Used for status changes and field updates.
|
|
312
|
+
|
|
313
|
+
**🔴 IMPORTANT: StateTransfer Trigger Parameter**
|
|
314
|
+
|
|
315
|
+
The `trigger` parameter in `StateTransfer.create()` must ALWAYS be an Interaction instance reference, NOT a string!
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// ❌ WRONG: Using string as trigger
|
|
319
|
+
StateTransfer.create({
|
|
320
|
+
trigger: 'PublishStyle', // ERROR! Don't use string!
|
|
321
|
+
current: draftState,
|
|
322
|
+
next: activeState
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// ✅ CORRECT: Using Interaction instance reference
|
|
326
|
+
StateTransfer.create({
|
|
327
|
+
trigger: PublishStyle, // Correct! Reference to Interaction instance
|
|
328
|
+
current: draftState,
|
|
329
|
+
next: activeState
|
|
330
|
+
})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Basic StateMachine
|
|
334
|
+
```typescript
|
|
335
|
+
import { StateMachine, StateNode, StateTransfer } from 'interaqt';
|
|
336
|
+
|
|
337
|
+
// Define states first (must be declared before use)
|
|
338
|
+
const draftState = StateNode.create({ name: 'draft' });
|
|
339
|
+
const activeState = StateNode.create({ name: 'active' });
|
|
340
|
+
const offlineState = StateNode.create({ name: 'offline' });
|
|
341
|
+
|
|
342
|
+
// Define interactions
|
|
343
|
+
const PublishStyle = Interaction.create({
|
|
344
|
+
name: 'PublishStyle',
|
|
345
|
+
action: Action.create({ name: 'publishStyle' }),
|
|
346
|
+
payload: Payload.create({
|
|
347
|
+
items: [
|
|
348
|
+
PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
|
|
349
|
+
]
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const DeleteStyle = Interaction.create({
|
|
354
|
+
name: 'DeleteStyle',
|
|
355
|
+
action: Action.create({ name: 'deleteStyle' }),
|
|
356
|
+
payload: Payload.create({
|
|
357
|
+
items: [
|
|
358
|
+
PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
|
|
359
|
+
]
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Apply state machine to property
|
|
364
|
+
const statusProperty = Property.create({
|
|
365
|
+
name: 'status',
|
|
366
|
+
type: 'string'
|
|
367
|
+
});
|
|
368
|
+
statusProperty.computation = StateMachine.create({
|
|
369
|
+
name: 'StyleStatus',
|
|
370
|
+
states: [draftState, activeState, offlineState],
|
|
371
|
+
defaultState: draftState, // defaultState determines initial value
|
|
372
|
+
transfers: [
|
|
373
|
+
StateTransfer.create({
|
|
374
|
+
current: draftState,
|
|
375
|
+
next: activeState,
|
|
376
|
+
trigger: PublishStyle,
|
|
377
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
378
|
+
}),
|
|
379
|
+
StateTransfer.create({
|
|
380
|
+
current: activeState,
|
|
381
|
+
next: offlineState,
|
|
382
|
+
trigger: DeleteStyle,
|
|
383
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
384
|
+
})
|
|
385
|
+
]
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### StateMachine with Value Updates
|
|
390
|
+
```typescript
|
|
391
|
+
// Track timestamps using single-state machine with computeValue
|
|
392
|
+
const UpdateStyle = Interaction.create({
|
|
393
|
+
name: 'UpdateStyle',
|
|
394
|
+
action: Action.create({ name: 'updateStyle' }),
|
|
395
|
+
payload: Payload.create({
|
|
396
|
+
items: [
|
|
397
|
+
PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true }),
|
|
398
|
+
PayloadItem.create({ name: 'label' }),
|
|
399
|
+
PayloadItem.create({ name: 'description' })
|
|
400
|
+
]
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Define state node with computeValue
|
|
405
|
+
const updatedState = StateNode.create({
|
|
406
|
+
name: 'updated',
|
|
407
|
+
computeValue: () => Math.floor(Date.now()/1000) // Returns timestamp in seconds when state is entered
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const updatedAtProperty = Property.create({
|
|
411
|
+
name: 'updatedAt',
|
|
412
|
+
type: 'number'
|
|
413
|
+
});
|
|
414
|
+
updatedAtProperty.computation = StateMachine.create({
|
|
415
|
+
name: 'UpdatedAt',
|
|
416
|
+
states: [updatedState],
|
|
417
|
+
defaultState: updatedState, // computeValue in updatedState provides initial value
|
|
418
|
+
transfers: [
|
|
419
|
+
StateTransfer.create({
|
|
420
|
+
current: updatedState,
|
|
421
|
+
next: updatedState, // Self-loop to same state
|
|
422
|
+
trigger: UpdateStyle,
|
|
423
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
424
|
+
})
|
|
425
|
+
]
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### StateMachine with Event Context in computeValue
|
|
430
|
+
|
|
431
|
+
The `computeValue` function can access the interaction event as a second parameter, allowing you to use interaction context (user, payload) in value computation:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Track who made changes and what was changed
|
|
435
|
+
const UpdateArticle = Interaction.create({
|
|
436
|
+
name: 'UpdateArticle',
|
|
437
|
+
action: Action.create({ name: 'updateArticle' }),
|
|
438
|
+
payload: Payload.create({
|
|
439
|
+
items: [
|
|
440
|
+
PayloadItem.create({ name: 'id', base: Article, isRef: true, required: true }),
|
|
441
|
+
PayloadItem.create({ name: 'title' }),
|
|
442
|
+
PayloadItem.create({ name: 'content' }),
|
|
443
|
+
PayloadItem.create({ name: 'updateReason' })
|
|
444
|
+
]
|
|
445
|
+
})
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// State node that captures user and payload information
|
|
449
|
+
const modifiedState = StateNode.create({
|
|
450
|
+
name: 'modified',
|
|
451
|
+
// computeValue receives (lastValue, event) parameters
|
|
452
|
+
computeValue: (lastValue, event) => {
|
|
453
|
+
// Access user who triggered the update
|
|
454
|
+
const modifier = event?.user?.name || event?.user?.id || 'anonymous';
|
|
455
|
+
|
|
456
|
+
// Access payload to see what was changed
|
|
457
|
+
const changes = [];
|
|
458
|
+
if (event?.payload?.title) changes.push('title');
|
|
459
|
+
if (event?.payload?.content) changes.push('content');
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
modifiedAt: Math.floor(Date.now()/1000),
|
|
463
|
+
modifiedBy: modifier,
|
|
464
|
+
changedFields: changes,
|
|
465
|
+
updateReason: event?.payload?.updateReason || 'No reason provided',
|
|
466
|
+
// Preserve previous modification history
|
|
467
|
+
previousModifications: lastValue?.previousModifications || []
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Apply to property
|
|
473
|
+
const modificationInfoProperty = Property.create({
|
|
474
|
+
name: 'modificationInfo',
|
|
475
|
+
type: 'object'
|
|
476
|
+
});
|
|
477
|
+
modificationInfoProperty.computation = StateMachine.create({
|
|
478
|
+
name: 'ModificationTracker',
|
|
479
|
+
states: [modifiedState],
|
|
480
|
+
defaultState: modifiedState, // computeValue in modifiedState handles initial value
|
|
481
|
+
transfers: [
|
|
482
|
+
StateTransfer.create({
|
|
483
|
+
current: modifiedState,
|
|
484
|
+
next: modifiedState,
|
|
485
|
+
trigger: UpdateArticle,
|
|
486
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
487
|
+
})
|
|
488
|
+
]
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Another example: Approval workflow with approver tracking
|
|
492
|
+
const ApproveRequest = Interaction.create({
|
|
493
|
+
name: 'ApproveRequest',
|
|
494
|
+
action: Action.create({ name: 'approveRequest' }),
|
|
495
|
+
payload: Payload.create({
|
|
496
|
+
items: [
|
|
497
|
+
PayloadItem.create({ name: 'requestId', base: Request, isRef: true, required: true }),
|
|
498
|
+
PayloadItem.create({ name: 'comments' })
|
|
499
|
+
]
|
|
500
|
+
})
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const approvedState = StateNode.create({
|
|
504
|
+
name: 'approved',
|
|
505
|
+
computeValue: (lastValue, event) => {
|
|
506
|
+
// Capture complete approval context from event
|
|
507
|
+
return {
|
|
508
|
+
status: 'approved',
|
|
509
|
+
approvedAt: Math.floor(Date.now()/1000),
|
|
510
|
+
approvedBy: {
|
|
511
|
+
id: event?.user?.id,
|
|
512
|
+
name: event?.user?.name,
|
|
513
|
+
role: event?.user?.role
|
|
514
|
+
},
|
|
515
|
+
approvalComments: event?.payload?.comments,
|
|
516
|
+
// Keep approval history
|
|
517
|
+
approvalHistory: [
|
|
518
|
+
...(lastValue?.approvalHistory || []),
|
|
519
|
+
{
|
|
520
|
+
action: 'approved',
|
|
521
|
+
timestamp: Math.floor(Date.now()/1000),
|
|
522
|
+
user: event?.user?.name || 'unknown',
|
|
523
|
+
comments: event?.payload?.comments
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Key Points about Event Parameter:**
|
|
532
|
+
- The `event` parameter is optional and may be `undefined` during initial state setup
|
|
533
|
+
- Contains the full interaction context: `user`, `payload`, `interactionName`, etc.
|
|
534
|
+
- Useful for audit trails, tracking who made changes, and capturing interaction-specific data
|
|
535
|
+
- Always use optional chaining (`?.`) when accessing event properties as it may be undefined
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
### 3. Custom - Complete User Control (USE WITH CAUTION!)
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { Custom, Dictionary, GlobalBoundState, Entity, Property, Relation } from 'interaqt';
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**🔴 WARNING: Custom should be your LAST RESORT!**
|
|
545
|
+
|
|
546
|
+
Before using Custom computation, ask yourself:
|
|
547
|
+
1. Can I use Transform for entity/relation creation? → Use Transform
|
|
548
|
+
2. Can I use StateMachine for updates? → Use StateMachine
|
|
549
|
+
3. Can I use Count/Summation/Every/Any for aggregations? → Use those
|
|
550
|
+
4. Can I use computed/getValue for simple calculations? → Use those
|
|
551
|
+
5. Can I combine existing computations? → Combine them
|
|
552
|
+
|
|
553
|
+
**Only use Custom when:**
|
|
554
|
+
- You need complex business logic that doesn't fit ANY existing computation type
|
|
555
|
+
- You need stateful calculations with custom persistence logic
|
|
556
|
+
- You need advanced data transformations that require full control
|
|
557
|
+
|
|
558
|
+
**Example of PROPER use:**
|
|
559
|
+
```typescript
|
|
560
|
+
// ✅ CORRECT: Complex calculation using Custom computation
|
|
561
|
+
const totalProductValue = Dictionary.create({
|
|
562
|
+
name: 'totalProductValue',
|
|
563
|
+
type: 'number',
|
|
564
|
+
collection: false
|
|
565
|
+
});
|
|
566
|
+
totalProductValue.computation = Custom.create({
|
|
567
|
+
name: 'TotalValueCalculator',
|
|
568
|
+
dataDeps: {
|
|
569
|
+
products: {
|
|
570
|
+
type: 'records',
|
|
571
|
+
source: Product,
|
|
572
|
+
attributeQuery: ['price', 'quantity']
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
compute: async function(dataDeps) {
|
|
576
|
+
const products = dataDeps.products || [];
|
|
577
|
+
const total = products.reduce((sum, p) => {
|
|
578
|
+
return sum + (p.price || 0) * (p.quantity || 0);
|
|
579
|
+
}, 0);
|
|
580
|
+
return total;
|
|
581
|
+
},
|
|
582
|
+
getDefaultValue: function() {
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ✅ CORRECT: Property-level Custom for computed field
|
|
588
|
+
const Product = Entity.create({
|
|
589
|
+
name: 'Product',
|
|
590
|
+
properties: [
|
|
591
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
592
|
+
Property.create({ name: 'basePrice', type: 'number' }),
|
|
593
|
+
Property.create({ name: 'taxRate', type: 'number', defaultValue: () => 0.1 }),
|
|
594
|
+
Property.create({ name: 'discount', type: 'number', defaultValue: () => 0 })
|
|
595
|
+
]
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Computed property based on other properties of same record
|
|
599
|
+
const finalPriceProperty = Property.create({
|
|
600
|
+
name: 'finalPrice',
|
|
601
|
+
type: 'number'
|
|
602
|
+
});
|
|
603
|
+
finalPriceProperty.computation = Custom.create({
|
|
604
|
+
name: 'FinalPriceCalculator',
|
|
605
|
+
dataDeps: {
|
|
606
|
+
_current: { // Special key for current record's properties
|
|
607
|
+
type: 'property',
|
|
608
|
+
attributeQuery: ['basePrice', 'taxRate', 'discount']
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
compute: async function(dataDeps, record) {
|
|
612
|
+
const basePrice = dataDeps._current?.basePrice || 0;
|
|
613
|
+
const taxRate = dataDeps._current?.taxRate || 0;
|
|
614
|
+
const discount = dataDeps._current?.discount || 0;
|
|
615
|
+
|
|
616
|
+
// Calculate: basePrice * (1 + taxRate) * (1 - discount)
|
|
617
|
+
const priceWithTax = basePrice * (1 + taxRate);
|
|
618
|
+
const finalPrice = priceWithTax * (1 - discount);
|
|
619
|
+
|
|
620
|
+
return Math.round(finalPrice * 100) / 100; // Round to 2 decimals
|
|
621
|
+
},
|
|
622
|
+
getDefaultValue: function() {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
Product.properties.push(finalPriceProperty);
|
|
627
|
+
|
|
628
|
+
// ✅ CORRECT: Accessing related entity properties through relations
|
|
629
|
+
const Department = Entity.create({
|
|
630
|
+
name: 'Department',
|
|
631
|
+
properties: [
|
|
632
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
633
|
+
Property.create({ name: 'budget', type: 'number' })
|
|
634
|
+
]
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const Employee = Entity.create({
|
|
638
|
+
name: 'Employee',
|
|
639
|
+
properties: [
|
|
640
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
641
|
+
Property.create({ name: 'salary', type: 'number' })
|
|
642
|
+
]
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Define the relation between Employee and Department
|
|
646
|
+
const EmployeeDepartmentRelation = Relation.create({
|
|
647
|
+
source: Employee,
|
|
648
|
+
sourceProperty: 'department',
|
|
649
|
+
target: Department,
|
|
650
|
+
targetProperty: 'employees',
|
|
651
|
+
type: 'n:1' // Many employees to one department
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Property that accesses related department data
|
|
655
|
+
const EmployeeDepartmentInfoProperty = Property.create({
|
|
656
|
+
name: 'departmentInfo',
|
|
657
|
+
type: 'string'
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
Employee.properties.push(EmployeeDepartmentInfoProperty);
|
|
661
|
+
|
|
662
|
+
EmployeeDepartmentInfoProperty.computation = Custom.create({
|
|
663
|
+
name: 'DepartmentInfoGenerator',
|
|
664
|
+
dataDeps: {
|
|
665
|
+
_current: {
|
|
666
|
+
type: 'property',
|
|
667
|
+
// Access properties and related entities through nested attributeQuery
|
|
668
|
+
attributeQuery: [
|
|
669
|
+
'name',
|
|
670
|
+
'salary',
|
|
671
|
+
['department', { // Access related entity through relation
|
|
672
|
+
attributeQuery: ['name', 'budget'] // Specify which properties of related entity
|
|
673
|
+
}]
|
|
674
|
+
]
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
compute: async function(dataDeps, record) {
|
|
678
|
+
const employeeName = dataDeps._current?.name || 'Unknown';
|
|
679
|
+
const salary = dataDeps._current?.salary || 0;
|
|
680
|
+
const department = dataDeps._current?.department;
|
|
681
|
+
|
|
682
|
+
if (department) {
|
|
683
|
+
return `${employeeName} ($${salary}) works in ${department.name} with budget $${department.budget}`;
|
|
684
|
+
}
|
|
685
|
+
return `${employeeName} ($${salary}) - No department assigned`;
|
|
686
|
+
},
|
|
687
|
+
getDefaultValue: function() {
|
|
688
|
+
return 'No info available';
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Custom Computation dataDeps Types:**
|
|
694
|
+
- `type: 'records'` - Access entity/relation records from storage
|
|
695
|
+
- `type: 'global'` - Access global dictionary values
|
|
696
|
+
- `type: 'property'` - Access current record's properties and related entities
|
|
697
|
+
|
|
698
|
+
**🔴 IMPORTANT: Property Type Custom Computation**
|
|
699
|
+
|
|
700
|
+
When using `type: 'property'` with Custom computation:
|
|
701
|
+
- Access same record properties: `attributeQuery: ['propertyName1', 'propertyName2']`
|
|
702
|
+
- Access related entities through relations: Use nested attributeQuery
|
|
703
|
+
```typescript
|
|
704
|
+
attributeQuery: [
|
|
705
|
+
'ownProperty', // Current record's property
|
|
706
|
+
['relationName', { // Access related entity
|
|
707
|
+
attributeQuery: ['relatedProp1', 'relatedProp2'] // Properties of related entity
|
|
708
|
+
}]
|
|
709
|
+
]
|
|
710
|
+
```
|
|
711
|
+
- The framework automatically tracks dependencies and recomputes when related data changes
|
|
712
|
+
|
|
713
|
+
**Custom Computation Best Practices:**
|
|
714
|
+
1. **Document WHY** you need Custom instead of other computations
|
|
715
|
+
2. **Minimize dependencies** - only include data you absolutely need
|
|
716
|
+
3. **Handle errors gracefully** - Custom computations can fail
|
|
717
|
+
|
|
718
|
+
**Remember:** The power of interaqt comes from its declarative computations. Custom computation breaks this paradigm and should only be used when absolutely necessary. Always try to express your logic using the standard computation types first!
|
|
719
|
+
|
|
720
|
+
## Implementation Steps
|
|
721
|
+
|
|
722
|
+
### Step 1: Entity Creation Pattern
|
|
723
|
+
```typescript
|
|
724
|
+
// 1. Define interaction
|
|
725
|
+
export const CreateStyle = Interaction.create({
|
|
726
|
+
name: 'CreateStyle',
|
|
727
|
+
action: Action.create({ name: 'createStyle' }),
|
|
728
|
+
payload: Payload.create({
|
|
729
|
+
items: [
|
|
730
|
+
PayloadItem.create({ name: 'label', required: true }),
|
|
731
|
+
PayloadItem.create({ name: 'slug', required: true })
|
|
732
|
+
]
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// 2. Entity with Transform
|
|
737
|
+
export const Style = Entity.create({
|
|
738
|
+
name: 'Style',
|
|
739
|
+
properties: [
|
|
740
|
+
Property.create({ name: 'label', type: 'string' }),
|
|
741
|
+
Property.create({ name: 'slug', type: 'string' }),
|
|
742
|
+
Property.create({
|
|
743
|
+
name: 'status',
|
|
744
|
+
type: 'string',
|
|
745
|
+
defaultValue: () => 'draft'
|
|
746
|
+
})
|
|
747
|
+
]
|
|
748
|
+
});
|
|
749
|
+
Style.computation = Transform.create({
|
|
750
|
+
record: InteractionEventEntity,
|
|
751
|
+
callback: (event) => {
|
|
752
|
+
if (event.interactionName === 'CreateStyle') {
|
|
753
|
+
return {
|
|
754
|
+
label: event.payload.label,
|
|
755
|
+
slug: event.payload.slug,
|
|
756
|
+
status: 'draft',
|
|
757
|
+
createdAt: Math.floor(Date.now()/1000)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Step 2: Update Pattern with StateMachine
|
|
766
|
+
```typescript
|
|
767
|
+
// Update interaction
|
|
768
|
+
export const UpdateStyle = Interaction.create({
|
|
769
|
+
name: 'UpdateStyle',
|
|
770
|
+
action: Action.create({ name: 'updateStyle' }),
|
|
771
|
+
payload: Payload.create({
|
|
772
|
+
items: [
|
|
773
|
+
PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true }),
|
|
774
|
+
PayloadItem.create({ name: 'label' }),
|
|
775
|
+
PayloadItem.create({ name: 'description' })
|
|
776
|
+
]
|
|
777
|
+
})
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Property with update tracking
|
|
781
|
+
const updatedState = StateNode.create({
|
|
782
|
+
name: 'updated',
|
|
783
|
+
computeValue: () => Math.floor(Date.now()/1000)
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const updatedAtProperty = Property.create({
|
|
787
|
+
name: 'updatedAt',
|
|
788
|
+
type: 'number'
|
|
789
|
+
});
|
|
790
|
+
updatedAtProperty.computation = StateMachine.create({
|
|
791
|
+
states: [updatedState],
|
|
792
|
+
defaultState: updatedState,
|
|
793
|
+
transfers: [
|
|
794
|
+
StateTransfer.create({
|
|
795
|
+
current: updatedState,
|
|
796
|
+
next: updatedState,
|
|
797
|
+
trigger: UpdateStyle,
|
|
798
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
799
|
+
})
|
|
800
|
+
]
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Step 3: Soft Delete Pattern
|
|
805
|
+
```typescript
|
|
806
|
+
// Delete as state transition
|
|
807
|
+
const DeleteStyle = Interaction.create({
|
|
808
|
+
name: 'DeleteStyle',
|
|
809
|
+
action: Action.create({ name: 'deleteStyle' }),
|
|
810
|
+
payload: Payload.create({
|
|
811
|
+
items: [
|
|
812
|
+
PayloadItem.create({ name: 'id', base: Style, isRef: true, required: true })
|
|
813
|
+
]
|
|
814
|
+
})
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Declare states first
|
|
818
|
+
const activeState = StateNode.create({ name: 'active' });
|
|
819
|
+
const offlineState = StateNode.create({ name: 'offline' });
|
|
820
|
+
|
|
821
|
+
// Status property handles soft delete
|
|
822
|
+
const statusProperty = Property.create({
|
|
823
|
+
name: 'status',
|
|
824
|
+
type: 'string'
|
|
825
|
+
});
|
|
826
|
+
statusProperty.computation = StateMachine.create({
|
|
827
|
+
states: [activeState, offlineState],
|
|
828
|
+
defaultState: activeState,
|
|
829
|
+
transfers: [
|
|
830
|
+
StateTransfer.create({
|
|
831
|
+
current: activeState,
|
|
832
|
+
next: offlineState,
|
|
833
|
+
trigger: DeleteStyle,
|
|
834
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
835
|
+
})
|
|
836
|
+
]
|
|
837
|
+
});
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
## Critical Rules
|
|
841
|
+
|
|
842
|
+
### ✅ DO
|
|
843
|
+
- Use Transform ONLY in Entity/Relation computation property
|
|
844
|
+
- Use StateMachine for updates and state management
|
|
845
|
+
- Use computed/getValue for simple property calculations
|
|
846
|
+
- Keep computations pure and side-effect free
|
|
847
|
+
- Declare StateNode variables before using them in StateMachine
|
|
848
|
+
- Use Interaction instance references (not strings) as trigger in StateTransfer
|
|
849
|
+
|
|
850
|
+
### ❌ DON'T
|
|
851
|
+
- Never use Transform in Property computation
|
|
852
|
+
- **Don't use both defaultValue and computation on the same property** - they are mutually exclusive!
|
|
853
|
+
- Don't create circular dependencies
|
|
854
|
+
- Don't perform side effects in computations
|
|
855
|
+
- Don't access external services in computations
|
|
856
|
+
- Don't manually update computed values
|
|
857
|
+
- Don't create StateNode inside StateTransfer
|
|
858
|
+
- **Don't use strings as trigger in StateTransfer** - always use Interaction instance references
|
|
859
|
+
|
|
860
|
+
## Common Patterns
|
|
861
|
+
|
|
862
|
+
### 🔴 CRITICAL: Timestamp Tracking - Always Use Seconds!
|
|
863
|
+
|
|
864
|
+
**The database does NOT support millisecond precision. You MUST use `Math.floor(Date.now()/1000)` to convert milliseconds to seconds.**
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
// ❌ WRONG: Using milliseconds directly
|
|
868
|
+
Property.create({
|
|
869
|
+
name: 'createdAt',
|
|
870
|
+
type: 'number',
|
|
871
|
+
defaultValue: () => Date.now() // ERROR! Returns milliseconds, but database only supports seconds!
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
// ✅ CORRECT: Convert to seconds
|
|
875
|
+
Property.create({
|
|
876
|
+
name: 'createdAt',
|
|
877
|
+
type: 'number',
|
|
878
|
+
defaultValue: () => Math.floor(Date.now()/1000) // Correct! Unix timestamp in seconds
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
// ✅ CORRECT: Created at - set once (in seconds)
|
|
882
|
+
Property.create({
|
|
883
|
+
name: 'createdAt',
|
|
884
|
+
type: 'number',
|
|
885
|
+
defaultValue: () => Math.floor(Date.now()/1000)
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
// ✅ CORRECT: Updated at - updates on changes (in seconds)
|
|
889
|
+
const updatedState = StateNode.create({
|
|
890
|
+
name: 'updated',
|
|
891
|
+
computeValue: () => Math.floor(Date.now()/1000) // Always convert to seconds!
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const updatedAtProperty = Property.create({
|
|
895
|
+
name: 'updatedAt',
|
|
896
|
+
type: 'number'
|
|
897
|
+
});
|
|
898
|
+
updatedAtProperty.computation = StateMachine.create({
|
|
899
|
+
states: [updatedState],
|
|
900
|
+
defaultState: updatedState,
|
|
901
|
+
transfers: [
|
|
902
|
+
StateTransfer.create({
|
|
903
|
+
current: updatedState,
|
|
904
|
+
next: updatedState,
|
|
905
|
+
trigger: UpdateInteraction,
|
|
906
|
+
computeTarget: (event) => ({ id: event.payload.id })
|
|
907
|
+
})
|
|
908
|
+
]
|
|
909
|
+
});
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Version Management
|
|
913
|
+
```typescript
|
|
914
|
+
// Create version from style
|
|
915
|
+
export const PublishStyle = Interaction.create({
|
|
916
|
+
name: 'PublishStyle',
|
|
917
|
+
action: Action.create({ name: 'publishStyle' }),
|
|
918
|
+
payload: Payload.create({
|
|
919
|
+
items: [
|
|
920
|
+
PayloadItem.create({ name: 'styleId', base: Style, isRef: true, required: true })
|
|
921
|
+
]
|
|
922
|
+
})
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
export const Version = Entity.create({
|
|
926
|
+
name: 'Version',
|
|
927
|
+
properties: [
|
|
928
|
+
Property.create({ name: 'version', type: 'number' }),
|
|
929
|
+
Property.create({ name: 'publishedAt', type: 'bigint' }),
|
|
930
|
+
Property.create({ name: 'isActive', type: 'boolean', defaultValue: () => true })
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
Version.computation = Transform.create({
|
|
934
|
+
record: InteractionEventEntity,
|
|
935
|
+
attributeQuery: ['interactionName', 'payload', 'user', 'createdAt'],
|
|
936
|
+
dataDeps: {
|
|
937
|
+
styles: {
|
|
938
|
+
type: 'records',
|
|
939
|
+
source: Style,
|
|
940
|
+
attributeQuery: ['*']
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
callback: function(event, dataDeps) {
|
|
944
|
+
if (event.interactionName === 'PublishStyle') {
|
|
945
|
+
const style = dataDeps.styles.find(s => s.id === event.payload.styleId);
|
|
946
|
+
if (style) {
|
|
947
|
+
return {
|
|
948
|
+
version: Math.floor(Date.now()/1000), // Version number in seconds
|
|
949
|
+
publishedAt: Math.floor(Date.now()/1000), // In seconds
|
|
950
|
+
isActive: true,
|
|
951
|
+
publishedBy: event.user,
|
|
952
|
+
style: { id: style.id }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
```
|