git ssb

16+

Dominic / patchbay



Tree: 1c2926f12ff8e365e1d469f23180b50d50b9c749

Files: 1c2926f12ff8e365e1d469f23180b50d50b9c749 / app / page / query.js

5716 bytesRaw
1const nest = require('depnest')
2const { h, Value, computed, when, resolve } = require('mutant')
3const Scroller = require('mutant-scroll')
4const next = require('pull-next-query')
5const json5 = require('json5')
6const get = require('lodash/get')
7const isEqual = require('lodash/isEqual')
8
9exports.gives = nest({
10 'app.html.menuItem': true,
11 'app.page.query': true
12})
13
14exports.needs = nest({
15 'app.sync.goTo': 'first',
16 'message.html.render': 'first',
17 'sbot.pull.stream': 'first'
18})
19
20// TODO ?? extract a module patchbay-devtools ?
21exports.create = function (api) {
22 return nest({
23 'app.html.menuItem': menuItem,
24 'app.page.query': queryPage
25 })
26
27 function menuItem () {
28 return h('a', {
29 'ev-click': () => api.app.sync.goTo({ page: 'query' })
30 }, '/query')
31 }
32
33 function queryPage (location) {
34 const { initialOpts, initialValue } = getInitialState(location)
35
36 const state = {
37 opts: Value(initialOpts),
38 input: Value()
39 }
40
41 const error = computed(state.input, i => {
42 try {
43 var newOpts = json5.parse(i)
44 } catch (err) {
45 // console.error(err)
46 return err
47 }
48 // NOTE - this is the piece which auto-runs the quers
49 if (!isValidOpts(newOpts)) return
50 if (isEqual(resolve(state.opts), newOpts)) return
51 state.opts.set(newOpts)
52 })
53
54 const activateQuery = () => state.opts.set(json5.parse(resolve(state.input)))
55
56 const page = h('Query', { title: '/query' }, [
57 h('section.query', [
58 h('textarea', { 'ev-input': ev => state.input.set(ev.target.value), value: initialValue }),
59 h('button', {
60 className: when(error, '', '-primary'),
61 disabled: when(error, 'disabled'),
62 'ev-click': activateQuery
63 }, 'Go!')
64 ]),
65 h('section.output', [
66 computed(state.opts, opts => {
67 return Scroller({
68 streamToBottom: source(opts),
69 render: buildRawMsg,
70 comparer: (a, b) => {
71 if (a && b && a.key && b.key) return a.key === b.key
72 return a === b
73 }
74 // cb: console.error // TODO better error catching with stream
75 })
76 })
77 ])
78 ])
79
80 page.scroll = () => {}
81 return page
82 }
83
84 function source (opts) {
85 return api.sbot.pull.stream(server => {
86 var stepOn
87 if (get(opts, 'query[0].$first.timestamp')) stepOn = ['timestamp']
88 else if (get(opts, 'query[0].$first.value.timestamp')) stepOn = ['value', 'timestamp']
89
90 const hasReduce = opts.query.some(el => Object.keys(el)[0] === '$reduce')
91
92 if (opts.limit && stepOn && !hasReduce) return next(server.query.read, opts, stepOn)
93 else return server.query.read(opts)
94 })
95 }
96}
97
98function getInitialState (location) {
99 const { initialOpts, initialQuery, initialValue } = location
100 if (isValidOpts(initialOpts)) {
101 // TODO check initialValue === initialOpts
102 return {
103 initialOpts,
104 initialValue: initialValue || json5.stringify(initialOpts, null, 2)
105 }
106 }
107 if (isValidQuery(initialQuery)) {
108 const opts = {
109 reverse: true,
110 query: initialQuery
111 }
112 return {
113 initialOpts: opts,
114 initialValue: json5.stringify(opts, null, 2)
115 }
116 }
117
118 const defaultValue = defaulSSBQueryValue()
119
120 return {
121 initialOpts: json5.parse(defaultValue),
122 initialValue: defaultValue
123 }
124}
125
126function isValidOpts (opts) {
127 if (!opts) return false
128 if (typeof opts !== 'object') return false
129 if (!isValidQuery(opts.query)) return false
130
131 return true
132}
133
134function isValidQuery (query) {
135 if (!Array.isArray(query)) return false
136 if (!query.map(q => Object.keys(q)[0]).every(q => ['$filter', '$map', '$reduce'].includes(q))) return false
137
138 return true
139}
140
141function defaulSSBQueryValue () {
142 const day = 24 * 60 * 20 * 1e3
143 return `{
144 reverse: true,
145 limit: 50,
146 // live: true,
147 // old: false // good with live: true
148 query: [
149 {
150 $filter: {
151 value: {
152 timestamp: {$gt: ${Date.now() - day}},
153 content: { type: 'post' }
154 }
155 }
156 },
157 {
158 $map: {
159 author: ['value', 'author'],
160 text: ['value', 'content', 'text'],
161 ts: {
162 received: ['timestamp'],
163 asserted: ['value', 'timestamp']
164 }
165 }
166 },
167 // {
168 // $reduce: {
169 // author: ['author'],
170 // count: { $count: true }
171 // }
172 // }
173 ]
174}
175
176// $filter - used to prune down results. This must be the first entry, as ssb-query uses it to determine the most optimal index for fast lookup.
177
178// $map - optional, can be used to pluck data you want out. Doing this reduces the amount of data sent over muxrpc, which speeds up loading
179`
180}
181
182// forked from message/html/meta/raw.js
183// but modified
184
185function buildRawMsg (msg) {
186 return h('pre',
187 linkify(colorKeys(splitLines(
188 json5.stringify(msg, 0, 2)
189 )))
190 )
191}
192
193function splitLines (text) {
194 const chunks = text.split(/(\n)/g)
195 return chunks
196}
197
198function colorKeys (chunks) {
199 var newArray = []
200 chunks.forEach(chunk => {
201 if (typeof chunk !== 'string') return newArray.push(chunk)
202
203 var arr = chunk.split(/^(\s*\w+)/)
204 for (var i = 1; i < arr.length; i += 2) {
205 arr[i] = h('span', arr[i])
206 }
207 newArray = [...newArray, ...arr]
208 })
209
210 return newArray
211}
212
213function linkify (chunks) {
214 var newArray = []
215 chunks.forEach(chunk => {
216 if (typeof chunk !== 'string') return newArray.push(chunk)
217
218 // regex lifted from ssb-ref
219 var arr = chunk.split(/((?:@|%|&)[A-Za-z0-9/+]{43}=\.[\w\d]+)/g)
220 for (var i = 1; i < arr.length; i += 2) {
221 arr[i] = h('a', { href: arr[i] }, arr[i])
222 }
223 newArray = [...newArray, ...arr]
224 })
225
226 return newArray
227}
228

Built with git-ssb-web