From 4ff242a44e5cbbf3a83858bc6762f7532b391948 Mon Sep 17 00:00:00 2001 From: Jerry Saravia Date: Thu, 11 Dec 2014 23:52:04 -0500 Subject: [PATCH] Added methods to save and fetch documents. --- collection.go | 74 ++++++++++++++++++++--- collection_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++ connection.go | 7 ++- database.go | 127 ++++++++++++++++++++++++++++++++++---- database_test.go | 45 ++++++++++++-- document.go | 76 +++++++++++++++++++++++ document_test.go | 1 + errors.go | 8 +++ 8 files changed, 456 insertions(+), 30 deletions(-) create mode 100644 document.go create mode 100644 document_test.go diff --git a/collection.go b/collection.go index 1e02e77..4e10566 100644 --- a/collection.go +++ b/collection.go @@ -2,6 +2,7 @@ package arango import ( "fmt" + "strings" ) //Collection types @@ -52,7 +53,7 @@ type CollectionCreationOptions struct { //KeyOptions stores information about how a collection's key is configured. //It is used during collection creation to specify how the new collection's -//key should be setup. +//key should be setup. // //It is also used for existing collections so you know how the collection's //key is configured. @@ -111,11 +112,11 @@ func (c *Collection) Name() string { //The result is cached. Call Properties() to refresh. //The status can be any of the constants defined above. //const ( - //NEW_BORN_STATUS = 1 - //UNLOADED_STATUS = 2 - //LOADED_STATUS = 3 - //BEING_UNLOADED_STATUS = 4 - //DELETED_STATUS = 5 +//NEW_BORN_STATUS = 1 +//UNLOADED_STATUS = 2 +//LOADED_STATUS = 3 +//BEING_UNLOADED_STATUS = 4 +//DELETED_STATUS = 5 //) func (c *Collection) Status() int { return c.json.Status @@ -168,7 +169,6 @@ func (c *Collection) KeyOptions() *KeyOptions { return c.json.KeyOptions } - //Properties fetches additional properties of the collection. //It queries the /_api/collection/{collection-name}/properties //endpoint and causes the collection to switch to the loaded @@ -200,8 +200,64 @@ func (c *Collection) Properties() error { //Drop deletes the collection from the database //DO NOT expect the collection to work after dropping it. -//Calling any further methods on it will result in +//Calling any further methods on it will result in //unexpected behavior func (c *Collection) Drop() error { - return c.db.DropCollection( c.Name() ) + return c.db.DropCollection(c.Name()) +} + +//Save creates a document in the collection. +//Uses the POST /_api/document endpoint +//If your document embeds the DocumentImplementation type +//or it has fields to hold the Id, Rev, and Key fields +//from arango, then it will be populated with the Id, Rev, Key +//fields during the json.Unmarshal call +func (c *Collection) Save(document interface{}) error { + return c.db.SaveDocumentWithOptions(document, &SaveOptions{ + Collection: c.Name(), + CreateCollection: false, + WaitForSync: false, + }) +} + +//SaveWithOptions lets you save a document but lets you specify some options +//See the POST /_api/document endpoint for more info. +func (c *Collection) SaveWithOptions(document interface{}, options *SaveOptions) error { + + options.Collection = c.Name() + options.CreateCollection = false + + return c.db.SaveDocumentWithOptions(document, options) +} + +//Document will fetch the document associated with the documentHandle. +//An error will be returned if anything goes wrong. Otherwise, document +//be populated with the document values from arango. json.Unmarhshal +//is used under the hood so make sure you understand how string literals +//tags work with the encoding/json package. +//This uses the GET /_api/document/{document-handle} endpoint. +//The method provides a extra bit of functionality in that it won't let +//you fetch an document NOT from this collection. For example, +//you can't request 'users/123413' when the collection name is 'places'. +//If you want to look up an arbitrary document then use db.Document() +func (c *Collection) Document(documentHandle interface{}, + document interface{}) error { + return c.DocumentWithOptions(documentHandle, document, nil) +} + +func (c *Collection) DocumentWithOptions(documentHandle interface{}, + document interface{}, + options *FetchDocumentOptions) error { + switch id := documentHandle.(type) { + case string: + idParts := strings.Split(id, "/") + if len(idParts) == 1 { + return c.db.DocumentWithOptions(c.Name()+"/"+idParts[0], document, options) + } else if len(idParts) == 2 { + if idParts[0] != c.Name() { + return newError(fmt.Sprintf("The id %s won't be found in the collection named %s.", id, c.Name())) + } + } + } + return c.db.DocumentWithOptions(documentHandle, document, options) } diff --git a/collection_test.go b/collection_test.go index 8845429..6a75961 100644 --- a/collection_test.go +++ b/collection_test.go @@ -1,3 +1,151 @@ package arango +import ( + + "testing" +) +var ( + db *Database +) + +func setup() { + var err error + db, err = Conn( "http://root@localhost:8529" ) + if err != nil { + panic( err ) + } + + db.DropDatabase( "testing" ) + err = db.CreateDatabase( "testing", nil, users ) + + if err != nil { + panic( err ) + } + db, err = db.UseDatabase( "testing" ) + if err != nil { + panic( err ) + } +} + +func teardown(){ + var err error + db, err = db.UseDatabase( "_system" ) + if err != nil { + panic( err ) + } + err = db.DropDatabase( "testing" ) + if err != nil { + panic( err ) + } +} + +type DummyDocument struct{ + Hi string +} + +type DummyFullDocument struct { + DocumentImplementation + Hi string +} + +func TestSavingAndRetrievingDocument( t *testing.T ){ + + setup() + defer teardown() + + c, err := db.CreateDocumentCollection( "testing" ) + + err = c.Save( &DummyDocument{ Hi : "Hello World" } ) + + if err != nil { + t.Fatal( err ) + } + + fulld := &DummyFullDocument{ + Hi : "Hello World", + } + + err = c.Save( fulld ) + + if err != nil { + t.Fatal( err ) + } + + if fulld.Id() == "" { + t.Fatal( "Expected id to be populated in document after a save." ) + } + + //Find it via full id + ret := &DummyFullDocument{} + err = c.Document( fulld.Id(), ret ) + + if err != nil { + t.Fatal( err ) + } + + if ret.Hi != "Hello World" { + t.Fatal( "Expected to have the value for the document correctly fetched.") + } + + if ret.Id() != fulld.Id() { + t.Fatal( "Expected to have the ids for documents be equal since they are the same document.") + } + + if ret.Key() != fulld.Key() { + t.Fatal( "Expected to have the keys for documents be equal since they are the same document.") + } + + if ret.Rev() != fulld.Rev() { + t.Fatal( "Expected to have the revs for documents be equal since they are the same document.") + } + + //Find it using a Document struct + ret = &DummyFullDocument{} + err = c.Document( fulld, ret ) + + if err != nil { + t.Fatal( err ) + } + + if ret.Hi != "Hello World" { + t.Fatal( "Expected to have the value for the document correctly fetched.") + } + + if ret.Id() != fulld.Id() { + t.Fatal( "Expected to have the ids for documents be equal since they are the same document.") + } + + if ret.Key() != fulld.Key() { + t.Fatal( "Expected to have the keys for documents be equal since they are the same document.") + } + + if ret.Rev() != fulld.Rev() { + t.Fatal( "Expected to have the revs for documents be equal since they are the same document.") + } + + //Find it by key + ret = &DummyFullDocument{} + err = c.Document( fulld.Key(), ret ) + + if err != nil { + t.Fatal( err ) + } + + if ret.Hi != "Hello World" { + t.Fatal( "Expected to have the value for the document correctly fetched.") + } + + if ret.Id() != fulld.Id() { + t.Fatal( "Expected to have the ids for documents be equal since they are the same document.") + } + + if ret.Key() != fulld.Key() { + t.Fatal( "Expected to have the keys for documents be equal since they are the same document.") + } + + if ret.Rev() != fulld.Rev() { + t.Fatal( "Expected to have the revs for documents be equal since they are the same document.") + } + +} diff --git a/connection.go b/connection.go index f9a2f1d..66a6563 100644 --- a/connection.go +++ b/connection.go @@ -101,11 +101,12 @@ func ConnDbUserPassword(host, databaseName, user, password string) (*Database, e } switch response.Status() { - case 200: + case 200: + return db, nil case 401: return nil, newError( "401 Unauthorized: check user password." ) + default: + return nil, e } - return db, nil - } diff --git a/database.go b/database.go index 2ab665a..0a05e1a 100644 --- a/database.go +++ b/database.go @@ -4,6 +4,7 @@ import ( na "github.com/jmcvetta/napping" //"log" "fmt" + "net/http" "net/url" ) @@ -59,8 +60,11 @@ func (db *Database) IsSystem() bool { } //UseDatabase will switch databases as if -//you called db._useDatabase in arangosh -//No error if successful, otherwise an erro +//you called db._useDatabase in arangosh. +//No error if successful, otherwise an error. +//Under the hood it just makes another call to ConnDb +//Using the same credentials used for the original database +//and returns the results. func (db *Database) UseDatabase(databaseName string) (*Database, error) { //create a new connection instead of re-using the old //object because re-use will cause collections @@ -112,7 +116,9 @@ func (db *Database) DropDatabase(name string) error { var result dropDatabaseResult var e ArangoError - response, err := db.session.Delete(db.serverUrl.String()+"/database/"+name, &result, &e) + endpoint := fmt.Sprintf("%s/database/%s", db.serverUrl.String(), name) + + response, err := db.session.Delete(endpoint, &result, &e) if err != nil { return newError(err.Error()) @@ -130,14 +136,13 @@ func (db *Database) DropDatabase(name string) error { //Shortcut method for CreateCollection //that will use default options to create the document //collection. -//Similar to db._create( 'collection-name' ) in arangosh -func (db *Database) CreateDocumentCollection(collectionName string) error { +func (db *Database) CreateDocumentCollection(collectionName string) (*Collection, error) { return db.CreateCollection(collectionName, DefaultCollectionOptions()) } //Shortcut method for CreateCollection that will //use default options to create the edge collection -func (db *Database) CreateEdgeCollection(collectionName string) error { +func (db *Database) CreateEdgeCollection(collectionName string) (*Collection, error) { options := DefaultCollectionOptions() options.Type = EDGE_COLLECTION return db.CreateCollection(collectionName, options) @@ -145,14 +150,21 @@ func (db *Database) CreateEdgeCollection(collectionName string) error { //CreateCollection is the generic collection creating method. Use it for more control. //It allows you finer control by using the CollectionCreationOptions -func (db *Database) CreateCollection(collectionName string, options CollectionCreationOptions) error { +//An error is returned if there was an issue. Otherwise, it was a success +//and you can use db.Collection( collectionName ) to get the collection +//you just created and work with it. +func (db *Database) CreateCollection(collectionName string, options CollectionCreationOptions) (*Collection, error) { options.Name = collectionName var e ArangoError + var c *Collection = new(Collection) + c.db = db + c.json = new(collectionResult) + endpoint := fmt.Sprintf("%s/collection", db.serverUrl.String()) - response, err := db.session.Post(endpoint, options, nil, &e) + response, err := db.session.Post(endpoint, options, c.json, &e) //fmt.Printf( "( %T, %+v )\n( %T, %+v )\n ( %T, %+v )\n", //response,response, @@ -160,16 +172,15 @@ func (db *Database) CreateCollection(collectionName string, options CollectionCr //e, e ) if err != nil { - return newError(err.Error()) + return nil, newError(err.Error()) } switch response.Status() { case 200, 201: - return nil + return c, nil default: - return e + return nil, e } - return nil } //Collection gets a collection by name from the database. @@ -211,6 +222,7 @@ type dropCollectionResult struct { ArangoError } +//DropCollection drops the collection in the database by name. func (db *Database) DropCollection(collectionName string) error { var result dropCollectionResult @@ -232,3 +244,94 @@ func (db *Database) DropCollection(collectionName string) error { return nil } + + +//Document looks for a document in the database +func (db *Database) Document(documentHandle interface{}, document interface{}) error { + return db.DocumentWithOptions(documentHandle, document, nil) +} + +//DocumentWithOptions looks for a document in the database +func (db *Database) DocumentWithOptions(documentHandle interface{}, document interface{}, options *FetchDocumentOptions) error { + var id string + switch dh := documentHandle.(type) { + case string: + id = dh + case HasArangoId: + id = dh.Id() + default: + return newError("The document handle you passed in is not valid.") + } + + if id == "" { + return newError("You must specify a documentHandle when fetching a document.") + } + + if options != nil { + if db.session.Header == nil { + db.session.Header = &http.Header{} + defer func() { db.session.Header = nil }() + } + + if options.IfNoneMatch != "" { + db.session.Header.Add("If-None-Match", options.IfNoneMatch) + defer func() { db.session.Header.Del("If-None-Match") }() + } + if options.IfMatch != "" { + db.session.Header.Add("If-Match", options.IfMatch) + defer func() { db.session.Header.Del("If-Match") }() + } + } + + var e ArangoError + + endpoint := fmt.Sprintf("%s/document/%s", db.serverUrl.String(), id) + + response, err := db.session.Get(endpoint, nil, document, &e) + + if err != nil { + return newError(err.Error()) + } + + switch response.Status() { + case 200, 304: + return nil + default: + return e + } +} + +//Saves a document using the POST /_api/document endpoint. +//Look at arango api docs for more info. +func (db *Database) SaveDocumentWithOptions(document interface{}, options *SaveOptions) error { + + if options == nil { + return newError("You must provide save options when using the database.SaveWithOptions method.") + } + + if options.Collection == "" { + return newError("You must provide save options when using the database.SaveWithOptions method.") + } + + var e ArangoError + + endpoint := fmt.Sprintf("%s/document?collection=%s&createCollection=%s&waitForSync=%s", + db.serverUrl.String(), + options.Collection, + options.CreateCollection, + options.WaitForSync, + ) + + response, err := db.session.Post(endpoint, document, document, &e) + + if err != nil { + return newError(err.Error()) + } + + switch response.Status() { + case 200, 201, 202: + return nil + default: + return e + } +} diff --git a/database_test.go b/database_test.go index 1af13e1..48a79e7 100644 --- a/database_test.go +++ b/database_test.go @@ -76,12 +76,19 @@ func TestDatabaseCreateUseDropMethods( t *testing.T ) { t.Fatal( "Dropping the database we are in should fail. Drop can only be called from _system" ) } - //Clean up everything db, err = db.UseDatabase( "_system" ) + err = db.DropDatabase( "testing" ) if err != nil { - t.Fatal( "Dropping the database should work from the _system database." ) + t.Fatal( "Dropping the database should work from the _system database.", err ) } + + db, err = db.UseDatabase( "testing" ) + + if err == nil { + t.Fatal( "Expected to get an error for using a database that doesn't exist." ) + } + } func TestDatabaseCollectionMethods( t *testing.T ) { @@ -104,7 +111,7 @@ func TestDatabaseCollectionMethods( t *testing.T ) { db, err = db.UseDatabase( "testing" ) - err = db.CreateDocumentCollection( "testing" ) + c, err := db.CreateDocumentCollection( "testing" ) if err != nil { t.Fatalf( "Was not expecting an error when creating testing collection:%s", err) @@ -117,7 +124,32 @@ func TestDatabaseCollectionMethods( t *testing.T ) { t.Fatal( "Expecting an error because the collection didn't exist." ) } - c, err := db.Collection( "testing" ) + if c == nil { + t.Fatal( "Expecting the collection returned to not be nil.") + } + + if c.Id() == "" { + t.Fatal( "Collection should have an Id associated with it." ) + } + + if c.Status() == 0 { + t.Fatal( "Collection should have a status other than 0 after creation." ) + } + + if c.Name() != "testing" { + t.Fatal( "Collection doesn't have expected name.") + } + + if c.Type() != DOCUMENT_COLLECTION { + t.Fatalf( "Collection should be document (%d) type but got something else (%d).", DOCUMENT_COLLECTION, c.Type() ) + } + + if c.IsSystem() { + t.Fatal( "Collection should not be a system collection." ) + } + + //Fetch the database using the Collection method + c, err = db.Collection( "testing" ) if err != nil { t.Fatal( "Got an unexpected error when getting the testing collection.") @@ -168,17 +200,18 @@ func TestDatabaseCollectionMethods( t *testing.T ) { err = db.DropCollection( "testing_no_exist" ) if err == nil { - t.Fatalf( "Expected an error when dropping a non-existent database.") + t.Fatalf( "Expected an error when dropping a non-existent collection.") } err = db.DropCollection( c.Name() ) if err != nil { - t.Fatalf( "Could not drop the database: %+v", err ) + t.Fatalf( "Could not drop the collection: %+v", err ) } //Clean up everything db, err = db.UseDatabase( "_system" ) + err = db.DropDatabase( "testing" ) if err != nil { t.Fatal( "Dropping the database should work from the _system database." ) diff --git a/document.go b/document.go new file mode 100644 index 0000000..80bba4b --- /dev/null +++ b/document.go @@ -0,0 +1,76 @@ +package arango + +//Document represents an Arango document. +type FullDocument interface { + HasArangoId + HasArangoRev + HasArangoKey +} + +type HasArangoId interface { + Id() string + SetId( string ) +} + +type HasArangoRev interface { + Rev() string + SetRev( string ) +} + +type HasArangoKey interface { + Key() string + SetKey( string) +} + +//DocumentImplementation is an embeddable type that +//you can use that already implements the Document interface. +type DocumentImplementation struct { + ArangoId string `json:"_id,omitempty"` + ArangoRev string `json:"_rev,omitempty"` + ArangoKey string `json:"_key,omitempty"` +} + +func (d *DocumentImplementation) Id() string { + return d.ArangoId +} + +func (d *DocumentImplementation) SetId( id string ) { + d.ArangoId = id +} + +func (d *DocumentImplementation) Rev() string { + return d.ArangoRev +} + + +func (d *DocumentImplementation) SetRev( rev string ) { + d.ArangoRev = rev +} + +func (d *DocumentImplementation) Key() string { + return d.ArangoKey +} + +func (d *DocumentImplementation) SetKey( key string ) { + d.ArangoKey = key +} + +//FetchDocumentOptions are used when fetching documents +//Read the GET /_api/document/{document-handle} info +type FetchDocumentOptions struct { + IfNoneMatch string + IfMatch string +} + +//Save options represent options available when calling the Post /_api/document/ endpoint +type SaveOptions struct { + //The collection to save the item to. Irrelevant when called from a Collection struct + Collection string + + //CreateCollection specifies if the collection should be created at the same time as this document is being saved. + //Irrelevant if called from + CreateCollection bool + + //Wait until document has been synced to disk. + WaitForSync bool +} diff --git a/document_test.go b/document_test.go new file mode 100644 index 0000000..8ec2fca --- /dev/null +++ b/document_test.go @@ -0,0 +1 @@ +package arango diff --git a/errors.go b/errors.go index f637a49..60cedcf 100644 --- a/errors.go +++ b/errors.go @@ -18,6 +18,14 @@ type ArangoError struct { Code int `json:"code"` ErrorNum int `json:"errorNum"` ErrorMessage string `json:"errorMessage"` + + //Reserved for some operations that will return + //the id, rev, or key of the document. + //For example, /_api/document/{doc-handle} when it + //return a 412 error + Id string `json:"_id"` + Rev string `json:"_rev"` + Key string `json:"_key"` } func (a ArangoError) Error() string {