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