Files: ed0d0518717fe16a686399588766fb35b3e6e37a / app / page / query.js
5716 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Value, computed, when, resolve } = require('mutant') |
3 | const Scroller = require('mutant-scroll') |
4 | const next = require('pull-next-query') |
5 | const json5 = require('json5') |
6 | const get = require('lodash/get') |
7 | const isEqual = require('lodash/isEqual') |
8 | |
9 | exports.gives = nest({ |
10 | 'app.html.menuItem': true, |
11 | 'app.page.query': true |
12 | }) |
13 | |
14 | exports.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 ? |
21 | exports.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 | |
98 | function 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 | |
126 | function 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 | |
134 | function 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 | |
141 | function 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 | |
185 | function buildRawMsg (msg) { |
186 | return h('pre', |
187 | linkify(colorKeys(splitLines( |
188 | json5.stringify(msg, 0, 2) |
189 | ))) |
190 | ) |
191 | } |
192 | |
193 | function splitLines (text) { |
194 | const chunks = text.split(/(\n)/g) |
195 | return chunks |
196 | } |
197 | |
198 | function 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 | |
213 | function 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