Mercurial > hg > ng2-query-ismi
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/query.service.ts Fri Mar 17 20:16:52 2017 +0100 @@ -0,0 +1,353 @@ +import {Injectable} from '@angular/core'; +import {Http, Headers} from '@angular/http'; + +import 'rxjs/Rx'; // import all RxJS operators +//import 'rxjs/add/operator/map'; + +import {NEO4J_BASE_URL, NEO4J_AUTHENTICATION} from './app-config'; +import {QueryMode, QUERY_MODES, FIRST_QUERY_MODES} from './query-mode'; +import {QueryState} from './query-state'; +import {QueryStep} from './query-step'; +import {getResultType} from './result-type'; +import {ISMI_RESULT_TYPES} from './ismi-result-types'; +import {getRelationType} from './ismi-relation-types'; + +@Injectable() +export class QueryService { + + public typeAttribute = '_type'; + public excludedAttributes = {}; + public state: QueryState; + public objectTypes: string[]; + + constructor(private _http: Http) { + // init query state + this.state = new QueryState(); + } + + setup() { + this.setupObjectTypes(); + } + + getState() { + return this.state; + } + + getQueryModes(index: number): QueryMode[] { + if (index == 0) { + return FIRST_QUERY_MODES; + } else { + return QUERY_MODES; + } + } + + /** + * return the first set of options for the given query mode. + */ + getQueryOptions(queryMode: QueryMode) { + let options: any[] = []; + if (queryMode == null) return options; + if (queryMode.id === 'type_is') { + options = this.objectTypes; + } else if (queryMode.id === 'relation_is') { + options = this.state.resultRelations; + } else if (queryMode.id === 'att_contains') { + options = this.filterAttributes(this.state.resultAttributes); + } else if (queryMode.id === 'att_contains_norm') { + options = this.filterAttributes(this.state.resultAttributes, true); + } else if (queryMode.id === 'att_num_range') { + options = this.filterAttributes(this.state.resultAttributes); + } + console.debug("getQueryOptions returns: ", options); + return options; + } + + /** + * fetch all object types from Neo4j and store in this.objectTypes. + */ + setupObjectTypes() { + let query = `MATCH (n) WITH DISTINCT labels(n) AS labels + UNWIND labels AS label + RETURN DISTINCT label ORDER BY label`; + + let res = this.fetchCypherResults([query]); + res.subscribe( + data => { + console.debug("neo4j data=", data); + this.objectTypes = data.results[0].data.map(elem => elem.row[0]).filter(elem => elem[0] != "_"); + console.debug("object types=", this.objectTypes); + }, + err => console.error("neo4j error=", err), + () => console.debug('neo4j query Complete') + ); + } + + /** + * Set the query step at index. + */ + setQueryStep(index: number, step: QueryStep) { + this.state.steps[index] = step; + } + + /** + * Create the cypher queries for the current query state. + * + * Updates the queries for results, attributes and relations. + */ + createCypherQuery() { + let queryMatch = ''; + let queryWhere = ''; + let queryReturn = ''; + let queryParams = {}; + let resultQuery = ''; + let attributesQuery = ''; + let outRelsQuery = ''; + let inRelsQuery = ''; + let returnType = ''; + let nIdx = 1; + this.state.steps.forEach((step, stepIdx) => { + let mode = step.mode.id; + let params = step.params; + + /* + * step: object type is + */ + if (mode === 'type_is') { + queryMatch = `MATCH (n${nIdx}:${params.objectType})`; + queryWhere = ''; + queryReturn = `RETURN n${nIdx}`; + returnType = 'node'; + } + + /* + * step: object id is + */ + if (mode === 'id_is') { + if (!queryMatch) { + // first step - use match clause + queryMatch = `MATCH (n${nIdx} {ismi_id: {att_val${stepIdx}}})`; + queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); + queryWhere = ''; + queryReturn = `RETURN n${nIdx}`; + returnType = 'node'; + } else { + // use where clause + if (!queryWhere) { + queryWhere = 'WHERE '; + } else { + queryWhere += ' AND '; + } + queryWhere += `n${nIdx}.ismi_id = {att_val${stepIdx}}`; + queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); + } + } + + /* + * step: relation type is + */ + if (mode === 'relation_is') { + nIdx += 1; + let rel = params.relationType; + if (rel.isOutgoing()) { + queryMatch += `-[:\`${rel.getRelType()}\`]->(n${nIdx})`; + } else { + // inverse relation + queryMatch += `<-[:\`${rel.getRelType()}\`]-(n${nIdx})`; + } + queryReturn = `RETURN DISTINCT n${nIdx}`; + returnType = 'node'; + } + + /* + * step: attribute contains(_norm) + */ + if (mode === 'att_contains' || mode === 'att_contains_norm') { + if (!queryWhere) { + queryWhere = 'WHERE '; + } else { + queryWhere += ' AND '; + } + if (params.attribute === 'ismi_id') { + // ismi_id is integer + queryWhere += `n${nIdx}.ismi_id = {att_val${stepIdx}}`; + queryParams[`att_val${stepIdx}`] = parseInt(params.value, 10); + } else { + if (mode === 'att_contains_norm') { + // match _n_attribute with normValue + queryWhere += `lower(n${nIdx}._n_${params.attribute}) CONTAINS lower({att_val${stepIdx}})`; + queryParams[`att_val${stepIdx}`] = params.normValue; + } else { + queryWhere += `lower(n${nIdx}.${params.attribute}) CONTAINS lower({att_val${stepIdx}})`; + queryParams[`att_val${stepIdx}`] = params.value; + } + } + } + + /* + * step: attribute number range + */ + if (mode === 'att_num_range') { + if (!queryWhere) { + queryWhere = 'WHERE '; + } else { + queryWhere += ' AND '; + } + queryWhere += `toint(n${nIdx}.${params.attribute}) >= toint({att_nlo${stepIdx}})` + + ` AND toint(n${nIdx}.${params.attribute}) <= toint({att_nhi${stepIdx}})`; + queryParams[`att_nlo${stepIdx}`] = params.numLo; + queryParams[`att_nhi${stepIdx}`] = params.numHi; + } + + }); + // compose query + resultQuery = queryMatch + (queryWhere ? '\n'+queryWhere : '') + '\n' + queryReturn; + // compose query for attributes of result + attributesQuery = queryMatch + ' ' + queryWhere + ` WITH DISTINCT keys(n${nIdx}) AS atts` + + ` UNWIND atts AS att RETURN DISTINCT att ORDER BY att`; + // compose query for relations of result + outRelsQuery = queryMatch + '-[r]->() ' + queryWhere + ' RETURN DISTINCT type(r)'; + inRelsQuery = queryMatch + '<-[r]-() ' + queryWhere + ' RETURN DISTINCT type(r)'; + this.state.resultCypherQuery = resultQuery; + this.state.cypherQueryParams = queryParams; + this.state.attributesCypherQuery = attributesQuery; + this.state.outRelsCypherQuery = outRelsQuery; + this.state.inRelsCypherQuery = inRelsQuery; + this.state.resultTypes = returnType; + } + + /** + * Create and run the cypher queries for the current query state. + * + * Updates the results and nextQuery attributes and relations. + */ + runQuery() { + this.createCypherQuery(); + this.state.resultInfo = 'loading...'; + /* + * run query for result table + */ + let queries = [this.state.resultCypherQuery]; + let params = [this.state.cypherQueryParams]; + if (this.state.attributesCypherQuery) { + queries.push(this.state.attributesCypherQuery); + params.push(this.state.cypherQueryParams); + } + if (this.state.outRelsCypherQuery) { + queries.push(this.state.outRelsCypherQuery); + params.push(this.state.cypherQueryParams); + } + if (this.state.inRelsCypherQuery) { + queries.push(this.state.inRelsCypherQuery); + params.push(this.state.cypherQueryParams); + } + let res = this.fetchCypherResults(queries, params); + res.subscribe( + data => { + console.debug("neo4j result data=", data); + let resIdx = 0; + /* + * results for result table + */ + this.state.results = data.results[resIdx].data.map(elem => elem.row[0]); + this.state.numResults = this.state.results.length; + // count all types + let resTypes = {}; + this.state.results.forEach((r) => { + let t = r[this.typeAttribute]; + if (resTypes[t] == null) { + resTypes[t] = 1; + } else { + resTypes[t] += 1; + } + }); + let info = ''; + for (let t in resTypes) { + info += t + '(' + resTypes[t] + ') '; + } + info = info.substr(0, info.length-1); + this.state.resultInfo = info; + // save info also in last step + this.state.steps[this.state.steps.length-1].resultInfo = info; + /* + * results for attribute list + */ + if (this.state.attributesCypherQuery) { + resIdx += 1; + let atts = data.results[resIdx].data.map(elem => elem.row[0]); + this.state.resultAttributes = atts; + // the following assumes only one type in the result + for (let t in resTypes) { + this.state.resultType = getResultType(t, ISMI_RESULT_TYPES); + break; + } + this.state.resultColumns = this.state.resultType.getColumns(atts); + } + /* + * results for relations list + */ + if (this.state.outRelsCypherQuery) { + // outgoing aka forward relations + resIdx += 1; + let rels = data.results[resIdx].data.map(elem => elem.row[0]) + .filter(elem => elem[0] != "_") + .map(elem => getRelationType(elem, true)); + this.state.resultRelations = rels; + } + if (this.state.inRelsCypherQuery) { + // incoming aka reverse relations + resIdx += 1; + let rels = data.results[resIdx].data.map(elem => elem.row[0]) + .filter(elem => elem[0] != "_") + .map(elem => getRelationType(elem, false)); + this.state.resultRelations = this.state.resultRelations.concat(rels); + } + }, + err => console.error("neo4j result error=", err), + () => console.debug('neo4j result query Complete') + ); + } + + + filterAttributes(attributes: string[], normalized=false) { + let atts = []; + if (normalized) { + attributes.forEach((att) => { + if (att.substr(0, 3) == "_n_") { + atts.push(att.substr(3)); + } + }); + } else { + atts = attributes.filter(elem => elem[0] != "_" && !this.excludedAttributes[elem]); + } + return atts; + } + + /** + * Run the given queries on the Neo4J server. + * + * Returns an Observable with the results. + */ + fetchCypherResults(queries: string[], params=[{}]) { + console.debug("fetching cypher queries: ", queries); + let headers = new Headers(); + let auth = NEO4J_AUTHENTICATION; + headers.append('Authorization', 'Basic ' + btoa(`${auth.user}:${auth.password}`)); + headers.append('Content-Type', 'application/json'); + headers.append('Accept', 'application/json'); + // put headers in options + let opts = {'headers': headers}; + // unpack queries into statements + let statements = queries.map((q, i) => { + return {'statement': q, 'parameters': (params[i])?params[i]:{}}; + }); + // create POST data from query + let data = JSON.stringify({'statements': statements}); + // make post request asynchronously + let resp = this._http.post(NEO4J_BASE_URL+'/transaction/commit', data, opts) + // filter result as JSON + .map(res => res.json()); + // return Observable + return resp; + } + +} \ No newline at end of file