@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
@@ -6,19 +6,29 @@ import {
6
6
 
7
7
  import { dbAdminRoutes } from "@live-change/db-admin"
8
8
 
9
- export function wysiwygRoutes(config = {}) {
9
+ export function flowRoutes(config = {}) {
10
10
  const { prefix = '/', route = (r) => r } = config
11
11
  return [
12
-
13
- /*route({
14
- name: 'wysiwyg:editorWithPreview', path: prefix + '', meta: { },
15
- component: () => import("./EditorWithPreview.vue"),
16
- props: {
17
- }
18
- }),*/
12
+ route({
13
+ name: 'index',
14
+ path: prefix,
15
+ meta: { raw: true },
16
+ component: () => import("./demo/FlowKitchenSink.vue")
17
+ }),
18
+ route({
19
+ name: 'calculation',
20
+ path: prefix + 'calc',
21
+ meta: { raw: true },
22
+ component: () => import("./demo/calculation/FlowCalculationDemo.vue")
23
+ }),
24
+ route({
25
+ name: 'calculationDetail',
26
+ path: prefix + 'calc/:calculation',
27
+ meta: { raw: true },
28
+ component: () => import("./demo/calculation/FlowCalculationDemo.vue")
29
+ }),
19
30
 
20
31
  ...dbAdminRoutes({ prefix: '/_db', route: r => ({ ...r, meta: { ...r.meta, raw: true }}) })
21
-
22
32
  ]
23
33
  }
24
34
 
@@ -26,12 +36,10 @@ export async function sitemap(route, api) {
26
36
 
27
37
  }
28
38
 
29
-
30
39
  export function createRouter(app, config) {
31
40
  const router = _createRouter({
32
41
  history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
33
- routes: wysiwygRoutes(config)
42
+ routes: flowRoutes(config)
34
43
  })
35
44
  return router
36
45
  }
37
-
package/package.json CHANGED
@@ -1,46 +1,58 @@
1
1
  {
2
2
  "name": "@live-change/flow-frontend",
3
- "version": "0.9.225",
3
+ "version": "0.9.227",
4
4
  "scripts": {
5
- "memDev": "node server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
- "localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
7
- "localDev": "node server/start.js localDev --enableSessions --dbAccess",
8
- "dev": "node server/start.js dev --enableSessions",
9
- "ssrDev": "node server/start.js ssrDev --enableSessions",
10
- "serveAllMem": "cross-env NODE_ENV=production lcli ssrServer --withApi --withServices --updateServices --enableSessions --withDb --dbBackend mem --createDb",
11
- "serveAll": "cross-env NODE_ENV=production lcli ssrServer --withApi --withServices --updateServices --enableSessions",
12
- "serve": "cross-env NODE_ENV=production lcli ssrServer --enableSessions",
13
- "apiServer": "node server/start.js apiServer --enableSessions",
14
- "devApiServer": "node server/start.js devApiServer --enableSessions",
15
- "memApiServer": "node server/start.js memApiServer --enableSessions",
5
+ "memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
+ "localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
7
+ "localDev": "tsx server/start.js localDev --enableSessions --dbAccess",
8
+ "dev": "tsx --inspect --expose-gc server/start.js dev --enableSessions",
9
+ "ssrDev": "tsx server/start.js ssrDev --enableSessions",
10
+ "serveAllMem": "cross-env NODE_ENV=production node dist/server/start.js ssrServer --withApi --withServices --updateServices --enableSessions --withDb --dbBackend mem --createDb",
11
+ "serveAll": "cross-env NODE_ENV=production node dist/server/start.js ssrServer --withApi --withServices --updateServices --enableSessions",
12
+ "serve": "cross-env NODE_ENV=production node dist/server/start.js ssrServer --enableSessions",
13
+ "apiServer": "node dist/server/start.js apiServer --enableSessions",
14
+ "devApiServer": "tsx server/start.js devApiServer --enableSessions",
15
+ "memApiServer": "tsx server/start.js memApiServer --enableSessions",
16
16
  "build": "cd front; yarn build:client && yarn build:server",
17
17
  "build:client": "cd front; vite build --ssrManifest --outDir dist/client",
18
18
  "build:server": "cd front; vite build --ssr src/entry-server.js --outDir dist/server",
19
19
  "generate": "vite build --ssrManifest --outDir dist/static && yarn build:server && node prerender",
20
- "debug": "node --inspect-brk server"
20
+ "debug": "node --inspect-brk server",
21
+ "describe": "tsx server/start.js describe",
22
+ "test": "NODE_ENV=test node --import tsx --test --test-concurrency=1 server/calculation/tests/*.test.js",
23
+ "e2e": "fnm exec -- node --import tsx e2e/runner.ts",
24
+ "e2e:file": "fnm exec -- node --import tsx e2e/runner.ts",
25
+ "e2e:headed": "SHOW_BROWSER=1 fnm exec -- node --import tsx e2e/runner.ts"
21
26
  },
22
27
  "type": "module",
23
28
  "dependencies": {
24
- "@live-change/access-control-service": "^0.9.225",
25
- "@live-change/cli": "^0.9.225",
26
- "@live-change/dao": "^0.9.225",
27
- "@live-change/dao-vue3": "^0.9.225",
28
- "@live-change/dao-websocket": "^0.9.225",
29
- "@live-change/db-admin": "^0.9.225",
30
- "@live-change/framework": "^0.9.225",
31
- "@live-change/frontend-base": "^0.9.225",
32
- "@live-change/password-authentication-service": "^0.9.225",
33
- "@live-change/secret-code-service": "^0.9.225",
34
- "@live-change/secret-link-service": "^0.9.225",
35
- "@live-change/session-service": "^0.9.225",
36
- "@live-change/user-frontend": "0.9.221",
37
- "@live-change/user-service": "^0.9.225",
38
- "@live-change/vue3-components": "^0.9.225",
39
- "@live-change/vue3-ssr": "^0.9.225",
29
+ "@live-change/access-control-frontend": "^0.9.227",
30
+ "@live-change/access-control-service": "^0.9.227",
31
+ "@live-change/cli": "^0.9.227",
32
+ "@live-change/dao": "^0.9.227",
33
+ "@live-change/dao-vue3": "^0.9.227",
34
+ "@live-change/dao-websocket": "^0.9.227",
35
+ "@live-change/db-admin": "^0.9.227",
36
+ "@live-change/e2e-test": "^0.9.227",
37
+ "@live-change/flow-service": "^0.9.227",
38
+ "@live-change/framework": "^0.9.227",
39
+ "@live-change/frontend-base": "^0.9.227",
40
+ "@live-change/password-authentication-service": "^0.9.227",
41
+ "@live-change/relations-plugin": "^0.9.227",
42
+ "@live-change/secret-code-service": "^0.9.227",
43
+ "@live-change/secret-link-service": "^0.9.227",
44
+ "@live-change/server": "^0.9.227",
45
+ "@live-change/session-service": "^0.9.227",
46
+ "@live-change/user-frontend": "^0.9.227",
47
+ "@live-change/user-service": "^0.9.227",
48
+ "@live-change/vue3-components": "^0.9.227",
49
+ "@live-change/vue3-ssr": "^0.9.227",
50
+ "@primeuix/themes": "^1.2.1",
40
51
  "@vueuse/core": "^12.3.0",
41
- "codeceptjs-assert": "^0.0.5",
42
52
  "compression": "^1.7.5",
43
53
  "cross-env": "^7.0.3",
54
+ "deepmerge": "^4.2.2",
55
+ "dotenv": "^17.2.1",
44
56
  "get-port-sync": "1.0.1",
45
57
  "primeflex": "^3.3.1",
46
58
  "primeicons": "^7.0.0",
@@ -53,16 +65,11 @@
53
65
  "vue3-scroll-border": "0.1.7"
54
66
  },
55
67
  "devDependencies": {
56
- "@live-change/codeceptjs-helper": "^0.9.225",
57
- "codeceptjs": "^3.7.6",
58
- "generate-password": "^1.7.1",
59
68
  "playwright": "=1.50.1",
60
- "random-profile-generator": "^2.3.0",
61
- "txtgen": "^3.0.7",
62
- "webdriverio": "^9.5.1"
69
+ "tsx": "^4.21.0"
63
70
  },
64
71
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
65
72
  "license": "BSD-3-Clause",
66
73
  "description": "",
67
- "gitHead": "e44dc8b9eefd8b67c40a29b3117781662093c922"
74
+ "gitHead": "7d4dc92dcb9a50381292dded7d8766a3cadbecf9"
68
75
  }
@@ -0,0 +1,61 @@
1
+ import dotenv from 'dotenv'
2
+ dotenv.config()
3
+
4
+ import App from '@live-change/framework'
5
+ const app = App.app()
6
+
7
+ import { fileURLToPath } from 'url'
8
+ import { dirname, join } from 'path'
9
+ import { accessSync, readFileSync } from 'fs'
10
+
11
+ const packageJsonPath = dirname(fileURLToPath(import.meta.url))
12
+ .split('/').map((part, i, arr) =>
13
+ join(arr.slice(0, arr.length - i).join('/'), 'package.json')
14
+ ).find(p => { try { accessSync(p); return true } catch (e) { return false } })
15
+ const packageJson = packageJsonPath ? JSON.parse(readFileSync(packageJsonPath, 'utf-8')) : {}
16
+
17
+ const name = packageJson.name ?? 'flow-frontend'
18
+ const brandName = process.env.BRAND_NAME || 'Flow editor'
19
+ const homepage = process.env.BASE_HREF ?? packageJson.homepage
20
+ const brandDomain = process.env.BRAND_DOMAIN ||
21
+ (homepage && homepage.match(/https:\/\/([^/]+)/)?.[1]) || 'example.com'
22
+ const baseHref = process.env.BASE_HREF || homepage || 'http://localhost:8001'
23
+ const version = process.env.VERSION || packageJson.version
24
+
25
+ const clientConfig = {
26
+ version,
27
+ name,
28
+ brandName,
29
+ brandDomain,
30
+ homepage,
31
+ baseHref
32
+ }
33
+
34
+ app.config = {
35
+ ...clientConfig,
36
+ clientConfig: { ...clientConfig },
37
+ services: [
38
+ {
39
+ name: 'session',
40
+ path: '@live-change/session-service',
41
+ createSessionOnUpdate: true
42
+ },
43
+ {
44
+ name: 'flow',
45
+ path: '@live-change/flow-service',
46
+ ownerTypes: ['calculation_Calculation'],
47
+ logicTypes: [
48
+ 'calculation_Constant',
49
+ 'calculation_Add',
50
+ 'calculation_Multiply',
51
+ 'calculation_Power'
52
+ ],
53
+ connectionTypes: ['calculation_Wire']
54
+ },
55
+ {
56
+ name: 'calculation'
57
+ }
58
+ ]
59
+ }
60
+
61
+ export default app.config
@@ -0,0 +1,20 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+
4
+ import definition from './definition.js'
5
+ import config from './config.js'
6
+
7
+ const Session = definition.foreignModel('session', 'Session')
8
+
9
+ export const Calculation = definition.model({
10
+ name: 'Calculation',
11
+ itemOf: {
12
+ what: Session,
13
+ readAccess: config.readAccess,
14
+ writeAccess: config.writeAccess,
15
+ createAccess: config.writeAccess,
16
+ updateAccess: config.writeAccess,
17
+ deleteAccess: config.writeAccess
18
+ },
19
+ properties: {}
20
+ })
@@ -0,0 +1,15 @@
1
+ import definition from './definition.js'
2
+
3
+ const open = () => true
4
+
5
+ const {
6
+ readAccess = open,
7
+ writeAccess = open
8
+ } = definition.config || {}
9
+
10
+ definition.clientConfig = {}
11
+
12
+ export default {
13
+ readAccess,
14
+ writeAccess
15
+ }
@@ -0,0 +1,11 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+
4
+ import relationsPlugin from '@live-change/relations-plugin'
5
+
6
+ const definition = app.createServiceDefinition({
7
+ name: 'calculation',
8
+ use: [relationsPlugin]
9
+ })
10
+
11
+ export default definition
@@ -0,0 +1,12 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+
4
+ import definition from './definition.js'
5
+
6
+ import './calculation.js'
7
+ import './ops.js'
8
+ import './wire.js'
9
+ import './run.js'
10
+ import './triggers.js'
11
+
12
+ export default definition
@@ -0,0 +1,69 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+
4
+ import definition from './definition.js'
5
+ import config from './config.js'
6
+ import { Calculation } from './calculation.js'
7
+
8
+ const access = {
9
+ readAccess: config.readAccess,
10
+ writeAccess: config.writeAccess,
11
+ createAccess: config.writeAccess,
12
+ updateAccess: config.writeAccess,
13
+ deleteAccess: config.writeAccess
14
+ }
15
+
16
+ export const Constant = definition.model({
17
+ name: 'Constant',
18
+ itemOf: {
19
+ what: Calculation,
20
+ ...access
21
+ },
22
+ properties: {
23
+ value: {
24
+ type: Number,
25
+ default: 1,
26
+ validation: ['number']
27
+ }
28
+ }
29
+ })
30
+
31
+ export const Add = definition.model({
32
+ name: 'Add',
33
+ itemOf: {
34
+ what: Calculation,
35
+ ...access
36
+ },
37
+ properties: {}
38
+ })
39
+
40
+ export const Multiply = definition.model({
41
+ name: 'Multiply',
42
+ itemOf: {
43
+ what: Calculation,
44
+ ...access
45
+ },
46
+ properties: {}
47
+ })
48
+
49
+ export const Power = definition.model({
50
+ name: 'Power',
51
+ itemOf: {
52
+ what: Calculation,
53
+ ...access
54
+ },
55
+ properties: {
56
+ exponent: {
57
+ type: Number,
58
+ default: 2,
59
+ validation: ['number']
60
+ }
61
+ }
62
+ })
63
+
64
+ export const OP_TYPES = [
65
+ 'calculation_Constant',
66
+ 'calculation_Add',
67
+ 'calculation_Multiply',
68
+ 'calculation_Power'
69
+ ]
@@ -0,0 +1,69 @@
1
+ import definition from './definition.js'
2
+ import config from './config.js'
3
+ import { Calculation } from './calculation.js'
4
+ import { runCalculationGraph } from './runGraph.js'
5
+
6
+ function rowId(row) {
7
+ return row?.to || row?.id
8
+ }
9
+
10
+ async function loadOwned(model, calculation) {
11
+ const rows = await model.sortedIndexRangeGet('byCalculation', [calculation])
12
+ const objects = []
13
+ for(const row of rows || []) {
14
+ const id = rowId(row)
15
+ const object = await model.get(id)
16
+ objects.push(object || row)
17
+ }
18
+ return objects
19
+ }
20
+
21
+ function opTypeName(kind) {
22
+ return {
23
+ constant: 'calculation_Constant',
24
+ add: 'calculation_Add',
25
+ multiply: 'calculation_Multiply',
26
+ power: 'calculation_Power'
27
+ }[kind]
28
+ }
29
+
30
+ definition.action({
31
+ name: 'runCalculation',
32
+ properties: {
33
+ calculation: {
34
+ type: Calculation,
35
+ validation: ['nonEmpty']
36
+ }
37
+ },
38
+ access: config.readAccess,
39
+ async execute({ calculation }, { service }) {
40
+ const constants = await loadOwned(service.models.Constant, calculation)
41
+ const adds = await loadOwned(service.models.Add, calculation)
42
+ const multiplies = await loadOwned(service.models.Multiply, calculation)
43
+ const powers = await loadOwned(service.models.Power, calculation)
44
+
45
+ const ops = [
46
+ ...constants.map(row => ({ type: opTypeName('constant'), id: row.id })),
47
+ ...adds.map(row => ({ type: opTypeName('add'), id: row.id })),
48
+ ...multiplies.map(row => ({ type: opTypeName('multiply'), id: row.id })),
49
+ ...powers.map(row => ({ type: opTypeName('power'), id: row.id }))
50
+ ]
51
+
52
+ const seen = new Set()
53
+ const wires = []
54
+ for(const op of ops) {
55
+ const rows = await service.models.Wire.sortedIndexRangeGet('bySource', [op.type, op.id])
56
+ for(const row of rows || []) {
57
+ const id = rowId(row)
58
+ if(seen.has(id)) continue
59
+ seen.add(id)
60
+ const wire = (await service.models.Wire.get(id)) || row
61
+ wires.push(wire)
62
+ }
63
+ }
64
+
65
+ return runCalculationGraph({ constants, adds, multiplies, powers, wires })
66
+ }
67
+ })
68
+
69
+ export { runCalculationGraph }
@@ -0,0 +1,85 @@
1
+ function rowId(row) {
2
+ return row?.to || row?.id
3
+ }
4
+
5
+ export function runCalculationGraph({
6
+ constants = [],
7
+ adds = [],
8
+ multiplies = [],
9
+ powers = [],
10
+ wires = []
11
+ } = {}) {
12
+ const nodes = new Map()
13
+ for(const constant of constants) {
14
+ nodes.set(rowId(constant), {
15
+ kind: 'constant',
16
+ value: Number(constant.value ?? 1)
17
+ })
18
+ }
19
+ for(const add of adds) {
20
+ nodes.set(rowId(add), { kind: 'add' })
21
+ }
22
+ for(const multiply of multiplies) {
23
+ nodes.set(rowId(multiply), { kind: 'multiply' })
24
+ }
25
+ for(const power of powers) {
26
+ nodes.set(rowId(power), {
27
+ kind: 'power',
28
+ exponent: Number(power.exponent ?? 2)
29
+ })
30
+ }
31
+
32
+ const incoming = new Map()
33
+ const outgoing = new Map()
34
+ for(const id of nodes.keys()) {
35
+ incoming.set(id, [])
36
+ outgoing.set(id, [])
37
+ }
38
+ for(const wire of wires) {
39
+ const source = wire.source
40
+ const destination = wire.destination
41
+ if(!nodes.has(source) || !nodes.has(destination)) continue
42
+ if((wire.destinationPort || 'in') != 'in') continue
43
+ incoming.get(destination).push(source)
44
+ outgoing.get(source).push(destination)
45
+ }
46
+
47
+ const indegree = new Map()
48
+ for(const [id, sources] of incoming) {
49
+ indegree.set(id, sources.length)
50
+ }
51
+ const queue = [...nodes.keys()].filter(id => indegree.get(id) == 0)
52
+ const order = []
53
+ while(queue.length) {
54
+ const id = queue.shift()
55
+ order.push(id)
56
+ for(const next of outgoing.get(id)) {
57
+ indegree.set(next, indegree.get(next) - 1)
58
+ if(indegree.get(next) == 0) queue.push(next)
59
+ }
60
+ }
61
+
62
+ if(order.length != nodes.size) {
63
+ return { values: {}, error: 'Cycle in the graph' }
64
+ }
65
+
66
+ const values = {}
67
+ for(const id of order) {
68
+ const node = nodes.get(id)
69
+ const inputs = incoming.get(id).map(sourceId => values[sourceId])
70
+ if(node.kind == 'constant') {
71
+ values[id] = node.value
72
+ } else if(node.kind == 'add') {
73
+ values[id] = inputs.reduce((sum, value) => sum + value, 0)
74
+ } else if(node.kind == 'multiply') {
75
+ values[id] = inputs.length ? inputs.reduce((product, value) => product * value, 1) : 1
76
+ } else if(node.kind == 'power') {
77
+ if(!inputs.length) {
78
+ return { values, error: 'Power is missing an input' }
79
+ }
80
+ values[id] = inputs[0] ** node.exponent
81
+ }
82
+ }
83
+
84
+ return { values }
85
+ }
@@ -0,0 +1,73 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { runCalculationGraph } from '../runGraph.js'
4
+
5
+ test('constant returns its value', () => {
6
+ const result = runCalculationGraph({
7
+ constants: [{ id: 'c', value: 4 }]
8
+ })
9
+ assert.equal(result.error, undefined)
10
+ assert.equal(result.values.c, 4)
11
+ })
12
+
13
+ test('add sums inbound values', () => {
14
+ const result = runCalculationGraph({
15
+ constants: [{ id: 'a', value: 2 }, { id: 'b', value: 5 }],
16
+ adds: [{ id: 'sum' }],
17
+ wires: [
18
+ { source: 'a', destination: 'sum', sourcePort: 'out', destinationPort: 'in' },
19
+ { source: 'b', destination: 'sum', sourcePort: 'out', destinationPort: 'in' }
20
+ ]
21
+ })
22
+ assert.equal(result.values.sum, 7)
23
+ })
24
+
25
+ test('empty add is 0 and empty multiply is 1', () => {
26
+ const result = runCalculationGraph({
27
+ adds: [{ id: 'sum' }],
28
+ multiplies: [{ id: 'prod' }]
29
+ })
30
+ assert.equal(result.values.sum, 0)
31
+ assert.equal(result.values.prod, 1)
32
+ })
33
+
34
+ test('multiply products inbound values', () => {
35
+ const result = runCalculationGraph({
36
+ constants: [{ id: 'a', value: 3 }, { id: 'b', value: 4 }],
37
+ multiplies: [{ id: 'prod' }],
38
+ wires: [
39
+ { source: 'a', destination: 'prod', sourcePort: 'out', destinationPort: 'in' },
40
+ { source: 'b', destination: 'prod', sourcePort: 'out', destinationPort: 'in' }
41
+ ]
42
+ })
43
+ assert.equal(result.values.prod, 12)
44
+ })
45
+
46
+ test('power raises the inbound base to exponent', () => {
47
+ const result = runCalculationGraph({
48
+ constants: [{ id: 'base', value: 3 }],
49
+ powers: [{ id: 'pow', exponent: 2 }],
50
+ wires: [
51
+ { source: 'base', destination: 'pow', sourcePort: 'out', destinationPort: 'in' }
52
+ ]
53
+ })
54
+ assert.equal(result.values.pow, 9)
55
+ })
56
+
57
+ test('cycle returns an error', () => {
58
+ const result = runCalculationGraph({
59
+ adds: [{ id: 'a' }, { id: 'b' }],
60
+ wires: [
61
+ { source: 'a', destination: 'b', sourcePort: 'out', destinationPort: 'in' },
62
+ { source: 'b', destination: 'a', sourcePort: 'out', destinationPort: 'in' }
63
+ ]
64
+ })
65
+ assert.ok(result.error)
66
+ })
67
+
68
+ test('power without input returns an error', () => {
69
+ const result = runCalculationGraph({
70
+ powers: [{ id: 'pow', exponent: 2 }]
71
+ })
72
+ assert.ok(result.error)
73
+ })
@@ -0,0 +1,95 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+
4
+ import definition from './definition.js'
5
+
6
+ function payloadFromTrigger(trig) {
7
+ const body = trig?.data && typeof trig.data == 'object' ? trig.data : trig
8
+ return {
9
+ identifiers: body.identifiers || body,
10
+ data: body.data,
11
+ object: body.object,
12
+ changeType: body.changeType
13
+ }
14
+ }
15
+
16
+ function wireQueueKey(trig) {
17
+ const { identifiers, data } = payloadFromTrigger(trig)
18
+ return JSON.stringify([
19
+ identifiers.sourceType,
20
+ identifiers.source,
21
+ identifiers.destinationType,
22
+ identifiers.destination,
23
+ data?.sourcePort,
24
+ data?.destinationPort
25
+ ])
26
+ }
27
+
28
+ definition.trigger({
29
+ name: 'createCalculation_Calculation',
30
+ properties: {
31
+ object: { type: String },
32
+ identifiers: { type: Object },
33
+ data: { type: Object },
34
+ changeType: { type: String }
35
+ },
36
+ async execute({ object }, { triggerService }) {
37
+ await triggerService({
38
+ service: 'flow',
39
+ type: 'flow_setGraph'
40
+ }, {
41
+ ownerType: 'calculation_Calculation',
42
+ owner: object
43
+ })
44
+ }
45
+ })
46
+
47
+ definition.trigger({
48
+ name: 'changeCalculation_Wire',
49
+ properties: {
50
+ object: { type: String },
51
+ identifiers: { type: Object },
52
+ data: { type: Object },
53
+ oldData: { type: Object },
54
+ changeType: { type: String }
55
+ },
56
+ queuedBy: wireQueueKey,
57
+ async execute({ object, identifiers, data, changeType }, { service }) {
58
+ if(changeType == 'delete' || !data) return
59
+ if(!identifiers?.sourceType || !identifiers?.destinationType) {
60
+ throw app.logicError('invalid_wire')
61
+ }
62
+ if(data.sourcePort != 'out' || data.destinationPort != 'in') {
63
+ throw app.logicError('invalid_wire_ports')
64
+ }
65
+
66
+ const samePair = await service.models.Wire.sortedIndexRangeGet('bySourceAndDestination', [
67
+ identifiers.sourceType,
68
+ identifiers.source,
69
+ identifiers.destinationType,
70
+ identifiers.destination
71
+ ])
72
+ const duplicates = (samePair || []).filter(row => {
73
+ const id = row.to || row.id
74
+ if(id == object) return false
75
+ return row.sourcePort == data.sourcePort && row.destinationPort == data.destinationPort
76
+ })
77
+ if(duplicates.length) throw app.logicError('wire_exists')
78
+
79
+ if(identifiers.destinationType == 'calculation_Constant' && data.destinationPort == 'in') {
80
+ throw app.logicError('constant_has_input')
81
+ }
82
+
83
+ if(identifiers.destinationType == 'calculation_Power' && data.destinationPort == 'in') {
84
+ const existing = await service.models.Wire.sortedIndexRangeGet('byDestination', [
85
+ identifiers.destinationType,
86
+ identifiers.destination
87
+ ])
88
+ const inbound = (existing || []).filter(row => {
89
+ const id = row.to || row.id
90
+ return id != object && (row.destinationPort || 'in') == 'in'
91
+ })
92
+ if(inbound.length) throw app.logicError('power_single_input')
93
+ }
94
+ }
95
+ })