-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
219 lines (180 loc) · 6.08 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
var balanced = require('balanced-match')
var debug = require('debug')('aggsy')
var leading = /^[\s,]+/
var findName = /^[\w.]+(?=:)/
var defaultReducers = require('./reducers')
function aggsy (query, data, options) {
query = query.replace(/"/g,'')
debug(query)
if (!Array.isArray(data)) {
options = data
data = undefined
}
options = options || {}
var reducers = options.reducers || defaultReducers
if (options.reducers) {
for (var n in defaultReducers) {
if (!reducers[n]) reducers[n] = defaultReducers[n]
}
}
var reducersText = Object.keys(reducers).map((name) => {
if (typeof reducers[name] !== 'function') { throw new Error('reducers must be functions') }
if (query.indexOf(name) !== -1) {
return `var ${name} = ${reducers[name].toString()}`
}
return false
}).filter(Boolean).join('\n')
const ast = parse({ reducers }, query, options)
if (ast.options) {
options = Object.assign(ast.options)
}
debug(JSON.stringify(ast, null, 2))
var funcText = `
/** Add used reducers */
${reducersText}
const _tmp = {}
/** Create the iterator function */
return function (result, item) {
${genFunction(ast, options)}
}
`
debug(funcText)
var createFunction = new Function (funcText) // eslint-disable-line
var func = createFunction()
if (!data) {
return func
}
var result = {}
for (var i = 0; i < data.length; i++) {
func(result, data[i])
}
if (options.flatten) return resolveFlatStructure(result)
return result
}
function propPath (name, path) {
var parts = path.split('.')
return Array(parts.length).join('(') + name + '["' + parts.join('"] || false)["') + '"]'
}
// create lookup path into the result structure
function createPath (prefix, path, options) {
if (!path || !path.length) return prefix
if (options.flatten) {
const parts = path.map((part, index) => {
if (index === 0) return `${part.slice(0, -1)}.'`
if (index % 2) return part
return `'.${part.slice(1, -1)}.'`
})
return `${prefix}[${parts.join('+')}]`
}
return `${prefix}[${path.join('][')}]`
}
function genFunction ({ name, id, groups, reducers}, options, parentPath) {
var path
const leaf = options.flatten && !groups.length
if (id) {
var prop = 'group' + id
path = parentPath.concat(options.showGroups || options.flatten ? [`'${name}'`, prop] : prop)
} else {
path = parentPath || []
}
let func = ''
const resultObject = !options.flatten || leaf ? 'result' : '_tmp'
const resultProp = createPath(resultObject, path, options)
if (name) {
func += `\n/** GROUP: ${name} */\n`
func += `var ${prop} = ${propPath('item', name)}\n`
if (!options.missing) func += `if (typeof ${prop} !== 'undefined') {\n`
else func += `if (typeof ${prop} === 'undefined') ${prop} = '${options.missing}';\n`
if (options.showGroups && path.length > 1) {
func += ensureExist(createPath('result', path, options))
}
if (!groups.length && !reducers.length) {
// no reducers or groups defined return all items in grouping
func += ensureExist(resultProp, '[]')
func += `${resultProp}.push(item)\n`
} else {
// reducers or groups defined
let newObject = '{}'
if (options.flatten && path.length > 2) {
newObject = `Object.create(${createPath('_tmp', parentPath, options)})`
}
func += ensureExist(resultProp, newObject)
if (options.flatten) func += `${resultProp}['${name}'] = ${prop}\n`
}
}
reducers.forEach((reducer) => {
const reducerProp = `${resultProp}['${reducer.as}']`
const itemProp = reducer.body ? propPath('item', reducer.body) : undefined
func += '\n/** REDUCER: ' + reducer.as + ' */\n'
func += `if (typeof ${reducerProp} === 'undefined') `
if (typeof reducer.initialValue !== 'undefined') {
func += `${reducerProp} = ${reducer.name}(${JSON.stringify(reducer.initialValue)}${itemProp ? `, ${itemProp}` : ''}, '${reducer.body}')\n`
} else if (itemProp) {
func += `${reducerProp} = ${itemProp}\n`
}
func += `else ${reducerProp} = ${reducer.name}(${reducerProp}${itemProp ? `, ${itemProp}` : ''}, '${reducer.body}')\n`
})
groups.forEach(group => {
func += genFunction(group, options, path)
})
if (name) {
if (!options.missing) func += '}\n'
func += `/** END-GROUP: ${name} */\n`
}
return func
}
function parse (state, agg, options, ast) {
ast = ast || { groups: [], reducers: [] }
const reducers = state.reducers
const parsed = balanced('(', ')', agg)
if (!parsed) {
throw new Error('aggsy query faulty. check if used reducers are defined')
}
let pre = parsed.pre.replace(leading, '')
// if named function remove name
let as = findName.exec(pre)
if (as) {
as = as[0]
pre = pre.substring(as.length + 1).trim()
}
if (pre === '_flatten') {
ast.options = { flatten: true }
options = Object.assign({}, { flatten: true })
if (parsed.body) parse(state, parsed.body, options, ast)
if (parsed.post) parse(state, parsed.post, options, ast)
} else if (reducers[pre]) {
if (!as) as = pre + '(' + parsed.body + ')'
ast.reducers.push({
name: pre,
initialValue: reducers[pre].initialValue,
as,
body: parsed.body
})
if (parsed.post) parse(state, parsed.post, options, ast)
} else {
state.currentGroupId = (state.currentGroupId || 0) + 1
const group = { name: pre, id: state.currentGroupId, groups: [], reducers: [] }
ast.groups.push(group)
if (parsed.body) parse(state, parsed.body, options, group)
if (parsed.post) parse(state, parsed.post, options, ast)
}
return ast
}
function ensureExist (path, set) {
return `if (typeof ${path} === 'undefined') ${path} = ${set || '{}'}\n`
}
function resolveFlatStructure (structure) {
const resolved = []
// we want to change it from an object to an array
for (let key in structure) {
const obj = {}
// we also need to resolve the prototype chain
for (let prop in structure[key]) {
obj[prop] = structure[key][prop]
}
resolved.push(obj)
}
return resolved
}
aggsy.parse = parse
module.exports = aggsy