@live-change/flow-frontend 0.9.225 → 0.9.227

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.
Files changed (57) hide show
  1. package/.node-version +1 -0
  2. package/.nvmrc +1 -0
  3. package/README.md +33 -0
  4. package/e2e/calculation.test.ts +247 -0
  5. package/e2e/e2eSuite.ts +8 -0
  6. package/e2e/env.ts +86 -0
  7. package/e2e/kitchen-sink.test.ts +85 -0
  8. package/e2e/runner.ts +10 -0
  9. package/e2e/withBrowser.ts +5 -0
  10. package/front/components.d.ts +25 -0
  11. package/front/locales/en.js +27 -0
  12. package/front/locales/en.json +5 -0
  13. package/front/src/App.vue +38 -12
  14. package/front/src/NavBar.vue +31 -0
  15. package/front/src/components/Edge.vue +1 -1
  16. package/front/src/components/EdgeEndHandle.vue +1 -1
  17. package/front/src/components/Flow.vue +5 -4
  18. package/front/src/components/Node.vue +1 -1
  19. package/front/src/components/NodeHandle.vue +1 -1
  20. package/front/src/components/NodePort.vue +1 -2
  21. package/front/src/components/index.js +10 -10
  22. package/front/src/config.js +90 -0
  23. package/front/src/demo/DemoEdge.vue +38 -0
  24. package/front/src/demo/DemoNodeCard.vue +86 -0
  25. package/front/src/demo/DemoPort.vue +34 -0
  26. package/front/src/demo/EnrichNode.vue +37 -0
  27. package/front/src/demo/FlowKitchenSink.vue +165 -0
  28. package/front/src/demo/OutputNode.vue +45 -0
  29. package/front/src/demo/SourceNode.vue +49 -0
  30. package/front/src/demo/calculation/AddNode.vue +35 -0
  31. package/front/src/demo/calculation/ConstantNode.vue +61 -0
  32. package/front/src/demo/calculation/FlowCalculationDemo.vue +300 -0
  33. package/front/src/demo/calculation/MultiplyNode.vue +35 -0
  34. package/front/src/demo/calculation/PowerNode.vue +62 -0
  35. package/front/src/demo/flowTestId.js +3 -0
  36. package/front/src/demo/runDemoGraph.js +76 -0
  37. package/front/src/demo/seedGraph.js +38 -0
  38. package/front/src/entry-client.js +2 -2
  39. package/front/src/entry-server.js +2 -1
  40. package/front/src/router.js +20 -12
  41. package/package.json +44 -37
  42. package/server/app.config.js +61 -0
  43. package/server/calculation/calculation.js +20 -0
  44. package/server/calculation/config.js +15 -0
  45. package/server/calculation/definition.js +11 -0
  46. package/server/calculation/index.js +12 -0
  47. package/server/calculation/ops.js +69 -0
  48. package/server/calculation/run.js +69 -0
  49. package/server/calculation/runGraph.js +85 -0
  50. package/server/calculation/tests/run.test.js +73 -0
  51. package/server/calculation/triggers.js +95 -0
  52. package/server/calculation/wire.js +30 -0
  53. package/server/init.js +1 -6
  54. package/server/services.list.js +11 -0
  55. package/server/start.js +39 -0
  56. package/LICENSE.md +0 -11
  57. package/server/services.config.js +0 -26
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <DemoNodeCard :node="node" title="Source" icon="pi-database" @delete="deleteNode">
3
+ <div class="demo-node-section">Sample record</div>
4
+ <div class="flex items-center p-1">
5
+ <Textarea v-model="recordJson" class="w-full text-sm mx-2" rows="6" autoResize />
6
+ <DemoPort :x="1" :node="node" portId="out" />
7
+ </div>
8
+ <div v-if="jsonError" class="text-red-600 text-xs px-3 pb-1">{{ jsonError }}</div>
9
+ </DemoNodeCard>
10
+ </template>
11
+
12
+ <script setup>
13
+
14
+ import DemoNodeCard from "./DemoNodeCard.vue"
15
+ import DemoPort from "./DemoPort.vue"
16
+ import { useFlow } from "../components/index.js"
17
+
18
+ import { computed, defineProps, ref, toRefs } from "vue"
19
+
20
+ const props = defineProps({
21
+ node: {
22
+ type: Object,
23
+ required: true
24
+ }
25
+ })
26
+
27
+ const { node } = toRefs(props)
28
+ const flow = useFlow()
29
+ const jsonError = ref('')
30
+
31
+ const recordJson = computed({
32
+ get() {
33
+ return JSON.stringify(node.value.record ?? {}, null, 2)
34
+ },
35
+ set(value) {
36
+ try {
37
+ node.value.record = JSON.parse(value)
38
+ jsonError.value = ''
39
+ } catch (err) {
40
+ jsonError.value = 'Invalid JSON'
41
+ }
42
+ }
43
+ })
44
+
45
+ function deleteNode() {
46
+ flow.deleteNode(node.value)
47
+ }
48
+
49
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <DemoNodeCard :node="node" title="Add" icon="pi-plus" @delete="deleteNode"
3
+ :data-testid="`node-${node.logicType}`">
4
+ <div class="demo-node-section">Sum</div>
5
+ <div class="flex items-center p-1">
6
+ <DemoPort :x="-1" :node="node" portId="in" />
7
+ <div class="flex-1 mx-2 text-sm">N inputs</div>
8
+ <DemoPort :x="1" :node="node" portId="out" />
9
+ </div>
10
+ </DemoNodeCard>
11
+ </template>
12
+
13
+ <script setup>
14
+
15
+ import DemoNodeCard from "../DemoNodeCard.vue"
16
+ import DemoPort from "../DemoPort.vue"
17
+ import { useFlow } from "../../components/index.js"
18
+
19
+ import { defineProps, toRefs } from "vue"
20
+
21
+ const props = defineProps({
22
+ node: {
23
+ type: Object,
24
+ required: true
25
+ }
26
+ })
27
+
28
+ const { node } = toRefs(props)
29
+ const flow = useFlow()
30
+
31
+ function deleteNode() {
32
+ flow.deleteNode(node.value)
33
+ }
34
+
35
+ </script>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <DemoNodeCard :node="node" title="Constant" icon="pi-hashtag" @delete="deleteNode"
3
+ :data-testid="`node-${node.logicType}`">
4
+ <div class="demo-node-section">Value</div>
5
+ <div class="flex items-center p-1">
6
+ <div class="flex-1 mx-2">
7
+ <InputNumber v-if="editable" v-model="editable.value" class="w-full" :max-fraction-digits="6"
8
+ data-testid="constant-value" />
9
+ </div>
10
+ <DemoPort :x="1" :node="node" portId="out" />
11
+ </div>
12
+ </DemoNodeCard>
13
+ </template>
14
+
15
+ <script setup>
16
+
17
+ import DemoNodeCard from "../DemoNodeCard.vue"
18
+ import DemoPort from "../DemoPort.vue"
19
+ import { useFlow } from "../../components/index.js"
20
+ import { synchronized } from '@live-change/vue3-components'
21
+ import { useApi } from '@live-change/vue3-ssr'
22
+ import InputNumber from 'primevue/inputnumber'
23
+
24
+ import { computed, defineProps, toRefs } from "vue"
25
+
26
+ const props = defineProps({
27
+ node: {
28
+ type: Object,
29
+ required: true
30
+ },
31
+ constant: {
32
+ type: Object,
33
+ default: null
34
+ },
35
+ calculationId: {
36
+ type: String,
37
+ default: null
38
+ }
39
+ })
40
+
41
+ const { node } = toRefs(props)
42
+ const flow = useFlow()
43
+ const api = useApi()
44
+
45
+ const source = computed(() => props.constant)
46
+ const sync = synchronized({
47
+ source,
48
+ update: (params) => api.command(['calculation', 'updateConstant'], params),
49
+ identifiers: computed(() => ({
50
+ constant: node.value.logic,
51
+ calculation: props.calculationId || props.constant?.calculation
52
+ })),
53
+ recursive: true
54
+ })
55
+ const { value: editable } = sync
56
+
57
+ function deleteNode() {
58
+ flow.deleteNode(node.value)
59
+ }
60
+
61
+ </script>
@@ -0,0 +1,300 @@
1
+ <template>
2
+ <div class="w-full h-screen flex flex-col">
3
+ <NavBar />
4
+ <div class="bg-surface-0 dark:bg-surface-900 py-2 px-4 shadow flex items-center gap-2 z-10 flex-wrap">
5
+ <Button label="Add constant" icon="pi pi-plus" size="small" data-testid="add-constant"
6
+ :disabled="!graphId" @click="addOp('Constant')" />
7
+ <Button label="Add add" icon="pi pi-plus" size="small" severity="secondary" data-testid="add-add"
8
+ :disabled="!graphId" @click="addOp('Add')" />
9
+ <Button label="Add multiply" icon="pi pi-plus" size="small" severity="secondary"
10
+ data-testid="add-multiply" :disabled="!graphId" @click="addOp('Multiply')" />
11
+ <Button label="Add power" icon="pi pi-plus" size="small" severity="secondary" data-testid="add-power"
12
+ :disabled="!graphId" @click="addOp('Power')" />
13
+ <Button label="Run" icon="pi pi-play" size="small" severity="success" data-testid="run-calculation"
14
+ :disabled="!calculationId" @click="run" />
15
+ <span v-if="runError" class="text-red-600 text-sm ml-2">{{ runError }}</span>
16
+ <span v-if="calculationId" data-testid="calculation-id" :data-id="calculationId" class="hidden">
17
+ {{ calculationId }}
18
+ </span>
19
+ </div>
20
+ <div class="flex flex-1 min-h-0">
21
+ <Flow class="flex-1 grow">
22
+ <template #default>
23
+ <component
24
+ v-for="node of nodes"
25
+ :key="node.id"
26
+ :is="nodeComponent(node)"
27
+ :node="node"
28
+ :constant="constantById(node.logic)"
29
+ :power="powerById(node.logic)"
30
+ :calculation-id="calculationId"
31
+ class="absolute"
32
+ :style="{ left: (node.position?.x || 0) + 'px', top: (node.position?.y || 0) + 'px' }"
33
+ />
34
+ </template>
35
+ <template #foreground-edges>
36
+ <DemoEdge v-for="edge of canvasEdges" :key="edge.id" :edge="edge" />
37
+ </template>
38
+ <template #free-edge="{ freeEdge }">
39
+ <DemoEdge v-if="freeEdge" :edge="freeEdge" />
40
+ </template>
41
+ </Flow>
42
+ <aside class="w-80 bg-surface-100 dark:bg-surface-800 border-l border-gray-200 overflow-auto p-3">
43
+ <div class="font-semibold mb-2">Last run</div>
44
+ <pre class="text-xs m-0 whitespace-pre-wrap" data-testid="run-results">{{ lastRunText }}</pre>
45
+ </aside>
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup>
51
+
52
+ import Flow from '../../components/Flow.vue'
53
+ import { useFlow } from '../../components/index.js'
54
+ import NavBar from '../../NavBar.vue'
55
+ import DemoEdge from '../DemoEdge.vue'
56
+ import ConstantNode from './ConstantNode.vue'
57
+ import AddNode from './AddNode.vue'
58
+ import MultiplyNode from './MultiplyNode.vue'
59
+ import PowerNode from './PowerNode.vue'
60
+
61
+ import { computed, ref, watch } from 'vue'
62
+ import { useRoute, useRouter } from 'vue-router'
63
+ import { usePath, live, useApi, useClient } from '@live-change/vue3-ssr'
64
+ import { synchronizedList } from '@live-change/vue3-components'
65
+
66
+ const nodeComponentByType = {
67
+ calculation_Constant: ConstantNode,
68
+ calculation_Add: AddNode,
69
+ calculation_Multiply: MultiplyNode,
70
+ calculation_Power: PowerNode
71
+ }
72
+
73
+ const deleteActionByType = {
74
+ calculation_Constant: { name: 'deleteConstant', key: 'constant' },
75
+ calculation_Add: { name: 'deleteAdd', key: 'add' },
76
+ calculation_Multiply: { name: 'deleteMultiply', key: 'multiply' },
77
+ calculation_Power: { name: 'deletePower', key: 'power' }
78
+ }
79
+
80
+ const path = usePath()
81
+ const api = useApi()
82
+ const client = useClient()
83
+ const route = useRoute()
84
+ const router = useRouter()
85
+
86
+ const creating = ref(false)
87
+ const lastRun = ref(null)
88
+ const runError = ref('')
89
+ const nextNodeSlot = ref(0)
90
+
91
+ const calculations = await live(computed(() => client.value?.session
92
+ ? path.calculation.sessionOwnedCalculations({ session: client.value.session })
93
+ : null))
94
+
95
+ const calculationId = computed(() => route.params.calculation || calculations.value?.[0]?.id || null)
96
+
97
+ watch(calculationId, () => {
98
+ nextNodeSlot.value = 0
99
+ })
100
+
101
+ watch([calculations, () => client.value?.session, () => route.params.calculation], async () => {
102
+ if(typeof window == 'undefined') return
103
+ if(route.params.calculation) return
104
+ const session = client.value?.session
105
+ if(!session) return
106
+ const list = calculations.value || []
107
+ if(list.length) {
108
+ await router.replace({ name: 'calculationDetail', params: { calculation: list[0].id } })
109
+ return
110
+ }
111
+ if(creating.value) return
112
+ creating.value = true
113
+ try {
114
+ const id = await api.command(['calculation', 'createCalculation'], { session })
115
+ await router.replace({ name: 'calculationDetail', params: { calculation: id } })
116
+ } finally {
117
+ creating.value = false
118
+ }
119
+ }, { immediate: true })
120
+
121
+ const graph = await live(computed(() => calculationId.value
122
+ ? path.flow.graph({
123
+ ownerType: 'calculation_Calculation',
124
+ owner: calculationId.value
125
+ })
126
+ : null))
127
+
128
+ const graphId = computed(() => graph.value?.id || null)
129
+
130
+ function rowObjectId(row) {
131
+ return row?.to || row?.id
132
+ }
133
+
134
+ const nodeRows = await live(computed(() => graphId.value
135
+ ? path.flow.graphOwnedNodes({
136
+ graphType: 'flow_Graph',
137
+ graph: graphId.value
138
+ })
139
+ : null))
140
+
141
+ const edgeRows = await live(computed(() => graphId.value
142
+ ? path.flow.graphOwnedEdges({
143
+ graphType: 'flow_Graph',
144
+ graph: graphId.value
145
+ })
146
+ : null))
147
+
148
+ const constants = await live(computed(() => calculationId.value
149
+ ? path.calculation.calculationOwnedConstants({ calculation: calculationId.value })
150
+ : null))
151
+
152
+ const powers = await live(computed(() => calculationId.value
153
+ ? path.calculation.calculationOwnedPowers({ calculation: calculationId.value })
154
+ : null))
155
+
156
+ const nodeSource = computed(() => (nodeRows.value || []).map(row => ({
157
+ ...row,
158
+ id: rowObjectId(row)
159
+ })))
160
+ const syncNodes = synchronizedList({
161
+ source: nodeSource,
162
+ update: (params) => api.command(['flow', 'updateNode'], params),
163
+ objectIdentifiers: (object) => ({
164
+ graphType: object.graphType,
165
+ graph: object.graph,
166
+ logicType: object.logicType,
167
+ logic: object.logic
168
+ }),
169
+ recursive: true
170
+ })
171
+
172
+ const nodes = syncNodes.value
173
+
174
+ const canvasEdges = computed(() => (edgeRows.value || []).map(edge => ({
175
+ id: rowObjectId(edge),
176
+ src: { node: edge.source, port: edge.sourcePort },
177
+ dest: { node: edge.destination, port: edge.destinationPort },
178
+ connection: edge.connection
179
+ })))
180
+
181
+ function nodeById(nodeId) {
182
+ return nodes.value.find(node => node.id == nodeId)
183
+ }
184
+
185
+ function isConnectable(fromNode, fromPort, toNode, toPort) {
186
+ const fromId = typeof fromNode == 'string' ? fromNode : fromNode?.id
187
+ const toId = typeof toNode == 'string' ? toNode : toNode?.id
188
+ if(fromId && toId && fromId === toId) return false
189
+ if(!fromPort || !toPort) return true
190
+ return fromPort !== toPort
191
+ && (fromPort === 'in' || fromPort === 'out')
192
+ && (toPort === 'in' || toPort === 'out')
193
+ }
194
+
195
+ async function connect(newEdge) {
196
+ if(newEdge.src.port !== 'out' && newEdge.dest.port === 'out') {
197
+ const tmp = newEdge.src
198
+ newEdge.src = newEdge.dest
199
+ newEdge.dest = tmp
200
+ }
201
+ const srcNode = nodeById(newEdge.src.node)
202
+ const destNode = nodeById(newEdge.dest.node)
203
+ if(!srcNode || !destNode || !graphId.value) return
204
+ const wireId = await api.command(['calculation', 'createWire'], {
205
+ sourceType: srcNode.logicType,
206
+ source: srcNode.logic,
207
+ destinationType: destNode.logicType,
208
+ destination: destNode.logic,
209
+ sourcePort: newEdge.src.port,
210
+ destinationPort: newEdge.dest.port
211
+ })
212
+ await api.command(['flow', 'createEdge'], {
213
+ graphType: 'flow_Graph',
214
+ graph: graphId.value,
215
+ sourceType: 'flow_Node',
216
+ source: newEdge.src.node,
217
+ destinationType: 'flow_Node',
218
+ destination: newEdge.dest.node,
219
+ connectionType: 'calculation_Wire',
220
+ connection: wireId,
221
+ sourcePort: newEdge.src.port,
222
+ destinationPort: newEdge.dest.port
223
+ })
224
+ }
225
+
226
+ async function deleteLogicNode(node) {
227
+ const spec = deleteActionByType[node.logicType]
228
+ if(!spec) return
229
+ await api.command(['calculation', spec.name], { [spec.key]: node.logic })
230
+ }
231
+
232
+ async function deleteWireEdge(edge) {
233
+ if(!edge.connection) return
234
+ await api.command(['calculation', 'deleteWire'], { wire: edge.connection })
235
+ }
236
+
237
+ const flow = useFlow({
238
+ nodes,
239
+ edges: canvasEdges,
240
+ width: 1600,
241
+ height: 900,
242
+ edgeConnectDistance: 48,
243
+ isConnectable,
244
+ connect,
245
+ deleteNode: deleteLogicNode,
246
+ deleteEdge: deleteWireEdge
247
+ })
248
+
249
+ function nodeComponent(node) {
250
+ return nodeComponentByType[node.logicType]
251
+ }
252
+
253
+ function constantById(id) {
254
+ const row = (constants.value || []).find(row => rowObjectId(row) == id || row.id == id)
255
+ if(!row) return null
256
+ return { ...row, id: rowObjectId(row) }
257
+ }
258
+
259
+ function powerById(id) {
260
+ const row = (powers.value || []).find(row => rowObjectId(row) == id || row.id == id)
261
+ if(!row) return null
262
+ return { ...row, id: rowObjectId(row) }
263
+ }
264
+
265
+ async function addOp(kind) {
266
+ if(!calculationId.value || !graphId.value) return
267
+ const createName = 'create' + kind
268
+ const logicType = 'calculation_' + kind
269
+ const logic = await api.command(['calculation', createName], {
270
+ calculation: calculationId.value
271
+ })
272
+ const slot = nextNodeSlot.value
273
+ nextNodeSlot.value = slot + 1
274
+ await api.command(['flow', 'setNode'], {
275
+ graphType: 'flow_Graph',
276
+ graph: graphId.value,
277
+ logicType,
278
+ logic,
279
+ position: {
280
+ x: 80 + (slot % 4) * 360,
281
+ y: 80 + Math.floor(slot / 4) * 260
282
+ }
283
+ })
284
+ }
285
+
286
+ async function run() {
287
+ if(!calculationId.value) return
288
+ const result = await api.command(['calculation', 'runCalculation'], {
289
+ calculation: calculationId.value
290
+ })
291
+ lastRun.value = result
292
+ runError.value = result?.error || ''
293
+ }
294
+
295
+ const lastRunText = computed(() => {
296
+ if(!lastRun.value) return 'Press Run to execute the calculation graph.'
297
+ return JSON.stringify(lastRun.value, null, 2)
298
+ })
299
+
300
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <DemoNodeCard :node="node" title="Multiply" icon="pi-times" @delete="deleteNode"
3
+ :data-testid="`node-${node.logicType}`">
4
+ <div class="demo-node-section">Product</div>
5
+ <div class="flex items-center p-1">
6
+ <DemoPort :x="-1" :node="node" portId="in" />
7
+ <div class="flex-1 mx-2 text-sm">N inputs</div>
8
+ <DemoPort :x="1" :node="node" portId="out" />
9
+ </div>
10
+ </DemoNodeCard>
11
+ </template>
12
+
13
+ <script setup>
14
+
15
+ import DemoNodeCard from "../DemoNodeCard.vue"
16
+ import DemoPort from "../DemoPort.vue"
17
+ import { useFlow } from "../../components/index.js"
18
+
19
+ import { defineProps, toRefs } from "vue"
20
+
21
+ const props = defineProps({
22
+ node: {
23
+ type: Object,
24
+ required: true
25
+ }
26
+ })
27
+
28
+ const { node } = toRefs(props)
29
+ const flow = useFlow()
30
+
31
+ function deleteNode() {
32
+ flow.deleteNode(node.value)
33
+ }
34
+
35
+ </script>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <DemoNodeCard :node="node" title="Power" icon="pi-sort-amount-up" @delete="deleteNode"
3
+ :data-testid="`node-${node.logicType}`">
4
+ <div class="demo-node-section">Exponent</div>
5
+ <div class="flex items-center p-1">
6
+ <DemoPort :x="-1" :node="node" portId="in" />
7
+ <div class="flex-1 mx-2">
8
+ <InputNumber v-if="editable" v-model="editable.exponent" class="w-full" :max-fraction-digits="6"
9
+ data-testid="power-exponent" />
10
+ </div>
11
+ <DemoPort :x="1" :node="node" portId="out" />
12
+ </div>
13
+ </DemoNodeCard>
14
+ </template>
15
+
16
+ <script setup>
17
+
18
+ import DemoNodeCard from "../DemoNodeCard.vue"
19
+ import DemoPort from "../DemoPort.vue"
20
+ import { useFlow } from "../../components/index.js"
21
+ import { synchronized } from '@live-change/vue3-components'
22
+ import { useApi } from '@live-change/vue3-ssr'
23
+ import InputNumber from 'primevue/inputnumber'
24
+
25
+ import { computed, defineProps, toRefs } from "vue"
26
+
27
+ const props = defineProps({
28
+ node: {
29
+ type: Object,
30
+ required: true
31
+ },
32
+ power: {
33
+ type: Object,
34
+ default: null
35
+ },
36
+ calculationId: {
37
+ type: String,
38
+ default: null
39
+ }
40
+ })
41
+
42
+ const { node } = toRefs(props)
43
+ const flow = useFlow()
44
+ const api = useApi()
45
+
46
+ const source = computed(() => props.power)
47
+ const sync = synchronized({
48
+ source,
49
+ update: (params) => api.command(['calculation', 'updatePower'], params),
50
+ identifiers: computed(() => ({
51
+ power: node.value.logic,
52
+ calculation: props.calculationId || props.power?.calculation
53
+ })),
54
+ recursive: true
55
+ })
56
+ const { value: editable } = sync
57
+
58
+ function deleteNode() {
59
+ flow.deleteNode(node.value)
60
+ }
61
+
62
+ </script>
@@ -0,0 +1,3 @@
1
+ export function flowTestId(value) {
2
+ return String(value ?? '').replace(/[^A-Za-z0-9._-]+/g, '_')
3
+ }
@@ -0,0 +1,76 @@
1
+ function portKind(portId) {
2
+ if (typeof portId != 'string') return null
3
+ if (portId === 'in' || portId.endsWith('/in')) return 'in'
4
+ if (portId === 'out' || portId.endsWith('/out')) return 'out'
5
+ return null
6
+ }
7
+
8
+ function normalizeLink(edge) {
9
+ const srcKind = portKind(edge.src?.port)
10
+ const destKind = portKind(edge.dest?.port)
11
+ if (srcKind === 'out' && destKind === 'in') {
12
+ return { from: edge.src.node, to: edge.dest.node }
13
+ }
14
+ if (srcKind === 'in' && destKind === 'out') {
15
+ return { from: edge.dest.node, to: edge.src.node }
16
+ }
17
+ return null
18
+ }
19
+
20
+ function executeNode(node, inputRecords) {
21
+ if (node.type === 'source') {
22
+ return { ...(node.record || {}) }
23
+ }
24
+ const base = Object.assign({}, ...inputRecords)
25
+ if (node.type === 'enrich') {
26
+ const field = node.field || 'field'
27
+ return { ...base, [field]: node.value }
28
+ }
29
+ return base
30
+ }
31
+
32
+ export function runDemoGraph(nodes, edges) {
33
+ const nodeById = new Map(nodes.map(node => [node.id, node]))
34
+ const incoming = new Map(nodes.map(node => [node.id, []]))
35
+ const outgoing = new Map(nodes.map(node => [node.id, []]))
36
+
37
+ for (const edge of edges) {
38
+ const link = normalizeLink(edge)
39
+ if (!link) continue
40
+ if (!nodeById.has(link.from) || !nodeById.has(link.to)) continue
41
+ incoming.get(link.to).push(link.from)
42
+ outgoing.get(link.from).push(link.to)
43
+ }
44
+
45
+ const indegree = new Map(nodes.map(node => [node.id, incoming.get(node.id).length]))
46
+ const queue = nodes.filter(node => indegree.get(node.id) === 0).map(node => node.id)
47
+ const order = []
48
+ while (queue.length) {
49
+ const id = queue.shift()
50
+ order.push(id)
51
+ for (const nextId of outgoing.get(id)) {
52
+ indegree.set(nextId, indegree.get(nextId) - 1)
53
+ if (indegree.get(nextId) === 0) queue.push(nextId)
54
+ }
55
+ }
56
+
57
+ if (order.length !== nodes.length) {
58
+ return { error: 'Cycle in the graph', traces: [], outputs: [] }
59
+ }
60
+
61
+ const values = new Map()
62
+ const traces = []
63
+ for (const id of order) {
64
+ const node = nodeById.get(id)
65
+ const inputRecords = incoming.get(id).map(fromId => values.get(fromId)).filter(Boolean)
66
+ const record = executeNode(node, inputRecords)
67
+ values.set(id, record)
68
+ traces.push({ nodeId: id, type: node.type, record })
69
+ }
70
+
71
+ const outputs = nodes
72
+ .filter(node => node.type === 'output')
73
+ .map(node => ({ id: node.id, record: values.get(node.id) }))
74
+
75
+ return { traces, outputs }
76
+ }
@@ -0,0 +1,38 @@
1
+ export const STORAGE_KEY = 'flow-frontend-demo-graph'
2
+
3
+ export function createSeedGraph() {
4
+ return {
5
+ nodes: [
6
+ {
7
+ id: 'source',
8
+ type: 'source',
9
+ position: { x: 80, y: 140 },
10
+ record: { sku: 'ABC-12', qty: 2, nip: '1234567890' }
11
+ },
12
+ {
13
+ id: 'enrich',
14
+ type: 'enrich',
15
+ position: { x: 420, y: 140 },
16
+ field: 'vatRate',
17
+ value: 23
18
+ },
19
+ {
20
+ id: 'output',
21
+ type: 'output',
22
+ position: { x: 760, y: 140 }
23
+ }
24
+ ],
25
+ edges: [
26
+ {
27
+ id: 'e-source-enrich',
28
+ src: { node: 'source', port: 'out' },
29
+ dest: { node: 'enrich', port: 'in' }
30
+ },
31
+ {
32
+ id: 'e-enrich-output',
33
+ src: { node: 'enrich', port: 'out' },
34
+ dest: { node: 'output', port: 'in' }
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -1,6 +1,6 @@
1
1
  import { clientEntry } from '@live-change/frontend-base/client-entry.js'
2
2
  import App from './App.vue'
3
3
  import { createRouter } from './router'
4
+ import config from './config.js'
4
5
 
5
-
6
- clientEntry(App, createRouter)
6
+ clientEntry(App, createRouter, config)
@@ -1,6 +1,7 @@
1
1
  import { serverEntry } from '@live-change/frontend-base/server-entry.js'
2
2
  import App from './App.vue'
3
3
  import { createRouter } from './router'
4
+ import config from './config.js'
4
5
 
5
- const render = serverEntry(App, createRouter)
6
+ const render = serverEntry(App, createRouter, config)
6
7
  export { render }