Coder Social home page Coder Social logo

arangolite's Introduction

Arangolite Build Status Coverage Status Code Climate GoDoc

Arangolite is a lightweight ArangoDB driver for Go.

It focuses on pure AQL querying. See AranGO for a more ORM-like experience.

IMPORTANT: Looking for maintainers

I don't have as much time as I used to have and I am not as a frequent user of ArangoDB as I used to be. This project would definitely benefit from some new maintainers. Any PR is greatly appreciated.

Installation

To install Arangolite:

go get -u github.com/solher/arangolite/v2

Basic Usage

package main

import (
  "context"
  "fmt"
  "log"

  "github.com/solher/arangolite/v2"
  "github.com/solher/arangolite/v2/requests"
)

type Node struct {
  arangolite.Document
}

func main() {
  ctx := context.Background()

  // We declare the database definition.
  db := arangolite.NewDatabase(
    arangolite.OptEndpoint("http://localhost:8529"),
    arangolite.OptBasicAuth("root", "rootPassword"),
    arangolite.OptDatabaseName("_system"),
  )

  // The Connect method does two things:
  // - Initializes the connection if needed (JWT authentication).
  // - Checks the database connectivity.
  if err := db.Connect(ctx); err != nil {
    log.Fatal(err)
  }

  // We create a new database.
  err := db.Run(ctx, nil, &requests.CreateDatabase{
    Name: "testDB",
    Users: []map[string]interface{}{
      {"username": "root", "passwd": "rootPassword"},
      {"username": "user", "passwd": "password"},
    },
  })
  if err != nil {
    log.Fatal(err)
  }

  // We sign in as the new created user on the new database.
  // We could eventually rerun a "db.Connect()" to confirm the connectivity.
  db.Options(
    arangolite.OptBasicAuth("user", "password"),
    arangolite.OptDatabaseName("testDB"),
  )

  // We create a new "nodes" collection.
  if err := db.Run(ctx, nil, &requests.CreateCollection{Name: "nodes"}); err != nil {
    log.Fatal(err)
  }

  // We declare a new AQL query with options and bind parameters.
  key := "48765564346"
  r := requests.NewAQL(`
    FOR n
    IN nodes
    FILTER n._key == @key
    RETURN n
  `, key).
    Bind("key", key).
    Cache(true).
    BatchSize(500) // The caching feature is unavailable prior to ArangoDB 2.7

  // The Run method returns all the query results of every pages
  // available in the cursor and unmarshal it into the given struct.
  // Cancelling the context cancels every running request. 
  nodes := []Node{}
  if err := db.Run(ctx, &nodes, r); err != nil {
    log.Fatal(err)
  }

  // The Send method gives more control to the user and doesn't follow an eventual cursor.
  // It returns a raw result object.
  result, err := db.Send(ctx, r)
  if err != nil {
    log.Fatal(err)
  }
  nodes = []Node{}
  result.UnmarshalResult(&nodes)

  for result.HasMore() {
    result, err = db.Send(ctx, &requests.FollowCursor{Cursor: result.Cursor()})
    if err != nil {
      log.Fatal(err)
    }
    tmp := []Node{}
    result.UnmarshalResult(&tmp)

    nodes = append(nodes, tmp...)
  }

  fmt.Println(nodes)
}

Document and Edge

// Document represents a basic ArangoDB document
type Document struct {
  // The document handle. Format: ':collection/:key'
  ID string `json:"_id,omitempty"`
  // The document's revision token. Changes at each update.
  Rev string `json:"_rev,omitempty"`
  // The document's unique key.
  Key string `json:"_key,omitempty"`
}

// Edge represents a basic ArangoDB edge
type Edge struct {
  Document
  // Reference to another document. Format: ':collection/:key'
  From string `json:"_from,omitempty"`
  // Reference to another document. Format: ':collection/:key'
  To string `json:"_to,omitempty"`
}

Transactions

Overview

Arangolite provides an abstraction layer to the Javascript ArangoDB transactions.

The only limitation is that no Javascript processing can be manually added inside the transaction. The queries can be connected using the Go templating conventions.

Usage

t := requests.NewTransaction([]string{"nodes"}, nil).
  AddAQL("nodes", `
    FOR n
    IN nodes
    RETURN n
`).
  AddAQL("ids", `
    FOR n
    IN {{.nodes}}
    RETURN n._id
`).
  Return("ids")

ids := []string{}
if err := db.Run(ctx, ids, t); err != nil {
  log.Fatal(err)
}

Graphs

Overview

AQL may be used for querying graph data. But to manage graphs, Arangolite offers a few specific requests:

  • CreateGraph to create a graph.
  • ListGraphs to list available graphs.
  • GetGraph to get an existing graph.
  • DropGraph to delete a graph.

Usage

// Check graph existence.
if err := db.Run(ctx, nil, &requests.GetGraph{Name: "graphName"}); err != nil {
  switch {
  case arangolite.IsErrNotFound(err):
    // If graph does not exist, create a new one.
    edgeDefinitions := []requests.EdgeDefinition{
      {
        Collection: "edgeCollectionName",
        From:       []string{"firstCollectionName"},
        To:         []string{"secondCollectionName"},
      },
    }
    db.Run(ctx, nil, &requests.CreateGraph{Name: "graphName", EdgeDefinitions: edgeDefinitions})
  default:
    log.Fatal(err)
  }
}

// List existing graphs.
list := &requests.GraphList{}
db.Run(ctx, list, &requests.ListGraphs{})
fmt.Printf("Graph list: %v\n", list)

// Destroy the graph we just created, and the related collections.
db.Run(ctx, nil, &requests.DropGraph{Name: "graphName", DropCollections: true})

Error Handling

Errors can be handled using the provided basic testers:

// IsErrInvalidRequest returns true when the database returns a 400.
func IsErrInvalidRequest(err error) bool {
  return HasStatusCode(err, 400)
}

// IsErrUnauthorized returns true when the database returns a 401.
func IsErrUnauthorized(err error) bool {
  return HasStatusCode(err, 401)
}

// IsErrForbidden returns true when the database returns a 403.
func IsErrForbidden(err error) bool {
  return HasStatusCode(err, 403)
}

// IsErrUnique returns true when the error num is a 1210 - ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED.
func IsErrUnique(err error) bool {
  return HasErrorNum(err, 1210)
}

// IsErrNotFound returns true when the database returns a 404 or when the error num is:
// 1202 - ERROR_ARANGO_DOCUMENT_NOT_FOUND
// 1203 - ERROR_ARANGO_COLLECTION_NOT_FOUND
func IsErrNotFound(err error) bool {
  return HasStatusCode(err, 404) || HasErrorNum(err, 1202, 1203)
}

Or manually via the HasStatusCode and HasErrorNum methods.

Contributing

Currently, very few methods of the ArangoDB HTTP API are implemented in Arangolite. Fortunately, it is really easy to add your own by implementing the Runnable interface. You can then use the regular Run and Send methods.

// Runnable defines requests runnable by the Run and Send methods.
// A Runnable library is located in the 'requests' package.
type Runnable interface {
  // The body of the request.
  Generate() []byte
  // The path where to send the request.
  Path() string
  // The HTTP method to use.
  Method() string
}

Please pull request when you implement some new features so everybody can use it.

License

MIT

arangolite's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

arangolite's Issues

Accessing error code from returned error

When performing a CreateDatabase where the database already exists, the database returns error code 1207 (https://docs.arangodb.com/3.0/Manual/Appendix/ErrorCodes.html, ERROR_ARANGO_DUPLICATE_NAME).

This error code is wrapped in a numberedError at https://github.com/solher/arangolite/blob/v2.0.1/database.go#L207

However, at the next step, the numberedError is wrapped in a statusCodedError at https://github.com/solher/arangolite/blob/v2.0.1/database.go#L213

The statusCodedError is what is returned by Send.

My question is how should the error code be accessed?

HasErrorNum will not work, since it requires a numberedError as input, and casting to statusCodedError to access the error attribute doesn't work, since statusCodedError is not exported.

Current result of arangolite.HasStatusCode(err) is 409, but arangolite.HasErrorNum(e_status, 1207) gives false.

Support 3.x

The API changed between the version. For example Collects returns "results". The current library can't read that.

Basic example error

Hi

In the basic example, in the readme, I believe you should switch these 2 lines:

_, _ := db.Run(&arangolite.CreateCollection{Name: "nodes"})

db.SwitchDatabase("testDB").SwitchUser("user", "password")

Otherwise you create the collection in the wrong database

Unmarshalling Result Has Duplicate Methods

Response unmarshalling requires wrapping the unmarshalled interface in a result tag.

`json"result"`

I believe this is because both the Unmarshal and UnmarshalResult methods are both operating on the raw response which is enclosed in a result tag.

func (r *response) Unmarshal(v interface{}) error {
	if err := json.Unmarshal(r.raw, v); err != nil {
		return errors.Wrap(err, "response unmarshalling failed")
	}
	return nil
}

func (r *response) UnmarshalResult(v interface{}) error {
	if err := json.Unmarshal(r.raw, v); err != nil {
		return errors.Wrap(err, "response result unmarshalling failed")
	}
	return nil
}

I believe UnmarshalResult should be operating on the parsedResponse instead.

Adding new commands

I will extend the driver. I already started with an edge creation feature. But something worries me: in your readme you suggest to add everything in requests.go. So this file could become very long...
I believe the code should be organised by around the business domain, and not the technical notions. By example the create edge request definition should be in edge.go.
Do you agree with this?

Make db fields public

I trying to use this library as the basis for an ArangoDB migration tool. Creating a new database is harder than it should be since the DB properties are hidden. I'm sure I could get them with reflect, but I don't think that should be necessary. Would you be open to making the fields for NewDatabase public via an interface or just regular public?

database.Run() response result (parsedResponse.Result) always empty

The response object returned by database.Send()
is passed to database.followCursor() in the database.Run() function.

When the response object is created in senders.Send(), the response's parsed (parsedResponse) attribute is unmarshalled.

It seem the parsedResponse.Result attribute is never populated, and is simply an empty array.

The problem then occurs in database.followCursor() where the call to r.RawResult() always returns an empty array, and hence, the result is not unmarshalled into the v input argument.

I'm I doing something wrong?

Add support for jwt

The new http api has a jwt auth method, it would be cool to add it. ๐Ÿ˜„

Prevent AQL injection in transaction

To prevent AQL injection in transaction, my understanding of the documentation is that we could use javascript variables.

I created a modified version of transaction.

Its usage:

transaction := arangolite.NewTransaction([]string{}, []string{}).
    AddQuery("var1", "FOR d IN nodes FILTER d._key == {{.key}} RETURN d").
    AddQuery("var2", "FOR d IN nodes FILTER d._id == {{.var1}}._id RETURN d.key").Return("var2")

transaction.Bind("key", 123)

result, err := db.Run(transaction) 

But the result is not what I expect; It returns an empty array instead of [123].
The reason is that the replacement of var1 in the second query is not working.

Do you have any idea to fix this?

BTW, this solution does not seem to work with collection names. It looks like they cannot by defined by a javascript variable. But I assume that's a minor limitation.

Quote in json value breaks the queries

Hi Fabien

When inserting a struct represeting a user, if any field of this user contains a simple quote, the resulting AQL query is invalid:

My code is the following one

userJsonBytes, err := json.Marshal(user)
	if err != nil {
		return err
	}
	userJson := string(userJsonBytes)

	query := requests.NewAQL(` UPSERT { "id":"%s" } INSERT %s UPDATE %s IN %s `, user.ID, userJson, userJson, CollectionName)

I probably could find a workaround by creating a dedicated json marshaller; but shouldn't the driver prevent this issue?

Allow for extension of the library with change to Runnable

I've just started using your library in a project and I really like the API. I can definitely understand wanting to avoid creating an overly complex API as it makes it complicated to maintain and becomes a barrier to adoption.

My use case for Arango is a bit different than the original intent of arangolite in that I need to be able to create documents and perform a few other API calls. Instead of adding bloat to your library, I would like to propose a change to the Runnable interface to allow for extensions to be created instead.

The change would be to export the methods in Runnable as follows:

type Runnable interface {
    Description() string // Description shown in the logger
    Generate() []byte    // The body of the request
    Path() string        // The path where to send the request
    Method() string      // The HTTP method to use
}

This would require changing all the existing Runnables in your library but would then allow others to extend the library by simple creating new Runnable implementations and passing them to DB.Run.

Let me know if this would be acceptable and I can create a PR to make the changes.

Support cursors without goroutines

Creating something like pagination seems expensive with this way.

Say I run a query like FOR n IN nodes LIMIT 1000 RETURN 20, by using Run, all 1000 results if found in the DB are returned, and no cursor key to manage the request.

Using RunAsync means having a goroutine followCursor created everytime there is more results in the cursor, and I am forced to get everything by checking if there is more just not to have hanging goroutine(s) in the background, or close the channel(meaning I have to begin going through the cursor again).

If I had multiple clients in an application running the same query, I have to keep these goroutines even though none of them might be interested in going through the list of results.

I propose we return the result.ID and let the user(developer) decide a proper method for getting the results.

Possibility to get []byte instead of umarshaled struct as result

We are using this lib and extracting large chunks of json. The problem is that these jsons does not have strict structure so they should be unmarshaled only to map[string]interface{} for latter use, but in our scenario we do not need to unmarshal json at all and raw []byte of json is ok. It would be nice:

  • To have func (db *Database) GetRaw(ctx context.Context, q Runnable) (json.RawMessage, error) or equivalent.

Or

  • Make followCursor as exported so this can be easily achieved with Send.

Any opinions?

Requests produce no result

When running the example below (derived from the readme example), I have two problems:

  • The node collection is not created.
  • the async request returns an empty array
package main

import (
  "encoding/json"
  "fmt"

  "github.com/solher/arangolite"
)

type Node struct {
  arangolite.Document
}

type MyNode struct {
  // The document handle. Format: ':collection/:key'
  ID string `json:"_id,omitempty"`
  // The document's revision token. Changes at each update.
  Rev string `json:"_rev,omitempty"`
  // The document's unique key.
  Key string `json:"_key,omitempty"`
}

func main() {
  db := arangolite.New().
    LoggerOptions(false, false, false).
    Connect("http://localhost:8529", "_system", "root", "")

  db.Run(&arangolite.CreateDatabase{
        Name: "testDB",
        Users: []map[string]interface{}{
            {"username": "root", "passwd": ""},
            {"username": "user", "passwd": ""},
        },
    })
  db.SwitchDatabase("testDB").SwitchUser("user", "")

  db.Run(&arangolite.CreateCollection{Name: "nodes"})

  // The Run method returns all the query results of every batches
  // available in the cursor as a slice of byte.
  key1 := "48765564346"
  key2 := "48765564347"

  _, err := remove(db, key1)
  _, err = remove(db, key2)

  _, err = insert(db, key1)
  _, err = insert(db, key2)

  if (err != nil) {
    return
  }

  q := arangolite.NewQuery(`
    FOR n
    IN nodes
    FILTER n._key == "%s"
    LIMIT 1
    RETURN n
  `, key1)

  async, asyncErr := db.RunAsync(q)

  if(asyncErr != nil){
    fmt.Printf("asyncErr %v", asyncErr)
  }

  nodes := []Node{}
  decoder := json.NewDecoder(async.Buffer())

  for async.HasMore() {
    batch := []Node{}
    decoder.Decode(batch)
    fmt.Printf("%v", decoder)
    nodes = append(nodes, batch...)
  }

  fmt.Printf("%v", nodes)
}

func remove(db *arangolite.DB, key string) ([]byte, error) {
  removeQuery := arangolite.NewQuery(`
      REMOVE  { _key: '%s' }  IN nodes
    `, key)
  res, err := db.Run(removeQuery)
  if(err != nil) {
    fmt.Printf("Remove error %v", err)
  }
  return res, err
}

func insert(db *arangolite.DB, key string) ([]byte, error) {

  node := MyNode{
     ID: "nodes/" + key,
     Rev: key,
     Key: key,
  }

  value, marshallErr := json.Marshal(node)
  if(marshallErr != nil) {
    fmt.Printf("Insertion error %v. Cannot convert %v to JSON", marshallErr, value)
    return nil, marshallErr
  }
  insertQuery := arangolite.NewQuery(`
      INSERT %s
      IN nodes
    `, value)

  insertResult, err := db.Run(insertQuery)

  if (err != nil) {
    fmt.Printf("Insertion error %v", err)
  }
  return insertResult, err
}

What did I get wrong?

How Do I Remove Edge

Is this even possible using AQL only?

From here it says that currently there is no way to do this via AQL. But arangolite doesn't seems to have a way to remove edge using graph management interface?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.