Browse Source

chore(release): 1.0.0

Matthias Ladkau 5 months ago
parent
commit
da3121aeca
100 changed files with 97697 additions and 388 deletions
  1. 5 1
      .gitignore
  2. 7 1
      .goreleaser.yml
  3. 11 0
      CHANGELOG.md
  4. 47 0
      Dockerfile
  5. 1 0
      Jenkinsfile
  6. 89 27
      README.md
  7. 5 0
      api/ac/access_test.go
  8. 4 4
      api/ac/login.go
  9. 1 1
      api/ac/logout_test.go
  10. 5 5
      api/ac/user.go
  11. 1 1
      api/ac/user_test.go
  12. 5 0
      api/rest_test.go
  13. 3 3
      api/v1/blob.go
  14. 2 2
      api/v1/cluster.go
  15. 1 1
      api/v1/cluster_test.go
  16. 1 1
      api/v1/eql.go
  17. 4 4
      api/v1/find.go
  18. 9 9
      api/v1/graph.go
  19. 14 14
      api/v1/graph_test.go
  20. 9 13
      api/v1/graphql-query.go
  21. 34 0
      api/v1/graphql-subscriptions.go
  22. 32 28
      api/v1/graphql.go
  23. 7 7
      api/v1/index.go
  24. 1 1
      api/v1/info.go
  25. 6 6
      api/v1/query.go
  26. 1 1
      api/v1/query_test.go
  27. 1 1
      api/v1/queryresult.go
  28. 5 0
      api/v1/rest_test.go
  29. 2 3
      cli/eliasdb.go
  30. 2 1
      cluster/distributedstoragemanager.go
  31. 0 1
      cluster/distributiontable_test.go
  32. 1 1
      cluster/manager/client.go
  33. 1 1
      cluster/manager/config.go
  34. 1 1
      cluster/manager/server.go
  35. 1 1
      cluster/memberaddresstable.go
  36. 4 5
      cluster/memberstorage.go
  37. 2 2
      config/config.go
  38. 32 24
      console/console.go
  39. 44 30
      embedding.md
  40. 9 6
      eql/interpreter/func.go
  41. 2 2
      eql/parser/lexer.go
  42. 59 59
      eql/parser/parser.go
  43. 13 13
      eql/parser/parser_test.go
  44. 20 0
      examples/chat/doc/chat.md
  45. 53 0
      examples/chat/res/access.db
  46. 12 0
      examples/chat/res/chat/.eslintrc.js
  47. 8 0
      examples/chat/res/chat/.prettierrc.js
  48. 373 0
      examples/chat/res/chat/LICENSE
  49. 20 0
      examples/chat/res/chat/README.md
  50. 11 0
      examples/chat/res/chat/dist/chat.js
  51. 10 0
      examples/chat/res/chat/index.html
  52. 29 0
      examples/chat/res/chat/package.json
  53. 80 0
      examples/chat/res/chat/src/component/ChatTextArea.vue
  54. 83 0
      examples/chat/res/chat/src/component/ChatWindow.vue
  55. 19 0
      examples/chat/res/chat/src/index.ts
  56. 231 0
      examples/chat/res/chat/src/lib/eliasdb-graphql.ts
  57. 4 0
      examples/chat/res/chat/src/vue-shims.d.ts
  58. 66 0
      examples/chat/res/chat/tsconfig.json
  59. 104 0
      examples/chat/res/chat/webpack.config.js
  60. 3743 0
      examples/chat/res/chat/yarn.lock
  61. 25 0
      examples/chat/res/eliasdb.config.json
  62. 14 0
      examples/chat/start.bat
  63. 11 0
      examples/chat/start.sh
  64. 5 0
      examples/chat/start_console.bat
  65. 5 0
      examples/chat/start_console.sh
  66. 78 0
      examples/tutorial/doc/tutorial.md
  67. BIN
      examples/tutorial/doc/tutorial1.png
  68. BIN
      examples/tutorial/doc/tutorial1_graphql.png
  69. BIN
      examples/tutorial/doc/tutorial2.png
  70. BIN
      examples/tutorial/doc/tutorial2_graphql.png
  71. BIN
      examples/tutorial/doc/tutorial3.png
  72. BIN
      examples/tutorial/doc/tutorial3_graphql.png
  73. 143 0
      examples/tutorial/doc/tutorial_graphql.md
  74. 22 0
      examples/tutorial/res/graphiql/LICENSE
  75. 5 0
      examples/tutorial/res/graphiql/NOTICE
  76. 1 0
      examples/tutorial/res/graphiql/es6-promise.auto.min.js
  77. 1 0
      examples/tutorial/res/graphiql/fetch.min.js
  78. 1609 0
      examples/tutorial/res/graphiql/graphiql.css
  79. 74899 0
      examples/tutorial/res/graphiql/graphiql.js
  80. 13515 0
      examples/tutorial/res/graphiql/graphql-subscriptions-fetcher-client.js
  81. 163 0
      examples/tutorial/res/graphiql/index.html
  82. 15 0
      examples/tutorial/res/graphiql/react-dom.min.js
  83. 12 0
      examples/tutorial/res/graphiql/react.min.js
  84. 1678 0
      examples/tutorial/res/graphiql/subscription-transport-ws-client.js
  85. BIN
      examples/tutorial/res/tutorial_data.zip
  86. 13 0
      examples/tutorial/start.bat
  87. 13 0
      examples/tutorial/start.sh
  88. 5 0
      examples/tutorial/start_console.bat
  89. 5 0
      examples/tutorial/start_console.sh
  90. 2 2
      graph/graphmanager_edges.go
  91. 2 2
      graph/graphmanager_edges_test.go
  92. 1 1
      graph/graphmanager_nodes_test.go
  93. 8 8
      graph/rules_test.go
  94. 2 2
      graph/trans.go
  95. 1 1
      graphql/interpreter/runtime.go
  96. 74 71
      graphql/interpreter/selectionset.go
  97. 2 2
      graphql/interpreter/selectionset_test.go
  98. 3 3
      graphql/query_test.go
  99. 15 15
      hash/htreepage_test.go
  100. 0 0
      integration/rumble/edge_test.go

+ 5 - 1
.gitignore

@@ -1,8 +1,12 @@
 .cache
 .cover
+.eliasdb_console_history
 coverage.txt
 coverage.out
 coverage.html
 test
-dist
+/dist
 build
+eliasdb
+examples/tutorial/run/
+examples/chat/run/

+ 7 - 1
.goreleaser.yml

@@ -14,6 +14,11 @@ builds:
     - amd64
 checksum:
   name_template: 'checksums.txt'
+archives:
+  - files:
+    - LICENSE
+    - NOTICE
+    - examples/**/*
 snapshot:
   name_template: "{{ .Tag }}"
 changelog:
@@ -24,4 +29,5 @@ changelog:
     - '^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'
+# mkdir .cache
+# 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

+ 11 - 0
CHANGELOG.md

@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+## 1.0.0 (2019-08-30)
+
+
+### Features
+
+* BREAKING CHANGE: Restructure EliasDB code for go modules / Adding GraphQL interface ([65c38db59e](https://devt.de///commit/65c38db59e))
+* Initial commit ([b2e9cd9a8a](https://devt.de///commit/b2e9cd9a8a))

+ 47 - 0
Dockerfile

@@ -0,0 +1,47 @@
+# Start from the latest alpine based golang base image
+FROM golang:alpine as builder
+
+# Install git
+RUN apk update && apk add --no-cache git
+
+# Add maintainer info
+LABEL maintainer="Matthias Ladkau <matthias@ladkau.de>"
+
+# Set the current working directory inside the container
+WORKDIR /app
+
+# Copy go mod and sum files
+COPY go.mod go.sum ./
+
+# Download all dependencies
+RUN go mod download
+
+# Copy the source from the current directory to the working directory inside the container
+COPY . .
+
+# Build eliasdb and link statically (no CGO)
+# Use ldflags -w -s to omit the symbol table, debug information and the DWARF table
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-w -s" cli/eliasdb.go
+
+# Start again from scratch
+FROM scratch
+
+# Copy the eliasdb binary
+COPY --from=builder /app/eliasdb /eliasdb
+
+# Set the working directory to data so all created files (e.g. eliasdb.config.json)
+# can be mapped to physical files on disk
+WORKDIR /data
+
+# Run eliasdb binary
+ENTRYPOINT ["../eliasdb"]
+
+# To run the server as the current user, expose port 9090 and preserve 
+# all runtime related files on disk in the local directory run:
+#
+# docker run --rm --user $(id -u):$(id -g) -v $PWD:/data -p 9090:9090 krotik/eliasdb server
+
+# To run the console as the current user, use the eliasdb.config.json in 
+# the local directory
+
+# docker run --rm --network="host" -it -v $PWD:/data --user $(id -u):$(id -g) -v $PWD:/data krotik/eliasdb console

+ 1 - 0
Jenkinsfile

@@ -145,6 +145,7 @@ pipeline {
                   
                   // Copy distribution packages in place
                   sh 'scp -P 7000 -o StrictHostKeyChecking=no dist/*.tar.gz krotik@devt.de:~/pub/eliasdb'
+                  sh 'scp -P 7000 -o StrictHostKeyChecking=no dist/checksums.txt krotik@devt.de:~/pub/eliasdb'
 
                   // Copy coverage in place
                   sh 'scp -P 7000 -o StrictHostKeyChecking=no coverage.* krotik@devt.de:~/pub/eliasdb'

+ 89 - 27
README.md

@@ -4,43 +4,67 @@ EliasDB is a graph-based database which aims to provide a lightweight solution f
 
 <p>
 <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="https://goreportcard.com/report/devt.de/krotik/eliasdb">
+<img src="https://goreportcard.com/badge/devt.de/krotik/eliasdb?style=flat-square" alt="Go Report Card"></a>
+<a href="https://godoc.org/devt.de/krotik/eliasdb">
+<img src="https://godoc.org/devt.de/krotik/eliasdb?status.svg" alt="Go Doc"></a>
 </p>
 
 Features
 --------
-- Build on top of a fast key-value store which supports transactions and memory-only storage.
+- Build on top of a custom key-value store which supports transactions and memory-only storage.
 - Data is stored in nodes (key-value objects) which are connected via edges.
 - Stored graphs can be separated via partitions.
 - 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.
 - EliasDB has a GraphQL interface which can be used to store and retrieve data.
+- For more complex queries EliasDB has an own query language called EQL with an sql-like syntax.
 - 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.
-- When used as an embedded database it supports transactions with rollbacks, iteration of data
-  and rule based consistency management.
+- When used as a standalone application it comes with an internal HTTPS webserver which provides user management, a REST API and a basic file server.
+- When used as an embedded database it supports transactions with rollbacks, iteration of data and rule based consistency management.
 
 Getting Started (standalone application)
 ----------------------------------------
-You can download a precompiled package for Windows (win64) or Linux (amd64) [here](https://devt.de/build_status.html).
+You can download a precompiled package for Windows (win64) or Linux (amd64) [here](https://void.devt.de/pub/eliasdb).
 
-Extract it and execute the executable. The executable should automatically create 3 subfolders and a configuration file. It should start an HTTPS server on port 9090. To see a terminal point your webbrowser to:
+Extract it and execute the executable with:
+```
+eliasdb server
+```
+The executable should automatically create 3 subfolders and a configuration file. It should start an HTTPS server on port 9090. To see a terminal point your webbrowser to:
 ```
 https://localhost:9090/db/term.html
 ```
 After accepting the self-signed certificate from the server you should see a web terminal. EliasDB can be stopped with a simple CTRL+C or by overwriting the content in eliasdb.lck with a single character.
 
+Getting Started (docker image)
+------------------------------
+You can pull the latest docker image of EliasDB from [Dockerhub](https://hub.docker.com/r/krotik/eliasdb):
+```
+docker pull krotik/eliasdb
+```
+
+Create an empty directory, change into it and run the following to start the server:
+```
+docker run --user $(id -u):$(id -g) -v $PWD:/data -p 9090:9090 krotik/eliasdb server
+```
+This exposes port 9090 from the container on the local machine. All runtime related files are written to the current directory as the current user/group.
+
+Connect to the running server with a console by running:
+```
+docker run --rm --network="host" -it -v $PWD:/data --user $(id -u):$(id -g) -v $PWD:/data krotik/eliasdb console
+```
+
 ### Tutorial:
 
-To get an idea of what EliasDB is about have a look at the [tutorial](/examples/tutorial/doc/tutorial.md).
+To get an idea of what EliasDB is about have a look at the [tutorial](https://devt.de/krotik/eliasdb/src/master/examples/tutorial/doc/tutorial.md). This tutorial will cover the basics of EQL and show how data is organized.
+
+There is a separate [tutorial](https://devt.de/krotik/eliasdb/src/master/examples/tutorial/doc/tutorial_graphql.md) on using ELiasDB with GraphQL.
 
 ### REST API:
 
-The terminal uses a REST API to communicate with the backend. The REST API can be browsed using a dynamically generated swagger.json definition (https://localhost:9090/db/swagger.json). You can browse the API of EliasDB's latest version [here](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/krotik/eliasdb/master/doc/swagger.json#/default).
+The terminal uses a REST API to communicate with the backend. The REST API can be browsed using a dynamically generated swagger.json definition (https://localhost:9090/db/swagger.json). You can browse the API of EliasDB's latest version [here](http://petstore.swagger.io/?url=https://devt.de/krotik/eliasdb/raw/master/swagger.json).
 
 ### Command line options
 The main EliasDB executable has two main tools:
@@ -92,7 +116,7 @@ info    Returns general database information.
 part    Displays or sets the current partition.
 ver     Displays server version information.
 ```
-It is also possible to directly run EQL queries on the console. Use the arrow keys to cycle through the command history.
+It is also possible to directly run EQL and GraphQL queries on the console. Use the arrow keys to cycle through the command history.
 
 ### Configuration
 EliasDB uses a single configuration file called eliasdb.config.json. After starting EliasDB for the first time it should create a default configuration file. Available configurations are:
@@ -125,39 +149,77 @@ EliasDB uses a single configuration file called eliasdb.config.json. After start
 
 Note: It is not (and will never be) possible to access the REST API via HTTP.
 
+Enabling Access Control
+-----------------------
+It is possible to enforce access control by enabling the `EnableAccessControl` configuration option. When started with enabled access control EliasDB will only allow known users to connect. Users must authenticate with a password before connecting to the web interface or the REST API. On the first start with the flag enabled the following users are created by default:
+
+|Username|Default Password|Groups|Description|
+|---|---|---|---|
+|elias|elias|admin/public|Default Admin|
+|johndoe|doe|public|Default unprivileged user|
+
+Users can be managed from the console. Please do either delete the default users or change their password after starting EliasDB.
+
+Users are organized in groups and permissions are assigned to groups. Permissions are given to endpoints of the REST API. The following permissions are available:
+
+|Type|Allowed HTTP Request Type|Description|
+|---|---|---|
+|Create|Post|Creating new data|
+|Read|Get|Read data|
+|Update|Put|Modify existing data|
+|Delete|Delete|Delete data|
+
+The default group permissions are:
+
+|Group|Path|Permissions|
+|---|---|---|
+|admin|/db/*|`CRUD`|
+|public|/|`-R--`|
+||/css/*|`-R--`|
+||/db/*|`-R--`|
+||/img/*|`-R--`|
+||/js/*|`-R--`|
+||/vendor/*|`-R--`|
+
+
 Building EliasDB
 ----------------
-To build EliasDB from source you need to have Go installed. There a are two options:
-
-### Checkout from github (use this method if you want code + documentation and tutorials):
+To build EliasDB from source you need to have Go installed (go >= 1.12):
 
-Create a directory, change into it and run:
+- Create a directory, change into it and run:
 ```
 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:
+- You can build EliasDB's executable with:
 ```
-go install devt.de/eliasdb/cli
+go build cli/eliasdb.go
 ```
 
-### Using go get (use this method if you want to embed EliasDB in your project):
+Building EliasDB as Docker image
+--------------------------------
+EliasDB can be build as a secure and compact Docker image.
 
-Create a directory, change into it and run:
+- Create a directory, change into it and run:
 ```
-go get devt.de/common/... devt.de/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:
+- You can now build the Docker image with:
 ```
-go build devt.de/eliasdb/cli
+docker build --tag krotik/eliasdb .
 ```
 
+Example Applications
+--------------------
+[Chat](https://devt.de/krotik/eliasdb/src/master/examples/chat/doc/chat.md) - A simple chat application showing user management and subscriptions.
+
+
 Further Reading
 ---------------
-- 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)
+- A design document which describes the different components of the graph database. [Link](https://devt.de/krotik/eliasdb/src/master/eliasdb_design.md)
+- A reference for the EliasDB query language EQL. [Link](https://devt.de/krotik/eliasdb/src/master/eql.md)
+- A quick overview of what you can do when you embed EliasDB in your own Go project. [Link](https://devt.de/krotik/eliasdb/src/master/embedding.md)
 
 License
 -------

+ 5 - 0
api/ac/access_test.go

@@ -213,6 +213,11 @@ func sendTestRequestResponse(contentType string, url string, method string,
 	} else {
 		req, err = http.NewRequest(method, url, nil)
 	}
+
+	if err != nil {
+		panic(err)
+	}
+
 	req.Header.Set("Content-Type", contentType)
 
 	if reqMod != nil {

+ 4 - 4
api/ac/login.go

@@ -183,28 +183,28 @@ func (le *loginEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"text/plain",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "user",
 					"in":          "formData",
 					"description": "Username to log in.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "pass",
 					"in":          "formData",
 					"description": "Cleartext password of the username.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "redirect_ok",
 					"in":          "formData",
 					"description": "Redirect URL if the log in is successful.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "redirect_notok",
 					"in":          "formData",
 					"description": "Redirect URL if the log in is not successful.",

+ 1 - 1
api/ac/logout_test.go

@@ -77,7 +77,7 @@ func TestLogoutEndpoint(t *testing.T) {
 		t.Error("Unexpected response:", res, resp)
 	}
 
-	res, resp = sendTestRequestResponse("application/json", queryURL+"/foo?abc=123", "GET", nil,
+	_, resp = sendTestRequestResponse("application/json", queryURL+"/foo?abc=123", "GET", nil,
 		func(req *http.Request) {
 			req.AddCookie(authCookie)
 		})

+ 5 - 5
api/ac/user.go

@@ -471,7 +471,7 @@ SwaggerDefs is used to describe the endpoint in swagger.
 func (ue *userEndpoint) SwaggerDefs(s map[string]interface{}) {
 
 	username := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "name",
 			"in":          "path",
 			"description": "Name of user.",
@@ -481,7 +481,7 @@ func (ue *userEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	groupname := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "name",
 			"in":          "path",
 			"description": "Name of group.",
@@ -491,7 +491,7 @@ func (ue *userEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	createParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "user_creation_data",
 			"in":          "body",
 			"description": "Additional data to create a user account",
@@ -520,7 +520,7 @@ func (ue *userEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	updateParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "user_update_data",
 			"in":          "body",
 			"description": "Additional data to update a user account",
@@ -549,7 +549,7 @@ func (ue *userEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	permParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "permission_data",
 			"in":          "body",
 			"description": "Resource paths and their permissions.",

+ 1 - 1
api/ac/user_test.go

@@ -75,7 +75,7 @@ func TestUserEndpoint(t *testing.T) {
 		return
 	}
 
-	res, resp = sendTestRequestResponse("application/json", queryURL+EndpointUser+"u/elias", "GET", nil,
+	sendTestRequestResponse("application/json", queryURL+EndpointUser+"u/elias", "GET", nil,
 		func(req *http.Request) {
 			req.AddCookie(authCookie)
 		})

+ 5 - 0
api/rest_test.go

@@ -223,6 +223,11 @@ func sendTestRequestResponse(url string, method string, content []byte) (string,
 	} else {
 		req, err = http.NewRequest(method, url, nil)
 	}
+
+	if err != nil {
+		panic(err)
+	}
+
 	req.Header.Set("Content-Type", "application/json")
 
 	client := &http.Client{}

+ 3 - 3
api/v1/blob.go

@@ -591,7 +591,7 @@ SwaggerDefs is used to describe the endpoint in swagger.
 func (be *blobEndpoint) SwaggerDefs(s map[string]interface{}) {
 
 	idParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "id",
 			"in":          "path",
 			"description": "ID of the binary blob.",
@@ -601,7 +601,7 @@ func (be *blobEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	partitionParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "partition",
 			"in":          "path",
 			"description": "Partition to select.",
@@ -611,7 +611,7 @@ func (be *blobEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	binaryData := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "data",
 			"in":          "body",
 			"description": "The data to store.",

+ 2 - 2
api/v1/cluster.go

@@ -229,14 +229,14 @@ func (ce *clusterEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "command",
 					"in":          "path",
 					"description": "Valid commands are: ping, join and eject.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "args",
 					"in":          "body",
 					"description": "Arguments for a command",

+ 1 - 1
api/v1/cluster_test.go

@@ -157,7 +157,7 @@ func TestClusterQuery(t *testing.T) {
 	manager.MemberErrors = make(map[string]error)
 	manager.MemberErrors[cluster2[1].Name()] = errors.New("testerror")
 
-	st, _, res = sendTestRequest(queryURL+"eject", "PUT", jsonString)
+	sendTestRequest(queryURL+"eject", "PUT", jsonString)
 
 	st, _, res = sendTestRequest(queryURL+"ping", "PUT", jsonString)
 

+ 1 - 1
api/v1/eql.go

@@ -128,7 +128,7 @@ func (e *eqlEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "data",
 					"in":          "body",
 					"description": "Query or AST which should be converted.",

+ 4 - 4
api/v1/find.go

@@ -174,28 +174,28 @@ func (ie *findEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "text",
 					"in":          "query",
 					"description": "A word or phrase to search for.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "value",
 					"in":          "query",
 					"description": "A node value to search for.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "lookup",
 					"in":          "query",
 					"description": "Flag if a complete node lookup should be done (otherwise only key and kind are returned).",
 					"required":    false,
 					"type":        "boolean",
 				},
-				map[string]interface{}{
+				{
 					"name":        "part",
 					"in":          "query",
 					"description": "Limit the search to a partition (without the option all partitions are searched).",

+ 9 - 9
api/v1/graph.go

@@ -393,7 +393,7 @@ SwaggerDefs is used to describe the endpoint in swagger.
 func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 
 	partitionParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "partition",
 			"in":          "path",
 			"description": "Partition to select.",
@@ -403,7 +403,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	entityParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name": "entity_type",
 			"in":   "path",
 			"description": "Datastore entity type which should selected. " +
@@ -414,7 +414,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	defaultParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "kind",
 			"in":          "path",
 			"description": "Node or edge kind to be queried.",
@@ -426,7 +426,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	defaultParams = append(defaultParams, entityParams...)
 
 	optionalQueryParams := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "limit",
 			"in":          "query",
 			"description": "How many list items to return.",
@@ -434,7 +434,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 			"type":        "number",
 			"format":      "integer",
 		},
-		map[string]interface{}{
+		{
 			"name":        "offset",
 			"in":          "query",
 			"description": "Offset in the dataset.",
@@ -445,7 +445,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	keyParam := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "key",
 			"in":          "path",
 			"description": "Node or edge key to be queried.",
@@ -455,7 +455,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	travParam := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "traversal_spec",
 			"in":          "path",
 			"description": "Traversal to be followed from a single node.",
@@ -465,7 +465,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	graphPost := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "entities",
 			"in":          "body",
 			"description": "Nodes and Edges which should be stored",
@@ -495,7 +495,7 @@ func (ge *graphEndpoint) SwaggerDefs(s map[string]interface{}) {
 	}
 
 	entitiesPost := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "entities",
 			"in":          "body",
 			"description": "Nodes or Edges which should be stored",

+ 14 - 14
api/v1/graph_test.go

@@ -68,15 +68,15 @@ func TestNestedStorage(t *testing.T) {
 
 	nf, err := datautil.GetNestedValue(nested.(map[string]interface{}), []string{"nested_float"})
 
-	if nft := fmt.Sprintf("%T %v", nf, nf); nft != "float64 1.234" {
-		t.Error("Unexpected type:", nft)
+	if nft := fmt.Sprintf("%T %v", nf, nf); nft != "float64 1.234" || err != nil {
+		t.Error("Unexpected type:", nft, err)
 		return
 	}
 
 	ns, err := datautil.GetNestedValue(nested.(map[string]interface{}), []string{"more nesting", "atom"})
 
-	if nst := fmt.Sprintf("%T %v", ns, ns); nst != "string value42" {
-		t.Error("Unexpected type:", nst)
+	if nst := fmt.Sprintf("%T %v", ns, ns); nst != "string value42" || err != nil {
+		t.Error("Unexpected type:", nst, err)
 		return
 	}
 
@@ -759,8 +759,8 @@ func TestGraphOperation(t *testing.T) {
 	edge.SetAttr("name", "updateedge")
 
 	jsonString, err = json.Marshal(map[string][]map[string]interface{}{
-		"nodes": []map[string]interface{}{node.Data(), node2.Data()},
-		"edges": []map[string]interface{}{edge.Data()},
+		"nodes": {node.Data(), node2.Data()},
+		"edges": {edge.Data()},
 	})
 
 	if err != nil {
@@ -795,10 +795,10 @@ func TestGraphOperation(t *testing.T) {
 
 	// Delete edge
 
-	jsonString, err = json.Marshal(map[string][]map[string]interface{}{
-		"nodes": []map[string]interface{}{},
-		"edges": []map[string]interface{}{
-			map[string]interface{}{
+	jsonString, _ = json.Marshal(map[string][]map[string]interface{}{
+		"nodes": {},
+		"edges": {
+			{
 				"key":  edge.Key(),
 				"kind": edge.Kind(),
 			},
@@ -827,10 +827,10 @@ func TestGraphOperation(t *testing.T) {
 
 	// Delete node
 
-	jsonString, err = json.Marshal(map[string][]map[string]interface{}{
-		"edges": []map[string]interface{}{},
-		"nodes": []map[string]interface{}{
-			map[string]interface{}{
+	jsonString, _ = json.Marshal(map[string][]map[string]interface{}{
+		"edges": {},
+		"nodes": {
+			{
 				"key":  node.Key(),
 				"kind": node.Kind(),
 			},

+ 9 - 13
api/v1/graphql-query.go

@@ -98,7 +98,7 @@ 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{}{
+	s["paths"].(map[string]interface{})["/v1/graphql-query/{partition}"] = 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.",
@@ -110,37 +110,33 @@ func (e *graphQLQueryEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"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,
+					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "operationName",
 					"in":          "query",
 					"description": "GraphQL query operation name.",
 					"required":    false,
+					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "query",
 					"in":          "query",
 					"description": "GraphQL query.",
 					"required":    true,
+					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "variables",
 					"in":          "query",
 					"description": "GraphQL query variable values.",
 					"required":    false,
+					"type":        "string",
 				},
 			},
 			"responses": map[string]interface{}{

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

@@ -14,6 +14,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"sync"
 	"time"
 
 	"github.com/gorilla/websocket"
@@ -62,6 +63,13 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 	// If the upgrade fails then the client gets an HTTP error response.
 
 	conn, err := upgrader.Upgrade(w, r, nil)
+
+	// Websocket connections support one concurrent reader and one concurrent writer.
+	// See: https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency
+
+	connRMutex := &sync.Mutex{}
+	connWMutex := &sync.Mutex{}
+
 	if err != nil {
 
 		// We give details here on what went wrong
@@ -80,11 +88,15 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 	}
 
 	if partition == "" {
+		connWMutex.Lock()
 		e.WriteError(conn, subID, "Need a 'partition' in path or as url parameter", true)
+		connWMutex.Unlock()
 		return
 	}
 
+	connWMutex.Lock()
 	conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"init_success","payload":{}}`))
+	connWMutex.Unlock()
 
 	// Create the callback handler for the subscription
 
@@ -108,11 +120,15 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 			}
 
 			if err != nil {
+				connWMutex.Lock()
 				e.WriteError(conn, subID, err.Error(), true)
+				connWMutex.Unlock()
 				return
 			}
 
+			connWMutex.Lock()
 			conn.WriteMessage(websocket.TextMessage, res)
+			connWMutex.Unlock()
 		},
 	}
 
@@ -120,7 +136,10 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 
 		// Read websocket message
 
+		connRMutex.Lock()
 		_, msg, err := conn.ReadMessage()
+		connRMutex.Unlock()
+
 		if err != nil {
 
 			// Unregister the callback handler
@@ -130,14 +149,21 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 			// If the client is still listening write the error message
 			// This is a NOP if the client hang up
 
+			connWMutex.Lock()
 			e.WriteError(conn, subID, err.Error(), true)
+			connWMutex.Unlock()
+
 			return
 		}
 
 		data := make(map[string]interface{})
 
 		if err := json.Unmarshal(msg, &data); err != nil {
+
+			connWMutex.Lock()
 			e.WriteError(conn, subID, err.Error(), false)
+			connWMutex.Unlock()
+
 			continue
 		}
 
@@ -172,14 +198,22 @@ func (e *graphQLSubscriptionsEndpoint) HandleGET(w http.ResponseWriter, r *http.
 				}
 
 				if err != nil {
+
+					connWMutex.Lock()
 					e.WriteError(conn, subID, err.Error(), false)
+					connWMutex.Unlock()
+
 					continue
 				}
 
+				connWMutex.Lock()
+
 				conn.WriteMessage(websocket.TextMessage, []byte(
 					fmt.Sprintf(`{"id":"%s","type":"subscription_success","payload":{}}`, subID)))
 
 				conn.WriteMessage(websocket.TextMessage, res)
+
+				connWMutex.Unlock()
 			}
 		}
 	}

+ 32 - 28
api/v1/graphql.go

@@ -89,7 +89,17 @@ 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{}{
+	graphqlRequestParam := map[string]interface{}{
+		"name":        "graphql_request",
+		"in":          "body",
+		"description": "GraphQL request",
+		"required":    true,
+		"schema": map[string]interface{}{
+			"$ref": "#/definitions/GraphQLRequest",
+		},
+	}
+
+	s["paths"].(map[string]interface{})["/v1/graphql/{partition}"] = map[string]interface{}{
 		"post": map[string]interface{}{
 			"summary":     "GraphQL interface.",
 			"description": "The GraphQL interface can be used to query and modify data.",
@@ -101,38 +111,14 @@ func (e *graphQLEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"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,
+					"type":        "string",
 				},
-				map[string]interface{}{
-					"name":        "variables",
-					"in":          "body",
-					"description": "GraphQL query variable values.",
-					"required":    false,
-				},
+				graphqlRequestParam,
 			},
 			"responses": map[string]interface{}{
 				"200": map[string]interface{}{
@@ -147,4 +133,22 @@ func (e *graphQLEndpoint) SwaggerDefs(s map[string]interface{}) {
 			},
 		},
 	}
+
+	s["definitions"].(map[string]interface{})["GraphQLRequest"] = map[string]interface{}{
+		"type": "object",
+		"properties": map[string]interface{}{
+			"operationName": map[string]interface{}{
+				"description": "GraphQL query operation name.",
+				"type":        "string",
+			},
+			"query": map[string]interface{}{
+				"description": "GraphQL query.",
+				"type":        "string",
+			},
+			"variables": map[string]interface{}{
+				"description": "GraphQL query variable values.",
+				"type":        "object",
+			},
+		},
+	}
 }

+ 7 - 7
api/v1/index.go

@@ -138,14 +138,14 @@ func (ie *indexEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "partition",
 					"in":          "path",
 					"description": "Partition to query.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name": "entity_type",
 					"in":   "path",
 					"description": "Datastore entity type which should selected. " +
@@ -153,35 +153,35 @@ func (ie *indexEndpoint) SwaggerDefs(s map[string]interface{}) {
 					"required": true,
 					"type":     "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "kind",
 					"in":          "path",
 					"description": "Node or edge kind to be queried.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "attr",
 					"in":          "query",
 					"description": "Attribute which should contain the word, phrase or value.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "word",
 					"in":          "query",
 					"description": "Word to search for in word queries.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "phrase",
 					"in":          "query",
 					"description": "Phrase to search for in phrase queries.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "value",
 					"in":          "query",
 					"description": "Value (node/edge attribute value) to search for in value queries.",

+ 1 - 1
api/v1/info.go

@@ -139,7 +139,7 @@ func (ie *infoEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "kind",
 					"in":          "path",
 					"description": "Node or edge kind to be queried.",

+ 6 - 6
api/v1/query.go

@@ -304,21 +304,21 @@ func (eq *queryEndpoint) SwaggerDefs(s map[string]interface{}) {
 				"application/json",
 			},
 			"parameters": []map[string]interface{}{
-				map[string]interface{}{
+				{
 					"name":        "partition",
 					"in":          "path",
 					"description": "Partition to query.",
 					"required":    true,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "q",
 					"in":          "query",
 					"description": "URL encoded query to execute.",
 					"required":    false,
 					"type":        "string",
 				},
-				map[string]interface{}{
+				{
 					"name":        "rid",
 					"in":          "query",
 					"description": "Result ID to retrieve from the result cache.",
@@ -326,7 +326,7 @@ func (eq *queryEndpoint) SwaggerDefs(s map[string]interface{}) {
 					"type":        "number",
 					"format":      "integer",
 				},
-				map[string]interface{}{
+				{
 					"name":        "limit",
 					"in":          "query",
 					"description": "How many list items to return.",
@@ -334,7 +334,7 @@ func (eq *queryEndpoint) SwaggerDefs(s map[string]interface{}) {
 					"type":        "number",
 					"format":      "integer",
 				},
-				map[string]interface{}{
+				{
 					"name":        "offset",
 					"in":          "query",
 					"description": "Offset in the dataset.",
@@ -342,7 +342,7 @@ func (eq *queryEndpoint) SwaggerDefs(s map[string]interface{}) {
 					"type":        "number",
 					"format":      "integer",
 				},
-				map[string]interface{}{
+				{
 					"name":        "groups",
 					"in":          "query",
 					"description": "Include group information in the result if set to any value.",

+ 1 - 1
api/v1/query_test.go

@@ -376,7 +376,7 @@ func TestQuery(t *testing.T) {
 
 	// Test first real query
 
-	st, _, res := sendTestRequest(queryURL+"//main?q=get+Song+with+ordering(ascending+key)", "GET", nil)
+	_, _, res = sendTestRequest(queryURL+"//main?q=get+Song+with+ordering(ascending+key)", "GET", nil)
 	st, _, res2 := sendTestRequest(queryURL+"//main?q=get+Song+with+ordering(ascending+key)&offset=0&limit=9", "GET", nil)
 
 	if st != "200 OK" || res2 != res || res != `

+ 1 - 1
api/v1/queryresult.go

@@ -535,7 +535,7 @@ SwaggerDefs is used to describe the endpoint in swagger.
 func (qre *queryResultEndpoint) SwaggerDefs(s map[string]interface{}) {
 
 	required := []map[string]interface{}{
-		map[string]interface{}{
+		{
 			"name":        "rid",
 			"in":          "path",
 			"description": "Result ID of a query result.",

+ 5 - 0
api/v1/rest_test.go

@@ -91,6 +91,11 @@ func sendTestRequest(url string, method string, content []byte) (string, http.He
 	} else {
 		req, err = http.NewRequest(method, url, nil)
 	}
+
+	if err != nil {
+		panic(err)
+	}
+
 	req.Header.Set("Content-Type", "application/json")
 
 	client := &http.Client{}

+ 2 - 3
cli/eliasdb.go

@@ -272,9 +272,8 @@ func getHostPortFromConfig() (string, string) {
 	host := fileutil.ConfStr(config.DefaultConfig, config.HTTPSHost)
 	port := fileutil.ConfStr(config.DefaultConfig, config.HTTPSPort)
 
-	configFile := filepath.Join(filepath.Dir(os.Args[0]), config.DefaultConfigFile)
-	if ok, _ := fileutil.PathExists(configFile); ok {
-		cfg, _ := fileutil.LoadConfig(configFile, config.DefaultConfig)
+	if ok, _ := fileutil.PathExists(config.DefaultConfigFile); ok {
+		cfg, _ := fileutil.LoadConfig(config.DefaultConfigFile, config.DefaultConfig)
 		if cfg != nil {
 
 			host = fileutil.ConfStr(cfg, config.HTTPSHost)

+ 2 - 1
cluster/distributedstoragemanager.go

@@ -195,7 +195,8 @@ func (dsm *DistributedStorageManager) insertOrUpdate(insert bool, loc uint64, o
 		return cloc.(uint64), err
 
 	}
-	// An error has occured we need to use another member
+
+	// An error has occurred we need to use another member
 
 	if rtype == RTInsert {
 

+ 0 - 1
cluster/distributiontable_test.go

@@ -116,7 +116,6 @@ f: [a b c]
 		return
 	}
 
-
 	// 2 members with replication factor of 2 (location range of 30)
 
 	dt, _ = createDistributionTable([]string{"a", "b"}, 2, 30)

+ 1 - 1
cluster/manager/client.go

@@ -409,7 +409,7 @@ func (mc *Client) SendAcquireClusterLock(lockName string) error {
 /*
 SendReleaseClusterLock tries to release a named lock on all members of the cluster.
 It is not an error if a lock is not takfen (or has expired) on this member or any other
-target memeber.
+target member.
 */
 func (mc *Client) SendReleaseClusterLock(lockName string) error {
 

+ 1 - 1
cluster/manager/config.go

@@ -68,7 +68,7 @@ const ConfigReplicationFactor = "ReplicationFactor"
 DefaultConfig is the defaut configuration
 */
 var DefaultConfig = map[string]interface{}{
-	ConfigRPC:               "localhost:9030",
+	ConfigRPC:               "127.0.0.1:9030",
 	ConfigMemberName:        "member1",
 	ConfigClusterSecret:     "secret123",
 	ConfigReplicationFactor: 1.0,

+ 1 - 1
cluster/manager/server.go

@@ -80,7 +80,7 @@ const (
 
 	// General arguments
 
-	RequestTARGET       RequestArgument = iota // Required argument which identifies the target cluster memeber
+	RequestTARGET       RequestArgument = iota // Required argument which identifies the target cluster member
 	RequestTOKEN                               // Client token which is used for authorization checks
 	RequestLOCK                                // Lock name which a member requests to take
 	RequestMEMBERNAME                          // Name for a member

+ 1 - 1
cluster/memberaddresstable.go

@@ -122,7 +122,7 @@ func (mat *memberAddressTable) NewClusterLoc(dsname string) (uint64, error) {
 
 	// Get counter
 
-	newLocCounter, _, err := mat.newlocCounter(dsname)
+	newLocCounter, _, _ := mat.newlocCounter(dsname)
 
 	// Check that rangeCounter is sensible
 

+ 4 - 5
cluster/memberstorage.go

@@ -248,7 +248,7 @@ func (ms *memberStorage) handleInsertRequest(distTable *DistributionTable, reque
 
 					// Add transfer request for replication
 
-					// At this point the operation has succedded. We still need to
+					// At this point the operation has succeeded. We still need to
 					// replicate the change to all the replicating members but
 					// any errors happening during this shall not fail this operation.
 					// The next rebalancing will then synchronize all members again.
@@ -338,7 +338,7 @@ func (ms *memberStorage) handleUpdateRequest(distTable *DistributionTable, reque
 
 						// Add transfer request for replication
 
-						// At this point the operation has succedded. We still need to
+						// At this point the operation has succeeded. We still need to
 						// replicate the change to all the replicating members but
 						// any errors happening during this shall not fail this operation.
 						// The next rebalancing will then synchronize all members again.
@@ -406,7 +406,7 @@ func (ms *memberStorage) handleFreeRequest(distTable *DistributionTable, request
 
 					// Add transfer request for replication
 
-					// At this point the operation has succedded. We still need to
+					// At this point the operation has succeeded. We still need to
 					// replicate the change to all the replicating members but
 					// any errors happening during this shall not fail this operation.
 					// The next rebalancing will then synchronize all members again.
@@ -519,7 +519,6 @@ func (ms *memberStorage) handleRebalanceRequest(distTable *DistributionTable, re
 		// Check if there was an error from the previous iteration
 
 		handleError(err)
-		err = nil
 
 		smname := smnames.([]string)[i]
 		ver := vers.([]uint64)[i]
@@ -624,7 +623,7 @@ func (ms *memberStorage) handleRebalanceRequest(distTable *DistributionTable, re
 					fmt.Sprintf("(Store): Rebalance removes %v location: %v from member %v",
 						smname, tr.loc, rsource))
 
-				res, err = ms.ds.sendDataRequest(rsource, &DataRequest{RTFree, map[DataRequestArg]interface{}{
+				_, err = ms.ds.sendDataRequest(rsource, &DataRequest{RTFree, map[DataRequestArg]interface{}{
 					RPStoreName: smname,
 					RPLoc:       cloc,
 				}, nil, true})

+ 2 - 2
config/config.go

@@ -25,7 +25,7 @@ import (
 /*
 ProductVersion is the current version of EliasDB
 */
-const ProductVersion = "0.0.0"
+const ProductVersion = "1.0.0"
 
 /*
 DefaultConfigFile is the default config file which will be used to configure EliasDB
@@ -77,7 +77,7 @@ var DefaultConfig = map[string]interface{}{
 	LocationWebFolder:        "web",
 	LocationUserDB:           "users.db",
 	LocationAccessDB:         "access.db",
-	HTTPSHost:                "localhost",
+	HTTPSHost:                "127.0.0.1",
 	HTTPSPort:                "9090",
 	CookieMaxAgeSeconds:      "86400",
 	HTTPSCertificate:         "cert.pem",

+ 32 - 24
console/console.go

@@ -315,7 +315,8 @@ func (c *EliasDBConsole) RunCommand(cmdString string) (bool, error) {
 
 				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 != "?" && cmd != "export" {
 
 				// Do not authenticate if running local commands
 
@@ -326,8 +327,10 @@ func (c *EliasDBConsole) RunCommand(cmdString string) (bool, error) {
 			}
 		}
 
-		if cmd, ok := c.CommandMap[cmd]; ok {
-			return true, cmd.Run(args, c)
+		if cmdObj, ok := c.CommandMap[cmd]; ok {
+			return true, cmdObj.Run(args, c)
+		} else if cmd == "?" {
+			return true, c.CommandMap["help"].Run(args, c)
 		}
 	}
 
@@ -443,6 +446,7 @@ func (c *EliasDBConsole) SendRequest(endpoint string, contentType string, method
 
 	var bodyStr string
 	var req *http.Request
+	var resp *http.Response
 	var err error
 
 	if content != nil {
@@ -450,36 +454,40 @@ func (c *EliasDBConsole) SendRequest(endpoint string, contentType string, method
 	} else {
 		req, err = http.NewRequest(method, c.url+endpoint, nil)
 	}
-	req.Header.Set("Content-Type", contentType)
 
-	// Set auth cookie
+	if err == nil {
 
-	if c.authCookie != nil {
-		req.AddCookie(c.authCookie)
-	}
+		req.Header.Set("Content-Type", contentType)
 
-	if reqMod != nil {
-		reqMod(req)
-	}
+		// Set auth cookie
 
-	// Console client does not verify the SSL keys
+		if c.authCookie != nil {
+			req.AddCookie(c.authCookie)
+		}
 
-	tlsConfig := &tls.Config{
-		InsecureSkipVerify: true,
-	}
-	transport := &http.Transport{TLSClientConfig: tlsConfig}
+		if reqMod != nil {
+			reqMod(req)
+		}
 
-	client := &http.Client{
-		Transport: transport,
-	}
+		// Console client does not verify the SSL keys
 
-	resp, err := client.Do(req)
+		tlsConfig := &tls.Config{
+			InsecureSkipVerify: true,
+		}
+		transport := &http.Transport{TLSClientConfig: tlsConfig}
 
-	if err == nil {
-		defer resp.Body.Close()
+		client := &http.Client{
+			Transport: transport,
+		}
 
-		body, _ := ioutil.ReadAll(resp.Body)
-		bodyStr = strings.Trim(string(body), " \n")
+		resp, err = client.Do(req)
+
+		if err == nil {
+			defer resp.Body.Close()
+
+			body, _ := ioutil.ReadAll(resp.Body)
+			bodyStr = strings.Trim(string(body), " \n")
+		}
 	}
 
 	// Just return the body

+ 44 - 30
embedding.md

@@ -1,38 +1,38 @@
 EliasDB Code Tutorial
 =====================
-The following text will give you an introduction to EliasDB's code structure and how to embed EliasDB in another Go project.
+The following text will give you an introduction on how to embed EliasDB in another Go project.
 
-Getting the source code
------------------------
-The easiest way to get the source code of EliasDB is to use go get. Assuming you have a normal go project with GOROOT pointing to its root.
-You can checkout the source code of EliasDB with:
+Prerequisites
+-------------
+You have a `go modules` (see [here](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more)) based go project.
+
+You can create a simple one by running:
 ```
-go get -d devt.de/common devt.de/eliasdb
+go mod init example.com/test
 ```
-For the rest of this tutorial it is assumed that you have the following directory structure:
-
-| Path | Description |
-| --- | --- |
-| src/devt.de/common | Common code used by EliasDB |
-| 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 |
-
-For this tutorial we create a demo file:
+and creating a file called `main.go` with the following content:
+```
+package main
 
-src/devt.de/demo/demo.go
+import "fmt"
 
+func main() {
+	fmt.Println("Test")
+}
+```
+Running `go build` should create a `test` executable in the current folder. Running `./test` will just output `Test`.
 
 Simple graph database setup
 ---------------------------
-The first step is to create a graph storage which will store the data. The following code will
-create a disk storage in the db/ subdirectory (the false flag opens the store in read / write mode):
+The first step is to create a graph storage which will store the data. The following code will create a disk storage in the db/ subdirectory (the false flag opens the store in read / write mode):
 ```
-func main() {
+import (
+	...
+		"devt.de/krotik/eliasdb/graph/graphstorage"
+)
 
+func main() {
+...
 	// Create a graph storage
 
 	gs, err := graphstorage.NewDiskGraphStorage("db", false)
@@ -43,6 +43,20 @@ func main() {
 	defer gs.Close()
 ...
 ```
+Running `go build` again should now download eliasdb as additional dependency (the actual versions might be different):
+```
+go: finding devt.de/krotik/eliasdb/graph/graphstorage latest
+go: finding devt.de/krotik/eliasdb/graph latest
+go: finding devt.de/krotik/eliasdb v1.0.0
+go: downloading devt.de/krotik/eliasdb v1.0.0
+go: extracting devt.de/krotik/eliasdb v1.0.0
+go: finding github.com/gorilla/websocket v1.4.1
+go: finding devt.de/krotik/common v1.0.0
+go: downloading devt.de/krotik/common v1.0.0
+go: extracting devt.de/krotik/common v1.0.0
+```
+The `go build` command will have modified the `go.mod` file and created a `go.sum` file.
+
 It is important to close a disk storage before shutdown. It is also possible to create a memory-only storage with:
 ```
 	gs = graphstorage.NewMemoryGraphStorage("memdb")
@@ -125,7 +139,7 @@ To iterate over all nodes of a specific kind you can use a node iterator:
 it, err := gm.NodeKeyIterator("main", "mynode")
 for it.HasNext() {
 	key := it.Next()
-	
+
 	if it.LastError != nil {
 		break
 	}
@@ -154,7 +168,7 @@ if idxerr == nil {
 	}
 }
 ```
-For even more complex searches you can use EQL (see also the EQL manual):
+For even more complex searches you can use EQL (see also the EQL manual  [here](https://devt.de/krotik/eliasdb/src/master/eql.md)):
 ```
 res, err := eql.RunQuery("myquery", "main", "get mynode where name = 'Node2'", gm)
 
@@ -173,16 +187,16 @@ Example source
 --------------
 An example demo.go could look like this:
 ```
-package demo
+package main
 
 import (
 	"fmt"
 	"log"
 
-	"devt.de/eliasdb/eql"
-	"devt.de/eliasdb/graph"
-	"devt.de/eliasdb/graph/data"
-	"devt.de/eliasdb/graph/graphstorage"
+	"devt.de/krotik/eliasdb/eql"
+	"devt.de/krotik/eliasdb/graph"
+	"devt.de/krotik/eliasdb/graph/data"
+	"devt.de/krotik/eliasdb/graph/graphstorage"
 )
 
 func main() {

+ 9 - 6
eql/interpreter/func.go

@@ -130,16 +130,19 @@ func whereParseDate(astNode *parser.ASTNode, rtp *eqlRuntimeProvider,
 		layout = fmt.Sprint(datestr)
 	}
 
-	// Convert the date string
-
-	datestr, err = astNode.Children[1].Runtime.(CondRuntime).CondEval(node, edge)
-
 	if err == nil {
 
-		t, err = time.Parse(layout, fmt.Sprint(datestr))
+		// Convert the date string
+
+		datestr, err = astNode.Children[1].Runtime.(CondRuntime).CondEval(node, edge)
 
 		if err == nil {
-			ret = t.Unix()
+
+			t, err = time.Parse(layout, fmt.Sprint(datestr))
+
+			if err == nil {
+				ret = t.Unix()
+			}
 		}
 	}
 

+ 2 - 2
eql/parser/lexer.go

@@ -408,10 +408,10 @@ r' ... ' or r" ... "
 Characters are parsed plain between quote
 */
 func lexValue(l *lexer) lexFunc {
-	l.startNew()
+	var endToken rune
 
+	l.startNew()
 	allowEscapes := false
-	endToken := ' '
 
 	r := l.next(false)
 

+ 59 - 59
eql/parser/parser.go

@@ -64,7 +64,7 @@ func ASTFromPlain(plainAST map[string]interface{}) (*ASTNode, error) {
 		if ic, ok := children.([]interface{}); ok {
 
 			// Do a list conversion if necessary - this is necessary when we parse
-			// JSON with map[string]interface{} this 
+			// JSON with map[string]interface{} this
 
 			childrenList := make([]map[string]interface{}, len(ic))
 			for i := range ic {
@@ -174,81 +174,81 @@ const TokenSHOWTERM = LexTokenID(-1)
 
 func init() {
 	astNodeMap = map[LexTokenID]*ASTNode{
-		TokenEOF:           &ASTNode{NodeEOF, nil, nil, nil, 0, ndTerm, nil},
-		TokenVALUE:         &ASTNode{NodeVALUE, nil, nil, nil, 0, ndTerm, nil},
-		TokenNODEKIND:      &ASTNode{NodeVALUE, nil, nil, nil, 0, ndTerm, nil},
-		TokenTRUE:          &ASTNode{NodeTRUE, nil, nil, nil, 0, ndTerm, nil},
-		TokenFALSE:         &ASTNode{NodeFALSE, nil, nil, nil, 0, ndTerm, nil},
-		TokenNULL:          &ASTNode{NodeNULL, nil, nil, nil, 0, ndTerm, nil},
-		TokenAT:            &ASTNode{NodeFUNC, nil, nil, nil, 0, ndFunc, nil},
-		TokenORDERING:      &ASTNode{NodeORDERING, nil, nil, nil, 0, ndWithFunc, nil},
-		TokenFILTERING:     &ASTNode{NodeFILTERING, nil, nil, nil, 0, ndWithFunc, nil},
-		TokenNULLTRAVERSAL: &ASTNode{NodeNULLTRAVERSAL, nil, nil, nil, 0, ndWithFunc, nil},
+		TokenEOF:           {NodeEOF, nil, nil, nil, 0, ndTerm, nil},
+		TokenVALUE:         {NodeVALUE, nil, nil, nil, 0, ndTerm, nil},
+		TokenNODEKIND:      {NodeVALUE, nil, nil, nil, 0, ndTerm, nil},
+		TokenTRUE:          {NodeTRUE, nil, nil, nil, 0, ndTerm, nil},
+		TokenFALSE:         {NodeFALSE, nil, nil, nil, 0, ndTerm, nil},
+		TokenNULL:          {NodeNULL, nil, nil, nil, 0, ndTerm, nil},
+		TokenAT:            {NodeFUNC, nil, nil, nil, 0, ndFunc, nil},
+		TokenORDERING:      {NodeORDERING, nil, nil, nil, 0, ndWithFunc, nil},
+		TokenFILTERING:     {NodeFILTERING, nil, nil, nil, 0, ndWithFunc, nil},
+		TokenNULLTRAVERSAL: {NodeNULLTRAVERSAL, nil, nil, nil, 0, ndWithFunc, nil},
 
 		// Special tokens - always handled in a denotation function
 
-		TokenCOMMA:  &ASTNode{NodeCOMMA, nil, nil, nil, 0, nil, nil},
-		TokenGROUP:  &ASTNode{NodeGROUP, nil, nil, nil, 0, nil, nil},
-		TokenEND:    &ASTNode{NodeEND, nil, nil, nil, 0, nil, nil},
-		TokenAS:     &ASTNode{NodeAS, nil, nil, nil, 0, nil, nil},
-		TokenFORMAT: &ASTNode{NodeFORMAT, nil, nil, nil, 0, nil, nil},
+		TokenCOMMA:  {NodeCOMMA, nil, nil, nil, 0, nil, nil},
+		TokenGROUP:  {NodeGROUP, nil, nil, nil, 0, nil, nil},
+		TokenEND:    {NodeEND, nil, nil, nil, 0, nil, nil},
+		TokenAS:     {NodeAS, nil, nil, nil, 0, nil, nil},
+		TokenFORMAT: {NodeFORMAT, nil, nil, nil, 0, nil, nil},
 
 		// Keywords
 
-		TokenGET:    &ASTNode{NodeGET, nil, nil, nil, 0, ndGet, nil},
-		TokenLOOKUP: &ASTNode{NodeLOOKUP, nil, nil, nil, 0, ndLookup, nil},
-		TokenFROM:   &ASTNode{NodeFROM, nil, nil, nil, 0, ndFrom, nil},
-		TokenWHERE:  &ASTNode{NodeWHERE, nil, nil, nil, 0, ndPrefix, nil},
+		TokenGET:    {NodeGET, nil, nil, nil, 0, ndGet, nil},
+		TokenLOOKUP: {NodeLOOKUP, nil, nil, nil, 0, ndLookup, nil},
+		TokenFROM:   {NodeFROM, nil, nil, nil, 0, ndFrom, nil},
+		TokenWHERE:  {NodeWHERE, nil, nil, nil, 0, ndPrefix, nil},
 
-		TokenUNIQUE:      &ASTNode{NodeUNIQUE, nil, nil, nil, 0, ndPrefix, nil},
-		TokenUNIQUECOUNT: &ASTNode{NodeUNIQUECOUNT, nil, nil, nil, 0, ndPrefix, nil},
-		TokenISNOTNULL:   &ASTNode{NodeISNOTNULL, nil, nil, nil, 0, ndPrefix, nil},
-		TokenASCENDING:   &ASTNode{NodeASCENDING, nil, nil, nil, 0, ndPrefix, nil},
-		TokenDESCENDING:  &ASTNode{NodeDESCENDING, nil, nil, nil, 0, ndPrefix, nil},
+		TokenUNIQUE:      {NodeUNIQUE, nil, nil, nil, 0, ndPrefix, nil},
+		TokenUNIQUECOUNT: {NodeUNIQUECOUNT, nil, nil, nil, 0, ndPrefix, nil},
+		TokenISNOTNULL:   {NodeISNOTNULL, nil, nil, nil, 0, ndPrefix, nil},
+		TokenASCENDING:   {NodeASCENDING, nil, nil, nil, 0, ndPrefix, nil},
+		TokenDESCENDING:  {NodeDESCENDING, nil, nil, nil, 0, ndPrefix, nil},
 
-		TokenTRAVERSE: &ASTNode{NodeTRAVERSE, nil, nil, nil, 0, ndTraverse, nil},
-		TokenPRIMARY:  &ASTNode{NodePRIMARY, nil, nil, nil, 0, ndPrefix, nil},
-		TokenSHOW:     &ASTNode{NodeSHOW, nil, nil, nil, 0, ndShow, nil},
-		TokenSHOWTERM: &ASTNode{NodeSHOWTERM, nil, nil, nil, 0, ndShow, nil},
-		TokenWITH:     &ASTNode{NodeWITH, nil, nil, nil, 0, ndWith, nil},
-		TokenLIST:     &ASTNode{NodeLIST, nil, nil, nil, 0, nil, nil},
+		TokenTRAVERSE: {NodeTRAVERSE, nil, nil, nil, 0, ndTraverse, nil},
+		TokenPRIMARY:  {NodePRIMARY, nil, nil, nil, 0, ndPrefix, nil},
+		TokenSHOW:     {NodeSHOW, nil, nil, nil, 0, ndShow, nil},
+		TokenSHOWTERM: {NodeSHOWTERM, nil, nil, nil, 0, ndShow, nil},
+		TokenWITH:     {NodeWITH, nil, nil, nil, 0, ndWith, nil},
+		TokenLIST:     {NodeLIST, nil, nil, nil, 0, nil, nil},
 
 		// Boolean operations
 
-		TokenNOT: &ASTNode{NodeNOT, nil, nil, nil, 20, ndPrefix, nil},
-		TokenOR:  &ASTNode{NodeOR, nil, nil, nil, 30, nil, ldInfix},
-		TokenAND: &ASTNode{NodeAND, nil, nil, nil, 40, nil, ldInfix},
-
-		TokenGEQ: &ASTNode{NodeGEQ, nil, nil, nil, 60, nil, ldInfix},
-		TokenLEQ: &ASTNode{NodeLEQ, nil, nil, nil, 60, nil, ldInfix},
-		TokenNEQ: &ASTNode{NodeNEQ, nil, nil, nil, 60, nil, ldInfix},
-		TokenEQ:  &ASTNode{NodeEQ, nil, nil, nil, 60, nil, ldInfix},
-		TokenGT:  &ASTNode{NodeGT, nil, nil, nil, 60, nil, ldInfix},
-		TokenLT:  &ASTNode{NodeLT, nil, nil, nil, 60, nil, ldInfix},
-
-		TokenLIKE:        &ASTNode{NodeLIKE, nil, nil, nil, 60, nil, ldInfix},
-		TokenIN:          &ASTNode{NodeIN, nil, nil, nil, 60, nil, ldInfix},
-		TokenCONTAINS:    &ASTNode{NodeCONTAINS, nil, nil, nil, 60, nil, ldInfix},
-		TokenBEGINSWITH:  &ASTNode{NodeBEGINSWITH, nil, nil, nil, 60, nil, ldInfix},
-		TokenENDSWITH:    &ASTNode{NodeENDSWITH, nil, nil, nil, 60, nil, ldInfix},
-		TokenCONTAINSNOT: &ASTNode{NodeCONTAINSNOT, nil, nil, nil, 60, nil, ldInfix},
-		TokenNOTIN:       &ASTNode{NodeNOTIN, nil, nil, nil, 60, nil, ldInfix},
+		TokenNOT: {NodeNOT, nil, nil, nil, 20, ndPrefix, nil},
+		TokenOR:  {NodeOR, nil, nil, nil, 30, nil, ldInfix},
+		TokenAND: {NodeAND, nil, nil, nil, 40, nil, ldInfix},
+
+		TokenGEQ: {NodeGEQ, nil, nil, nil, 60, nil, ldInfix},
+		TokenLEQ: {NodeLEQ, nil, nil, nil, 60, nil, ldInfix},
+		TokenNEQ: {NodeNEQ, nil, nil, nil, 60, nil, ldInfix},
+		TokenEQ:  {NodeEQ, nil, nil, nil, 60, nil, ldInfix},
+		TokenGT:  {NodeGT, nil, nil, nil, 60, nil, ldInfix},
+		TokenLT:  {NodeLT, nil, nil, nil, 60, nil, ldInfix},
+
+		TokenLIKE:        {NodeLIKE, nil, nil, nil, 60, nil, ldInfix},
+		TokenIN:          {NodeIN, nil, nil, nil, 60, nil, ldInfix},
+		TokenCONTAINS:    {NodeCONTAINS, nil, nil, nil, 60, nil, ldInfix},
+		TokenBEGINSWITH:  {NodeBEGINSWITH, nil, nil, nil, 60, nil, ldInfix},
+		TokenENDSWITH:    {NodeENDSWITH, nil, nil, nil, 60, nil, ldInfix},
+		TokenCONTAINSNOT: {NodeCONTAINSNOT, nil, nil, nil, 60, nil, ldInfix},
+		TokenNOTIN:       {NodeNOTIN, nil, nil, nil, 60, nil, ldInfix},
 
 		// Simple arithmetic expressions
 
-		TokenPLUS:   &ASTNode{NodePLUS, nil, nil, nil, 110, ndPrefix, ldInfix},
-		TokenMINUS:  &ASTNode{NodeMINUS, nil, nil, nil, 110, ndPrefix, ldInfix},
-		TokenTIMES:  &ASTNode{NodeTIMES, nil, nil, nil, 120, nil, ldInfix},
-		TokenDIV:    &ASTNode{NodeDIV, nil, nil, nil, 120, nil, ldInfix},
-		TokenMODINT: &ASTNode{NodeMODINT, nil, nil, nil, 120, nil, ldInfix},
-		TokenDIVINT: &ASTNode{NodeDIVINT, nil, nil, nil, 120, nil, ldInfix},
+		TokenPLUS:   {NodePLUS, nil, nil, nil, 110, ndPrefix, ldInfix},
+		TokenMINUS:  {NodeMINUS, nil, nil, nil, 110, ndPrefix, ldInfix},
+		TokenTIMES:  {NodeTIMES, nil, nil, nil, 120, nil, ldInfix},
+		TokenDIV:    {NodeDIV, nil, nil, nil, 120, nil, ldInfix},
+		TokenMODINT: {NodeMODINT, nil, nil, nil, 120, nil, ldInfix},
+		TokenDIVINT: {NodeDIVINT, nil, nil, nil, 120, nil, ldInfix},
 
 		// Brackets
 
-		TokenLPAREN: &ASTNode{NodeLPAREN, nil, nil, nil, 150, ndInner, nil},
-		TokenRPAREN: &ASTNode{NodeRPAREN, nil, nil, nil, 0, nil, nil},
-		TokenLBRACK: &ASTNode{NodeLBRACK, nil, nil, nil, 150, ndList, nil},
-		TokenRBRACK: &ASTNode{NodeRBRACK, nil, nil, nil, 0, nil, nil},
+		TokenLPAREN: {NodeLPAREN, nil, nil, nil, 150, ndInner, nil},
+		TokenRPAREN: {NodeRPAREN, nil, nil, nil, 0, nil, nil},
+		TokenLBRACK: {NodeLBRACK, nil, nil, nil, 150, ndList, nil},
+		TokenRBRACK: {NodeRBRACK, nil, nil, nil, 0, nil, nil},
 	}
 }
 

+ 13 - 13
eql/parser/parser_test.go

@@ -513,9 +513,9 @@ func TestParserErrorCases(t *testing.T) {
 	// Test "Get" parsing with invalid lexer output
 
 	res, err := testParserRun([]LexToken{
-		LexToken{TokenGET, 1, "", 1, 1},
-		LexToken{TokenGET, 1, "", 1, 1},
-		LexToken{TokenEOF, 1, "", 1, 1},
+		{TokenGET, 1, "", 1, 1},
+		{TokenGET, 1, "", 1, 1},
+		{TokenEOF, 1, "", 1, 1},
 	})
 	if err.Error() != "Parse error in special test: Unexpected term (Line:1 Pos:1)" {
 		t.Error("Unexpected result", res, err)
@@ -523,9 +523,9 @@ func TestParserErrorCases(t *testing.T) {
 	}
 
 	res, err = testParserRun([]LexToken{
-		LexToken{TokenLOOKUP, 1, "", 1, 1},
-		LexToken{TokenGET, 1, "", 1, 1},
-		LexToken{TokenEOF, 1, "", 1, 1},
+		{TokenLOOKUP, 1, "", 1, 1},
+		{TokenGET, 1, "", 1, 1},
+		{TokenEOF, 1, "", 1, 1},
 	})
 	if err.Error() != "Parse error in special test: Unexpected term (Line:1 Pos:1)" {
 		t.Error("Unexpected result", res, err)
@@ -631,8 +631,8 @@ func TestParserErrorCases(t *testing.T) {
 	var TokenUnknown LexTokenID = -5
 
 	res, err = testParserRun([]LexToken{
-		LexToken{TokenUnknown, 1, "", 1, 1},
-		LexToken{TokenEOF, 1, "", 1, 1},
+		{TokenUnknown, 1, "", 1, 1},
+		{TokenEOF, 1, "", 1, 1},
 	})
 	if err.Error() != "Parse error in special test: Unknown term (id:-5 (\"\")) (Line:1 Pos:1)" {
 		t.Error("Unexpected result", res, err)
@@ -640,10 +640,10 @@ func TestParserErrorCases(t *testing.T) {
 	}
 
 	res, err = testParserRun([]LexToken{
-		LexToken{TokenVALUE, 1, "", 1, 1},
-		LexToken{TokenMINUS, 1, "", 1, 1},
-		LexToken{TokenUnknown, 1, "", 1, 1},
-		LexToken{TokenEOF, 1, "", 1, 1},
+		{TokenVALUE, 1, "", 1, 1},
+		{TokenMINUS, 1, "", 1, 1},
+		{TokenUnknown, 1, "", 1, 1},
+		{TokenEOF, 1, "", 1, 1},
 	})
 	if err.Error() != "Parse error in special test: Unknown term (id:-5 (\"\")) (Line:1 Pos:1)" {
 		t.Error("Unexpected result", res, err)
@@ -897,7 +897,7 @@ get
 	if _, err := ASTFromPlain(map[string]interface{}{
 		"name":  "bla",
 		"value": "",
-		"children": []map[string]interface{}{map[string]interface{}{
+		"children": []map[string]interface{}{{
 			"fame": "bla",
 		}},
 	}); err.Error() != "Found plain ast node without a name: map[fame:bla]" {

+ 20 - 0
examples/chat/doc/chat.md

@@ -0,0 +1,20 @@
+EliasDB Chat Example
+==
+This example demonstrates a simple application which uses advanced features of EliasDB:
+- User Management
+- GraphQL subscriptions
+
+The tutorial assumes you have downloaded EliasDB and extracted it. For this tutorial please execute "start.sh" or "start.bat" in the subdirectory: examples/chat
+
+After starting EliasDB point your browser to:
+```
+https://localhost:9090
+```
+
+The generated default key and certificate for https are self-signed which should give a security warning in the browser. After accepting you should see a login prompt. Enter the credentials for the default user elias:
+```
+Username: elias
+Password: elias
+```
+
+The browser should display the chat application after clicking `Login`. Open a second window and write some chat messages. You can see that both windows update immediately. This is done with GraphQL subscriptions.

+ 53 - 0
examples/chat/res/access.db

@@ -0,0 +1,53 @@
+/*
+Access control file for EliasDB. This file controls the access rights for each user.
+Rights to resources are assigned to groups. Users are assigned to groups.
+
+This file is monitored by the server - any changes to this file are picked up
+by the server immediately. Equally, any change on the server side is immediately
+written to this file.
+
+The comments in this file are for initial comprehension only. They will be
+removed as soon as the users, groups or permissions are modified from the
+server side.
+*/
+{
+  "groups": {
+    "public": {
+
+      // Page access
+      // ===========
+
+      "/": "-R--",               // Access to the root page
+      "/dist/chat.js": "-R--",   // Access to the chat application
+
+      // Resource access
+      // ===============
+
+      "/css/*": "-R--",    // Access to CSS rules
+      "/js/*": "-R--",     // Access to JavaScript files
+      "/img/*": "-R--",    // Access to image files
+      "/vendor/*": "-R--", // Access to frontend libraries
+
+      // REST API access
+      // ===============
+
+      "/db/*": "-R--"      // Access to database (read)
+    },
+    "admin": {
+
+      // REST API access
+      // ===============
+
+      "/db/*": "CRUD"      // Access to database
+    }
+  },
+  "users": {
+    "elias": [    // Default EliasDB admin user
+      "public",
+      "admin"
+    ],
+	"johndoe" : [ // Default EliasDB public user
+	  "public"
+	]
+  }
+}

+ 12 - 0
examples/chat/res/chat/.eslintrc.js

@@ -0,0 +1,12 @@
+module.exports = {
+  parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+  extends: [
+    "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
+    "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
+    "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
+  ],
+  parserOptions: {
+    ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
+    sourceType: "module" // Allows for the use of imports
+  }
+};

+ 8 - 0
examples/chat/res/chat/.prettierrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  semi: true,
+  printWidth: 80,
+  bracketSpacing: false,
+  trailingComma: "all",
+  singleQuote: true,
+  tabWidth: 4
+};

+ 373 - 0
examples/chat/res/chat/LICENSE

@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 20 - 0
examples/chat/res/chat/README.md

@@ -0,0 +1,20 @@
+Chat Example
+--
+The chat is an example application demonstrating EliasDB's GraphQL interface. The application uses mutation operations to create chat messages and a subscription to receive new messages.
+
+The subscription uses a WebSocket which is used to "push" new messages from the server to the client. As soon as a client sends a new message to the server the subscription ensures that all clients are updated.
+
+The chat application comes as a compiled .js file in the dist/ directory and should work out of the box.
+
+Point a browser to: https://localhost:9090
+
+To rebuild the application use yarn:
+
+First install all necessary dependencies:
+```
+yarn
+```
+Then build the application:
+```
+yarn build
+```

File diff suppressed because it is too large
+ 11 - 0
examples/chat/res/chat/dist/chat.js


+ 10 - 0
examples/chat/res/chat/index.html

@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head></head>
+
+<body>
+    <div id="app"></div>
+</body>
+<script src="./dist/chat.js"></script>
+
+</html>

+ 29 - 0
examples/chat/res/chat/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "chat",
+  "version": "1.0.0",
+  "description": "A chat demo using a GraphQL subscription",
+  "main": "bundle.js",
+  "scripts": {
+    "build": "webpack",
+    "watch": "webpack --watch",
+    "pretty": "tsc --noEmit && eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
+  },
+  "author": "Matthias Ladkau",
+  "license": "ISC",
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^1.13.0",
+    "@typescript-eslint/parser": "^1.13.0",
+    "css-loader": "^3.1.0",
+    "eslint": "^6.1.0",
+    "eslint-config-prettier": "^6.0.0",
+    "eslint-plugin-prettier": "^3.1.0",
+    "prettier": "^1.18.2",
+    "ts-loader": "^6.0.4",
+    "typescript": "^3.5.3",
+    "vue": "^2.6.10",
+    "vue-loader": "^15.7.1",
+    "vue-template-compiler": "^2.6.10",
+    "webpack": "^4.39.1",
+    "webpack-cli": "^3.3.6"
+  }
+}

+ 80 - 0
examples/chat/res/chat/src/component/ChatTextArea.vue

@@ -0,0 +1,80 @@
+<!-- 
+*
+* EliasDB - Chat example
+*
+* Copyright 2019 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/.
+*
+
+Chat text area which lets the user enter new messages.
+-->
+<template>
+    <div>
+        <textarea class="chat-textarea"
+            v-model="message" 
+            v-on:keyup="eventKeyup"
+            placeholder="Your message here ..."/>
+        <input 
+            value="Send" 
+            title="Send message [CTRL+Return]"
+            v-on:click="eventClick"
+            type="button"/>
+    </div>
+</template>
+
+<script lang="ts">
+
+import Vue from "vue";
+import {EliasDBGraphQLClient} from "../lib/eliasdb-graphql";
+
+export default Vue.extend({
+    props: ['channel'],
+    data() {
+        return {
+            client : new EliasDBGraphQLClient(),
+            message : "",
+        }
+    },
+    mounted: function () {
+        let input = document.querySelector('textarea.chat-textarea');
+        if (input) {
+            (input as HTMLTextAreaElement).focus();
+        }
+    },
+    methods : {
+        sendData() {
+            if (this.message) {
+                this.client.req(`
+mutation($node : NodeTemplate) {
+  ${this.channel}(storeNode : $node) { }
+}`,
+                    {
+                        node : {
+                            key : Date.now().toString(),
+                            kind : this.channel,
+                            message : this.message,
+                        }
+                    })
+                    .catch(e => {
+                        console.error("Could not join channel:", e);
+                    });
+                this.message = '';
+            }
+        },
+        eventKeyup(event : KeyboardEvent) {
+            if (event.keyCode === 13 && event.ctrlKey) {
+                this.sendData();
+            }
+        },
+        eventClick(event : KeyboardEvent) {
+            this.sendData();
+        }
+    },
+});
+</script>
+
+<style>
+</style>

+ 83 - 0
examples/chat/res/chat/src/component/ChatWindow.vue

@@ -0,0 +1,83 @@
+<!-- 
+*
+* EliasDB - Chat example
+*
+* Copyright 2019 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/.
+*
+
+Chat window which displays an ongoing chat channel.
+-->
+<template>
+    <div>
+        <div class="chat-msg-window" 
+            v-for="msg in messages"
+            v-bind:key="msg.key">
+                <div>{{msg.message}}</div>
+        </div>
+        <chat-text-area :channel="channel"/>
+    </div>
+</template>
+
+<script lang="ts">
+
+import Vue from "vue";
+import ChatTextArea from './ChatTextArea.vue';
+import {EliasDBGraphQLClient} from "../lib/eliasdb-graphql";
+
+interface Message {
+    key: string
+    message: string
+}
+
+export default Vue.extend({
+    props: ['channel'],
+    data() {
+        return {
+            client : new EliasDBGraphQLClient(),
+            messages : [] as Message[],
+        }
+    },
+    mounted: function () {
+
+        // Ensure channel node exists
+
+        this.client.req(`
+mutation($node : NodeTemplate) {
+  ${this.channel}(storeNode : $node) { }
+}`,
+            {
+                node : {
+                    key : this.channel,
+                    kind : this.channel,
+                }
+            })
+            .catch(e => {
+                console.error("Could not join channel:", e);
+            });
+
+        // Start subscription
+
+        this.client.subscribe(`
+subscription {
+  ${this.channel}(ascending:key, last:11) { # last:11 because channel node will be last
+      key,
+      message,
+  }
+}`,
+            data => {
+                const messages = data.data[this.channel] as Message[];
+                this.messages = messages.filter(m => !!m.message);
+            });
+    },
+    components: {
+        ChatTextArea,
+    },
+});
+</script>
+
+<style>
+</style>

+ 19 - 0
examples/chat/res/chat/src/index.ts

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import ChatWindow from './component/ChatWindow.vue';
+
+let v = new Vue({
+    el: '#app',
+    template: `
+    <div>
+        <chat-window :channel="channel" />
+    </div>
+    `,
+    data() {
+        return {
+            channel: 'general',
+        };
+    },
+    components: {
+        ChatWindow,
+    },
+});

+ 231 - 0
examples/chat/res/chat/src/lib/eliasdb-graphql.ts

@@ -0,0 +1,231 @@
+/**
+ * EliasDB - JavaScript GraphQL client library
+ *
+ * Copyright 2019 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/.
+ *
+ */
+export enum RequestMetod {
+    Post = 'post',
+    Get = 'get',
+}
+
+export class EliasDBGraphQLClient {
+    /**
+     * Host this client is connected to.
+     */
+    protected host: string;
+
+    /**
+     * Partition this client is working on.
+     */
+    protected partition: string;
+
+    /**
+     * Websocket over which we can handle subscriptions.
+     */
+    private ws?: WebSocket;
+
+    /**
+     * EliasDB GraphQL endpoints.
+     */
+    private graphQLEndpoint: string;
+    private graphQLReadOnlyEndpoint: string;
+
+    /**
+     * List of operations to execute once the websocket connection is established.
+     */
+    private delayedOperations: {(): void}[] = [];
+
+    /**
+     * Queue of subscriptions which await an id;
+     */
+    private subscriptionQueue: {(data: any): void}[] = [];
+
+    /**
+     * Map of active subscriptions.
+     */
+    private subscriptionCallbacks: {[id: string]: {(data: any): void}} = {};
+
+    /**
+     * Createa a new EliasDB GraphQL Client.
+     *
+     * @param host Host to connect to.
+     * @param partition Partition to query.
+     */
+    public constructor(
+        host: string = window.location.host,
+        partition: string = 'main',
+    ) {
+        this.host = host;
+        this.partition = partition;
+        this.graphQLEndpoint = `https://${host}/db/v1/graphql/${partition}`;
+        this.graphQLReadOnlyEndpoint = `https://${host}/db/v1/graphql-query/${partition}`;
+    }
+
+    /**
+     * Initialize a websocket to support subscriptions.
+     */
+    private initWebsocket() {
+        const url = `wss://${this.host}/db/v1/graphql-subscriptions/${this.partition}`;
+        this.ws = new WebSocket(url);
+        this.ws.onmessage = this.message.bind(this);
+
+        this.ws.onopen = () => {
+            if (this.ws) {
+                this.ws.send(
+                    JSON.stringify({
+                        type: 'init',
+                        payload: {},
+                    }),
+                );
+            }
+        };
+    }
+
+    /**
+     * Run a GraphQL query or mutation and return the response.
+     *
+     * @param query Query to run.
+     * @param variables List of variable values. The query must define these
+     *                  variables.
+     * @param operationName Name of the named operation to run. The query must
+     *                      specify this named operation.
+     * @param method  Request method to use. Get requests cannot run mutations.
+     */
+    public req(
+        query: string,
+        variables: {[key: string]: any} = {},
+        operationName: string = '',
+        method: RequestMetod = RequestMetod.Post,
+    ): Promise<any> {
+        const http = new XMLHttpRequest();
+
+        const toSend: {[key: string]: any} = {
+            operationName,
+            variables,
+            query,
+        };
+
+        // Send an async ajax call
+
+        if (method === RequestMetod.Post) {
+            http.open(method, this.graphQLEndpoint, true);
+        } else {
+            const params = Object.keys(toSend)
+                .map(key => {
+                    const val =
+                        key !== 'variables'
+                            ? toSend[key]
+                            : JSON.stringify(toSend[key]);
+                    return `${key}=${encodeURIComponent(val)}`;
+                })
+                .join('&');
+            const url = `${this.graphQLReadOnlyEndpoint}?${params}`;
+
+            http.open(method, url, true);
+        }
+
+        http.setRequestHeader('content-type', 'application/json');
+
+        return new Promise(function(resolve, reject) {
+            http.onload = function() {
+                try {
+                    if (http.status === 200) {
+                        resolve(http.response);
+                    } else {
+                        let err: string;
+                        try {
+                            err = JSON.parse(http.responseText)['errors'];
+                        } catch {
+                            err = http.responseText.trim();
+                        }
+                        reject(err);
+                    }
+                } catch (e) {
+                    reject(e);
+                }
+            };
+
+            if (method === RequestMetod.Post) {
+                http.send(JSON.stringify(toSend));
+            } else {
+                http.send();
+            }
+        });
+    }
+
+    /**