@live-change/pattern-db 0.2.2 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,306 @@
1
+ const test = require('tape')
2
+ const testDb = require('./testDb.js')
3
+ const lcp = require('@live-change/pattern')
4
+ const { relationsStore } = require('../lib/relations-store.js')
5
+ const svg = require('./svg.js')
6
+
7
+ let model
8
+
9
+ test("simple chain", async (t) => {
10
+ t.plan(4)
11
+
12
+ const db = await testDb()
13
+ const store = relationsStore(db, 'test', 'relations')
14
+ await store.createTable()
15
+
16
+ t.test('compile', (t) => {
17
+ t.plan(1)
18
+
19
+ let { model: compiled } = lcp.chain([
20
+ "enter-website",
21
+ "sessionId",
22
+ "start-register",
23
+ "userId",
24
+ "finish-register"
25
+ ])
26
+
27
+ console.log(JSON.stringify(compiled, null, ' '))
28
+
29
+ model = compiled
30
+
31
+ t.deepEqual(model,{
32
+ "elements": {
33
+ "enter-website": {
34
+ "type": "enter-website",
35
+ "id": "enter-website",
36
+ "prev": [],
37
+ "next": [
38
+ "enter-website/sessionId/start-register"
39
+ ]
40
+ },
41
+ "enter-website/sessionId/start-register": {
42
+ "type": "start-register",
43
+ "id": "enter-website/sessionId/start-register",
44
+ "prev": [
45
+ "enter-website/sessionId/start-register"
46
+ ],
47
+ "next": [
48
+ "enter-website/sessionId/start-register/userId/finish-register"
49
+ ]
50
+ },
51
+ "enter-website/sessionId/start-register/userId/finish-register": {
52
+ "type": "finish-register",
53
+ "id": "enter-website/sessionId/start-register/userId/finish-register",
54
+ "prev": [
55
+ "enter-website/sessionId/start-register/userId/finish-register"
56
+ ],
57
+ "next": []
58
+ }
59
+ },
60
+ "relations": {
61
+ "enter-website/sessionId/start-register": {
62
+ "eq": [
63
+ {
64
+ "prev": "sessionId",
65
+ "next": "sessionId"
66
+ }
67
+ ],
68
+ "id": "enter-website/sessionId/start-register",
69
+ "prev": [
70
+ "enter-website"
71
+ ],
72
+ "next": [
73
+ "enter-website/sessionId/start-register"
74
+ ]
75
+ },
76
+ "enter-website/sessionId/start-register/userId/finish-register": {
77
+ "eq": [
78
+ {
79
+ "prev": "userId",
80
+ "next": "userId"
81
+ }
82
+ ],
83
+ "id": "enter-website/sessionId/start-register/userId/finish-register",
84
+ "prev": [
85
+ "enter-website/sessionId/start-register"
86
+ ],
87
+ "next": [
88
+ "enter-website/sessionId/start-register/userId/finish-register"
89
+ ]
90
+ }
91
+ }
92
+ }, 'model compiled')
93
+
94
+ })
95
+
96
+ t.test('live processor', async (t) => {
97
+ t.plan(2)
98
+
99
+ lcp.prepareModelForLive(model)
100
+ const processor = new lcp.LiveProcessor(model, store)
101
+ const sessionId = (Math.random()*1000).toFixed()
102
+ const userId = (Math.random()*1000).toFixed()
103
+
104
+ t.test('push first event', async (t) => {
105
+ t.plan(1)
106
+ await processor.processEvent({ type: 'enter-website', keys: { sessionId }, time: 0 })
107
+ if((await store.getRelations('start-register', { sessionId })).length > 0) {
108
+ t.pass('processed')
109
+ } else {
110
+ t.fail('no reaction')
111
+ }
112
+ })
113
+
114
+ t.test('push second event', async (t) => {
115
+ t.plan(1)
116
+ await processor.processEvent({ type: 'start-register', keys: { sessionId, userId }, time: 100 })
117
+ if((await store.getRelations('finish-register', { userId })).length > 0) {
118
+ t.pass('processed')
119
+ } else {
120
+ t.fail('no reaction')
121
+ }
122
+ })
123
+
124
+ })
125
+
126
+ t.test("test relations search", async (t) => {
127
+ t.plan(4)
128
+
129
+ const sessionId = (Math.random()*1000).toFixed()
130
+ const userId = (Math.random()*1000).toFixed()
131
+
132
+ const events = [
133
+ { id: 0, type: 'enter-website', keys: { sessionId } },
134
+ { id: 1, type: 'start-register', keys: { sessionId, userId } },
135
+ { id: 2, type: 'finish-register', keys: { userId } }
136
+ ]
137
+
138
+ async function getEventsByRelation( types, keys, from, to ) { /// TODO: use events store
139
+ console.log("GETEVENTS", types, keys, from, to)
140
+ return events.filter(ev => {
141
+ if(types.indexOf(ev.type) == -1) return
142
+ for(let key in keys) if(ev.keys[key] != keys[key]) return
143
+ return true
144
+ })
145
+ }
146
+
147
+ t.test("related previous events", async (t) => {
148
+ t.plan(1)
149
+ const related = await lcp.findRelatedEvents([events[2]], true, model,
150
+ -Infinity, Infinity, getEventsByRelation)
151
+ console.log("RELATED", Array.from(related.values()))
152
+ t.deepEqual(Array.from(related.values()), [{
153
+ ...events[1], elements: ['enter-website/sessionId/start-register']
154
+ }], "found related events")
155
+ })
156
+
157
+ t.test("all related previous events", async (t) => {
158
+ t.plan(1)
159
+ const related = await lcp.findAllRelatedEvents([events[2]], true, model,
160
+ -Infinity, Infinity, getEventsByRelation)
161
+ console.log("RELATED", Array.from(related.values()))
162
+ t.deepEqual(Array.from(related.values()), [
163
+ { ...events[1], elements: [ 'enter-website/sessionId/start-register' ] },
164
+ { ...events[0], elements: [ 'enter-website' ] }
165
+ ], "found related events")
166
+ })
167
+
168
+ t.test("related next events", async (t) => {
169
+ t.plan(1)
170
+ const related = await lcp.findRelatedEvents([events[0]], false, model,
171
+ -Infinity, Infinity, getEventsByRelation)
172
+ console.log("RELATED", Array.from(related.values()))
173
+ t.deepEqual(Array.from(related.values()), [{
174
+ ...events[1], elements: ['enter-website/sessionId/start-register']
175
+ }], "found related events")
176
+ })
177
+
178
+ t.test("all related previous events", async (t) => {
179
+ t.plan(1)
180
+ const related = await lcp.findAllRelatedEvents([events[0]], false, model,
181
+ -Infinity, Infinity, getEventsByRelation)
182
+ console.log("RELATED", Array.from(related.values()))
183
+ t.deepEqual(Array.from(related.values()), [
184
+ { ...events[1], elements: [ 'enter-website/sessionId/start-register' ] },
185
+ { ...events[2], elements: [ 'enter-website/sessionId/start-register/userId/finish-register' ] }
186
+ ], "found related events")
187
+ })
188
+ })
189
+
190
+ t.test("test graphs", async (t) => {
191
+ t.plan(3)
192
+
193
+ const sessionId = (Math.random() * 1000).toFixed()
194
+ const userId = (Math.random() * 1000).toFixed()
195
+ const userId2 = userId + 1
196
+
197
+ const events = [
198
+ {id: 0, type: 'enter-website', keys: {sessionId}, time: 0},
199
+ {id: 1, type: 'enter-website', keys: {sessionId}, time: 1000},
200
+ {id: 2, type: 'start-register', keys: {sessionId, userId}, time: 2000},
201
+ {id: 3, type: 'start-register', keys: {sessionId, userId: userId2}, time: 3000},
202
+ {id: 4, type: 'finish-register', keys: {userId}, time: 4000}
203
+ ]
204
+
205
+ t.test("build full graph", async (t) => {
206
+ t.plan(1)
207
+ const processor = new lcp.FullGraphProcessor(model, lcp.relationsStore())
208
+ for(const ev of events) await processor.processEvent(ev)
209
+ const graph = processor.graph
210
+ console.log("GRAPH\n "+Array.from(graph.values()).map(n => JSON.stringify(n)).join('\n '))
211
+ t.deepEqual(Array.from(graph.values()), [
212
+ {"id":0,"type":"enter-website","keys":{"sessionId":""+sessionId},"time":0,"prev":[],"next":[
213
+ {"relation":"enter-website/sessionId/start-register","to":2},
214
+ {"relation":"enter-website/sessionId/start-register","to":3}],
215
+ "start":true},
216
+ {"id":1,"type":"enter-website","keys":{"sessionId":""+sessionId},"time":1000,"prev":[],"next":[
217
+ {"relation":"enter-website/sessionId/start-register","to":2},
218
+ {"relation":"enter-website/sessionId/start-register","to":3}],
219
+ "start":true},
220
+ {"id":2,"type":"start-register","keys":{"sessionId":""+sessionId,"userId":""+userId},"time":2000,
221
+ "prev":[
222
+ {"relation":"enter-website/sessionId/start-register","to":0},
223
+ {"relation":"enter-website/sessionId/start-register","to":1}],
224
+ "next":[
225
+ {"relation":"enter-website/sessionId/start-register/userId/finish-register","to":4}],
226
+ "start":false},
227
+ {"id":3,"type":"start-register","keys":{"sessionId":""+sessionId,"userId":""+userId2},"time":3000,"prev":[
228
+ {"relation":"enter-website/sessionId/start-register","to":0},
229
+ {"relation":"enter-website/sessionId/start-register","to":1}],
230
+ "next":[],"start":false},
231
+ {"id":4,"type":"finish-register","keys":{"userId":""+userId},"time":4000,"prev":[
232
+ {"relation":"enter-website/sessionId/start-register/userId/finish-register","to":2}],
233
+ "next":[],"start":false}
234
+ ], 'proper graph generated')
235
+
236
+
237
+ await svg.generateGraphSvg("simple-chain-full-graph.svg", graph,
238
+ n => ({ ...n, label: n.type, title: `${n.id} at ${n.time}`, sort: n.time }),
239
+ (rel, source, target) => ({ ...rel, value: 1, label: rel.relation, title: rel.relation })
240
+ )
241
+ })
242
+
243
+ t.test("build summary graph with count", async (t) => {
244
+ t.plan(1)
245
+ const processor = new lcp.SummaryGraphProcessor(model, lcp.relationsStore())
246
+ for(const ev of events) await processor.processEvent(ev)
247
+ const graph = processor.graph
248
+ console.log("GRAPH\n "+Array.from(graph.values()).map(n => JSON.stringify(n)).join('\n '))
249
+ t.deepEqual(Array.from(graph.values()), [
250
+ {"id":"enter-website:0","prev":[],
251
+ "next":[{"to":"enter-website/sessionId/start-register:1","counter":4}],
252
+ "start":true,"counter":2},
253
+ {"id":"enter-website/sessionId/start-register:1",
254
+ "prev":[{"to":"enter-website:0","counter":4}],
255
+ "next":[{"to":"enter-website/sessionId/start-register/userId/finish-register:2","counter":1}],
256
+ "start":false,"counter":2},
257
+ {"id":"enter-website/sessionId/start-register/userId/finish-register:2",
258
+ "prev":[{"to":"enter-website/sessionId/start-register:1","counter":1}],
259
+ "next":[],"start":false,"counter":1}
260
+ ], 'proper graph generated')
261
+
262
+ lcp.computeGraphDepth(graph,['enter-website:0'])
263
+
264
+ await svg.generateGraphSvg("simple-chain-summary-count.svg", graph,
265
+ n => ({ ...n, label: n.id.split(':')[0], title: `${n.id} at ${n.time}`, sort: n.depth }),
266
+ (rel, source, target) => ({ ...rel, value: rel.counter, label: rel.relation, title: rel.relation })
267
+ )
268
+ })
269
+
270
+ t.test("build summary graph with events", async (t) => {
271
+ t.plan(1)
272
+ const processor = new lcp.SummaryGraphProcessor(model, lcp.relationsStore(), {
273
+ ...lcp.graphAggregation.nodeElementDepth,
274
+ ...lcp.graphAggregation.relationSimple,
275
+ ...lcp.graphAggregation.summaryEvents
276
+ })
277
+ for (const ev of events) await processor.processEvent(ev)
278
+ const graph = processor.graph
279
+ console.log("GRAPH\n " + Array.from(graph.values()).map(n => JSON.stringify(n)).join('\n '))
280
+ t.deepEqual(Array.from(graph.values()), [
281
+ {"id":"enter-website:0","prev":[],
282
+ "next":[{"to":"enter-website/sessionId/start-register:1","events":[2,3]}],
283
+ "start":true,"events":[0,1]},
284
+ {"id":"enter-website/sessionId/start-register:1",
285
+ "prev":[{"to":"enter-website:0","events":[2,3]}],
286
+ "next":[{"to":"enter-website/sessionId/start-register/userId/finish-register:2","events":[4]}],
287
+ "start":false,"events":[2,3]},
288
+ {"id":"enter-website/sessionId/start-register/userId/finish-register:2",
289
+ "prev":[{"to":"enter-website/sessionId/start-register:1","events":[4]}],
290
+ "next":[],
291
+ "start":false,"events":[4]}
292
+
293
+ ], 'proper graph generated')
294
+
295
+ lcp.computeGraphDepth(graph,['enter-website:0'])
296
+
297
+ await svg.generateGraphSvg("simple-chain-summary-events-count.svg", graph,
298
+ n => ({ ...n, label: n.id.split(':')[0], title: `${n.id}`, sort: n.depth }),
299
+ (rel, source, target) => ({ ...rel, value: rel.events.length, label: rel.relation, title: rel.relation })
300
+ )
301
+ })
302
+ })
303
+
304
+ })
305
+
306
+
package/tests/svg.js ADDED
@@ -0,0 +1,115 @@
1
+ const rp = require('../index.js')
2
+ const d3 = Object.assign({}, require('d3'), require('d3-sankey-circular'), require('d3-path-arrows'))
3
+ const D3Node = require('d3-node')
4
+ const fs = require('fs')
5
+
6
+ function generateGraphSvg(filePath, graph,
7
+ nodeFunc = n => ({ ...n, label: n.type, title: `${n.id} at ${n.time}`, sort: n.time }),
8
+ linkFunc = (rel, source, target) => ({ ...rel, value: 1, label: rel.relation, title: rel.relation })
9
+ ) {
10
+
11
+ const width = 1280, height = 800, margin = { top: 30, right: 50, bottom: 30, left: 50}
12
+ const sankey = d3
13
+ .sankeyCircular()
14
+ .nodeWidth(10)
15
+ .nodePadding(20)
16
+ //.nodePaddingRatio(0.5)
17
+ .size([width - margin.left - margin.right, height - margin.top - margin.bottom])
18
+ .nodeId(d => d.id)
19
+ .nodeAlign(d3.sankeyLeft)
20
+ .iterations(5)
21
+ .circularLinkGap(1)
22
+ .sortNodes("sort")
23
+
24
+ const data = rp.graphToD3Sankey(graph, nodeFunc, linkFunc)
25
+ //console.log("SDATA", data)
26
+ const sankeyData = sankey(data)
27
+ /* const sankeyData = sankey(rp.graphToD3Sankey(
28
+ graph,
29
+ nodeFunc = n => ({ ...n, col:depth, name: n.id, label: n.type, }),
30
+
31
+ linkFunc = (rel, source, target) => ({ ...rel, value: 1, label: rel.relation })
32
+ ))*/
33
+ const sankeyNodes = sankeyData.nodes
34
+ const sankeyLinks = sankeyData.links
35
+ const depthExtent = d3.extent(sankeyNodes, function (d) { return d.depth })
36
+ const nodeColour = d3.scaleSequential(d3.interpolateCool)
37
+ .domain([0, width])
38
+
39
+ const d3n = new D3Node({ d3Module: d3 })
40
+ const svg = d3n.createSVG(width, height)
41
+ const g = svg.append("g")
42
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
43
+ const linkG = g.append("g")
44
+ .attr("class", "links")
45
+ .attr("stroke-opacity", 0.2)
46
+ .selectAll("path")
47
+ const nodeG = g.append("g")
48
+ .attr("class", "nodes")
49
+ .attr("font-family", "sans-serif")
50
+ .attr("font-size", 10)
51
+ .selectAll("g")
52
+ const linkLabels = g.append("g")
53
+
54
+ const node = nodeG.data(sankeyNodes)
55
+ .enter()
56
+ .append("g")
57
+
58
+ node.append("rect")
59
+ .attr("x", d => d.x0 )
60
+ .attr("y", d => d.y0 )
61
+ .attr("height", d => d.y1 - d.y0 )
62
+ .attr("width", d => d.x1 - d.x0 )
63
+ .style("fill", d => nodeColour(d.x0) )
64
+ .style("opacity", 0.5)
65
+ node.append("text")
66
+ .attr("x", d => (d.x0 + d.x1) / 2 )
67
+ .attr("y", d => d.y0 - 12 )
68
+ .attr("dy", "0.35em")
69
+ .attr("text-anchor", "middle")
70
+ .text( d => d.label )
71
+ node.append("title")
72
+ .text( d => d.title)
73
+
74
+ const link = linkG.data(sankeyLinks)
75
+ .enter()
76
+ .append("g")
77
+ link.append("path")
78
+ .attr("fill", "none")
79
+ .attr("class", "sankey-link")
80
+ .attr("d", link => link.path)
81
+ .style("stroke-width", d => Math.max(1, d.width))
82
+ .style("opacity", 0.7)
83
+ .style("stroke", (link, i) => link.circular ? "red" : "black")
84
+ link.append("title")
85
+ .text(d => d.title )
86
+ link.append("text")
87
+ .attr("x", d => d.source.x1 + 5)
88
+ .attr("y", d => d.y0)
89
+ .attr("text-anchor", "start")
90
+ .attr("font-family", "sans-serif")
91
+ .attr("font-size", 10)
92
+ .attr("dy", "0.35em")
93
+ .text(d => d.sourceLabel || d.label)
94
+ link.append("text")
95
+ .attr("x", d => d.target.x0 - 5)
96
+ .attr("y", d => d.y1)
97
+ .attr("text-anchor", "end")
98
+ .attr("font-family", "sans-serif")
99
+ .attr("font-size", 10)
100
+ .attr("dy", "0.35em")
101
+ .text(d => d.targetLabel || d.label)
102
+
103
+ // link.each(l => console.log("L",l))
104
+
105
+ return new Promise((resolve, reject) => {
106
+ fs.writeFile(filePath, d3n.svgString(), (err) => {
107
+ if(err) return reject(err)
108
+ resolve('ok')
109
+ })
110
+ })
111
+ }
112
+
113
+ module.exports = {
114
+ generateGraphSvg
115
+ }
@@ -0,0 +1,60 @@
1
+ const DbServer = require('@live-change/db-server')
2
+ const Dao = require("@live-change/dao")
3
+
4
+ async function createLoopbackDao(credentials, daoFactory) {
5
+ const server = new Dao.ReactiveServer(daoFactory)
6
+ const loopback = new Dao.LoopbackConnection(credentials, server, {})
7
+ const dao = new Dao(credentials, {
8
+ remoteUrl: 'dao',
9
+ protocols: { local: null },
10
+ defaultRoute: {
11
+ type: "remote",
12
+ generator: Dao.ObservableList
13
+ },
14
+ connectionSettings: {
15
+ disconnectDebug: true,
16
+ logLevel: 10,
17
+ },
18
+ })
19
+ dao.connections.set('local:dao', loopback)
20
+ await loopback.initialize()
21
+ if(!loopback.connected) {
22
+ console.error("LOOPBACK NOT CONNECTED?!")
23
+ process.exit(1)
24
+ }
25
+ return dao
26
+ }
27
+
28
+ async function testDb() {
29
+ const dbServer = new DbServer({
30
+ dbRoot: 'mem',
31
+ backend: 'mem',
32
+ slowStart: true,
33
+ temporary: true
34
+ })
35
+
36
+ process.on('unhandledRejection', (reason, promise) => {
37
+ if(reason.stack && reason.stack.match(/\s(userCode:([a-z0-9_.\/-]+):([0-9]+):([0-9]+))\n/i)) {
38
+ dbServer.handleUnhandledRejectionInQuery(reason, promise)
39
+ }
40
+ })
41
+
42
+ await dbServer.initialize()
43
+ console.info(`database initialized!`)
44
+
45
+ const loopbackDao = await createLoopbackDao('local', () => dbServer.createDao('local'))
46
+
47
+ const oldDispose = loopbackDao.dispose
48
+ loopbackDao.dbServer = dbServer
49
+ loopbackDao.dispose = () => {
50
+ dbServer.close()
51
+ oldDispose.apply(loopbackDao)
52
+ }
53
+
54
+ loopbackDao.databaseName = 'test'
55
+ await loopbackDao.request(['database', 'createDatabase'], loopbackDao.databaseName, { }).catch(err => 'exists')
56
+
57
+ return loopbackDao
58
+ }
59
+
60
+ module.exports = testDb