1 | const pick = (obj, keys = []) => {
|
2 | return Object.keys(obj)
|
3 | .filter(key => keys.includes(key))
|
4 | .reduce((newObj, key) => Object.assign(newObj, { [key]: obj[key] }), {})
|
5 | }
|
6 |
|
7 | const omit = (obj, keys = []) => {
|
8 | return Object.keys(obj)
|
9 | .filter(key => !keys.includes(key))
|
10 | .reduce((newObj, key) => Object.assign(newObj, { [key]: obj[key] }), {})
|
11 | }
|
12 |
|
13 | class QueryBuilder {
|
14 | constructor ({ query, path, init, text, postprocess = [] }, options) {
|
15 | this.query = query
|
16 | this.path = path
|
17 | this.init = init
|
18 | this.postprocess = postprocess
|
19 | this.options = options || {}
|
20 | this.onlyKeys = null
|
21 | this.withoutKeys = null
|
22 | this.sortKeys = []
|
23 | this.limitN = null
|
24 | this.skipN = null
|
25 |
|
26 | if (!text) {
|
27 |
|
28 | this.postprocess.unshift(data => data.map(item => omit(item, ['text'])))
|
29 | }
|
30 | }
|
31 |
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 | only (keys) {
|
38 |
|
39 | this.onlyKeys = Array.isArray(keys) ? keys : [keys]
|
40 |
|
41 | return this
|
42 | }
|
43 |
|
44 | |
45 |
|
46 |
|
47 |
|
48 |
|
49 | without (keys) {
|
50 |
|
51 | this.withoutKeys = Array.isArray(keys) ? keys : [keys]
|
52 |
|
53 | return this
|
54 | }
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | sortBy (field, direction) {
|
63 | this.sortKeys.push([field, direction === 'desc'])
|
64 | return this
|
65 | }
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 | where (query) {
|
73 | this.query = this.query.find(query)
|
74 | return this
|
75 | }
|
76 |
|
77 | |
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | search (query, value) {
|
84 |
|
85 | if (!query) { return this }
|
86 |
|
87 | let $fts
|
88 |
|
89 | if (typeof query === 'object') {
|
90 | $fts = query
|
91 | } else if (value) {
|
92 | $fts = {
|
93 | query: {
|
94 | type: 'match',
|
95 | field: query,
|
96 | value,
|
97 | prefix_length: 1,
|
98 | fuzziness: 1,
|
99 | extended: true,
|
100 | minimum_should_match: 1
|
101 | }
|
102 | }
|
103 | } else {
|
104 | $fts = {
|
105 | query: {
|
106 | type: 'bool',
|
107 | should: this.options.fullTextSearchFields.map(field => ({
|
108 | type: 'match',
|
109 | field,
|
110 | value: query,
|
111 | prefix_length: 1,
|
112 | operator: 'and',
|
113 | minimum_should_match: 1,
|
114 | fuzziness: 1,
|
115 | extended: true
|
116 | }))
|
117 | }
|
118 | }
|
119 | }
|
120 |
|
121 | this.query = this.query.find({ $fts }).sortByScoring()
|
122 |
|
123 | return this
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | surround (slugOrPath, { before = 1, after = 1 } = {}) {
|
133 | const _key = slugOrPath.indexOf('/') === 0 ? 'path' : 'slug'
|
134 |
|
135 |
|
136 | if (this.onlyKeys) {
|
137 | this.onlyKeys.push(_key)
|
138 | }
|
139 |
|
140 | if (this.withoutKeys) {
|
141 | this.withoutKeys = this.withoutKeys.filter(key => key !== _key)
|
142 | }
|
143 |
|
144 | const fn = (data) => {
|
145 | const index = data.findIndex(item => item[_key] === slugOrPath)
|
146 | const slice = new Array(before + after).fill(null, 0)
|
147 | if (index === -1) {
|
148 | return slice
|
149 | }
|
150 |
|
151 | const prevSlice = data.slice(index - before, index)
|
152 | const nextSlice = data.slice(index + 1, index + 1 + after)
|
153 |
|
154 | let prevIndex = 0
|
155 | for (let i = before - 1; i >= 0; i--) {
|
156 | slice[i] = prevSlice[prevIndex] || null
|
157 | prevIndex++
|
158 | }
|
159 |
|
160 | let nextIndex = 0
|
161 | for (let i = before; i <= after; i++) {
|
162 | slice[i] = nextSlice[nextIndex] || null
|
163 | nextIndex++
|
164 | }
|
165 |
|
166 | return slice
|
167 | }
|
168 |
|
169 | this.postprocess.push(fn)
|
170 | return this
|
171 | }
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 | limit (n) {
|
179 | if (typeof n === 'string') { n = parseInt(n) }
|
180 |
|
181 | this.limitN = n
|
182 | return this
|
183 | }
|
184 |
|
185 | |
186 |
|
187 |
|
188 |
|
189 |
|
190 | skip (n) {
|
191 | if (typeof n === 'string') { n = parseInt(n) }
|
192 |
|
193 | this.skipN = n
|
194 | return this
|
195 | }
|
196 |
|
197 | |
198 |
|
199 |
|
200 |
|
201 |
|
202 | async fetch () {
|
203 | if (this.sortKeys && this.sortKeys.length) {
|
204 | this.query = this.query.compoundsort(this.sortKeys)
|
205 | }
|
206 | if (this.skipN) {
|
207 | this.query = this.query.offset(this.skipN)
|
208 | }
|
209 | if (this.limitN) {
|
210 | this.query = this.query.limit(this.limitN)
|
211 | }
|
212 |
|
213 | let data = this.query.data({ removeMeta: true })
|
214 |
|
215 | if (this.onlyKeys) {
|
216 |
|
217 | if (this.options.watch) {
|
218 | this.onlyKeys.push('path', 'extension')
|
219 | }
|
220 |
|
221 | const fn = data => data.map(item => pick(item, this.onlyKeys))
|
222 |
|
223 | this.postprocess.unshift(fn)
|
224 | }
|
225 |
|
226 | if (this.withoutKeys) {
|
227 |
|
228 | if (this.options.watch) {
|
229 | this.withoutKeys = this.withoutKeys.filter(key => !['path', 'extension'].includes(key))
|
230 | }
|
231 |
|
232 | const fn = data => data.map(item => omit(item, this.withoutKeys))
|
233 |
|
234 | this.postprocess.unshift(fn)
|
235 | }
|
236 |
|
237 | for (const fn of this.postprocess) {
|
238 | data = fn(data)
|
239 | }
|
240 |
|
241 | if (!data) {
|
242 | throw new Error(`${this.path} not found`)
|
243 | }
|
244 |
|
245 | return JSON.parse(JSON.stringify(data))
|
246 | }
|
247 | }
|
248 |
|
249 | module.exports = QueryBuilder
|