| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 | /* * EliasDB * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */package v1import (	"encoding/json"	"fmt"	"net/http"	"sort"	"strconv"	"devt.de/krotik/eliasdb/api"	"devt.de/krotik/eliasdb/graph"	"devt.de/krotik/eliasdb/graph/data")/*EndpointGraph is the graph endpoint URL (rooted). Handles everything under graph/...*/const EndpointGraph = api.APIRoot + APIv1 + "/graph/"/*GraphEndpointInst creates a new endpoint handler.*/func GraphEndpointInst() api.RestEndpointHandler {	return &graphEndpoint{}}/*Handler object for graph operations.*/type graphEndpoint struct {	*api.DefaultEndpointHandler}/*HandleGET handles REST calls to retrieve data from the graph database.*/func (ge *graphEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resources []string) {	// Check parameters	if !checkResources(w, resources, 3, 5, "Need a partition, entity type (n or e) and a kind; optional key and traversal spec") {		return	}	if resources[1] != "n" && resources[1] != "e" {		http.Error(w, "Entity type must be n (nodes) or e (edges)", http.StatusBadRequest)		return	}	if len(resources) == 3 {		// Iterate over a list of nodes		if resources[1] == "n" {			// Get limit parameter; -1 if not set			limit, ok := queryParamPosNum(w, r, "limit")			if !ok {				return			}			// Get offset parameter; -1 if not set			offset, ok := queryParamPosNum(w, r, "offset")			if !ok {				return			}			it, err := api.GM.NodeKeyIterator(resources[0], resources[2])			if err != nil {				http.Error(w, err.Error(), http.StatusInternalServerError)				return			} else if it == nil {				http.Error(w, "Unknown partition or node kind", http.StatusBadRequest)				return			}			i := 0			if offset != -1 {				for i = 0; i < offset; i++ {					if !it.HasNext() {						http.Error(w, "Offset exceeds available nodes", http.StatusInternalServerError)						return					}					if it.Next(); it.LastError != nil {						http.Error(w, it.LastError.Error(), http.StatusInternalServerError)						return					}				}			} else {				offset = 0			}			var data []interface{}			if limit == -1 {				data = make([]interface{}, 0)			} else {				data = make([]interface{}, 0, limit)			}			for i = offset; it.HasNext(); i++ {				// Break out if the limit was reached				if limit != -1 && i > offset+limit-1 {					break				}				key := it.Next()				if it.LastError != nil {					http.Error(w, it.LastError.Error(), http.StatusInternalServerError)					return				}				node, err := api.GM.FetchNode(resources[0], key, resources[2])				if err != nil {					http.Error(w, err.Error(), http.StatusInternalServerError)					return				}				data = append(data, node.Data())			}			// Set total count header			w.Header().Add(HTTPHeaderTotalCount, strconv.FormatUint(api.GM.NodeCount(resources[2]), 10))			// Write data			w.Header().Set("content-type", "application/json; charset=utf-8")			ret := json.NewEncoder(w)			ret.Encode(data)		} else {			http.Error(w, "Entity type must be n (nodes) when requesting all items", http.StatusBadRequest)			return		}	} else if len(resources) == 4 {		// Fetch a specific node or relationship		var data map[string]interface{}		if resources[1] == "n" {			node, err := api.GM.FetchNode(resources[0], resources[3], resources[2])			if err != nil {				http.Error(w, err.Error(), http.StatusInternalServerError)				return			} else if node == nil {				http.Error(w, "Unknown partition or node kind", http.StatusBadRequest)				return			}			data = node.Data()		} else {			edge, err := api.GM.FetchEdge(resources[0], resources[3], resources[2])			if err != nil {				http.Error(w, err.Error(), http.StatusInternalServerError)				return			} else if edge == nil {				http.Error(w, "Unknown partition or edge kind", http.StatusBadRequest)				return			}			data = edge.Data()		}		// Write data		w.Header().Set("content-type", "application/json; charset=utf-8")		ret := json.NewEncoder(w)		ret.Encode(data)	} else {		if resources[1] == "n" {			node, err := api.GM.FetchNodePart(resources[0], resources[3], resources[2], []string{"key", "kind"})			if err != nil {				http.Error(w, err.Error(), http.StatusInternalServerError)				return			} else if node == nil {				http.Error(w, "Unknown partition or node kind", http.StatusBadRequest)				return			}			nodes, edges, err := api.GM.TraverseMulti(resources[0], resources[3],				resources[2], resources[4], true)			if err != nil {				http.Error(w, err.Error(), http.StatusInternalServerError)				return			}			data := make([][]map[string]interface{}, 2)			dataNodes := make([]map[string]interface{}, 0, len(nodes))			dataEdges := make([]map[string]interface{}, 0, len(edges))			if nodes != nil && edges != nil {				for i, n := range nodes {					e := edges[i]					dataNodes = append(dataNodes, n.Data())					dataEdges = append(dataEdges, e.Data())				}			}			data[0] = dataNodes			data[1] = dataEdges			// Sort the result			sort.Stable(&traversalResultComparator{data})			// Write data			w.Header().Set("content-type", "application/json; charset=utf-8")			ret := json.NewEncoder(w)			ret.Encode(data)		} else {			http.Error(w, "Entity type must be n (nodes) when requesting traversal results", http.StatusBadRequest)			return		}	}}/*HandlePUT handles a REST call to insert new elements into the graph or updateexisting elements. Nodes are updated if they already exist. Edges are replacedif they already exist.*/func (ge *graphEndpoint) HandlePUT(w http.ResponseWriter, r *http.Request, resources []string) {	ge.handleGraphRequest(w, r, resources,		func(trans graph.Trans, part string, node data.Node) error {			return trans.UpdateNode(part, node)		},		func(trans graph.Trans, part string, edge data.Edge) error {			return trans.StoreEdge(part, edge)		})}/*HandlePOST handles a REST call to insert new elements into the graph or updateexisting elements. Nodes and edges are replaced if they already exist.*/func (ge *graphEndpoint) HandlePOST(w http.ResponseWriter, r *http.Request, resources []string) {	ge.handleGraphRequest(w, r, resources,		func(trans graph.Trans, part string, node data.Node) error {			return trans.StoreNode(part, node)		},		func(trans graph.Trans, part string, edge data.Edge) error {			return trans.StoreEdge(part, edge)		})}/*HandleDELETE handles a REST call to delete elements from the graph.*/func (ge *graphEndpoint) HandleDELETE(w http.ResponseWriter, r *http.Request, resources []string) {	ge.handleGraphRequest(w, r, resources,		func(trans graph.Trans, part string, node data.Node) error {			return trans.RemoveNode(part, node.Key(), node.Kind())		},		func(trans graph.Trans, part string, edge data.Edge) error {			return trans.RemoveEdge(part, edge.Key(), edge.Kind())		})}/*handleGraphRequest handles a graph query REST call.*/func (ge *graphEndpoint) handleGraphRequest(w http.ResponseWriter, r *http.Request, resources []string,	transFuncNode func(trans graph.Trans, part string, node data.Node) error,	transFuncEdge func(trans graph.Trans, part string, edge data.Edge) error) {	var nDataList []map[string]interface{}	var eDataList []map[string]interface{}	// Check parameters	if !checkResources(w, resources, 1, 2, "Need a partition; optional entity type (n or e)") {		return	}	dec := json.NewDecoder(r.Body)	if len(resources) == 1 {		// No explicit type given - expecting a graph		gdata := make(map[string][]map[string]interface{})		if err := dec.Decode(&gdata); err != nil {			http.Error(w, "Could not decode request body as object with list of nodes and/or edges: "+err.Error(), http.StatusBadRequest)			return		}		nDataList = gdata["nodes"]		eDataList = gdata["edges"]	} else if resources[1] == "n" {		nDataList = make([]map[string]interface{}, 1)		if err := dec.Decode(&nDataList); err != nil {			http.Error(w, "Could not decode request body as list of nodes: "+err.Error(), http.StatusBadRequest)			return		}	} else if resources[1] == "e" {		eDataList = make([]map[string]interface{}, 1)		if err := dec.Decode(&eDataList); err != nil {			http.Error(w, "Could not decode request body as list of edges: "+err.Error(), http.StatusBadRequest)			return		}	}	// Create a transaction	trans := graph.NewGraphTrans(api.GM)	if nDataList != nil {		// Store nodes in transaction		for _, ndata := range nDataList {			node := data.NewGraphNodeFromMap(ndata)			if err := transFuncNode(trans, resources[0], node); err != nil {				http.Error(w, err.Error(), http.StatusBadRequest)				return			}		}	}	if eDataList != nil {		// Store edges in transaction		for _, edata := range eDataList {			edge := data.NewGraphEdgeFromNode(data.NewGraphNodeFromMap(edata))			if err := transFuncEdge(trans, resources[0], edge); err != nil {				http.Error(w, err.Error(), http.StatusBadRequest)				return			}		}	}	// Commit transaction	if err := trans.Commit(); err != nil {		http.Error(w, err.Error(), http.StatusInternalServerError)		return	}}/*SwaggerDefs is used to describe the endpoint in swagger.*/func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {	partitionParams := []map[string]interface{}{		{			"name":        "partition",			"in":          "path",			"description": "Partition to select.",			"required":    true,			"type":        "string",		},	}	entityParams := []map[string]interface{}{		{			"name": "entity_type",			"in":   "path",			"description": "Datastore entity type which should selected. " +				"Either n for nodes or e for edges.",			"required": true,			"type":     "string",		},	}	defaultParams := []map[string]interface{}{		{			"name":        "kind",			"in":          "path",			"description": "Node or edge kind to be queried.",			"required":    true,			"type":        "string",		},	}	defaultParams = append(defaultParams, partitionParams...)	defaultParams = append(defaultParams, entityParams...)	optionalQueryParams := []map[string]interface{}{		{			"name":        "limit",			"in":          "query",			"description": "How many list items to return.",			"required":    false,			"type":        "number",			"format":      "integer",		},		{			"name":        "offset",			"in":          "query",			"description": "Offset in the dataset.",			"required":    false,			"type":        "number",			"format":      "integer",		},	}	keyParam := []map[string]interface{}{		{			"name":        "key",			"in":          "path",			"description": "Node or edge key to be queried.",			"required":    true,			"type":        "string",		},	}	travParam := []map[string]interface{}{		{			"name":        "traversal_spec",			"in":          "path",			"description": "Traversal to be followed from a single node.",			"required":    true,			"type":        "string",		},	}	graphPost := []map[string]interface{}{		{			"name":        "entities",			"in":          "body",			"description": "Nodes and Edges which should be stored",			"required":    true,			"schema": map[string]interface{}{				"type": "object",				"properties": map[string]interface{}{					"nodes": map[string]interface{}{						"description": "List of nodes to be inserted / updated.",						"type":        "array",						"items": map[string]interface{}{							"description": "Node to be inserted / updated.",							"type":        "object",						},					},					"edges": map[string]interface{}{						"description": "List of edges to be inserted / updated.",						"type":        "array",						"items": map[string]interface{}{							"description": "Edge to be inserted / updated.",							"type":        "object",						},					},				},			},		},	}	entitiesPost := []map[string]interface{}{		{			"name":        "entities",			"in":          "body",			"description": "Nodes or Edges which should be stored",			"required":    true,			"schema": map[string]interface{}{				"type": "array",				"items": map[string]interface{}{					"description": "Node or edge to be inserted / updated.",					"type":        "object",				},			},		},	}	defaultError := map[string]interface{}{		"description": "Error response",		"schema": map[string]interface{}{			"$ref": "#/definitions/Error",		},	}	// Add endpoint to insert a graph with nodes and edges	s["paths"].(map[string]interface{})["/v1/graph/{partition}"] = map[string]interface{}{		"post": map[string]interface{}{			"summary": "Data can be send by using POST requests.",			"description": "A whole graph can be send. " +				"POST will store data in the datastore and always overwrite any existing data.",			"consumes": []string{				"application/json",			},			"produces": []string{				"text/plain",				"application/json",			},			"parameters": append(partitionParams, graphPost...),			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "No data is returned when data is created.",				},				"default": defaultError,			},		},	}	// Add endpoint to insert nodes / edges	s["paths"].(map[string]interface{})["/v1/graph/{partition}/{entity_type}"] = map[string]interface{}{		"post": map[string]interface{}{			"summary": "Data can be send by using POST requests.",			"description": "A list of nodes / edges can be send. " +				"POST will store data in the datastore and always overwrite any existing data.",			"consumes": []string{				"application/json",			},			"produces": []string{				"text/plain",				"application/json",			},			"parameters": append(append(partitionParams, entityParams...), entitiesPost...),			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "No data is returned when data is created.",				},				"default": defaultError,			},		},	}	// Add endpoint to query nodes for a specific node kind	s["paths"].(map[string]interface{})["/v1/graph/{partition}/{entity_type}/{kind}"] = map[string]interface{}{		"get": map[string]interface{}{			"summary": "The graph endpoint is the main entry point to request data.",			"description": "GET requests can be used to query a series of nodes. " +				"The X-Total-Count header contains the total number of nodes which were found.",			"produces": []string{				"text/plain",				"application/json",			},			"parameters": append(defaultParams, optionalQueryParams...),			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "The return data is a list of objects",					"schema": map[string]interface{}{						"type": "array",						"items": map[string]interface{}{							"type": "object",						},					},				},				"default": defaultError,			},		},	}	// Add endpoint to query/create a specific node	s["paths"].(map[string]interface{})["/v1/graph/{partition}/{entity_type}/{kind}/{key}"] = map[string]interface{}{		"get": map[string]interface{}{			"summary":     "The graph endpoint is the main entry point to request data.",			"description": "GET requests can be used to query a single node.",			"produces": []string{				"text/plain",				"application/json",			},			"parameters": append(append(defaultParams, keyParam...), optionalQueryParams...),			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "The return data is a single object",					"schema": map[string]interface{}{						"type": "object",					},				},				"default": defaultError,			},		},	}	// Add endpoint to traverse from a single node	s["paths"].(map[string]interface{})["/v1/graph/{partition}/{entity_type}/{kind}/{key}/{traversal_spec}"] = map[string]interface{}{		"get": map[string]interface{}{			"summary":     "The graph endpoint is the main entry point to request data.",			"description": "GET requests can be used to query a single node and then traverse to its neighbours.",			"produces": []string{				"text/plain",				"application/json",			},			"parameters": append(append(defaultParams, keyParam...), travParam...),			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "The return data are two lists containing traversed nodes and edges. " +						"The traversal endpoint does NOT support limit and offset parameters. " +						"Also the X-Total-Count header is not set.",					"schema": map[string]interface{}{						"type": "array",						"items": map[string]interface{}{							"type": "array",							"items": map[string]interface{}{								"type": "object",							},						},					},				},				"default": defaultError,			},		},	}}// Comparator object to sort traversal resultstype traversalResultComparator struct {	Data [][]map[string]interface{} // Data to sort}func (c traversalResultComparator) Len() int {	return len(c.Data[0])}func (c traversalResultComparator) Less(i, j int) bool {	c1 := c.Data[0][i]	c2 := c.Data[0][j]	return fmt.Sprintf("%v", c1[data.NodeKey]) < fmt.Sprintf("%v", c2[data.NodeKey])}func (c traversalResultComparator) Swap(i, j int) {	c.Data[0][i], c.Data[0][j] = c.Data[0][j], c.Data[0][i]	c.Data[1][i], c.Data[1][j] = c.Data[1][j], c.Data[1][i]}
 |