git ssb

16+

Dominic / patchbay



Tree: 5149067cc270bfcbf301fd7c1db66cd15659a53f

Files: 5149067cc270bfcbf301fd7c1db66cd15659a53f / app / page / query.js

5467 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')
7
8exports.gives = nest({
9 'app.html.menuItem': true,
10 'app.page.query': true
11})
12
13exports.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 ?
20exports.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
93function 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
121function 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
129function 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
136function 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
180function buildRawMsg (msg) {
181 return h('pre',
182 linkify(colorKeys(splitLines(
183 json5.stringify(msg, 0, 2)
184 )))
185 )
186}
187
188function splitLines (text) {
189 const chunks = text.split(/(\n)/g)
190 return chunks
191}
192
193function 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
208function 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