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