Browse Source

BREAKING CHANGE: Restructure EliasDB code for go modules / Adding GraphQL interface

Matthias Ladkau 7 months ago
parent
commit
65c38db59e
100 changed files with 1748 additions and 286 deletions
  1. 8 0
      .gitignore
  2. 27 0
      .goreleaser.yml
  3. 158 0
      Jenkinsfile
  4. 9 0
      NOTICE
  5. 8 12
      README.md
  6. 2 3
      src/devt.de/eliasdb/api/about.go
  7. 5 5
      src/devt.de/eliasdb/api/ac/access.go
  8. 7 7
      src/devt.de/eliasdb/api/ac/access_test.go
  9. 5 5
      src/devt.de/eliasdb/api/ac/login.go
  10. 0 0
      api/ac/login_test.go
  11. 2 2
      src/devt.de/eliasdb/api/ac/logout.go
  12. 1 1
      src/devt.de/eliasdb/api/ac/logout_test.go
  13. 3 3
      src/devt.de/eliasdb/api/ac/user.go
  14. 0 0
      api/ac/user_test.go
  15. 4 4
      src/devt.de/eliasdb/api/rest.go
  16. 3 4
      src/devt.de/eliasdb/api/rest_test.go
  17. 0 0
      api/swagger.go
  18. 12 2
      src/devt.de/eliasdb/api/v1/blob.go
  19. 1 1
      src/devt.de/eliasdb/api/v1/blob_test.go
  20. 1 1
      src/devt.de/eliasdb/api/v1/cluster.go
  21. 5 5
      src/devt.de/eliasdb/api/v1/cluster_test.go
  22. 3 3
      src/devt.de/eliasdb/api/v1/eql.go
  23. 0 0
      src/devt.de/eliasdb/api/v1/eql_test.go
  24. 4 4
      src/devt.de/eliasdb/api/v1/find.go
  25. 0 0
      api/v1/find_test.go
  26. 3 3
      src/devt.de/eliasdb/api/v1/graph.go
  27. 6 6
      src/devt.de/eliasdb/api/v1/graph_test.go
  28. 159 0
      api/v1/graphql-query.go
  29. 241 0
      api/v1/graphql-subscriptions.go
  30. 303 0
      api/v1/graphql-subscriptions_test.go
  31. 150 0
      api/v1/graphql.go
  32. 177 0
      api/v1/graphql_test.go
  33. 2 2
      src/devt.de/eliasdb/api/v1/index.go
  34. 2 2
      src/devt.de/eliasdb/api/v1/index_test.go
  35. 1 1
      src/devt.de/eliasdb/api/v1/info.go
  36. 0 0
      api/v1/info_test.go
  37. 5 5
      src/devt.de/eliasdb/api/v1/query.go
  38. 0 0
      api/v1/query_test.go
  39. 6 6
      src/devt.de/eliasdb/api/v1/queryresult.go
  40. 3 3
      src/devt.de/eliasdb/api/v1/queryresult_test.go
  41. 13 10
      src/devt.de/eliasdb/api/v1/rest.go
  42. 24 6
      src/devt.de/eliasdb/api/v1/rest_test.go
  43. 9 10
      src/devt.de/eliasdb/cli/eliasdb.go
  44. 4 4
      src/devt.de/eliasdb/cluster/distributedstorage.go
  45. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_fetch_test.go
  46. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_free_test.go
  47. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_insert_test.go
  48. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_maindb_test.go
  49. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_root_test.go
  50. 3 3
      src/devt.de/eliasdb/cluster/distributedstorage_test.go
  51. 1 1
      src/devt.de/eliasdb/cluster/distributedstorage_update_test.go
  52. 2 2
      src/devt.de/eliasdb/cluster/distributedstoragemanager.go
  53. 0 0
      cluster/distributiontable.go
  54. 0 0
      cluster/distributiontable_test.go
  55. 1 1
      src/devt.de/eliasdb/cluster/manager/client.go
  56. 4 4
      src/devt.de/eliasdb/cluster/manager/config.go
  57. 1 1
      src/devt.de/eliasdb/cluster/manager/config_test.go
  58. 0 0
      cluster/manager/globals.go
  59. 0 0
      cluster/manager/housekeeping.go
  60. 1 1
      src/devt.de/eliasdb/cluster/manager/manager.go
  61. 0 0
      cluster/manager/manager_test.go
  62. 0 0
      cluster/manager/managerserver_test.go
  63. 1 1
      src/devt.de/eliasdb/cluster/manager/server.go
  64. 4 4
      src/devt.de/eliasdb/cluster/memberaddresstable.go
  65. 2 2
      src/devt.de/eliasdb/cluster/memberaddresstable_test.go
  66. 5 5
      src/devt.de/eliasdb/cluster/memberstorage.go
  67. 2 2
      src/devt.de/eliasdb/cluster/rebalance.go
  68. 1 1
      src/devt.de/eliasdb/cluster/rebalance_test.go
  69. 0 0
      cluster/request.go
  70. 3 3
      src/devt.de/eliasdb/cluster/transfer.go
  71. 7 2
      src/devt.de/eliasdb/config/config.go
  72. 0 0
      config/config_test.go
  73. 5 5
      src/devt.de/eliasdb/console/cmd_base.go
  74. 2 2
      src/devt.de/eliasdb/console/cmd_graph.go
  75. 1 3
      src/devt.de/eliasdb/console/cmd_graph_test.go
  76. 2 2
      src/devt.de/eliasdb/console/cmd_users.go
  77. 1 3
      src/devt.de/eliasdb/console/cmd_users_test.go
  78. 36 30
      src/devt.de/eliasdb/console/console.go
  79. 15 17
      src/devt.de/eliasdb/console/console_test.go
  80. 2 2
      src/devt.de/eliasdb/console/eqlconsole.go
  81. 1 3
      src/devt.de/eliasdb/console/eqlconsole_test.go
  82. 73 0
      console/graphqlconsole.go
  83. 132 0
      console/graphqlconsole_test.go
  84. 0 1
      doc/swagger.json
  85. 0 0
      eliasdb_design.md
  86. 2 3
      doc/embedding.md
  87. 25 25
      doc/eql.md
  88. 4 4
      src/devt.de/eliasdb/eql/interpreter/func.go
  89. 0 0
      eql/interpreter/func_test.go
  90. 2 2
      src/devt.de/eliasdb/eql/interpreter/get.go
  91. 3 3
      src/devt.de/eliasdb/eql/interpreter/helpers.go
  92. 1 1
      src/devt.de/eliasdb/eql/interpreter/helpers_test.go
  93. 2 2
      src/devt.de/eliasdb/eql/interpreter/lookup.go
  94. 3 3
      src/devt.de/eliasdb/eql/interpreter/nodeinfo.go
  95. 3 3
      src/devt.de/eliasdb/eql/interpreter/runtime.go
  96. 7 7
      src/devt.de/eliasdb/eql/interpreter/runtime_test.go
  97. 1 1
      src/devt.de/eliasdb/eql/interpreter/runtimeerror.go
  98. 1 1
      src/devt.de/eliasdb/eql/interpreter/searchresult.go
  99. 5 5
      src/devt.de/eliasdb/eql/interpreter/searchresult_test.go
  100. 0 0
      src/devt.de/eliasdb/eql/interpreter/traversal.go

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+.cache
+.cover
+coverage.txt
+coverage.out
+coverage.html
+test
+dist
+build

+ 27 - 0
.goreleaser.yml

@@ -0,0 +1,27 @@
+# This is an example goreleaser.yaml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+before:
+  hooks:
+    - go mod download
+builds:
+- main: ./cli/eliasdb.go
+  env:
+  - CGO_ENABLED=0
+  goos:
+    - windows
+    - linux
+  goarch:
+    - amd64
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ .Tag }}"
+changelog:
+  sort: asc
+  filters:
+    exclude:
+    - '^docs:'
+    - '^test:'
+
+# Run with: 
+# docker run --rm --user $(id -u):$(id -g) -v $PWD/.cache:/.cache -v $PWD:/go/code -w /go/code goreleaser/goreleaser --snapshot --skip-publish --rm-dist'

+ 158 - 0
Jenkinsfile

@@ -0,0 +1,158 @@
+pipeline {
+    agent any
+
+    /**
+     * Build file for EliasDB
+     *
+     * Each build happens with 2 commits. The first commit is the actual 
+     * feature or fix commit. The commit message should follow conventional 
+     * commit messages (https://www.conventionalcommits.org/en/v1.0.0-beta.4/).
+     * In a second commit a program called standard version 
+     * (https://github.com/conventional-changelog/standard-version) calculates
+     * a new product version. The versioning will be according to the rules
+     * of “Semantic Versioning” (https://semver.org/).
+     *
+     * Building is done using goreleaser (https://goreleaser.com/) for different
+     * platforms.
+     *
+     * Testing produces code coverage badges which can be embedded on other
+     * pages.
+     *
+     * Everything runs in docker containers to ensure isolation of the build
+     * system and to allow painless upgrades.
+     */
+
+    stages {
+        stage('Commit Analysis') {
+            steps {
+
+                // Read the commit message into a variable
+                //
+                script {
+                  commit_msg = sh(returnStdout: true, script: 'git log -1')
+                }
+            }
+        }
+        stage('Prepare Release Build') {
+            
+            // Check for a release build (a commit by standard-version)
+            //
+            when { expression { return commit_msg =~ /chore\(release\)\:/ } }
+            steps {
+
+                // Find out the tagged version
+                //
+                script {
+                  version = sh(returnStdout: true, script: 'git log -1 | grep chore | tr -d "\\n" | sed "s/.*chore(release): \\([0-9\\.]*\\)/\\1/"')
+                }
+
+                echo "Building version: ${version} ..."
+            }
+        }
+        stage('Build') {
+            when { expression { return commit_msg =~ /chore\(release\)\:/ } }
+            steps {
+                
+                // Fetch all git tags and run goreleaser
+                //
+                checkout scm
+                sshagent (credentials: ['Gogs']) {
+                    sh 'git fetch --tags'
+                }
+
+                sh 'mkdir -p .cache'
+                sh 'docker run --rm --user $(id -u):$(id -g) -v $PWD/.cache:/.cache -v $PWD:/go/code -w /go/code goreleaser/goreleaser --snapshot --skip-publish --rm-dist'
+            }
+        }
+        stage('Test') {
+
+            // The tests are run in both stages - no release commit is made if the tests fail.
+            // The output is the coverage data and the badge.
+            //
+            steps {
+                echo 'Running tests ...'
+
+                sh """echo '<svg width="88" height="20" xmlns="http://www.w3.org/2000/svg"><g shape-rendering="crispEdges"><path fill="#555" d="M0 0h41v20H0z"/><path fill="#fc1" d="M41 0h40v20H41z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="20.5" y="14">tests</text><text x="60" y="14">fail</text></g></svg>' > test_result.svg"""
+
+                sh 'docker run --rm -e GOPATH=/tmp -v $PWD:/go golang go test -p 1 --coverprofile=coverage.out ./...'
+                sh 'docker run --rm -e GOPATH=/tmp -v $PWD:/go golang go tool cover --html=coverage.out -o coverage.html'
+
+                echo 'Determine overall coverage and writing badge'
+                script {
+                  coverage = sh(returnStdout: true, script: 'docker run --rm -e GOPATH=/tmp -v $PWD:/go golang go tool cover -func=coverage.out | tee coverage.txt | tail -1 | grep -o "[0-9]*.[0-9]*%$" | tr -d "\\n"')
+                  
+                  echo "Overall coverage is: ${coverage}"
+                  
+                  if (coverage.equals("100.0%")) {
+                    sh """echo '<svg width="110" height="20" xmlns="http://www.w3.org/2000/svg"><g shape-rendering="crispEdges"><path fill="#555" d="M0 0h61v20H0z"/><path fill="#4c1" d="M61 0h50v20H61z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="30.5" y="14">coverage</text><text x="85" y="14">$coverage</text></g></svg>' > test_result.svg"""
+                  } else {
+                    sh """echo '<svg width="110" height="20" xmlns="http://www.w3.org/2000/svg"><g shape-rendering="crispEdges"><path fill="#555" d="M0 0h61v20H0z"/><path fill="#fc1" d="M61 0h50v20H61z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="30.5" y="14">coverage</text><text x="85" y="14">$coverage</text></g></svg>' > test_result.svg"""
+                  }
+                }
+            }
+        }
+        stage('Create Release Build Commit') {
+            
+            // Check for a non-release build to avoid a commit loop
+            //
+            when { not { expression { return commit_msg =~ /chore\(release\)\:/ } } }
+            steps {
+
+                // Before running standard-version it is important to fetch
+                // the existing tags so next version can be calculated
+                //
+                echo 'Running standard version ...'
+                sshagent (credentials: ['Gogs']) {
+                    sh 'git fetch --tags'
+                }
+                sh 'docker run --rm -v $PWD:/app standard-version'
+  
+                // The new version is inserted into the code
+                //
+                script {
+                  new_version = sh(returnStdout: true, script: 'git tag | tail -1 | tr -d "\\n"')
+                }
+                echo "Inserting version $new_version into the code"
+                sh "find . -name '*.go' -exec sed -i -e 's/ProductVersion\\ =\\ \\\".*\\\"/ProductVersion = \\\"${new_version.substring(1)}\\\"/g' {} \\;"
+
+                // The commit is amended to include the code change
+                //
+                echo "Tagging the build and push the changes into the origin repository"
+                sshagent (credentials: ['Gogs']) {
+                    sh 'git config user.name "Matthias Ladkau"'
+                    sh 'git config user.email "webmaster@devt.de"'
+
+                    sh 'git commit -a --amend --no-edit'
+                    sh "git tag --force $new_version"
+
+                    sh 'git push --tags origin master'
+                }
+            }
+        }
+        stage('Upload Release Build Commit') {
+            when { expression { return commit_msg =~ /chore\(release\)\:/ } }
+            steps {
+                echo "Uploading release build ..."
+
+                // After a successful build the resulting artifacts are 
+                // uploaded for publication
+                //
+                sshagent (credentials: ['Gogs']) {
+                  
+                  // Clear distribution folder
+                  sh 'ssh -o StrictHostKeyChecking=no -p 7000 krotik@devt.de rm -fR pub/eliasdb'
+                  sh 'ssh -o StrictHostKeyChecking=no -p 7000 krotik@devt.de mkdir -p pub/eliasdb'
+                  
+                  // Copy distribution packages in place
+                  sh 'scp -P 7000 -o StrictHostKeyChecking=no dist/*.tar.gz krotik@devt.de:~/pub/eliasdb'
+
+                  // Copy coverage in place
+                  sh 'scp -P 7000 -o StrictHostKeyChecking=no coverage.* krotik@devt.de:~/pub/eliasdb'
+
+                  // Copy test result in place
+                  sh 'scp -P 7000 -o StrictHostKeyChecking=no test_result.svg krotik@devt.de:~/pub/eliasdb'
+                }
+            }
+        }
+    }
+}

+ 9 - 0
NOTICE

@@ -0,0 +1,9 @@
+EliasDB - Graph-based database
+Copyright (c) 2016 Matthias Ladkau
+
+The following components are included in this product:
+
+Gorilla WebSocket
+https://github.com/gorilla/websocket
+Copyright (c) 2013 The Gorilla WebSocket Authors
+Licensed under the MIT License

+ 8 - 12
README.md

@@ -1,16 +1,11 @@
 EliasDB
 =======
-EliasDB is a graph-based database which aims to provide a lightweight solution for projects which want to store their data as a graph. EliasDB does not require any third-party libraries.
+EliasDB is a graph-based database which aims to provide a lightweight solution for projects which want to store their data as a graph.
 
 <p>
-<a href="https://devt.de/build_status.html"><img src="https://devt.de/nightly/build.eliasdb.svg" alt="Build status"></a>
-<a href="https://devt.de/nightly/test.eliasdb.html"><img src="https://devt.de/nightly/test.eliasdb.svg" alt="Code coverage"></a>
+<a href="https://void.devt.de/pub/eliasdb/coverage.txt"><img src="https://void.devt.de/pub/eliasdb/test_result.svg" alt="Code coverage"></a>
 <a href="https://goreportcard.com/report/github.com/krotik/eliasdb">
 <img src="https://goreportcard.com/badge/github.com/krotik/eliasdb?style=flat-square" alt="Go Report Card"></a>
-<a href="http://devt.de/docs/pkg/devt.de/eliasdb/">
-<img src="https://devt.de/nightly/godoc_badge.svg" alt="Go Doc"></a>
-<a href="https://gitter.im/eliasdb/Lobby">
-<img src="https://badges.gitter.im/gitterHQ/gitter.svg" alt="Gitter Chat"></a>
 </p>
 
 Features
@@ -21,7 +16,8 @@ Features
 - Stored graphs support cascading deletions - delete one node and all its "children".
 - All stored data is indexed and can be quickly searched via a full text phrase search.
 - For more complex queries EliasDB has an own query language called EQL with an sql-like syntax.
-- Written in Go from scratch. No third party libraries were used apart from Go's standard library.
+- EliasDB has a GraphQL interface which can be used to store and retrieve data.
+- Written in Go from scratch. Only uses gorilla/websocket to support websockets for GraphQL subscriptions.
 - The database can be embedded or used as a standalone application.
 - When used as a standalone application it comes with an internal HTTPS webserver which
   provides a REST API and a basic file server.
@@ -137,7 +133,7 @@ To build EliasDB from source you need to have Go installed. There a are two opti
 
 Create a directory, change into it and run:
 ```
-git clone https://github.com/krotik/eliasdb/ .
+git clone https://devt.de/krotik/eliasdb/ .
 ```
 
 Assuming your GOPATH is set to the new directory you should be able to build the binary with:
@@ -159,9 +155,9 @@ go build devt.de/eliasdb/cli
 
 Further Reading
 ---------------
-- A design document which describes the different components of the graph database. [Link](/doc/elias_db_design.md)
-- A reference for the EliasDB query language EQL. [Link](/doc/eql.md)
-- A quick overview of what you can do when you embed EliasDB in your own Go project. [Link](/doc/embedding.md)
+- A design document which describes the different components of the graph database. [Link](/elias_db_design.md)
+- A reference for the EliasDB query language EQL. [Link](eql.md)
+- A quick overview of what you can do when you embed EliasDB in your own Go project. [Link](/embedding.md)
 
 License
 -------

+ 2 - 3
src/devt.de/eliasdb/api/about.go

@@ -36,7 +36,7 @@ import (
 	"encoding/json"
 	"net/http"
 
-	"devt.de/eliasdb/version"
+	"devt.de/krotik/eliasdb/config"
 )
 
 /*
@@ -66,8 +66,7 @@ func (a *aboutEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resour
 	data := map[string]interface{}{
 		"api_versions": []string{"v1"},
 		"product":      "EliasDB",
-		"version":      version.VERSION,
-		"revision":     version.REV,
+		"version":      config.ProductVersion,
 	}
 
 	// Write data

+ 5 - 5
src/devt.de/eliasdb/api/ac/access.go

@@ -20,11 +20,11 @@ import (
 	"net/url"
 	"strings"
 
-	"devt.de/common/datautil"
-	"devt.de/common/httputil/access"
-	"devt.de/common/httputil/auth"
-	"devt.de/common/httputil/user"
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/httputil/access"
+	"devt.de/krotik/common/httputil/auth"
+	"devt.de/krotik/common/httputil/user"
+	"devt.de/krotik/eliasdb/api"
 )
 
 // Code and datastructures relating to access control

+ 7 - 7
src/devt.de/eliasdb/api/ac/access_test.go

@@ -22,13 +22,13 @@ import (
 	"sync"
 	"testing"
 
-	"devt.de/common/datautil"
-	"devt.de/common/errorutil"
-	"devt.de/common/httputil"
-	"devt.de/common/httputil/access"
-	"devt.de/common/httputil/auth"
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/httputil"
+	"devt.de/krotik/common/httputil/access"
+	"devt.de/krotik/common/httputil/auth"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
 )
 
 const TESTPORT = ":9090"

+ 5 - 5
src/devt.de/eliasdb/api/ac/login.go

@@ -17,11 +17,11 @@ import (
 	"net/url"
 	"time"
 
-	"devt.de/common/datautil"
-	"devt.de/common/errorutil"
-	"devt.de/common/httputil"
-	"devt.de/common/httputil/auth"
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/httputil"
+	"devt.de/krotik/common/httputil/auth"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*

src/devt.de/eliasdb/api/ac/login_test.go → api/ac/login_test.go


+ 2 - 2
src/devt.de/eliasdb/api/ac/logout.go

@@ -13,8 +13,8 @@ package ac
 import (
 	"net/http"
 
-	"devt.de/common/httputil/user"
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/common/httputil/user"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*

+ 1 - 1
src/devt.de/eliasdb/api/ac/logout_test.go

@@ -14,7 +14,7 @@ import (
 	"net/http"
 	"testing"
 
-	"devt.de/common/httputil/user"
+	"devt.de/krotik/common/httputil/user"
 )
 
 func TestLogoutEndpoint(t *testing.T) {

+ 3 - 3
src/devt.de/eliasdb/api/ac/user.go

@@ -16,9 +16,9 @@ import (
 	"net/http"
 	"sort"
 
-	"devt.de/common/errorutil"
-	"devt.de/common/httputil/access"
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/httputil/access"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*

src/devt.de/eliasdb/api/ac/user_test.go → api/ac/user_test.go


+ 4 - 4
src/devt.de/eliasdb/api/rest.go

@@ -14,10 +14,10 @@ import (
 	"net/http"
 	"strings"
 
-	"devt.de/common/datautil"
-	"devt.de/eliasdb/cluster"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/graphstorage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/eliasdb/cluster"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
 )
 
 /*

+ 3 - 4
src/devt.de/eliasdb/api/rest_test.go

@@ -20,8 +20,8 @@ import (
 	"sync"
 	"testing"
 
-	"devt.de/common/httputil"
-	"devt.de/eliasdb/version"
+	"devt.de/krotik/common/httputil"
+	"devt.de/krotik/eliasdb/config"
 )
 
 const TESTPORT = ":9090"
@@ -125,9 +125,8 @@ func TestEndpointHandling(t *testing.T) {
     "v1"
   ],
   "product": "EliasDB",
-  "revision": "%v",
   "version": "%v"
-}`[1:], version.REV, version.VERSION) {
+}`[1:], config.ProductVersion) {
 		t.Error("Unexpected response:", res)
 		return
 	}

src/devt.de/eliasdb/api/swagger.go → api/swagger.go


+ 12 - 2
src/devt.de/eliasdb/api/v1/blob.go

@@ -269,6 +269,16 @@ The return data is a map of partitions to node kinds to a list of nodes:
 	}
 
 
+GraphQL request endpoint
+
+/graphql
+/graphql-query
+
+The GraphQL endpoints execute GraphQL queries on EliasDB's datastore. The
+query endpoint supports only read-queries (i.e. no mutations). EliasDB supports
+only executable definitions and introspection (i.e. no type system validation).
+
+
 General database information endpoint
 
 /info
@@ -404,8 +414,8 @@ import (
 	"net/http"
 	"strconv"
 
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 /*

+ 1 - 1
src/devt.de/eliasdb/api/v1/blob_test.go

@@ -14,7 +14,7 @@ import (
 	"fmt"
 	"testing"
 
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 func TestBlob(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/api/v1/cluster.go

@@ -15,7 +15,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*

+ 5 - 5
src/devt.de/eliasdb/api/v1/cluster_test.go

@@ -21,11 +21,11 @@ import (
 	"strings"
 	"testing"
 
-	"devt.de/common/datautil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/cluster"
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/graph/graphstorage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/cluster"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
 )
 
 func TestClusterQuery(t *testing.T) {

+ 3 - 3
src/devt.de/eliasdb/api/v1/eql.go

@@ -15,9 +15,9 @@ import (
 	"fmt"
 	"net/http"
 
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/eql"
-	"devt.de/eliasdb/eql/parser"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/eql"
+	"devt.de/krotik/eliasdb/eql/parser"
 )
 
 /*

+ 0 - 0
src/devt.de/eliasdb/api/v1/eql_test.go


+ 4 - 4
src/devt.de/eliasdb/api/v1/find.go

@@ -16,10 +16,10 @@ import (
 	"net/http"
 	"strings"
 
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
 )
 
 /*

src/devt.de/eliasdb/api/v1/find_test.go → api/v1/find_test.go


+ 3 - 3
src/devt.de/eliasdb/api/v1/graph.go

@@ -17,9 +17,9 @@ import (
 	"sort"
 	"strconv"
 
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
 )
 
 /*

+ 6 - 6
src/devt.de/eliasdb/api/v1/graph_test.go

@@ -15,12 +15,12 @@ import (
 	"fmt"
 	"testing"
 
-	"devt.de/common/datautil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
-	"devt.de/eliasdb/hash"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/hash"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 func TestNestedStorage(t *testing.T) {

+ 159 - 0
api/v1/graphql-query.go

@@ -0,0 +1,159 @@
+/*
+ * 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 v1
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graphql"
+)
+
+/*
+EndpointGraphQLQuery is a query-only GraphQL endpoint URL (rooted). Handles
+everything under graphql-query/...
+*/
+const EndpointGraphQLQuery = api.APIRoot + APIv1 + "/graphql-query/"
+
+/*
+GraphQLQueryEndpointInst creates a new endpoint handler.
+*/
+func GraphQLQueryEndpointInst() api.RestEndpointHandler {
+	return &graphQLQueryEndpoint{}
+}
+
+/*
+Handler object for GraphQL operations.
+*/
+type graphQLQueryEndpoint struct {
+	*api.DefaultEndpointHandler
+}
+
+/*
+HandleGET handles GraphQL queries.
+*/
+func (e *graphQLQueryEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resources []string) {
+
+	gqlquery := map[string]interface{}{
+		"variables":     nil,
+		"operationName": nil,
+	}
+
+	partition := r.URL.Query().Get("partition")
+	if partition == "" && len(resources) > 0 {
+		partition = resources[0]
+	}
+
+	if partition == "" {
+		http.Error(w, "Need a partition", http.StatusBadRequest)
+		return
+	}
+
+	query := r.URL.Query().Get("query")
+	if query == "" {
+		http.Error(w, "Need a query parameter", http.StatusBadRequest)
+		return
+	}
+	gqlquery["query"] = query
+
+	if operationName := r.URL.Query().Get("operationName"); operationName != "" {
+		gqlquery["operationName"] = operationName
+	}
+
+	if variables := r.URL.Query().Get("variables"); variables != "" {
+		varData := make(map[string]interface{})
+
+		if err := json.Unmarshal([]byte(variables), &varData); err != nil {
+			http.Error(w, "Could not decode variables: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		gqlquery["variables"] = varData
+	}
+
+	res, err := graphql.RunQuery(stringutil.CreateDisplayString(partition)+" query",
+		partition, gqlquery, api.GM, nil, true)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	w.Header().Set("content-type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(res)
+}
+
+/*
+SwaggerDefs is used to describe the endpoint in swagger.
+*/
+func (e *graphQLQueryEndpoint) SwaggerDefs(s map[string]interface{}) {
+
+	s["paths"].(map[string]interface{})["/v1/graphql-query"] = map[string]interface{}{
+		"get": map[string]interface{}{
+			"summary":     "GraphQL interface which only executes non-modifying queries.",
+			"description": "The GraphQL interface can be used to query data.",
+			"consumes": []string{
+				"application/json",
+			},
+			"produces": []string{
+				"text/plain",
+				"application/json",
+			},
+			"parameters": []map[string]interface{}{
+				map[string]interface{}{
+					"name":        "partition",
+					"in":          "path",
+					"description": "Partition to query.",
+					"required":    false,
+					"type":        "string",
+				},
+				map[string]interface{}{
+					"name":        "partition",
+					"in":          "query",
+					"description": "Partition to query.",
+					"required":    false,
+					"type":        "string",
+				},
+				map[string]interface{}{
+					"name":        "operationName",
+					"in":          "query",
+					"description": "GraphQL query operation name.",
+					"required":    false,
+				},
+				map[string]interface{}{
+					"name":        "query",
+					"in":          "query",
+					"description": "GraphQL query.",
+					"required":    true,
+				},
+				map[string]interface{}{
+					"name":        "variables",
+					"in":          "query",
+					"description": "GraphQL query variable values.",
+					"required":    false,
+				},
+			},
+			"responses": map[string]interface{}{
+				"200": map[string]interface{}{
+					"description": "The operation was successful.",
+				},
+				"default": map[string]interface{}{
+					"description": "Error response",
+					"schema": map[string]interface{}{
+						"$ref": "#/definitions/Error",
+					},
+				},
+			},
+		},
+	}
+}

+ 241 - 0
api/v1/graphql-subscriptions.go

@@ -0,0 +1,241 @@
+/*
+ * 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 v1
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/websocket"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graphql"
+)
+
+/*
+EndpointGraphQLSubscriptions is the GraphQL endpoint URL for subscriptions (rooted). Handles websockets under graphql-subscriptions/
+*/
+const EndpointGraphQLSubscriptions = api.APIRoot + APIv1 + "/graphql-subscriptions/"
+
+/*
+upgrader can upgrade normal requests to websocket communications
+*/
+var upgrader = websocket.Upgrader{
+	Subprotocols:    []string{"graphql-subscriptions"},
+	ReadBufferSize:  1024,
+	WriteBufferSize: 1024,
+}
+
+var subscriptionCallbackError error
+
+/*
+GraphQLSubscriptionsEndpointInst creates a new endpoint handler.
+*/
+func GraphQLSubscriptionsEndpointInst() api.RestEndpointHandler {
+	return &graphQLSubscriptionsEndpoint{}
+}
+
+/*
+Handler object for GraphQL operations.
+*/
+type graphQLSubscriptionsEndpoint struct {
+	*api.DefaultEndpointHandler
+}
+
+/*
+HandleGET handles GraphQL subscription queries.
+*/
+func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resources []string) {
+
+	// Update the incomming connection to a websocket
+	// If the upgrade fails then the client gets an HTTP error response.
+
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+
+		// We give details here on what went wrong
+
+		w.Write([]byte(err.Error()))
+		return
+	}
+
+	subID := ""
+
+	// Ensure we have a partition to query
+
+	partition := r.URL.Query().Get("partition")
+	if partition == "" && len(resources) > 0 {
+		partition = resources[0]
+	}
+
+	if partition == "" {
+		e.WriteError(conn, subID, "Need a 'partition' in path or as url parameter", true)
+		return
+	}
+
+	conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"init_success","payload":{}}`))
+
+	// Create the callback handler for the subscription
+
+	callbackHandler := &subscriptionCallbackHandler{
+		finished: false,
+		publish: func(data map[string]interface{}, err error) {
+			var res []byte
+
+			// Error for unit testing
+
+			err = subscriptionCallbackError
+
+			// This is called if data im the datastore changes
+
+			if err == nil {
+				res, err = json.Marshal(map[string]interface{}{
+					"id":      subID,
+					"type":    "subscription_data",
+					"payload": data,
+				})
+			}
+
+			if err != nil {
+				e.WriteError(conn, subID, err.Error(), true)
+				return
+			}
+
+			conn.WriteMessage(websocket.TextMessage, res)
+		},
+	}
+
+	for {
+
+		// Read websocket message
+
+		_, msg, err := conn.ReadMessage()
+		if err != nil {
+
+			// Unregister the callback handler
+
+			callbackHandler.finished = true
+
+			// If the client is still listening write the error message
+			// This is a NOP if the client hang up
+
+			e.WriteError(conn, subID, err.Error(), true)
+			return
+		}
+
+		data := make(map[string]interface{})
+
+		if err := json.Unmarshal(msg, &data); err != nil {
+			e.WriteError(conn, subID, err.Error(), false)
+			continue
+		}
+
+		// Check we got a message with a type
+
+		if msgType, ok := data["type"]; ok {
+
+			// Check if the user wants to start a new subscription
+
+			if _, ok := data["query"]; msgType == "subscription_start" && ok {
+				var res []byte
+
+				subID = fmt.Sprint(data["id"])
+
+				if _, ok := data["variables"]; !ok {
+					data["variables"] = nil
+				}
+
+				if _, ok := data["operationName"]; !ok {
+					data["operationName"] = nil
+				}
+
+				resData, err := graphql.RunQuery(stringutil.CreateDisplayString(partition)+" query",
+					partition, data, api.GM, callbackHandler, false)
+
+				if err == nil {
+					res, err = json.Marshal(map[string]interface{}{
+						"id":      subID,
+						"type":    "subscription_data",
+						"payload": resData,
+					})
+				}
+
+				if err != nil {
+					e.WriteError(conn, subID, err.Error(), false)
+					continue
+				}
+
+				conn.WriteMessage(websocket.TextMessage, []byte(
+					fmt.Sprintf(`{"id":"%s","type":"subscription_success","payload":{}}`, subID)))
+
+				conn.WriteMessage(websocket.TextMessage, res)
+			}
+		}
+	}
+}
+
+/*
+WriteError writes an error message to the websocket.
+*/
+func (e *graphQLSubscriptionsEndpoint) WriteError(conn *websocket.Conn,
+	subID string, msg string, close bool) {
+
+	// Write the error as cleartext message
+
+	data, _ := json.Marshal(map[string]interface{}{
+		"id":   subID,
+		"type": "subscription_fail",
+		"payload": map[string]interface{}{
+			"errors": []string{msg},
+		},
+	})
+
+	conn.WriteMessage(websocket.TextMessage, data)
+
+	if close {
+		// Write error as closing control message
+
+		conn.WriteControl(websocket.CloseMessage,
+			websocket.FormatCloseMessage(
+				websocket.CloseUnsupportedData, msg), time.Now().Add(10*time.Second))
+
+		conn.Close()
+	}
+}
+
+/*
+SwaggerDefs is used to describe the endpoint in swagger.
+*/
+func (e *graphQLSubscriptionsEndpoint) SwaggerDefs(s map[string]interface{}) {
+	// No swagger definitions for this endpoint as it only handles websocket requests
+}
+
+// Callback Handler
+// ================
+
+/*
+subscriptionCallbackHandler pushes new events to a subscription client via a websocket.
+*/
+type subscriptionCallbackHandler struct {
+	finished bool
+	publish  func(data map[string]interface{}, err error)
+}
+
+func (ch *subscriptionCallbackHandler) Publish(data map[string]interface{}, err error) {
+	ch.publish(data, err)
+}
+
+func (ch *subscriptionCallbackHandler) IsFinished() bool {
+	return ch.finished
+}

+ 303 - 0
api/v1/graphql-subscriptions_test.go

@@ -0,0 +1,303 @@
+/*
+ * 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 v1
+
+import (
+	"fmt"
+	"testing"
+
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/storage"
+	"github.com/gorilla/websocket"
+)
+
+func TestGraphQLSubscriptionConnectionErrors(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQLSubscriptions
+
+	_, _, res := sendTestRequest(queryURL+"main", "GET", nil)
+
+	if res != `Bad Request
+websocket: the client is not using the websocket protocol: 'upgrade' token not found in 'Connection' header` {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}
+
+func TestGraphQLSubscriptionMissingPartition(t *testing.T) {
+	queryURL := "ws://localhost" + TESTPORT + EndpointGraphQLSubscriptions
+
+	// Test missing partition
+
+	c, _, err := websocket.DefaultDialer.Dial(queryURL, nil)
+	if err != nil {
+		t.Error("Could not open websocket:", err)
+		return
+	}
+
+	_, message, err := c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "",
+  "payload": {
+    "errors": [
+      "Need a 'partition' in path or as url parameter"
+    ]
+  },
+  "type": "subscription_fail"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	_, _, err = c.ReadMessage()
+	if err == nil || err.Error() != "websocket: close 1003 (unsupported data): Need a 'partition' in path or as url parameter" {
+		t.Error("Unexpected response:", err)
+		return
+	}
+
+	if err = c.Close(); err != nil {
+		t.Error("Could not close websocket:", err)
+		return
+	}
+}
+
+func TestGraphQLSubscription(t *testing.T) {
+	queryURL := "ws://localhost" + TESTPORT + EndpointGraphQLSubscriptions + "main"
+
+	// Test missing partition
+
+	c, _, err := websocket.DefaultDialer.Dial(queryURL, nil)
+	if err != nil {
+		t.Error("Could not open websocket:", err)
+		return
+	}
+
+	_, message, err := c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "type": "init_success",
+  "payload": {}
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	err = c.WriteMessage(websocket.TextMessage, []byte("buu"))
+	if err != nil {
+		t.Error("Could not send message:", err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "",
+  "payload": {
+    "errors": [
+      "invalid character 'b' looking for beginning of value"
+    ]
+  },
+  "type": "subscription_fail"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	err = c.WriteJSON(map[string]interface{}{
+		"type":  "subscription_start",
+		"id":    "123",
+		"query": "subscription { Author { key, ",
+	})
+	if err != nil {
+		t.Error("Could not send message:", err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "payload": {
+    "errors": [
+      "Parse error in Main query: Unexpected end (Line:1 Pos:29)"
+    ]
+  },
+  "type": "subscription_fail"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	err = c.WriteJSON(map[string]interface{}{
+		"type":  "subscription_start",
+		"id":    "123",
+		"query": "subscription { Author { key, name }}",
+	})
+	if err != nil {
+		t.Error("Could not send message:", err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "type": "subscription_success",
+  "payload": {}
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "payload": {
+    "data": {
+      "Author": [
+        {
+          "key": "123",
+          "name": "Mike"
+        },
+        {
+          "key": "456",
+          "name": "Hans"
+        },
+        {
+          "key": "000",
+          "name": "John"
+        }
+      ]
+    }
+  },
+  "type": "subscription_data"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	api.GM.StoreNode("main", data.NewGraphNodeFromMap(map[string]interface{}{
+		"key":  "Hans",
+		"kind": "Author",
+	}))
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "payload": {
+    "data": {
+      "Author": [
+        {
+          "key": "123",
+          "name": "Mike"
+        },
+        {
+          "key": "456",
+          "name": "Hans"
+        },
+        {
+          "key": "000",
+          "name": "John"
+        },
+        {
+          "key": "Hans",
+          "name": null
+        }
+      ]
+    }
+  },
+  "type": "subscription_data"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	// Insert an error into the db
+
+	sm := gmMSM.StorageManager("mainAuthor.nodes", false)
+	msm := sm.(*storage.MemoryStorageManager)
+	msm.AccessMap[8] = storage.AccessCacheAndFetchSeriousError
+
+	err = api.GM.StoreNode("main", data.NewGraphNodeFromMap(map[string]interface{}{
+		"key":  "Hans2",
+		"kind": "Author",
+	}))
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "payload": {
+    "data": {
+      "Author": []
+    },
+    "errors": [
+      {
+        "locations": [
+          {
+            "column": 23,
+            "line": 1
+          }
+        ],
+        "message": "GraphError: Could not read graph information (Record is already in-use (? - ))",
+        "path": [
+          "Author"
+        ]
+      }
+    ]
+  },
+  "type": "subscription_data"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	delete(msm.AccessMap, 8)
+
+	// Create a callback error
+
+	subscriptionCallbackError = fmt.Errorf("Oh dear")
+
+	err = api.GM.StoreNode("main", data.NewGraphNodeFromMap(map[string]interface{}{
+		"key":  "Hans3",
+		"kind": "Author",
+	}))
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	_, message, err = c.ReadMessage()
+	if msg := formatJSONString(string(message)); err != nil || msg != `{
+  "id": "123",
+  "payload": {
+    "errors": [
+      "Oh dear"
+    ]
+  },
+  "type": "subscription_fail"
+}` {
+		t.Error("Unexpected response:", msg, err)
+		return
+	}
+
+	_, _, err = c.ReadMessage()
+	if err == nil || err.Error() != "websocket: close 1003 (unsupported data): Oh dear" {
+		t.Error("Unexpected response:", err)
+		return
+	}
+
+	if err = c.Close(); err != nil {
+		t.Error("Could not close websocket:", err)
+		return
+	}
+}

+ 150 - 0
api/v1/graphql.go

@@ -0,0 +1,150 @@
+/*
+ * 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 v1
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graphql"
+)
+
+/*
+EndpointGraphQL is the GraphQL endpoint URL (rooted). Handles everything under graphql/...
+*/
+const EndpointGraphQL = api.APIRoot + APIv1 + "/graphql/"
+
+/*
+GraphQLEndpointInst creates a new endpoint handler.
+*/
+func GraphQLEndpointInst() api.RestEndpointHandler {
+	return &graphQLEndpoint{}
+}
+
+/*
+Handler object for GraphQL operations.
+*/
+type graphQLEndpoint struct {
+	*api.DefaultEndpointHandler
+}
+
+/*
+HandlePOST handles GraphQL queries.
+*/
+func (e *graphQLEndpoint) HandlePOST(w http.ResponseWriter, r *http.Request, resources []string) {
+
+	dec := json.NewDecoder(r.Body)
+	data := make(map[string]interface{})
+
+	if err := dec.Decode(&data); err != nil {
+		http.Error(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	partData, ok := data["partition"]
+	if !ok && len(resources) > 0 {
+		partData = resources[0]
+		ok = true
+	}
+	if !ok || partData == "" {
+		http.Error(w, "Need a partition", http.StatusBadRequest)
+		return
+	}
+
+	part := fmt.Sprint(partData)
+
+	if _, ok := data["variables"]; !ok {
+		data["variables"] = nil
+	}
+
+	if _, ok := data["operationName"]; !ok {
+		data["operationName"] = nil
+	}
+
+	res, err := graphql.RunQuery(stringutil.CreateDisplayString(part)+" query",
+		part, data, api.GM, nil, false)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	w.Header().Set("content-type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(res)
+}
+
+/*
+SwaggerDefs is used to describe the endpoint in swagger.
+*/
+func (e *graphQLEndpoint) SwaggerDefs(s map[string]interface{}) {
+
+	s["paths"].(map[string]interface{})["/v1/graphql"] = map[string]interface{}{
+		"post": map[string]interface{}{
+			"summary":     "GraphQL interface.",
+			"description": "The GraphQL interface can be used to query and modify data.",
+			"consumes": []string{
+				"application/json",
+			},
+			"produces": []string{
+				"text/plain",
+				"application/json",
+			},
+			"parameters": []map[string]interface{}{
+				map[string]interface{}{
+					"name":        "partition",
+					"in":          "path",
+					"description": "Partition to query.",
+					"required":    false,
+					"type":        "string",
+				},
+				map[string]interface{}{
+					"name":        "partition",
+					"in":          "body",
+					"description": "Partition to query.",
+					"required":    false,
+					"type":        "string",
+				},
+				map[string]interface{}{
+					"name":        "operationName",
+					"in":          "body",
+					"description": "GraphQL query operation name.",
+					"required":    false,
+				},
+				map[string]interface{}{
+					"name":        "query",
+					"in":          "body",
+					"description": "GraphQL query.",
+					"required":    true,
+				},
+				map[string]interface{}{
+					"name":        "variables",
+					"in":          "body",
+					"description": "GraphQL query variable values.",
+					"required":    false,
+				},
+			},
+			"responses": map[string]interface{}{
+				"200": map[string]interface{}{
+					"description": "The operation was successful.",
+				},
+				"default": map[string]interface{}{
+					"description": "Error response",
+					"schema": map[string]interface{}{
+						"$ref": "#/definitions/Error",
+					},
+				},
+			},
+		},
+	}
+}

+ 177 - 0
api/v1/graphql_test.go

@@ -0,0 +1,177 @@
+/*
+ * 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 v1
+
+import (
+	"encoding/json"
+	"net/url"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+)
+
+func TestGraphQLQuery(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQLQuery
+
+	query := url.QueryEscape(`{
+  Song(key : "Aria1") {
+	key
+  }
+}`)
+	_, _, res := sendTestRequest(queryURL+"main?query="+query, "GET", nil)
+
+	if res != `
+{
+  "data": {
+    "Song": [
+      {
+        "key": "Aria1"
+      }
+    ]
+  }
+}`[1:] {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	query = url.QueryEscape(`query foo($bar : String) {
+  Song(key : $bar) {
+	key
+  }
+}`)
+	variables := url.QueryEscape(`{ "bar" : "Aria1" }`)
+	_, _, res = sendTestRequest(queryURL+"main?operationName=foo&query="+query+"&variables="+variables, "GET", nil)
+
+	if res != `
+{
+  "data": {
+    "Song": [
+      {
+        "key": "Aria1"
+      }
+    ]
+  }
+}`[1:] {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}
+
+func TestGraphQLQueryErrors(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQLQuery
+
+	query := url.QueryEscape(`{`)
+	_, _, res := sendTestRequest(queryURL+"main?query="+query, "GET", nil)
+
+	if res != "Parse error in Main query: Unexpected end (Line:1 Pos:1)" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	_, _, res = sendTestRequest(queryURL+"?query="+query, "GET", nil)
+
+	if res != "Need a partition" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+	_, _, res = sendTestRequest(queryURL+"main?ry="+query, "GET", nil)
+
+	if res != "Need a query parameter" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	_, _, res = sendTestRequest(queryURL+"main?query="+query+"&variables=123", "GET", nil)
+
+	if res != "Could not decode variables: json: cannot unmarshal number into Go value of type map[string]interface {}" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}
+
+func TestGraphQL(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQL
+
+	q, err := json.Marshal(map[string]interface{}{
+		"partition": "main",
+		"query": `{
+  Song(key : "Aria1") {
+	key
+  }
+}`,
+	})
+	errorutil.AssertOk(err)
+	_, _, res := sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != `
+{
+  "data": {
+    "Song": [
+      {
+        "key": "Aria1"
+      }
+    ]
+  }
+}`[1:] {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}
+
+func TestGraphQLErrors(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQL
+
+	q, err := json.Marshal(map[string]interface{}{
+		"operationName": nil,
+		"variables":     nil,
+		"query":         "{",
+	})
+	errorutil.AssertOk(err)
+	_, _, res := sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != "Parse error in Main query: Unexpected end (Line:1 Pos:1)" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	q, err = json.Marshal(map[string]interface{}{
+		"operationName": nil,
+		"variables":     nil,
+		"query":         "{",
+	})
+	errorutil.AssertOk(err)
+	_, _, res = sendTestRequest(queryURL, "POST", q)
+
+	if res != "Need a partition" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	q, err = json.Marshal(map[string]interface{}{
+		"partition":     "main",
+		"operationName": nil,
+		"variables":     nil,
+	})
+	errorutil.AssertOk(err)
+	_, _, res = sendTestRequest(queryURL, "POST", q)
+
+	if res != "Mandatory field 'query' missing from query object" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	_, _, res = sendTestRequest(queryURL, "POST", []byte("{"))
+
+	if res != "Could not decode request body: unexpected EOF" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}

+ 2 - 2
src/devt.de/eliasdb/api/v1/index.go

@@ -14,8 +14,8 @@ import (
 	"encoding/json"
 	"net/http"
 
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/graph"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/graph"
 )
 
 /*

+ 2 - 2
src/devt.de/eliasdb/api/v1/index_test.go

@@ -14,8 +14,8 @@ import (
 	"strings"
 	"testing"
 
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 func TestIndexQuery(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/api/v1/info.go

@@ -15,7 +15,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*

src/devt.de/eliasdb/api/v1/info_test.go → api/v1/info_test.go


+ 5 - 5
src/devt.de/eliasdb/api/v1/query.go

@@ -17,11 +17,11 @@ import (
 	"strings"
 	"time"
 
-	"devt.de/common/datautil"
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/eql"
-	"devt.de/eliasdb/graph/data"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/eql"
+	"devt.de/krotik/eliasdb/graph/data"
 )
 
 /*

src/devt.de/eliasdb/api/v1/query_test.go → api/v1/query_test.go


+ 6 - 6
src/devt.de/eliasdb/api/v1/queryresult.go

@@ -18,12 +18,12 @@ import (
 	"strconv"
 	"strings"
 
-	"devt.de/common/errorutil"
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/eql"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/eql"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
 )
 
 /*

+ 3 - 3
src/devt.de/eliasdb/api/v1/queryresult_test.go

@@ -14,9 +14,9 @@ import (
 	"fmt"
 	"testing"
 
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/eql/interpreter"
-	"devt.de/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/eql/interpreter"
+	"devt.de/krotik/eliasdb/graph/data"
 )
 
 func TestResultGroupingWithState(t *testing.T) {

+ 13 - 10
src/devt.de/eliasdb/api/v1/rest.go

@@ -15,7 +15,7 @@ import (
 	"strconv"
 	"strings"
 
-	"devt.de/eliasdb/api"
+	"devt.de/krotik/eliasdb/api"
 )
 
 /*
@@ -37,15 +37,18 @@ const HTTPHeaderCacheID = "X-Cache-Id"
 V1EndpointMap is a map of urls to endpoints for version 1 of the API
 */
 var V1EndpointMap = map[string]api.RestEndpointInst{
-	EndpointBlob:         BlobEndpointInst,
-	EndpointClusterQuery: ClusterEndpointInst,
-	EndpointEql:          EqlEndpointInst,
-	EndpointGraph:        GraphEndpointInst,
-	EndpointIndexQuery:   IndexEndpointInst,
-	EndpointFindQuery:    FindEndpointInst,
-	EndpointInfoQuery:    InfoEndpointInst,
-	EndpointQuery:        QueryEndpointInst,
-	EndpointQueryResult:  QueryResultEndpointInst,
+	EndpointBlob:                 BlobEndpointInst,
+	EndpointClusterQuery:         ClusterEndpointInst,
+	EndpointEql:                  EqlEndpointInst,
+	EndpointGraph:                GraphEndpointInst,
+	EndpointGraphQL:              GraphQLEndpointInst,
+	EndpointGraphQLQuery:         GraphQLQueryEndpointInst,
+	EndpointGraphQLSubscriptions: GraphQLSubscriptionsEndpointInst,
+	EndpointIndexQuery:           IndexEndpointInst,
+	EndpointFindQuery:            FindEndpointInst,
+	EndpointInfoQuery:            InfoEndpointInst,
+	EndpointQuery:                QueryEndpointInst,
+	EndpointQueryResult:          QueryResultEndpointInst,
 }
 
 // Helper functions

+ 24 - 6
src/devt.de/eliasdb/api/v1/rest_test.go

@@ -22,12 +22,13 @@ import (
 	"sync"
 	"testing"
 
-	"devt.de/common/httputil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/eql"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
-	"devt.de/eliasdb/graph/graphstorage"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/httputil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/eql"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
 )
 
 const TESTPORT = ":9090"
@@ -115,6 +116,23 @@ func sendTestRequest(url string, method string, content []byte) (string, http.He
 	return resp.Status, resp.Header, bodyStr
 }
 
+/*
+formatJSONString formats a given JSON string.
+*/
+func formatJSONString(str string) string {
+	out := bytes.Buffer{}
+	errorutil.AssertOk(json.Indent(&out, []byte(str), "", "  "))
+	return out.String()
+}
+
+/*
+formatData returns a given datastructure as JSON string.
+*/
+func formatData(data interface{}) string {
+	actualResultBytes, _ := json.MarshalIndent(data, "", "  ")
+	return string(actualResultBytes)
+}
+
 /*
 Start a HTTP test server.
 */

+ 9 - 10
src/devt.de/eliasdb/cli/eliasdb.go

@@ -48,14 +48,13 @@ import (
 	"path/filepath"
 	"strings"
 
-	"devt.de/common/errorutil"
-	"devt.de/common/fileutil"
-	"devt.de/common/termutil"
-	"devt.de/eliasdb/config"
-	"devt.de/eliasdb/console"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/server"
-	"devt.de/eliasdb/version"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/common/termutil"
+	"devt.de/krotik/eliasdb/config"
+	"devt.de/krotik/eliasdb/console"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/server"
 )
 
 func main() {
@@ -141,8 +140,8 @@ func RunCliConsole() {
 	}
 
 	if *cmdfile == "" && *cmdline == "" {
-		fmt.Println(fmt.Sprintf("EliasDB %v.%v - Console",
-			version.VERSION, version.REV))
+		fmt.Println(fmt.Sprintf("EliasDB %v - Console",
+			config.ProductVersion))
 	}
 
 	var clt termutil.ConsoleLineTerminal

+ 4 - 4
src/devt.de/eliasdb/cluster/distributedstorage.go

@@ -41,10 +41,10 @@ import (
 	"math"
 	"sync"
 
-	"devt.de/common/datautil"
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/graph/graphstorage"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 /*

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_fetch_test.go

@@ -15,7 +15,7 @@ import (
 	"testing"
 	"time"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationFetch(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_free_test.go

@@ -15,7 +15,7 @@ import (
 	"testing"
 	"time"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationFree(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_insert_test.go

@@ -15,7 +15,7 @@ import (
 	"testing"
 	"time"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationInsert(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_maindb_test.go

@@ -14,7 +14,7 @@ import (
 	"math"
 	"testing"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationMainDB(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_root_test.go

@@ -14,7 +14,7 @@ import (
 	"math"
 	"testing"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationRoot(t *testing.T) {

+ 3 - 3
src/devt.de/eliasdb/cluster/distributedstorage_test.go

@@ -21,9 +21,9 @@ import (
 	"math"
 	"testing"
 
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/graph/graphstorage"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 func TestDistributionStorageInitialisationError(t *testing.T) {

+ 1 - 1
src/devt.de/eliasdb/cluster/distributedstorage_update_test.go

@@ -15,7 +15,7 @@ import (
 	"testing"
 	"time"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestSimpleDataReplicationUpdate(t *testing.T) {

+ 2 - 2
src/devt.de/eliasdb/cluster/distributedstoragemanager.go

@@ -15,8 +15,8 @@ import (
 	"encoding/gob"
 	"fmt"
 
-	"devt.de/common/errorutil"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 /*

src/devt.de/eliasdb/cluster/distributiontable.go → cluster/distributiontable.go


src/devt.de/eliasdb/cluster/distributiontable_test.go → cluster/distributiontable_test.go


+ 1 - 1
src/devt.de/eliasdb/cluster/manager/client.go

@@ -20,7 +20,7 @@ import (
 	"sync"
 	"time"
 
-	"devt.de/common/datautil"
+	"devt.de/krotik/common/datautil"
 )
 
 func init() {

+ 4 - 4
src/devt.de/eliasdb/cluster/manager/config.go

@@ -31,10 +31,10 @@ import (
 	"fmt"
 	"sync"
 
-	"devt.de/common/datautil"
-	"devt.de/common/errorutil"
-	"devt.de/common/fileutil"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 // Cluster config

+ 1 - 1
src/devt.de/eliasdb/cluster/manager/config_test.go

@@ -18,7 +18,7 @@ import (
 	"strings"
 	"testing"
 
-	"devt.de/common/datautil"
+	"devt.de/krotik/common/datautil"
 )
 
 const invalidFileName = "**" + string(0x0)

src/devt.de/eliasdb/cluster/manager/globals.go → cluster/manager/globals.go


src/devt.de/eliasdb/cluster/manager/housekeeping.go → cluster/manager/housekeeping.go


+ 1 - 1
src/devt.de/eliasdb/cluster/manager/manager.go

@@ -21,7 +21,7 @@ import (
 	"sync"
 	"time"
 
-	"devt.de/common/datautil"
+	"devt.de/krotik/common/datautil"
 )
 
 /*

src/devt.de/eliasdb/cluster/manager/manager_test.go → cluster/manager/manager_test.go


src/devt.de/eliasdb/cluster/manager/managerserver_test.go → cluster/manager/managerserver_test.go


+ 1 - 1
src/devt.de/eliasdb/cluster/manager/server.go

@@ -15,7 +15,7 @@ import (
 	"fmt"
 	"net/rpc"
 
-	"devt.de/common/errorutil"
+	"devt.de/krotik/common/errorutil"
 )
 
 func init() {

+ 4 - 4
src/devt.de/eliasdb/cluster/memberaddresstable.go

@@ -16,10 +16,10 @@ import (
 	"sync"
 	"time"
 
-	"devt.de/common/timeutil"
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/hash"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/timeutil"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/hash"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 /*

+ 2 - 2
src/devt.de/eliasdb/cluster/memberaddresstable_test.go

@@ -15,8 +15,8 @@ import (
 	"math"
 	"testing"
 
-	"devt.de/eliasdb/hash"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/eliasdb/hash"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 func TestAddressTableClusterLoc(t *testing.T) {

+ 5 - 5
src/devt.de/eliasdb/cluster/memberstorage.go

@@ -18,11 +18,11 @@ import (
 	"strings"
 	"sync"
 
-	"devt.de/common/sortutil"
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/graph/graphstorage"
-	"devt.de/eliasdb/hash"
-	"devt.de/eliasdb/storage"
+	"devt.de/krotik/common/sortutil"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
+	"devt.de/krotik/eliasdb/hash"
+	"devt.de/krotik/eliasdb/storage"
 )
 
 /*

+ 2 - 2
src/devt.de/eliasdb/cluster/rebalance.go

@@ -15,8 +15,8 @@ import (
 	"strconv"
 	"strings"
 
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/hash"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/hash"
 )
 
 /*

+ 1 - 1
src/devt.de/eliasdb/cluster/rebalance_test.go

@@ -17,7 +17,7 @@ import (
 	"testing"
 	"time"
 
-	"devt.de/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/cluster/manager"
 )
 
 func TestRebalancing(t *testing.T) {

src/devt.de/eliasdb/cluster/request.go → cluster/request.go


+ 3 - 3
src/devt.de/eliasdb/cluster/transfer.go

@@ -13,9 +13,9 @@ package cluster
 import (
 	"fmt"
 
-	"devt.de/common/timeutil"
-	"devt.de/eliasdb/cluster/manager"
-	"devt.de/eliasdb/hash"
+	"devt.de/krotik/common/timeutil"
+	"devt.de/krotik/eliasdb/cluster/manager"
+	"devt.de/krotik/eliasdb/hash"
 )
 
 /*

+ 7 - 2
src/devt.de/eliasdb/config/config.go

@@ -15,13 +15,18 @@ import (
 	"path"
 	"strconv"
 
-	"devt.de/common/errorutil"
-	"devt.de/common/fileutil"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
 )
 
 // Global variables
 // ================
 
+/*
+ProductVersion is the current version of EliasDB
+*/
+const ProductVersion = "0.0.0"
+
 /*
 DefaultConfigFile is the default config file which will be used to configure EliasDB
 */

src/devt.de/eliasdb/config/config_test.go → config/config_test.go


+ 5 - 5
src/devt.de/eliasdb/console/cmd_base.go

@@ -14,9 +14,9 @@ import (
 	"bytes"
 	"fmt"
 
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/api/ac"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/api/ac"
 )
 
 // Command: ver
@@ -64,8 +64,8 @@ func (c *CmdVer) Run(args []string, capi CommandConsoleAPI) error {
 	if err == nil {
 		data := res.(map[string]interface{})
 
-		fmt.Fprintln(capi.Out(), fmt.Sprintf("%v %v.%v (REST versions: %v)",
-			data["product"], data["version"], data["revision"], data["api_versions"]))
+		fmt.Fprintln(capi.Out(), fmt.Sprintf("%v %v (REST versions: %v)",
+			data["product"], data["version"], data["api_versions"]))
 	}
 
 	return err

+ 2 - 2
src/devt.de/eliasdb/console/cmd_graph.go

@@ -16,8 +16,8 @@ import (
 	"sort"
 	"strings"
 
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api/v1"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api/v1"
 )
 
 // Command: info

+ 1 - 3
src/devt.de/eliasdb/console/cmd_graph_test.go

@@ -14,12 +14,11 @@ import (
 	"bytes"
 	"testing"
 
-	"devt.de/eliasdb/config"
+	"devt.de/krotik/eliasdb/config"
 )
 
 func TestGraphCommands(t *testing.T) {
 	var out bytes.Buffer
-	var export bytes.Buffer
 
 	ResetDB()
 	credGiver.Reset()
@@ -35,7 +34,6 @@ func TestGraphCommands(t *testing.T) {
 	c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
 		func() string { return "***pass***" },
 		func(args []string, e *bytes.Buffer) error {
-			export = *e
 			return nil
 		})
 

+ 2 - 2
src/devt.de/eliasdb/console/cmd_users.go

@@ -16,8 +16,8 @@ import (
 	"sort"
 	"strings"
 
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api/ac"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api/ac"
 )
 
 // Command: users

+ 1 - 3
src/devt.de/eliasdb/console/cmd_users_test.go

@@ -14,12 +14,11 @@ import (
 	"bytes"
 	"testing"
 
-	"devt.de/eliasdb/config"
+	"devt.de/krotik/eliasdb/config"
 )
 
 func TestUsersCommands(t *testing.T) {
 	var out bytes.Buffer
-	var export bytes.Buffer
 	var pass = "!El1as9845"
 
 	ResetDB()
@@ -35,7 +34,6 @@ func TestUsersCommands(t *testing.T) {
 	c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
 		func() string { return pass },
 		func(args []string, e *bytes.Buffer) error {
-			export = *e
 			return nil
 		})
 

+ 36 - 30
src/devt.de/eliasdb/console/console.go

@@ -24,9 +24,9 @@ import (
 	"sort"
 	"strings"
 
-	"devt.de/common/errorutil"
-	"devt.de/eliasdb/api/ac"
-	"devt.de/eliasdb/config"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/eliasdb/api/ac"
+	"devt.de/krotik/eliasdb/config"
 )
 
 /*
@@ -75,7 +75,7 @@ func NewConsole(url string, out io.Writer, getCredentials func() (string, string
 	c := &EliasDBConsole{url, "main", out, bytes.NewBuffer(nil), nil,
 		nil, false, cmdMap, getCredentials, getPassword}
 
-	c.childConsoles = []CommandConsole{&EQLConsole{c}}
+	c.childConsoles = []CommandConsole{&EQLConsole{c}, &GraphQLConsole{c}}
 
 	return c
 }
@@ -275,9 +275,9 @@ func (c *EliasDBConsole) Run(cmd string) (bool, error) {
 				if ok, err := c.Run(cmd); err != nil || ok {
 					return ok, err
 				}
-
-				return false, fmt.Errorf("Unknown command")
 			}
+
+			return false, fmt.Errorf("Unknown command")
 		}
 	}
 
@@ -292,40 +292,43 @@ a flag if the command was handled.
 */
 func (c *EliasDBConsole) RunCommand(cmdString string) (bool, error) {
 	cmdSplit := strings.Fields(cmdString)
-	cmd := cmdSplit[0]
-	args := cmdSplit[1:]
 
-	// Reset the export buffer if we are not exporting
+	if len(cmdSplit) > 0 {
+		cmd := cmdSplit[0]
+		args := cmdSplit[1:]
 
-	if cmd != CommandExport {
-		c.export.Reset()
-	}
+		// Reset the export buffer if we are not exporting
 
-	if config.Bool(config.EnableAccessControl) {
+		if cmd != CommandExport {
+			c.export.Reset()
+		}
+
+		if config.Bool(config.EnableAccessControl) {
 
-		// Extra commands when access control is enabled
+			// Extra commands when access control is enabled
 
-		if cmd == "logout" {
+			if cmd == "logout" {
 
-			// Special command "logout" to remove the current auth token
+				// Special command "logout" to remove the current auth token
 
-			c.authCookie = nil
+				c.authCookie = nil
 
-			fmt.Fprintln(c.out, "Current user logged out.")
+				fmt.Fprintln(c.out, "Current user logged out.")
 
-		} else if cmd != "ver" && cmd != "whoami" && cmd != "help" && cmd != "export" {
+			} else if cmd != "ver" && cmd != "whoami" && cmd != "help" && cmd != "export" {
 
-			// Do not authenticate if running local commands
+				// Do not authenticate if running local commands
 
-			// Authenticate user this is a NOP if the user is authenticated unless
-			// the command "login" is given. Then the user is reauthenticated.
+				// Authenticate user this is a NOP if the user is authenticated unless
+				// the command "login" is given. Then the user is reauthenticated.
 
-			c.Authenticate(cmd == "login")
+				c.Authenticate(cmd == "login")
+			}
 		}
-	}
 
-	if cmd, ok := c.CommandMap[cmd]; ok {
-		return true, cmd.Run(args, c)
+		if cmd, ok := c.CommandMap[cmd]; ok {
+			return true, cmd.Run(args, c)
+		}
 	}
 
 	return false, nil
@@ -493,11 +496,14 @@ of keywords.
 */
 func cmdStartsWithKeyword(cmd string, keywords []string) bool {
 	ss := strings.Fields(strings.ToLower(cmd))
-	firstCmd := strings.ToLower(ss[0])
 
-	for _, k := range keywords {
-		if k == firstCmd {
-			return true
+	if len(ss) > 0 {
+		firstCmd := strings.ToLower(ss[0])
+
+		for _, k := range keywords {
+			if k == firstCmd || strings.HasPrefix(firstCmd, k) {
+				return true
+			}
 		}
 	}
 

+ 15 - 17
src/devt.de/eliasdb/console/console_test.go

@@ -21,19 +21,19 @@ import (
 	"sync"
 	"testing"
 
-	"devt.de/common/datautil"
-	"devt.de/common/errorutil"
-	"devt.de/common/httputil"
-	"devt.de/common/httputil/access"
-	"devt.de/common/httputil/auth"
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api"
-	"devt.de/eliasdb/api/ac"
-	"devt.de/eliasdb/api/v1"
-	"devt.de/eliasdb/config"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
-	"devt.de/eliasdb/graph/graphstorage"
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/httputil"
+	"devt.de/krotik/common/httputil/access"
+	"devt.de/krotik/common/httputil/auth"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api"
+	"devt.de/krotik/eliasdb/api/ac"
+	v1 "devt.de/krotik/eliasdb/api/v1"
+	"devt.de/krotik/eliasdb/config"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
 )
 
 const TESTPORT = ":9090"
@@ -395,8 +395,8 @@ func TestNoAuthentication(t *testing.T) {
 	}
 
 	if res := out.String(); res != `
-EliasDB 1.0.0 (REST versions: [v1])
-`[1:] {
+EliasDB `[1:]+config.ProductVersion+` (REST versions: [v1])
+` {
 		t.Error("Unexpected result:", res)
 		return
 	}
@@ -481,7 +481,6 @@ ver     Displays server version information.
 
 func TestBasicCommands(t *testing.T) {
 	var out bytes.Buffer
-	var export bytes.Buffer
 
 	ResetDB()
 	credGiver.Reset()
@@ -496,7 +495,6 @@ func TestBasicCommands(t *testing.T) {
 	c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
 		func() string { return "***pass***" },
 		func(args []string, e *bytes.Buffer) error {
-			export = *e
 			return nil
 		})
 

+ 2 - 2
src/devt.de/eliasdb/console/eqlconsole.go

@@ -14,8 +14,8 @@ import (
 	"fmt"
 	"net/url"
 
-	"devt.de/common/stringutil"
-	"devt.de/eliasdb/api/v1"
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/eliasdb/api/v1"
 )
 
 // EQL Console

+ 1 - 3
src/devt.de/eliasdb/console/eqlconsole_test.go

@@ -14,12 +14,11 @@ import (
 	"bytes"
 	"testing"
 
-	"devt.de/eliasdb/config"
+	"devt.de/krotik/eliasdb/config"
 )
 
 func TestEQLConsole(t *testing.T) {
 	var out bytes.Buffer
-	var export bytes.Buffer
 
 	ResetDB()
 	credGiver.Reset()
@@ -40,7 +39,6 @@ func TestEQLConsole(t *testing.T) {
 	c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
 		func() string { return "***pass***" },
 		func(args []string, e *bytes.Buffer) error {
-			export = *e
 			return nil
 		})
 

+ 73 - 0
console/graphqlconsole.go

@@ -0,0 +1,73 @@
+/*
+ * 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 console
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"devt.de/krotik/common/errorutil"
+	v1 "devt.de/krotik/eliasdb/api/v1"
+)
+
+// GraphQL Console
+// ===============
+
+/*
+GraphQLConsole runs GraphQL queries.
+*/
+type GraphQLConsole struct {
+	parent CommandConsoleAPI // Parent console API
+}
+
+/*
+graphQLConsoleKeywords are all keywords which this console can process.
+*/
+var graphQLConsoleKeywords = []string{"{", "query", "mutation"}
+
+/*
+Run executes one or more commands. It returns an error if the command
+had an unexpected result and a flag if the command was handled.
+*/
+func (c *GraphQLConsole) Run(cmd string) (bool, error) {
+
+	if !cmdStartsWithKeyword(cmd, graphQLConsoleKeywords) {
+		return false, nil
+	}
+
+	q, err := json.Marshal(map[string]interface{}{
+		"operationName": nil,
+		"variables":     nil,
+		"query":         cmd,
+	})
+	errorutil.AssertOk(err)
+
+	resObj, err := c.parent.Req(
+		fmt.Sprintf("%s%s", v1.EndpointGraphQL, c.parent.Partition()), "POST", q)
+
+	if err == nil && resObj != nil {
+
+		actualResultBytes, _ := json.MarshalIndent(resObj, "", "  ")
+		out := string(actualResultBytes)
+
+		c.parent.ExportBuffer().WriteString(out)
+		fmt.Fprint(c.parent.Out(), out)
+	}
+
+	return true, err
+}
+
+/*
+Commands returns an empty list. The command line is interpreted as a GraphQL query.
+*/
+func (c *GraphQLConsole) Commands() []Command {
+	return nil
+}

+ 132 - 0
console/graphqlconsole_test.go

@@ -0,0 +1,132 @@
+/*
+ * 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 console
+
+import (
+	"bytes"
+	"testing"
+
+	"devt.de/krotik/eliasdb/config"
+)
+
+func TestGraphQLConsole(t *testing.T) {
+	var out bytes.Buffer
+
+	ResetDB()
+	credGiver.Reset()
+	createSongGraph()
+
+	// Dummy test
+
+	graphqlc := &GraphQLConsole{}
+	graphqlc.Commands()
+
+	// Enable access control
+
+	config.Config[config.EnableAccessControl] = true
+	defer func() {
+		config.Config[config.EnableAccessControl] = false
+	}()
+
+	c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
+		func() string { return "***pass***" },
+		func(args []string, e *bytes.Buffer) error {
+			return nil
+		})
+
+	out.Reset()
+
+	credGiver.UserQueue = []string{"elias"}
+	credGiver.PassQueue = []string{"elias"}
+
+	if ok, err := c.Run("users"); !ok || err != nil {
+		t.Error(ok, err)
+		return
+	}
+
+	if res := out.String(); res != `
+Login as user elias
+┌─────────┬─────────────┐
+│Username │Groups       │
+├─────────┼─────────────┤
+│elias    │admin/public │
+│johndoe  │public       │
+└─────────┴─────────────┘
+`[1:] {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	out.Reset()
+
+	if ok, err := c.Run("{ Song { key, name, ranking }}"); !ok || err != nil {
+		t.Error(ok, err)
+		return
+	}
+
+	if res := out.String(); res != `
+{
+  "data": {
+    "Song": [
+      {
+        "key": "StrangeSong1",
+        "name": "StrangeSong1",
+        "ranking": 5
+      },
+      {
+        "key": "FightSong4",
+        "name": "FightSong4",
+        "ranking": 3
+      },
+      {
+        "key": "DeadSong2",
+        "name": "DeadSong2",
+        "ranking": 6
+      },
+      {
+        "key": "LoveSong3",
+        "name": "LoveSong3",
+        "ranking": 1
+      },
+      {
+        "key": "MyOnlySong3",
+        "name": "MyOnlySong3",
+        "ranking": 19
+      },
+      {
+        "key": "Aria1",
+        "name": "Aria1",
+        "ranking": 8
+      },
+      {
+        "key": "Aria2",
+        "name": "Aria2",
+        "ranking": 2
+      },
+      {
+        "key": "Aria3",
+        "name": "Aria3",
+        "ranking": 4
+      },
+      {
+        "key": "Aria4",
+        "name": "Aria4",
+        "ranking": 18
+      }
+    ]
+  }
+}`[1:] {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	out.Reset()
+}

File diff suppressed because it is too large
+ 0 - 1
doc/swagger.json


doc/elias_db_design.md → eliasdb_design.md


+ 2 - 3
doc/embedding.md

@@ -14,13 +14,12 @@ For the rest of this tutorial it is assumed that you have the following director
 | Path | Description |
 | --- | --- |
 | src/devt.de/common | Common code used by EliasDB |
-| src/devt.de/eliasdb/ | Root directory for EliasDB containing the main package for the standalone server |
+| src/devt.de/eliasdb/cli | Main directory for EliasDB containing the main package for the standalone server |
 | src/devt.de/eliasdb/api | HTTP endpoints for EliasDB's REST API |
 | src/devt.de/eliasdb/eql | Parser and interpreter for EQL |
 | src/devt.de/eliasdb/graph | API to the graph storage |
 | src/devt.de/eliasdb/hash | H-Tree implementation for EliasDB's underlying key-value store |
 | src/devt.de/eliasdb/storage | Low level storage API |
-| src/dect.de/eliasdb/version | Version file |
 
 For this tutorial we create a demo file:
 
@@ -305,4 +304,4 @@ func main() {
 
 	fmt.Println("out4:", res, err)
 }
-```
+```

+ 25 - 25
doc/eql.md

@@ -5,7 +5,7 @@ EliasDB query language (EQL) is a query langugage to search nodes in a partition
 ```
 get <node kind> where <condition>
 ```
-It reads: "Get all graph nodes of a certain node kind which match a certain condition". The condition is evaluated for each node from the specified kind. For example to get all "Person" nodes with the name "John" you could write:
+It reads: "Get all graph nodes of a certain node kind which match a certain condition". The condition is evaluated for each node from the specified kind. For example to get all `Person` nodes with the name `John` you could write:
 ```
 get Person where name = John
 ```
@@ -16,27 +16,27 @@ Where clause
 
 A where clause supports the following operators:
 
-- Standard boolean operators: and, or, not
+- Standard boolean operators: `and, or, not`
 
-- Standard condition operators: =, !=, >, <, >=, <=, in, notin, contains, beginswith, endswith, containsnot
+- Standard condition operators: `=, !=, >, <, >=, <=, in, notin, contains, beginswith, endswith, containsnot`
 
-- Standard arithmetic operators: +, -, *, /
+- Standard arithmetic operators: `+, -, *, /``
 
-- Integer operations: // (integer division), % (modulo)
+- Integer operations: `//` (integer division), `%` (modulo)
 
-- Regular expression operator: like
+- Regular expression operator: `like`
 
-Operators can be combined. Expressions can be segregated using parentheses. Each where condition should end in a boolean value. List operators such as “in” and “notin” operate on sequences of values which can be declared with square brackets e.g. [1,2,3].