Mercurial > hg > ng2-query-ismi
comparison src/app/query.service.ts @ 47:b65a031c4967 ng2-final
first step to angular2-final (2.4) version of the query browser.
author | casties |
---|---|
date | Fri, 17 Mar 2017 20:16:52 +0100 |
parents | app/query.service.ts@dc4f0541f04d |
children | 308c96f734c8 |
comparison
equal
deleted
inserted
replaced
46:1f3fed01aef6 | 47:b65a031c4967 |
---|---|
1 import {Injectable} from '@angular/core'; | |
2 import {Http, Headers} from '@angular/http'; | |
3 | |
4 import 'rxjs/Rx'; // import all RxJS operators | |
5 //import 'rxjs/add/operator/map'; | |
6 | |
7 import {NEO4J_BASE_URL, NEO4J_AUTHENTICATION} from './app-config'; | |
8 import {QueryMode, QUERY_MODES, FIRST_QUERY_MODES} from './query-mode'; | |
9 import {QueryState} from './query-state'; | |
10 import {QueryStep} from './query-step'; | |
11 import {getResultType} from './result-type'; | |
12 import {ISMI_RESULT_TYPES} from './ismi-result-types'; | |
13 import {getRelationType} from './ismi-relation-types'; | |
14 | |
15 @Injectable() | |
16 export class QueryService { | |
17 | |
18 public typeAttribute = '_type'; | |
19 public excludedAttributes = {}; | |
20 public state: QueryState; | |
21 public objectTypes: string[]; | |
22 | |
23 constructor(private _http: Http) { | |
24 // init query state | |
25 this.state = new QueryState(); | |
26 } | |
27 | |
28 setup() { | |
29 this.setupObjectTypes(); | |
30 } | |
31 | |
32 getState() { | |
33 return this.state; | |
34 } | |
35 | |
36 getQueryModes(index: number): QueryMode[] { | |
37 if (index == 0) { | |
38 return FIRST_QUERY_MODES; | |
39 } else { | |
40 return QUERY_MODES; | |
41 } | |
42 } | |
43 | |
44 /** | |
45 * return the first set of options for the given query mode. | |
46 */ | |
47 getQueryOptions(queryMode: QueryMode) { | |
48 let options: any[] = []; | |
49 if (queryMode == null) return options; | |
50 if (queryMode.id === 'type_is') { | |
51 options = this.objectTypes; | |
52 } else if (queryMode.id === 'relation_is') { | |
53 options = this.state.resultRelations; | |
54 } else if (queryMode.id === 'att_contains') { | |
55 options = this.filterAttributes(this.state.resultAttributes); | |
56 } else if (queryMode.id === 'att_contains_norm') { | |
57 options = this.filterAttributes(this.state.resultAttributes, true); | |
58 } else if (queryMode.id === 'att_num_range') { | |
59 options = this.filterAttributes(this.state.resultAttributes); | |
60 } | |
61 console.debug("getQueryOptions returns: ", options); | |
62 return options; | |
63 } | |
64 | |
65 /** | |
66 * fetch all object types from Neo4j and store in this.objectTypes. | |
67 */ | |
68 setupObjectTypes() { | |
69 let query = `MATCH (n) WITH DISTINCT labels(n) AS labels | |
70 UNWIND labels AS label | |
71 RETURN DISTINCT label ORDER BY label`; | |
72 | |
73 let res = this.fetchCypherResults([query]); | |
74 res.subscribe( | |
75 data => { | |
76 console.debug("neo4j data=", data); | |
77 this.objectTypes = data.results[0].data.map(elem => elem.row[0]).filter(elem => elem[0] != "_"); | |
78 console.debug("object types=", this.objectTypes); | |
79 }, | |
80 err => console.error("neo4j error=", err), | |
81 () => console.debug('neo4j query Complete') | |
82 ); | |
83 } | |
84 | |
85 /** | |
86 * Set the query step at index. | |
87 */ | |
88 setQueryStep(index: number, step: QueryStep) { | |
89 this.state.steps[index] = step; | |
90 } | |
91 | |
92 /** | |
93 * Create the cypher queries for the current query state. | |
94 * | |
95 * Updates the queries for results, attributes and relations. | |
96 */ | |
97 createCypherQuery() { | |
98 let queryMatch = ''; | |
99 let queryWhere = ''; | |
100 let queryReturn = ''; | |
101 let queryParams = {}; | |
102 let resultQuery = ''; | |
103 let attributesQuery = ''; | |
104 let outRelsQuery = ''; | |
105 let inRelsQuery = ''; | |
106 let returnType = ''; | |
107 let nIdx = 1; | |
108 this.state.steps.forEach((step, stepIdx) => { | |
109 let mode = step.mode.id; | |
110 let params = step.params; | |
111 | |
112 /* | |
113 * step: object type is | |
114 */ | |
115 if (mode === 'type_is') { | |
116 queryMatch = `MATCH (n${nIdx}:${params.objectType})`; | |
117 queryWhere = ''; | |
118 queryReturn = `RETURN n${nIdx}`; | |
119 returnType = 'node'; | |
120 } | |
121 | |
122 /* | |
123 * step: object id is | |
124 */ | |
125 if (mode === 'id_is') { | |
126 if (!queryMatch) { | |
127 // first step - use match clause | |
128 queryMatch = `MATCH (n${nIdx} {ismi_id: {att_val${stepIdx}}})`; | |
129 queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); | |
130 queryWhere = ''; | |
131 queryReturn = `RETURN n${nIdx}`; | |
132 returnType = 'node'; | |
133 } else { | |
134 // use where clause | |
135 if (!queryWhere) { | |
136 queryWhere = 'WHERE '; | |
137 } else { | |
138 queryWhere += ' AND '; | |
139 } | |
140 queryWhere += `n${nIdx}.ismi_id = {att_val${stepIdx}}`; | |
141 queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); | |
142 } | |
143 } | |
144 | |
145 /* | |
146 * step: relation type is | |
147 */ | |
148 if (mode === 'relation_is') { | |
149 nIdx += 1; | |
150 let rel = params.relationType; | |
151 if (rel.isOutgoing()) { | |
152 queryMatch += `-[:\`${rel.getRelType()}\`]->(n${nIdx})`; | |
153 } else { | |
154 // inverse relation | |
155 queryMatch += `<-[:\`${rel.getRelType()}\`]-(n${nIdx})`; | |
156 } | |
157 queryReturn = `RETURN DISTINCT n${nIdx}`; | |
158 returnType = 'node'; | |
159 } | |
160 | |
161 /* | |
162 * step: attribute contains(_norm) | |
163 */ | |
164 if (mode === 'att_contains' || mode === 'att_contains_norm') { | |
165 if (!queryWhere) { | |
166 queryWhere = 'WHERE '; | |
167 } else { | |
168 queryWhere += ' AND '; | |
169 } | |
170 if (params.attribute === 'ismi_id') { | |
171 // ismi_id is integer | |
172 queryWhere += `n${nIdx}.ismi_id = {att_val${stepIdx}}`; | |
173 queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); | |
174 } else { | |
175 if (mode === 'att_contains_norm') { | |
176 // match _n_attribute with normValue | |
177 queryWhere += `lower(n${nIdx}._n_${params.attribute}) CONTAINS lower({att_val${stepIdx}})`; | |
178 queryParams[`att_val${stepIdx}`] = params.normValue; | |
179 } else { | |
180 queryWhere += `lower(n${nIdx}.${params.attribute}) CONTAINS lower({att_val${stepIdx}})`; | |
181 queryParams[`att_val${stepIdx}`] = params.value; | |
182 } | |
183 } | |
184 } | |
185 | |
186 /* | |
187 * step: attribute number range | |
188 */ | |
189 if (mode === 'att_num_range') { | |
190 if (!queryWhere) { | |
191 queryWhere = 'WHERE '; | |
192 } else { | |
193 queryWhere += ' AND '; | |
194 } | |
195 queryWhere += `toint(n${nIdx}.${params.attribute}) >= toint({att_nlo${stepIdx}})` | |
196 + ` AND toint(n${nIdx}.${params.attribute}) <= toint({att_nhi${stepIdx}})`; | |
197 queryParams[`att_nlo${stepIdx}`] = params.numLo; | |
198 queryParams[`att_nhi${stepIdx}`] = params.numHi; | |
199 } | |
200 | |
201 }); | |
202 // compose query | |
203 resultQuery = queryMatch + (queryWhere ? '\n'+queryWhere : '') + '\n' + queryReturn; | |
204 // compose query for attributes of result | |
205 attributesQuery = queryMatch + ' ' + queryWhere + ` WITH DISTINCT keys(n${nIdx}) AS atts` | |
206 + ` UNWIND atts AS att RETURN DISTINCT att ORDER BY att`; | |
207 // compose query for relations of result | |
208 outRelsQuery = queryMatch + '-[r]->() ' + queryWhere + ' RETURN DISTINCT type(r)'; | |
209 inRelsQuery = queryMatch + '<-[r]-() ' + queryWhere + ' RETURN DISTINCT type(r)'; | |
210 this.state.resultCypherQuery = resultQuery; | |
211 this.state.cypherQueryParams = queryParams; | |
212 this.state.attributesCypherQuery = attributesQuery; | |
213 this.state.outRelsCypherQuery = outRelsQuery; | |
214 this.state.inRelsCypherQuery = inRelsQuery; | |
215 this.state.resultTypes = returnType; | |
216 } | |
217 | |
218 /** | |
219 * Create and run the cypher queries for the current query state. | |
220 * | |
221 * Updates the results and nextQuery attributes and relations. | |
222 */ | |
223 runQuery() { | |
224 this.createCypherQuery(); | |
225 this.state.resultInfo = 'loading...'; | |
226 /* | |
227 * run query for result table | |
228 */ | |
229 let queries = [this.state.resultCypherQuery]; | |
230 let params = [this.state.cypherQueryParams]; | |
231 if (this.state.attributesCypherQuery) { | |
232 queries.push(this.state.attributesCypherQuery); | |
233 params.push(this.state.cypherQueryParams); | |
234 } | |
235 if (this.state.outRelsCypherQuery) { | |
236 queries.push(this.state.outRelsCypherQuery); | |
237 params.push(this.state.cypherQueryParams); | |
238 } | |
239 if (this.state.inRelsCypherQuery) { | |
240 queries.push(this.state.inRelsCypherQuery); | |
241 params.push(this.state.cypherQueryParams); | |
242 } | |
243 let res = this.fetchCypherResults(queries, params); | |
244 res.subscribe( | |
245 data => { | |
246 console.debug("neo4j result data=", data); | |
247 let resIdx = 0; | |
248 /* | |
249 * results for result table | |
250 */ | |
251 this.state.results = data.results[resIdx].data.map(elem => elem.row[0]); | |
252 this.state.numResults = this.state.results.length; | |
253 // count all types | |
254 let resTypes = {}; | |
255 this.state.results.forEach((r) => { | |
256 let t = r[this.typeAttribute]; | |
257 if (resTypes[t] == null) { | |
258 resTypes[t] = 1; | |
259 } else { | |
260 resTypes[t] += 1; | |
261 } | |
262 }); | |
263 let info = ''; | |
264 for (let t in resTypes) { | |
265 info += t + '(' + resTypes[t] + ') '; | |
266 } | |
267 info = info.substr(0, info.length-1); | |
268 this.state.resultInfo = info; | |
269 // save info also in last step | |
270 this.state.steps[this.state.steps.length-1].resultInfo = info; | |
271 /* | |
272 * results for attribute list | |
273 */ | |
274 if (this.state.attributesCypherQuery) { | |
275 resIdx += 1; | |
276 let atts = data.results[resIdx].data.map(elem => elem.row[0]); | |
277 this.state.resultAttributes = atts; | |
278 // the following assumes only one type in the result | |
279 for (let t in resTypes) { | |
280 this.state.resultType = getResultType(t, ISMI_RESULT_TYPES); | |
281 break; | |
282 } | |
283 this.state.resultColumns = this.state.resultType.getColumns(atts); | |
284 } | |
285 /* | |
286 * results for relations list | |
287 */ | |
288 if (this.state.outRelsCypherQuery) { | |
289 // outgoing aka forward relations | |
290 resIdx += 1; | |
291 let rels = data.results[resIdx].data.map(elem => elem.row[0]) | |
292 .filter(elem => elem[0] != "_") | |
293 .map(elem => getRelationType(elem, true)); | |
294 this.state.resultRelations = rels; | |
295 } | |
296 if (this.state.inRelsCypherQuery) { | |
297 // incoming aka reverse relations | |
298 resIdx += 1; | |
299 let rels = data.results[resIdx].data.map(elem => elem.row[0]) | |
300 .filter(elem => elem[0] != "_") | |
301 .map(elem => getRelationType(elem, false)); | |
302 this.state.resultRelations = this.state.resultRelations.concat(rels); | |
303 } | |
304 }, | |
305 err => console.error("neo4j result error=", err), | |
306 () => console.debug('neo4j result query Complete') | |
307 ); | |
308 } | |
309 | |
310 | |
311 filterAttributes(attributes: string[], normalized=false) { | |
312 let atts = []; | |
313 if (normalized) { | |
314 attributes.forEach((att) => { | |
315 if (att.substr(0, 3) == "_n_") { | |
316 atts.push(att.substr(3)); | |
317 } | |
318 }); | |
319 } else { | |
320 atts = attributes.filter(elem => elem[0] != "_" && !this.excludedAttributes[elem]); | |
321 } | |
322 return atts; | |
323 } | |
324 | |
325 /** | |
326 * Run the given queries on the Neo4J server. | |
327 * | |
328 * Returns an Observable with the results. | |
329 */ | |
330 fetchCypherResults(queries: string[], params=[{}]) { | |
331 console.debug("fetching cypher queries: ", queries); | |
332 let headers = new Headers(); | |
333 let auth = NEO4J_AUTHENTICATION; | |
334 headers.append('Authorization', 'Basic ' + btoa(`${auth.user}:${auth.password}`)); | |
335 headers.append('Content-Type', 'application/json'); | |
336 headers.append('Accept', 'application/json'); | |
337 // put headers in options | |
338 let opts = {'headers': headers}; | |
339 // unpack queries into statements | |
340 let statements = queries.map((q, i) => { | |
341 return {'statement': q, 'parameters': (params[i])?params[i]:{}}; | |
342 }); | |
343 // create POST data from query | |
344 let data = JSON.stringify({'statements': statements}); | |
345 // make post request asynchronously | |
346 let resp = this._http.post(NEO4J_BASE_URL+'/transaction/commit', data, opts) | |
347 // filter result as JSON | |
348 .map(res => res.json()); | |
349 // return Observable | |
350 return resp; | |
351 } | |
352 | |
353 } |