diff --git a/v2/arangodb/client_database_impl.go b/v2/arangodb/client_database_impl.go index 30945e39..44e7ccba 100644 --- a/v2/arangodb/client_database_impl.go +++ b/v2/arangodb/client_database_impl.go @@ -55,16 +55,20 @@ func (c clientDatabase) CreateDatabase(ctx context.Context, name string, options Name: name, } - resp, err := connection.CallPost(ctx, c.client.connection, url, nil, &createRequest) + response := struct { + shared.ResponseStruct `json:",inline"` + }{} + + resp, err := connection.CallPost(ctx, c.client.connection, url, &response, &createRequest) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusCreated: return newDatabase(c.client, name), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoError() } } @@ -93,16 +97,22 @@ func (c clientDatabase) Databases(ctx context.Context) ([]Database, error) { func (c clientDatabase) Database(ctx context.Context, name string) (Database, error) { url := connection.NewUrl("_db", name, "_api", "database", "current") - resp, err := connection.CallGet(ctx, c.client.connection, url, nil) + + var response struct { + shared.ResponseStruct `json:",inline"` + VersionInfo `json:",inline"` + } + + resp, err := connection.CallGet(ctx, c.client.connection, url, &response) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return newDatabase(c.client, name), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoErrorWithCode(code) } } @@ -117,7 +127,7 @@ func (c clientDatabase) databases(ctx context.Context, url string) ([]Database, return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: dbs := make([]Database, len(databases.Result)) @@ -127,6 +137,6 @@ func (c clientDatabase) databases(ctx context.Context, url string) ([]Database, return dbs, nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, databases.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/client_server_info_impl.go b/v2/arangodb/client_server_info_impl.go index 51782219..21728337 100644 --- a/v2/arangodb/client_server_info_impl.go +++ b/v2/arangodb/client_server_info_impl.go @@ -26,6 +26,8 @@ import ( "context" "net/http" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" "github.com/pkg/errors" ) @@ -45,17 +47,20 @@ type clientServerInfo struct { func (c clientServerInfo) Version(ctx context.Context) (VersionInfo, error) { url := connection.NewUrl("_api", "version") - var version VersionInfo + var response struct { + shared.ResponseStruct `json:",inline"` + VersionInfo + } - resp, err := connection.CallGet(ctx, c.client.connection, url, &version) + resp, err := connection.CallGet(ctx, c.client.connection, url, &response) if err != nil { return VersionInfo{}, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: - return version, nil + return response.VersionInfo, nil default: - return VersionInfo{}, connection.NewError(resp.Code(), "unexpected code") + return VersionInfo{}, response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/collection_documents.go b/v2/arangodb/collection_documents.go index 33a3f653..55512ffe 100644 --- a/v2/arangodb/collection_documents.go +++ b/v2/arangodb/collection_documents.go @@ -32,4 +32,5 @@ type CollectionDocuments interface { CollectionDocumentCreate CollectionDocumentRead + CollectionDocumentUpdate } diff --git a/v2/arangodb/collection_documents_create.go b/v2/arangodb/collection_documents_create.go index 16a1064a..96d38ae7 100644 --- a/v2/arangodb/collection_documents_create.go +++ b/v2/arangodb/collection_documents_create.go @@ -71,14 +71,13 @@ type CollectionDocumentCreate interface { } type CollectionDocumentCreateResponseReader interface { - Read() (CollectionDocumentCreateResponse, bool, error) + Read() (CollectionDocumentCreateResponse, error) } type CollectionDocumentCreateResponse struct { DocumentMeta - shared.ResponseStruct - - Old, New interface{} + shared.ResponseStruct `json:",inline"` + Old, New interface{} } type CollectionDocumentCreateOverwriteMode string diff --git a/v2/arangodb/collection_documents_create_impl.go b/v2/arangodb/collection_documents_create_impl.go new file mode 100644 index 00000000..075f40b3 --- /dev/null +++ b/v2/arangodb/collection_documents_create_impl.go @@ -0,0 +1,176 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package arangodb + +import ( + "context" + "io" + "net/http" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" + "github.com/arangodb/go-driver/v2/utils" + "github.com/pkg/errors" +) + +func newCollectionDocumentCreate(collection *collection) *collectionDocumentCreate { + return &collectionDocumentCreate{ + collection: collection, + } +} + +var _ CollectionDocumentCreate = &collectionDocumentCreate{} + +type collectionDocumentCreate struct { + collection *collection +} + +func (c collectionDocumentCreate) CreateDocumentsWithOptions(ctx context.Context, documents interface{}, opts *CollectionDocumentCreateOptions) (CollectionDocumentCreateResponseReader, error) { + if !utils.IsListPtr(documents) && !utils.IsList(documents) { + return nil, errors.Errorf("Input documents should be list") + } + + url := c.collection.url("document") + + req, err := c.collection.connection().NewRequest(http.MethodPost, url) + if err != nil { + return nil, err + } + + for _, modifier := range c.collection.withModifiers(opts.modifyRequest, connection.WithBody(documents), connection.WithFragment("multiple")) { + if err = modifier(req); err != nil { + return nil, err + } + } + + var arr connection.Array + + resp, err := c.collection.connection().Do(ctx, req, &arr) + if err != nil { + return nil, err + } + + switch code := resp.Code(); code { + case http.StatusCreated: + fallthrough + case http.StatusAccepted: + return newCollectionDocumentCreateResponseReader(arr, opts), nil + default: + return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) + } +} + +func (c collectionDocumentCreate) CreateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentCreateResponseReader, error) { + return c.CreateDocumentsWithOptions(ctx, documents, nil) +} + +func (c collectionDocumentCreate) CreateDocumentWithOptions(ctx context.Context, document interface{}, options *CollectionDocumentCreateOptions) (CollectionDocumentCreateResponse, error) { + url := c.collection.url("document") + + var meta CollectionDocumentCreateResponse + + if options != nil { + meta.Old = options.OldObject + meta.New = options.NewObject + } + + response := struct { + *DocumentMeta `json:",inline"` + *shared.ResponseStruct `json:",inline"` + Old *UnmarshalInto `json:"old,omitempty"` + New *UnmarshalInto `json:"new,omitempty"` + }{ + DocumentMeta: &meta.DocumentMeta, + ResponseStruct: &meta.ResponseStruct, + + Old: newUnmarshalInto(meta.Old), + New: newUnmarshalInto(meta.New), + } + + resp, err := connection.CallPost(ctx, c.collection.connection(), url, &response, document, c.collection.withModifiers(options.modifyRequest)...) + if err != nil { + return CollectionDocumentCreateResponse{}, err + } + + switch code := resp.Code(); code { + case http.StatusCreated: + fallthrough + case http.StatusAccepted: + return meta, nil + default: + return CollectionDocumentCreateResponse{}, response.AsArangoErrorWithCode(code) + } +} + +func (c collectionDocumentCreate) CreateDocument(ctx context.Context, document interface{}) (CollectionDocumentCreateResponse, error) { + return c.CreateDocumentWithOptions(ctx, document, nil) +} + +func newCollectionDocumentCreateResponseReader(array connection.Array, options *CollectionDocumentCreateOptions) *collectionDocumentCreateResponseReader { + c := &collectionDocumentCreateResponseReader{array: array, options: options} + + if c.options != nil { + c.response.Old = newUnmarshalInto(c.options.OldObject) + c.response.New = newUnmarshalInto(c.options.NewObject) + } + + return c +} + +var _ CollectionDocumentCreateResponseReader = &collectionDocumentCreateResponseReader{} + +type collectionDocumentCreateResponseReader struct { + array connection.Array + options *CollectionDocumentCreateOptions + response struct { + *DocumentMeta + *shared.ResponseStruct `json:",inline"` + Old *UnmarshalInto `json:"old,omitempty"` + New *UnmarshalInto `json:"new,omitempty"` + } +} + +func (c *collectionDocumentCreateResponseReader) Read() (CollectionDocumentCreateResponse, error) { + if !c.array.More() { + return CollectionDocumentCreateResponse{}, shared.NoMoreDocumentsError{} + } + + var meta CollectionDocumentCreateResponse + + if c.options != nil { + meta.Old = c.options.OldObject + meta.New = c.options.NewObject + } + + c.response.DocumentMeta = &meta.DocumentMeta + c.response.ResponseStruct = &meta.ResponseStruct + + if err := c.array.Unmarshal(&c.response); err != nil { + if err == io.EOF { + return CollectionDocumentCreateResponse{}, shared.NoMoreDocumentsError{} + } + return CollectionDocumentCreateResponse{}, err + } + + return meta, nil +} diff --git a/v2/arangodb/collection_documents_impl.go b/v2/arangodb/collection_documents_impl.go index d5655b6d..4c133ddb 100644 --- a/v2/arangodb/collection_documents_impl.go +++ b/v2/arangodb/collection_documents_impl.go @@ -27,15 +27,16 @@ import ( "net/http" "github.com/arangodb/go-driver/v2/arangodb/shared" - "github.com/arangodb/go-driver/v2/utils" - "github.com/arangodb/go-driver/v2/connection" - "github.com/pkg/errors" ) func newCollectionDocuments(collection *collection) *collectionDocuments { d := &collectionDocuments{collection: collection} + d.collectionDocumentUpdate = newCollectionDocumentUpdate(d.collection) + d.collectionDocumentRead = newCollectionDocumentRead(d.collection) + d.collectionDocumentCreate = newCollectionDocumentCreate(d.collection) + return d } @@ -45,151 +46,10 @@ var ( type collectionDocuments struct { collection *collection -} - -func (c collectionDocuments) ReadDocumentsWithOptions(ctx context.Context, keys []string, opts *CollectionDocumentReadOptions) (CollectionDocumentReadResponseReader, error) { - url := c.collection.url("document") - - req, err := c.collection.connection().NewRequest(http.MethodPut, url) - if err != nil { - return nil, err - } - - for _, modifier := range c.collection.withModifiers(opts.modifyRequest, connection.WithBody(keys), connection.WithFragment("get"), connection.WithQuery("onlyget", "true")) { - if err = modifier(req); err != nil { - return nil, err - } - } - - var arr connection.Array - - resp, err := c.collection.connection().Do(ctx, req, &arr) - if err != nil { - return nil, err - } - - switch resp.Code() { - case http.StatusOK: - return newCollectionDocumentReadResponseReader(arr, opts), nil - default: - return nil, connection.NewError(resp.Code(), "unexpected code") - } -} - -func (c collectionDocuments) ReadDocuments(ctx context.Context, keys []string) (CollectionDocumentReadResponseReader, error) { - return c.ReadDocumentsWithOptions(ctx, keys, nil) -} - -func (c collectionDocuments) ReadDocument(ctx context.Context, key string, result interface{}) (DocumentMeta, error) { - return c.ReadDocumentWithOptions(ctx, key, result, nil) -} - -func (c collectionDocuments) ReadDocumentWithOptions(ctx context.Context, key string, result interface{}, opts *CollectionDocumentReadOptions) (DocumentMeta, error) { - url := c.collection.url("document", key) - - var meta DocumentMeta - - response := struct { - *DocumentMeta - *unmarshalInto - }{ - DocumentMeta: &meta, - unmarshalInto: newUnmarshalInto(result), - } - - resp, err := connection.CallGet(ctx, c.collection.connection(), url, &response, c.collection.modifiers...) - if err != nil { - return DocumentMeta{}, err - } - - switch resp.Code() { - case http.StatusOK: - return meta, nil - default: - return DocumentMeta{}, connection.NewError(resp.Code(), "unexpected code") - } -} - -func (c collectionDocuments) CreateDocumentsWithOptions(ctx context.Context, documents interface{}, opts *CollectionDocumentCreateOptions) (CollectionDocumentCreateResponseReader, error) { - if !utils.IsListPtr(documents) && !utils.IsList(documents) { - return nil, errors.Errorf("Input documents should be list") - } - - url := c.collection.url("document") - - req, err := c.collection.connection().NewRequest(http.MethodPost, url) - if err != nil { - return nil, err - } - - for _, modifier := range c.collection.withModifiers(opts.modifyRequest, connection.WithBody(documents), connection.WithFragment("multiple")) { - if err = modifier(req); err != nil { - return nil, err - } - } - - var arr connection.Array - - resp, err := c.collection.connection().Do(ctx, req, &arr) - if err != nil { - return nil, err - } - - switch resp.Code() { - case http.StatusCreated: - fallthrough - case http.StatusAccepted: - return &collectionDocumentCreateResponseReader{array: arr, options: opts}, nil - default: - return nil, connection.NewError(resp.Code(), "unexpected code") - } -} -func (c collectionDocuments) CreateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentCreateResponseReader, error) { - return c.CreateDocumentsWithOptions(ctx, documents, nil) -} - -func (c collectionDocuments) CreateDocumentWithOptions(ctx context.Context, document interface{}, options *CollectionDocumentCreateOptions) (CollectionDocumentCreateResponse, error) { - url := c.collection.url("document") - - var meta CollectionDocumentCreateResponse - - if options != nil { - meta.Old = options.OldObject - meta.New = options.NewObject - } - - response := struct { - *DocumentMeta - *shared.ResponseStruct - - Old *unmarshalInto `json:"old,omitempty"` - New *unmarshalInto `json:"new,omitempty"` - }{ - DocumentMeta: &meta.DocumentMeta, - ResponseStruct: &meta.ResponseStruct, - - Old: newUnmarshalInto(meta.Old), - New: newUnmarshalInto(meta.New), - } - - resp, err := connection.CallPost(ctx, c.collection.connection(), url, &response, document, c.collection.withModifiers(options.modifyRequest)...) - if err != nil { - return CollectionDocumentCreateResponse{}, err - } - - switch resp.Code() { - case http.StatusCreated: - fallthrough - case http.StatusAccepted: - return meta, nil - default: - return CollectionDocumentCreateResponse{}, connection.NewError(resp.Code(), "unexpected code") - } -} - -func (c collectionDocuments) CreateDocument(ctx context.Context, document interface{}) (CollectionDocumentCreateResponse, error) { - return c.CreateDocumentWithOptions(ctx, document, nil) + *collectionDocumentUpdate + *collectionDocumentRead + *collectionDocumentCreate } func (c collectionDocuments) DocumentExists(ctx context.Context, key string) (bool, error) { @@ -203,93 +63,10 @@ func (c collectionDocuments) DocumentExists(ctx context.Context, key string) (bo return false, err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return true, nil default: - return false, connection.NewError(resp.Code(), "unexpected code") + return false, shared.NewResponseStruct().AsArangoErrorWithCode(code) } } - -// HELPERS - -func newCollectionDocumentCreateResponseReader(array connection.Array, options *CollectionDocumentCreateOptions) *collectionDocumentCreateResponseReader { - c := &collectionDocumentCreateResponseReader{array: array, options: options} - - if c.options != nil { - c.response.Old = newUnmarshalInto(c.options.OldObject) - c.response.New = newUnmarshalInto(c.options.NewObject) - } - - return c -} - -var _ CollectionDocumentCreateResponseReader = &collectionDocumentCreateResponseReader{} - -type collectionDocumentCreateResponseReader struct { - array connection.Array - options *CollectionDocumentCreateOptions - response struct { - *DocumentMeta - *shared.ResponseStruct - - Old *unmarshalInto `json:"old,omitempty"` - New *unmarshalInto `json:"new,omitempty"` - } -} - -func (c *collectionDocumentCreateResponseReader) Read() (CollectionDocumentCreateResponse, bool, error) { - if !c.array.More() { - return CollectionDocumentCreateResponse{}, false, nil - } - - var meta CollectionDocumentCreateResponse - - if c.options != nil { - meta.Old = c.options.OldObject - meta.New = c.options.NewObject - } - - c.response.DocumentMeta = &meta.DocumentMeta - c.response.ResponseStruct = &meta.ResponseStruct - - if err := c.array.Unmarshal(&c.response); err != nil { - return CollectionDocumentCreateResponse{}, false, err - } - - return meta, true, nil -} - -func newCollectionDocumentReadResponseReader(array connection.Array, options *CollectionDocumentReadOptions) *collectionDocumentReadResponseReader { - c := &collectionDocumentReadResponseReader{array: array, options: options} - - return c -} - -var _ CollectionDocumentReadResponseReader = &collectionDocumentReadResponseReader{} - -type collectionDocumentReadResponseReader struct { - array connection.Array - options *CollectionDocumentReadOptions - response struct { - *DocumentMeta - *unmarshalInto - } -} - -func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDocumentReadResponse, bool, error) { - if !c.array.More() { - return CollectionDocumentReadResponse{}, false, nil - } - - var meta CollectionDocumentReadResponse - - c.response.DocumentMeta = &meta.DocumentMeta - c.response.unmarshalInto = newUnmarshalInto(i) - - if err := c.array.Unmarshal(&c.response); err != nil { - return CollectionDocumentReadResponse{}, false, err - } - - return meta, true, nil -} diff --git a/v2/arangodb/collection_documents_read.go b/v2/arangodb/collection_documents_read.go index ce459d0e..d9796032 100644 --- a/v2/arangodb/collection_documents_read.go +++ b/v2/arangodb/collection_documents_read.go @@ -53,7 +53,7 @@ type CollectionDocumentRead interface { } type CollectionDocumentReadResponseReader interface { - Read(i interface{}) (CollectionDocumentReadResponse, bool, error) + Read(i interface{}) (CollectionDocumentReadResponse, error) } type CollectionDocumentReadResponse struct { diff --git a/v2/arangodb/collection_documents_read_impl.go b/v2/arangodb/collection_documents_read_impl.go new file mode 100644 index 00000000..0bc95829 --- /dev/null +++ b/v2/arangodb/collection_documents_read_impl.go @@ -0,0 +1,134 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package arangodb + +import ( + "context" + "io" + "net/http" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" +) + +func newCollectionDocumentRead(collection *collection) *collectionDocumentRead { + return &collectionDocumentRead{ + collection: collection, + } +} + +var _ CollectionDocumentRead = &collectionDocumentRead{} + +type collectionDocumentRead struct { + collection *collection +} + +func (c collectionDocumentRead) ReadDocumentsWithOptions(ctx context.Context, keys []string, opts *CollectionDocumentReadOptions) (CollectionDocumentReadResponseReader, error) { + url := c.collection.url("document") + + req, err := c.collection.connection().NewRequest(http.MethodPut, url) + if err != nil { + return nil, err + } + + for _, modifier := range c.collection.withModifiers(opts.modifyRequest, connection.WithBody(keys), connection.WithFragment("get"), connection.WithQuery("onlyget", "true")) { + if err = modifier(req); err != nil { + return nil, err + } + } + + var arr connection.Array + + resp, err := c.collection.connection().Do(ctx, req, &arr) + if err != nil { + return nil, err + } + + switch code := resp.Code(); code { + case http.StatusOK: + return newCollectionDocumentReadResponseReader(arr, opts), nil + default: + return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) + } +} + +func (c collectionDocumentRead) ReadDocuments(ctx context.Context, keys []string) (CollectionDocumentReadResponseReader, error) { + return c.ReadDocumentsWithOptions(ctx, keys, nil) +} + +func (c collectionDocumentRead) ReadDocument(ctx context.Context, key string, result interface{}) (DocumentMeta, error) { + return c.ReadDocumentWithOptions(ctx, key, result, nil) +} + +func (c collectionDocumentRead) ReadDocumentWithOptions(ctx context.Context, key string, result interface{}, opts *CollectionDocumentReadOptions) (DocumentMeta, error) { + url := c.collection.url("document", key) + + var response struct { + shared.ResponseStruct `json:",inline"` + DocumentMeta `json:",inline"` + } + + data := newUnmarshalInto(result) + + resp, err := connection.CallGet(ctx, c.collection.connection(), url, newMultiUnmarshaller(&response, data), c.collection.modifiers...) + if err != nil { + return DocumentMeta{}, err + } + + switch code := resp.Code(); code { + case http.StatusOK: + return response.DocumentMeta, nil + default: + return DocumentMeta{}, response.AsArangoErrorWithCode(code) + } +} + +func newCollectionDocumentReadResponseReader(array connection.Array, options *CollectionDocumentReadOptions) *collectionDocumentReadResponseReader { + c := &collectionDocumentReadResponseReader{array: array, options: options} + + return c +} + +var _ CollectionDocumentReadResponseReader = &collectionDocumentReadResponseReader{} + +type collectionDocumentReadResponseReader struct { + array connection.Array + options *CollectionDocumentReadOptions +} + +func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDocumentReadResponse, error) { + if !c.array.More() { + return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} + } + + var meta CollectionDocumentReadResponse + + if err := c.array.Unmarshal(newMultiUnmarshaller(&meta.DocumentMeta, newUnmarshalInto(i))); err != nil { + if err == io.EOF { + return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} + } + return CollectionDocumentReadResponse{}, err + } + + return meta, nil +} diff --git a/v2/arangodb/collection_documents_update.go b/v2/arangodb/collection_documents_update.go new file mode 100644 index 00000000..4f6f5dc6 --- /dev/null +++ b/v2/arangodb/collection_documents_update.go @@ -0,0 +1,91 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package arangodb + +import ( + "context" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" +) + +type CollectionDocumentUpdate interface { + + // UpdateDocument updates a single document with given key in the collection. + // The document meta data is returned. + // If no document exists with given key, a NotFoundError is returned. + UpdateDocument(ctx context.Context, key string, document interface{}) (CollectionDocumentUpdateResponse, error) + + // UpdateDocument updates a single document with given key in the collection. + // The document meta data is returned. + // If no document exists with given key, a NotFoundError is returned. + UpdateDocumentWithOptions(ctx context.Context, key string, document interface{}, options *CollectionDocumentUpdateOptions) (CollectionDocumentUpdateResponse, error) + + // UpdateDocuments updates multiple document with given keys in the collection. + // The updates are loaded from the given updates slice, the documents meta data are returned. + // If no document exists with a given key, a NotFoundError is returned at its errors index. + // If keys is nil, each element in the updates slice must contain a `_key` field. + UpdateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentUpdateResponseReader, error) + + // UpdateDocuments updates multiple document with given keys in the collection. + // The updates are loaded from the given updates slice, the documents meta data are returned. + // If no document exists with a given key, a NotFoundError is returned at its errors index. + // If keys is nil, each element in the updates slice must contain a `_key` field. + UpdateDocumentsWithOptions(ctx context.Context, documents interface{}, opts *CollectionDocumentUpdateOptions) (CollectionDocumentUpdateResponseReader, error) +} + +type CollectionDocumentUpdateResponseReader interface { + Read() (CollectionDocumentUpdateResponse, error) +} + +type CollectionDocumentUpdateResponse struct { + DocumentMeta + shared.ResponseStruct `json:",inline"` + Old, New interface{} +} + +type CollectionDocumentUpdateOptions struct { + WithWaitForSync *bool + NewObject interface{} + OldObject interface{} +} + +func (c *CollectionDocumentUpdateOptions) modifyRequest(r connection.Request) error { + if c == nil { + return nil + } + + if c.WithWaitForSync != nil { + r.AddQuery("waitForSync", boolToString(*c.WithWaitForSync)) + } + + if c.NewObject != nil { + r.AddQuery("returnNew", "true") + } + + if c.OldObject != nil { + r.AddQuery("returnOld", "true") + } + + return nil +} diff --git a/v2/arangodb/collection_documents_update_impl.go b/v2/arangodb/collection_documents_update_impl.go new file mode 100644 index 00000000..5b49c97d --- /dev/null +++ b/v2/arangodb/collection_documents_update_impl.go @@ -0,0 +1,176 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package arangodb + +import ( + "context" + "io" + "net/http" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" + "github.com/arangodb/go-driver/v2/utils" + "github.com/pkg/errors" +) + +func newCollectionDocumentUpdate(collection *collection) *collectionDocumentUpdate { + return &collectionDocumentUpdate{ + collection: collection, + } +} + +var _ CollectionDocumentUpdate = &collectionDocumentUpdate{} + +type collectionDocumentUpdate struct { + collection *collection +} + +func (c collectionDocumentUpdate) UpdateDocument(ctx context.Context, key string, document interface{}) (CollectionDocumentUpdateResponse, error) { + return c.UpdateDocumentWithOptions(ctx, key, document, nil) +} + +func (c collectionDocumentUpdate) UpdateDocumentWithOptions(ctx context.Context, key string, document interface{}, options *CollectionDocumentUpdateOptions) (CollectionDocumentUpdateResponse, error) { + url := c.collection.url("document", key) + + var meta CollectionDocumentUpdateResponse + + if options != nil { + meta.Old = options.OldObject + meta.New = options.NewObject + } + + response := struct { + *DocumentMeta `json:",inline"` + *shared.ResponseStruct `json:",inline"` + Old *UnmarshalInto `json:"old,omitempty"` + New *UnmarshalInto `json:"new,omitempty"` + }{ + DocumentMeta: &meta.DocumentMeta, + ResponseStruct: &meta.ResponseStruct, + + Old: newUnmarshalInto(meta.Old), + New: newUnmarshalInto(meta.New), + } + + resp, err := connection.CallPatch(ctx, c.collection.connection(), url, &response, document, c.collection.withModifiers(options.modifyRequest)...) + if err != nil { + return CollectionDocumentUpdateResponse{}, err + } + + switch code := resp.Code(); code { + case http.StatusCreated: + fallthrough + case http.StatusAccepted: + return meta, nil + default: + return CollectionDocumentUpdateResponse{}, response.AsArangoErrorWithCode(code) + } +} + +func (c collectionDocumentUpdate) UpdateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentUpdateResponseReader, error) { + return c.UpdateDocumentsWithOptions(ctx, documents, nil) +} + +func (c collectionDocumentUpdate) UpdateDocumentsWithOptions(ctx context.Context, documents interface{}, opts *CollectionDocumentUpdateOptions) (CollectionDocumentUpdateResponseReader, error) { + if !utils.IsListPtr(documents) && !utils.IsList(documents) { + return nil, errors.Errorf("Input documents should be list") + } + + url := c.collection.url("document") + + req, err := c.collection.connection().NewRequest(http.MethodPatch, url) + if err != nil { + return nil, err + } + + for _, modifier := range c.collection.withModifiers(opts.modifyRequest, connection.WithBody(documents), connection.WithFragment("multiple")) { + if err = modifier(req); err != nil { + return nil, err + } + } + + var arr connection.Array + + resp, err := c.collection.connection().Do(ctx, req, &arr) + if err != nil { + return nil, err + } + + switch code := resp.Code(); code { + case http.StatusCreated: + fallthrough + case http.StatusAccepted: + return newCollectionDocumentUpdateResponseReader(arr, opts), nil + default: + return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) + } +} + +func newCollectionDocumentUpdateResponseReader(array connection.Array, options *CollectionDocumentUpdateOptions) *collectionDocumentUpdateResponseReader { + c := &collectionDocumentUpdateResponseReader{array: array, options: options} + + if c.options != nil { + c.response.Old = newUnmarshalInto(c.options.OldObject) + c.response.New = newUnmarshalInto(c.options.NewObject) + } + + return c +} + +var _ CollectionDocumentUpdateResponseReader = &collectionDocumentUpdateResponseReader{} + +type collectionDocumentUpdateResponseReader struct { + array connection.Array + options *CollectionDocumentUpdateOptions + response struct { + *DocumentMeta + *shared.ResponseStruct `json:",inline"` + Old *UnmarshalInto `json:"old,omitempty"` + New *UnmarshalInto `json:"new,omitempty"` + } +} + +func (c *collectionDocumentUpdateResponseReader) Read() (CollectionDocumentUpdateResponse, error) { + if !c.array.More() { + return CollectionDocumentUpdateResponse{}, shared.NoMoreDocumentsError{} + } + + var meta CollectionDocumentUpdateResponse + + if c.options != nil { + meta.Old = c.options.OldObject + meta.New = c.options.NewObject + } + + c.response.DocumentMeta = &meta.DocumentMeta + c.response.ResponseStruct = &meta.ResponseStruct + + if err := c.array.Unmarshal(&c.response); err != nil { + if err == io.EOF { + return CollectionDocumentUpdateResponse{}, shared.NoMoreDocumentsError{} + } + return CollectionDocumentUpdateResponse{}, err + } + + return meta, nil +} diff --git a/v2/arangodb/collection_impl.go b/v2/arangodb/collection_impl.go index 0cf9eac5..e5e116b6 100644 --- a/v2/arangodb/collection_impl.go +++ b/v2/arangodb/collection_impl.go @@ -26,6 +26,8 @@ import ( "context" "net/http" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" ) @@ -52,16 +54,20 @@ type collection struct { func (c collection) Remove(ctx context.Context) error { url := c.url("collection") - resp, err := connection.CallDelete(ctx, c.connection(), url, nil) + var response struct { + shared.ResponseStruct `json:",inline"` + } + + resp, err := connection.CallDelete(ctx, c.connection(), url, &response) if err != nil { return err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/cursor_impl.go b/v2/arangodb/cursor_impl.go index 9cc4d3d8..d9180592 100644 --- a/v2/arangodb/cursor_impl.go +++ b/v2/arangodb/cursor_impl.go @@ -27,6 +27,8 @@ import ( "net/http" "sync" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" "github.com/pkg/errors" ) @@ -93,7 +95,7 @@ func (c *cursor) readDocument(ctx context.Context, result interface{}) (Document func (c *cursor) getNextBatch(ctx context.Context) error { if !c.data.HasMore { - return errors.WithStack(NoMoreDocumentsError{}) + return errors.WithStack(shared.NoMoreDocumentsError{}) } url := c.db.url("_api", "cursor", c.data.ID) @@ -103,11 +105,11 @@ func (c *cursor) getNextBatch(ctx context.Context) error { return err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return shared.NewResponseStruct().AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_collection_impl.go b/v2/arangodb/database_collection_impl.go index d3789dbd..ecec7367 100644 --- a/v2/arangodb/database_collection_impl.go +++ b/v2/arangodb/database_collection_impl.go @@ -46,16 +46,21 @@ type databaseCollection struct { func (d databaseCollection) Collection(ctx context.Context, name string) (Collection, error) { url := d.db.url("_api", "collection", name) - resp, err := connection.CallGet(ctx, d.db.connection(), url, nil) + + var response struct { + shared.ResponseStruct `json:",inline"` + } + + resp, err := connection.CallGet(ctx, d.db.connection(), url, &response) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return newCollection(d.db, name), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoErrorWithCode(code) } } @@ -75,17 +80,17 @@ func (d databaseCollection) CollectionExists(ctx context.Context, name string) ( func (d databaseCollection) Collections(ctx context.Context) ([]Collection, error) { url := d.db.url("_api", "collection") - response := struct { - shared.Response - Result []CollectionInfo `json:"result,omitempty"` - }{} + var response struct { + shared.ResponseStruct `json:",inline"` + Result []CollectionInfo `json:"result,omitempty"` + } resp, err := connection.CallGet(ctx, d.db.connection(), url, &response) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: colls := make([]Collection, len(response.Result)) @@ -95,14 +100,15 @@ func (d databaseCollection) Collections(ctx context.Context) ([]Collection, erro return colls, nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoErrorWithCode(code) } } func (d databaseCollection) CreateCollection(ctx context.Context, name string, options *CreateCollectionOptions) (Collection, error) { url := d.db.url("_api", "collection") reqData := struct { - Name string `json:"name"` + shared.ResponseStruct `json:",inline"` + Name string `json:"name"` *CreateCollectionOptions }{ Name: name, @@ -114,10 +120,10 @@ func (d databaseCollection) CreateCollection(ctx context.Context, name string, o return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return newCollection(d.db, name), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, reqData.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_impl.go b/v2/arangodb/database_impl.go index 44c77fa3..cd24e27a 100644 --- a/v2/arangodb/database_impl.go +++ b/v2/arangodb/database_impl.go @@ -60,11 +60,11 @@ func (d database) Remove(ctx context.Context) error { return err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return shared.NewResponseStruct().AsArangoErrorWithCode(code) } } @@ -84,9 +84,8 @@ func (d database) Info(ctx context.Context) (DatabaseInfo, error) { url := d.url("_api", "database", "current") var response struct { - shared.ResponseStruct - - Database DatabaseInfo `json:"result"` + shared.ResponseStruct `json:",inline"` + Database DatabaseInfo `json:"result"` } resp, err := connection.CallGet(ctx, d.client.connection, url, &response) @@ -94,10 +93,10 @@ func (d database) Info(ctx context.Context) (DatabaseInfo, error) { return DatabaseInfo{}, err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return response.Database, nil default: - return DatabaseInfo{}, connection.NewError(resp.Code(), "unexpected code") + return DatabaseInfo{}, response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_query_impl.go b/v2/arangodb/database_query_impl.go index 16859a9d..569e435f 100644 --- a/v2/arangodb/database_query_impl.go +++ b/v2/arangodb/database_query_impl.go @@ -26,6 +26,8 @@ import ( "context" "net/http" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" ) @@ -52,35 +54,42 @@ func (d databaseQuery) Query(ctx context.Context, query string, opts *QueryOptio QueryRequest: &QueryRequest{Query: query}, } - var out cursorData + var response struct { + shared.ResponseStruct `json:",inline"` + cursorData `json:",inline"` + } - resp, err := connection.CallPost(ctx, d.db.connection(), url, &out, &req, d.db.modifiers...) + resp, err := connection.CallPost(ctx, d.db.connection(), url, &response, &req, d.db.modifiers...) if err != nil { return nil, err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusCreated: - return newCursor(d.db, resp.Endpoint(), out), nil + return newCursor(d.db, resp.Endpoint(), response.cursorData), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoErrorWithCode(code) } } func (d databaseQuery) ValidateQuery(ctx context.Context, query string) error { url := d.db.url("_api", "query") + var response struct { + shared.ResponseStruct `json:",inline"` + } + queryStruct := QueryRequest{Query: query} - resp, err := connection.CallPost(ctx, d.db.connection(), url, nil, &queryStruct, d.db.modifiers...) + resp, err := connection.CallPost(ctx, d.db.connection(), url, &response, &queryStruct, d.db.modifiers...) if err != nil { return err } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_transaction.go b/v2/arangodb/database_transaction.go index fef0df8f..19be50f5 100644 --- a/v2/arangodb/database_transaction.go +++ b/v2/arangodb/database_transaction.go @@ -27,6 +27,9 @@ import ( ) type DatabaseTransaction interface { + ListTransactions(ctx context.Context) ([]Transaction, error) + ListTransactionsWithStatuses(ctx context.Context, statuses ...TransactionStatus) ([]Transaction, error) + BeginTransaction(ctx context.Context, cols TransactionCollections, opts *BeginTransactionOptions) (Transaction, error) Transaction(ctx context.Context, id TransactionID) (Transaction, error) diff --git a/v2/arangodb/database_transaction_impl.go b/v2/arangodb/database_transaction_impl.go index cd13799e..2a0813c0 100644 --- a/v2/arangodb/database_transaction_impl.go +++ b/v2/arangodb/database_transaction_impl.go @@ -44,6 +44,48 @@ type databaseTransaction struct { db *database } +func (d databaseTransaction) ListTransactions(ctx context.Context) ([]Transaction, error) { + return d.ListTransactionsWithStatuses(ctx, TransactionRunning, TransactionCommitted, TransactionAborted) +} + +func (d databaseTransaction) ListTransactionsWithStatuses(ctx context.Context, statuses ...TransactionStatus) ([]Transaction, error) { + return d.listTransactionsWithStatuses(ctx, statuses) +} + +func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, statuses TransactionStatuses) ([]Transaction, error) { + url := d.db.url("_api", "transaction") + + var result struct { + Transactions []struct { + ID TransactionID `json:"id"` + State TransactionStatus `json:"state"` + } `json:"transactions,omitempty"` + } + + var respose shared.ResponseStruct + + resp, err := connection.CallGet(ctx, d.db.connection(), url, newMultiUnmarshaller(&result, &respose), d.db.modifiers...) + if err != nil { + return nil, errors.WithStack(err) + } + + switch code := resp.Code(); code { + case http.StatusOK: + var t []Transaction + for _, r := range result.Transactions { + if !statuses.Contains(r.State) { + continue + } + + t = append(t, newTransaction(d.db, r.ID)) + } + + return t, nil + default: + return nil, respose.AsArangoErrorWithCode(code) + } +} + func (d databaseTransaction) WithTransaction(ctx context.Context, cols TransactionCollections, opts *BeginTransactionOptions, commitOptions *CommitTransactionOptions, abortOptions *AbortTransactionOptions, w TransactionWrap) error { return d.withTransactionPanic(ctx, cols, opts, commitOptions, abortOptions, w) } @@ -62,6 +104,12 @@ func (d databaseTransaction) withTransactionPanic(ctx context.Context, cols Tran transactionError = errors.Wrapf(transactionError, "Transaction abort failed with %s", err.Error()) } } else { + if p := recover(); p != nil { + if err = t.Abort(ctx, abortOptions); err != nil { + transactionError = errors.Wrapf(transactionError, "Transaction abort failed with %s", err.Error()) + } + panic(p) + } if err = t.Commit(ctx, commitOptions); err != nil { transactionError = err } @@ -85,37 +133,41 @@ func (d databaseTransaction) BeginTransaction(ctx context.Context, cols Transact } output := struct { - shared.ResponseStruct - - Response struct { + shared.ResponseStruct `json:",inline"` + Response struct { TransactionID TransactionID `json:"id,omitempty"` } `json:"result"` }{} - resp, err := connection.CallPost(ctx, d.db.connection(), url, &output, &input) + resp, err := connection.CallPost(ctx, d.db.connection(), url, &output, &input, d.db.modifiers...) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusCreated: return newTransaction(d.db, output.Response.TransactionID), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, output.AsArangoErrorWithCode(code) } } func (d databaseTransaction) Transaction(ctx context.Context, id TransactionID) (Transaction, error) { url := d.db.url("_api", "transaction", string(id)) - resp, err := connection.CallGet(ctx, d.db.connection(), url, nil) + + var response struct { + shared.ResponseStruct `json:",inline"` + } + + resp, err := connection.CallGet(ctx, d.db.connection(), url, &response, d.db.modifiers...) if err != nil { return nil, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return newTransaction(d.db, id), nil default: - return nil, connection.NewError(resp.Code(), "unexpected code") + return nil, response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_transaction_opts.go b/v2/arangodb/database_transaction_opts.go index 7c0fae24..02377031 100644 --- a/v2/arangodb/database_transaction_opts.go +++ b/v2/arangodb/database_transaction_opts.go @@ -61,6 +61,18 @@ type AbortTransactionOptions struct{} // TransactionID identifies a transaction type TransactionID string +// TransactionStatuses list of transaction statuses +type TransactionStatuses []TransactionStatus + +func (t TransactionStatuses) Contains(status TransactionStatus) bool { + for _, i := range t { + if i == status { + return true + } + } + return false +} + // TransactionStatus describes the status of an transaction type TransactionStatus string diff --git a/v2/arangodb/meta.go b/v2/arangodb/meta.go index eba40d18..8bc64ad0 100644 --- a/v2/arangodb/meta.go +++ b/v2/arangodb/meta.go @@ -23,6 +23,7 @@ package arangodb import ( + "github.com/arangodb/go-driver/v2/arangodb/shared" "github.com/pkg/errors" ) @@ -38,7 +39,7 @@ type DocumentMeta struct { // validateKey returns an error if the given key is empty otherwise invalid. func validateKey(key string) error { if key == "" { - return errors.WithStack(InvalidArgumentError{Message: "key is empty"}) + return errors.WithStack(shared.InvalidArgumentError{Message: "key is empty"}) } return nil } diff --git a/v2/arangodb/requests.go b/v2/arangodb/requests.go index 97317e05..4badfc23 100644 --- a/v2/arangodb/requests.go +++ b/v2/arangodb/requests.go @@ -34,6 +34,7 @@ type Requests interface { Put(ctx context.Context, output, input interface{}, urlParts ...string) (connection.Response, error) Delete(ctx context.Context, output interface{}, urlParts ...string) (connection.Response, error) Head(ctx context.Context, output interface{}, urlParts ...string) (connection.Response, error) + Patch(ctx context.Context, output, input interface{}, urlParts ...string) (connection.Response, error) } func NewRequests(connection connection.Connection, urlParts ...string) Requests { @@ -51,6 +52,10 @@ type requests struct { prefix []string } +func (r requests) Patch(ctx context.Context, output, input interface{}, urlParts ...string) (connection.Response, error) { + return connection.CallPatch(ctx, r.connection, r.path(urlParts...), output, input) +} + func (r requests) path(urlParts ...string) string { n := make([]string, len(r.prefix)+len(urlParts)) for id, s := range r.prefix { diff --git a/v2/arangodb/error.go b/v2/arangodb/shared/error.go similarity index 80% rename from v2/arangodb/error.go rename to v2/arangodb/shared/error.go index cf6f5283..fefd15c8 100644 --- a/v2/arangodb/error.go +++ b/v2/arangodb/shared/error.go @@ -20,15 +20,13 @@ // Author Ewout Prangsma // -package arangodb +package shared import ( "context" "fmt" - "net" + "io" "net/http" - "net/url" - "os" ) const ( @@ -96,28 +94,43 @@ func newArangoError(code, errorNum int, errorMessage string) error { // IsArangoError returns true when the given error is an ArangoError. func IsArangoError(err error) bool { - ae, ok := Cause(err).(ArangoError) - return ok && ae.HasError + return checkCause(err, func(err error) bool { + if a, ok := err.(ArangoError); ok { + return a.HasError + } + + return false + }) } // IsArangoErrorWithCode returns true when the given error is an ArangoError and its Code field is equal to the given code. func IsArangoErrorWithCode(err error, code int) bool { - ae, ok := Cause(err).(ArangoError) - return ok && ae.Code == code + return checkCause(err, func(err error) bool { + if a, ok := err.(ArangoError); ok { + return a.HasError && a.Code == code + } + + return false + }) } // IsArangoErrorWithErrorNum returns true when the given error is an ArangoError and its ErrorNum field is equal to one of the given numbers. func IsArangoErrorWithErrorNum(err error, errorNum ...int) bool { - ae, ok := Cause(err).(ArangoError) - if !ok { - return false - } - for _, x := range errorNum { - if ae.ErrorNum == x { - return true + return checkCause(err, func(err error) bool { + if a, ok := err.(ArangoError); ok { + if !a.HasError { + return false + } + + for _, num := range errorNum { + if num == a.ErrorNum { + return true + } + } } - } - return false + + return false + }) } // IsInvalidRequest returns true if the given error is an ArangoError with code 400, indicating an invalid request. @@ -142,6 +155,12 @@ func IsNotFound(err error) bool { IsArangoErrorWithErrorNum(err, ErrArangoDocumentNotFound, ErrArangoDataSourceNotFound) } +// IsOperationTimeout returns true if the given error is an ArangoError with code 412, indicating a Operation timeout error +func IsOperationTimeout(err error) bool { + return IsArangoErrorWithCode(err, http.StatusPreconditionFailed) || + IsArangoErrorWithErrorNum(err, ErrArangoConflict) +} + // IsConflict returns true if the given error is an ArangoError with code 409, indicating a conflict. func IsConflict(err error) bool { return IsArangoErrorWithCode(err, http.StatusConflict) || IsArangoErrorWithErrorNum(err, ErrUserDuplicate) @@ -176,8 +195,13 @@ func (e InvalidArgumentError) Error() string { // IsInvalidArgument returns true if the given error is an InvalidArgumentError. func IsInvalidArgument(err error) bool { - _, ok := Cause(err).(InvalidArgumentError) - return ok + return checkCause(err, func(err error) bool { + if _, ok := err.(InvalidArgumentError); ok { + return true + } + + return false + }) } // NoMoreDocumentsError is returned by Cursor's, when an attempt is made to read documents when there are no more. @@ -188,10 +212,21 @@ func (e NoMoreDocumentsError) Error() string { return "no more documents" } +func IsEOF(err error) bool { + return checkCause(err, func(err error) bool { + return err == io.EOF + }) || IsNoMoreDocuments(err) +} + // IsNoMoreDocuments returns true if the given error is an NoMoreDocumentsError. func IsNoMoreDocuments(err error) bool { - _, ok := Cause(err).(NoMoreDocumentsError) - return ok + return checkCause(err, func(err error) bool { + if _, ok := err.(NoMoreDocumentsError); ok { + return true + } + + return false + }) } // A ResponseError is returned when a request was completely written to a server, but @@ -207,55 +242,54 @@ func (e *ResponseError) Error() string { // IsResponse returns true if the given error is (or is caused by) a ResponseError. func IsResponse(err error) bool { - return isCausedBy(err, func(e error) bool { _, ok := e.(*ResponseError); return ok }) + return checkCause(err, func(err error) bool { + if _, ok := err.(*ResponseError); ok { + return true + } + + return false + }) } // IsCanceled returns true if the given error is the result on a cancelled context. func IsCanceled(err error) bool { - return isCausedBy(err, func(e error) bool { return e == context.Canceled }) + return checkCause(err, func(err error) bool { + return err == context.Canceled + }) } // IsTimeout returns true if the given error is the result on a deadline that has been exceeded. func IsTimeout(err error) bool { - return isCausedBy(err, func(e error) bool { return e == context.DeadlineExceeded }) + return checkCause(err, func(err error) bool { + return err == context.DeadlineExceeded + }) +} + +type causer interface { + Cause() error } -// isCausedBy returns true if the given error returns true on the given predicate, -// unwrapping various standard library error wrappers. -func isCausedBy(err error, p func(error) bool) bool { - if p(err) { +func checkCause(err error, f func(err error) bool) bool { + if err == nil { + return false + } + + if f(err) { return true } - err = Cause(err) - for { - if p(err) { - return true - } else if err == nil { - return false - } - if xerr, ok := err.(*ResponseError); ok { - err = xerr.Err - } else if xerr, ok := err.(*url.Error); ok { - err = xerr.Err - } else if xerr, ok := err.(*net.OpError); ok { - err = xerr.Err - } else if xerr, ok := err.(*os.SyscallError); ok { - err = xerr.Err - } else { + + if c, ok := err.(causer); ok { + cErr := c.Cause() + + if err == cErr { return false } + + return checkCause(cErr, f) } -} -var ( - // WithStack is called on every return of an error to add stacktrace information to the error. - // When setting this function, also set the Cause function. - // The interface of this function is compatible with functions in github.com/pkg/errors. - WithStack = func(err error) error { return err } - // Cause is used to get the root cause of the given error. - // The interface of this function is compatible with functions in github.com/pkg/errors. - Cause = func(err error) error { return err } -) + return false +} // ErrorSlice is a slice of errors type ErrorSlice []error diff --git a/v2/arangodb/shared/response.go b/v2/arangodb/shared/response.go index 1c1d6f09..530fa2b1 100644 --- a/v2/arangodb/shared/response.go +++ b/v2/arangodb/shared/response.go @@ -22,6 +22,10 @@ package shared +import ( + "fmt" +) + type Response struct { ResponseStruct `json:",inline"` } @@ -42,11 +46,83 @@ func (r ResponseStructList) HasError() bool { return false } +func NewResponseStruct() *ResponseStruct { + return &ResponseStruct{} +} + type ResponseStruct struct { - Error *bool `json:"error"` - Code *int `json:"code"` - ErrorMessage *string `json:"errorMessage"` - ErrorNum *int `json:"errorNum"` + Error *bool `json:"error,omitempty"` + Code *int `json:"code,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + ErrorNum *int `json:"errorNum,omitempty"` +} + +func (r ResponseStruct) ExpectCode(codes ...int) error { + if r.Error == nil || !*r.Error || r.Code == nil { + return nil + } + + for _, code := range codes { + if code == *r.Code { + return nil + } + } + + return r.AsArangoError() +} + +func (r *ResponseStruct) AsArangoErrorWithCode(code int) ArangoError { + if r == nil { + return (&ResponseStruct{}).AsArangoErrorWithCode(code) + } + //r.Code = &code + //t := true + //r.Error = &t + return r.AsArangoError() +} + +func (r ResponseStruct) AsArangoError() ArangoError { + a := ArangoError{} + + if r.Error != nil { + a.HasError = *r.Error + } + + if r.Code != nil { + a.Code = *r.Code + } + + if r.ErrorNum != nil { + a.ErrorNum = *r.ErrorNum + } + + if r.ErrorMessage != nil { + a.ErrorMessage = *r.ErrorMessage + } + + return a +} + +func (r ResponseStruct) String() string { + if r.Error == nil || !*r.Error { + return "" + } + + s := "Response error" + + if r.Code != nil { + s = fmt.Sprintf("%s (Code: %d)", s, *r.Code) + } + + if r.ErrorNum != nil { + s = fmt.Sprintf("%s (ErrorNum: %d)", s, *r.ErrorNum) + } + + if r.ErrorMessage != nil { + s = fmt.Sprintf("%s: %s", s, *r.ErrorMessage) + } + + return s } func (r Response) GetError() bool { diff --git a/v2/arangodb/transaction_impl.go b/v2/arangodb/transaction_impl.go index 16ca679a..fc909b02 100644 --- a/v2/arangodb/transaction_impl.go +++ b/v2/arangodb/transaction_impl.go @@ -52,37 +52,45 @@ type transaction struct { } func (t transaction) Commit(ctx context.Context, opts *CommitTransactionOptions) error { - resp, err := connection.CallPut(ctx, t.database.connection(), t.url(), nil, nil) + response := struct { + shared.ResponseStruct `json:",inline"` + }{} + + resp, err := connection.CallPut(ctx, t.database.connection(), t.url(), &response, nil) if err != nil { return errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return response.AsArangoErrorWithCode(code) } } func (t transaction) Abort(ctx context.Context, opts *AbortTransactionOptions) error { - resp, err := connection.CallDelete(ctx, t.database.connection(), t.url(), nil) + response := struct { + shared.ResponseStruct `json:",inline"` + }{} + + resp, err := connection.CallDelete(ctx, t.database.connection(), t.url(), &response) if err != nil { return errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return nil default: - return connection.NewError(resp.Code(), "unexpected code") + return response.AsArangoErrorWithCode(code) } } func (t transaction) Status(ctx context.Context) (TransactionStatusRecord, error) { response := struct { - shared.ResponseStruct - Result TransactionStatusRecord `json:"result"` + shared.ResponseStruct `json:",inline"` + Result TransactionStatusRecord `json:"result"` }{} resp, err := connection.CallGet(ctx, t.database.connection(), t.url(), &response) @@ -90,11 +98,11 @@ func (t transaction) Status(ctx context.Context) (TransactionStatusRecord, error return TransactionStatusRecord{}, errors.WithStack(err) } - switch resp.Code() { + switch code := resp.Code(); code { case http.StatusOK: return response.Result, nil default: - return TransactionStatusRecord{}, connection.NewError(resp.Code(), "unexpected code") + return TransactionStatusRecord{}, response.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/utils.go b/v2/arangodb/utils.go index f16d89de..2d803ad1 100644 --- a/v2/arangodb/utils.go +++ b/v2/arangodb/utils.go @@ -31,6 +31,50 @@ import ( "github.com/pkg/errors" ) +var _ json.Unmarshaler = &multiUnmarshaller{} +var _ json.Marshaler = &multiUnmarshaller{} + +func newMultiUnmarshaller(obj ...interface{}) json.Unmarshaler { + return &multiUnmarshaller{ + obj: obj, + } +} + +type multiUnmarshaller struct { + obj []interface{} +} + +func (m multiUnmarshaller) MarshalJSON() ([]byte, error) { + r := map[string]interface{}{} + for _, o := range m.obj { + z := map[string]interface{}{} + if d, err := json.Marshal(o); err != nil { + return nil, err + } else { + if err := json.Unmarshal(d, &z); err != nil { + return nil, err + } + } + + for k, v := range z { + r[k] = v + } + } + + return json.Marshal(r) +} + +func (m multiUnmarshaller) UnmarshalJSON(d []byte) error { + println(string(d)) + for _, o := range m.obj { + if err := json.Unmarshal(d, o); err != nil { + return err + } + } + + return nil +} + type byteDecoder struct { data []byte } @@ -47,17 +91,17 @@ func (b *byteDecoder) Unmarshal(i interface{}) error { return json.Unmarshal(b.data, i) } -func newUnmarshalInto(obj interface{}) *unmarshalInto { - return &unmarshalInto{obj} +func newUnmarshalInto(obj interface{}) *UnmarshalInto { + return &UnmarshalInto{obj} } -var _ json.Unmarshaler = &unmarshalInto{} +var _ json.Unmarshaler = &UnmarshalInto{} -type unmarshalInto struct { +type UnmarshalInto struct { obj interface{} } -func (u *unmarshalInto) UnmarshalJSON(d []byte) error { +func (u *UnmarshalInto) UnmarshalJSON(d []byte) error { if u.obj == nil { return nil } diff --git a/v2/connection/call.go b/v2/connection/call.go index ed0794f8..f97f2759 100644 --- a/v2/connection/call.go +++ b/v2/connection/call.go @@ -54,6 +54,10 @@ func CallPost(ctx context.Context, c Connection, url string, output interface{}, return Call(ctx, c, http.MethodPost, url, output, append(modifiers, WithBody(body))...) } +func CallPatch(ctx context.Context, c Connection, url string, output interface{}, body interface{}, modifiers ...RequestModifier) (Response, error) { + return Call(ctx, c, http.MethodPatch, url, output, append(modifiers, WithBody(body))...) +} + func CallHead(ctx context.Context, c Connection, url string, output interface{}, modifiers ...RequestModifier) (Response, error) { return Call(ctx, c, http.MethodHead, url, output, modifiers...) } diff --git a/v2/connection/connection_http_internal.go b/v2/connection/connection_http_internal.go index 022f463c..0eb42ff1 100644 --- a/v2/connection/connection_http_internal.go +++ b/v2/connection/connection_http_internal.go @@ -186,6 +186,7 @@ func (j httpConnection) doWithOutput(ctx context.Context, request *httpRequest, if output != nil { defer dropBodyData(body) // In case if there is data drop it all + if err = j.Decoder(resp.Content()).Decode(body, output); err != nil { if err != io.EOF { return nil, errors.WithStack(err) @@ -221,7 +222,7 @@ func (j httpConnection) do(ctx context.Context, req *httpRequest) (*httpResponse ctx = context.Background() } - if req.Method() == http.MethodPost || req.Method() == http.MethodPut { + if req.Method() == http.MethodPost || req.Method() == http.MethodPut || req.Method() == http.MethodPatch { decoder := j.Decoder(j.contentType) if !j.streamSender { b := bytes.NewBuffer([]byte{}) diff --git a/v2/tests/database_collection_operations_test.go b/v2/tests/database_collection_operations_test.go new file mode 100644 index 00000000..c496d202 --- /dev/null +++ b/v2/tests/database_collection_operations_test.go @@ -0,0 +1,203 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/arangodb/go-driver/v2/arangodb" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func Test_DatabaseCollectionOperations(t *testing.T) { + + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + ctx, c := context.WithTimeout(context.Background(), 5*time.Minute) + defer c() + + size := 512 + + docs := newDocs(size) + + for i := 0; i < size; i++ { + docs[i].Fields = uuid.New().String() + } + + docsIds := docs.asBasic().getKeys() + + t.Run("Create", func(t *testing.T) { + _, err := col.CreateDocuments(ctx, docs) + require.NoError(t, err) + + r, err := col.ReadDocuments(ctx, docsIds) + + nd := docs + + for { + var doc document + + meta, err := r.Read(&doc) + if shared.IsNoMoreDocuments(err) { + break + } + require.NoError(t, err) + + require.True(t, len(nd) > 0) + + require.Equal(t, nd[0].Key, meta.Key) + require.Equal(t, nd[0], doc) + + if len(nd) == 1 { + nd = nil + } else { + nd = nd[1:] + } + } + + require.Len(t, nd, 0) + }) + + t.Run("Cursor - single", func(t *testing.T) { + nd := docs + + query := fmt.Sprintf("FOR doc IN `%s` RETURN doc", col.Name()) + println(query) + + q, err := db.Query(ctx, query, &arangodb.QueryOptions{ + BatchSize: size, + }) + require.NoError(t, err) + + for { + var doc document + meta, err := q.ReadDocument(ctx, &doc) + if shared.IsNoMoreDocuments(err) { + break + } + require.NoError(t, err) + + require.True(t, len(nd) > 0) + + require.Equal(t, nd[0].Key, meta.Key) + require.Equal(t, nd[0], doc) + + if len(nd) == 1 { + nd = nil + } else { + nd = nd[1:] + } + } + }) + + t.Run("Cursor - batches", func(t *testing.T) { + nd := docs + + query := fmt.Sprintf("FOR doc IN `%s` RETURN doc", col.Name()) + println(query) + + q, err := db.Query(ctx, query, &arangodb.QueryOptions{ + BatchSize: size / 10, + }) + require.NoError(t, err) + + for { + var doc document + meta, err := q.ReadDocument(ctx, &doc) + if shared.IsNoMoreDocuments(err) { + break + } + require.NoError(t, err) + + require.True(t, len(nd) > 0) + + require.Equal(t, nd[0].Key, meta.Key) + require.Equal(t, nd[0], doc) + + if len(nd) == 1 { + nd = nil + } else { + nd = nd[1:] + } + } + }) + + t.Run("Update", func(t *testing.T) { + newDocs := make([]document, size) + defer func() { + docs = newDocs + }() + + for i := 0; i < size; i++ { + newDocs[i] = docs[i] + newDocs[i].Fields = uuid.New().String() + } + + ng := newDocs + nd := docs + + var old document + var new document + + r, err := col.UpdateDocumentsWithOptions(ctx, newDocs, &arangodb.CollectionDocumentUpdateOptions{ + OldObject: &old, + NewObject: &new, + }) + require.NoError(t, err) + + for { + + meta, err := r.Read() + if shared.IsNoMoreDocuments(err) { + break + } + require.NoError(t, err) + + require.True(t, len(nd) > 0) + + require.Equal(t, nd[0].Key, meta.Key) + require.Equal(t, ng[0].Key, meta.Key) + require.Equal(t, ng[0], new) + require.Equal(t, nd[0], old) + + if len(nd) == 1 { + nd = nil + ng = nil + } else { + nd = nd[1:] + ng = ng[1:] + } + } + + require.Len(t, nd, 0) + }) + }) + }) + }) +} diff --git a/v2/tests/database_collection_perf_test.go b/v2/tests/database_collection_perf_test.go index a0fe9d9a..ac42f96d 100644 --- a/v2/tests/database_collection_perf_test.go +++ b/v2/tests/database_collection_perf_test.go @@ -27,43 +27,19 @@ import ( "fmt" "testing" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/arangodb" - "github.com/google/uuid" "github.com/stretchr/testify/require" ) -type document struct { - Key string `json:"_key"` - Fields interface{} `json:",inline"` -} - -func newBenchDocs(c int) []benchDoc { - r := make([]benchDoc, c) - - for i := 0; i < c; i++ { - r[i] = newBenchDoc() - } - - return r -} - -func newBenchDoc() benchDoc { - return benchDoc{ - Key: uuid.New().String(), - } -} - -type benchDoc struct { - Key string `json:"_key"` -} - func insertDocuments(t testing.TB, col arangodb.Collection, documents, batch int, factory func(i int) interface{}) { b := make([]document, 0, batch) for i := 0; i < documents; i++ { b = append(b, document{ - Key: uuid.New().String(), - Fields: factory(i), + basicDocument: newBasicDocument(), + Fields: factory(i), }) if len(b) == batch { @@ -83,11 +59,11 @@ func insertBatch(t testing.TB, ctx context.Context, col arangodb.Collection, opt results, err := col.CreateDocumentsWithOptions(ctx, documents, opts) require.NoError(t, err) for { - meta, next, err := results.Read() - require.NoError(t, err) - if !next { + meta, err := results.Read() + if shared.IsNoMoreDocuments(err) { break } + require.NoError(t, err) require.False(t, getBool(meta.Error, false)) } @@ -116,7 +92,7 @@ func _b_insert(b *testing.B, db arangodb.Database, threads int) { return } - d := newBenchDoc() + d := newBasicDocument() _, err := col.CreateDocument(context.Background(), d) require.NoError(b, err) @@ -138,17 +114,17 @@ func _b_batchInsert(b *testing.B, db arangodb.Database, threads int) { return } - d := newBenchDocs(512) + d := newDocs(512) r, err := col.CreateDocuments(context.Background(), d) require.NoError(b, err) for { - _, ok, err := r.Read() - require.NoError(b, err) - if !ok { + _, err := r.Read() + if shared.IsNoMoreDocuments(err) { break } + require.NoError(b, err) } } }) diff --git a/v2/tests/database_transactions_test.go b/v2/tests/database_transactions_test.go new file mode 100644 index 00000000..479990a1 --- /dev/null +++ b/v2/tests/database_transactions_test.go @@ -0,0 +1,274 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package tests + +import ( + "context" + "testing" + "time" + + "github.com/arangodb/go-driver/v2/arangodb" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func Test_DatabaseTransactions_DataIsolation(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + t.Run("Transaction", func(t *testing.T) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + d := document{ + basicDocument: basicDocument{Key: "uniq_key"}, + Fields: "DOC", + } + + var tid arangodb.TransactionID + + // Start transaction + require.NoError(t, db.WithTransaction(ctx, arangodb.TransactionCollections{ + Write: []string{ + col.Name(), + }, + }, &arangodb.BeginTransactionOptions{ + WaitForSync: true, + }, nil, nil, func(ctx context.Context, transaction arangodb.Transaction) error { + tid = transaction.ID() + + // Get collection in transaction + tCol, err := transaction.Collection(ctx, col.Name()) + require.NoError(t, err) + + _, err = tCol.CreateDocument(ctx, d) + require.NoError(t, err) + + // Check if non transaction handler can read document + DocumentNotExists(t, col, d) + + // Check if transaction handler can read document + DocumentExists(t, tCol, d) + // Do commit + return nil + })) + + DocumentExists(t, col, d) + + ensureTransactionStatus(t, db, tid, arangodb.TransactionCommitted) + }) + }) + }) + + t.Run("Transaction - With Error", func(t *testing.T) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + d := document{ + basicDocument: basicDocument{Key: "uniq_key"}, + Fields: "DOC", + } + + var tid arangodb.TransactionID + + // Start transaction + require.EqualError(t, db.WithTransaction(ctx, arangodb.TransactionCollections{ + Write: []string{ + col.Name(), + }, + }, &arangodb.BeginTransactionOptions{ + WaitForSync: true, + }, nil, nil, func(ctx context.Context, transaction arangodb.Transaction) error { + tid = transaction.ID() + + // Get collection in transaction + tCol, err := transaction.Collection(ctx, col.Name()) + require.NoError(t, err) + + _, err = tCol.CreateDocument(ctx, d) + require.NoError(t, err) + + // Check if non transaction handler can read document + DocumentNotExists(t, col, d) + + // Check if transaction handler can read document + DocumentExists(t, tCol, d) + + // Do abort + return errors.Errorf("CustomAbortError") + }), "CustomAbortError") + + DocumentNotExists(t, col, d) + + ensureTransactionStatus(t, db, tid, arangodb.TransactionAborted) + }) + }) + }) + + t.Run("Transaction - With Panic", func(t *testing.T) { + t.Skipf("") + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + d := document{ + basicDocument: basicDocument{Key: "uniq_key"}, + Fields: "DOC", + } + + var tid arangodb.TransactionID + + // Start transaction + ExpectPanic(t, func() { + require.NoError(t, db.WithTransaction(ctx, arangodb.TransactionCollections{ + Write: []string{ + col.Name(), + }, + }, &arangodb.BeginTransactionOptions{ + WaitForSync: true, + }, nil, nil, func(ctx context.Context, transaction arangodb.Transaction) error { + tid = transaction.ID() + + // Get collection in transaction + tCol, err := transaction.Collection(ctx, col.Name()) + require.NoError(t, err) + + _, err = tCol.CreateDocument(ctx, d) + require.NoError(t, err) + + // Check if non transaction handler can read document + DocumentNotExists(t, col, d) + + // Check if transaction handler can read document + DocumentExists(t, tCol, d) + + // Do abort + panic("CustomPanicError") + })) + }, "CustomPanicError") + + DocumentNotExists(t, col, d) + + ensureTransactionStatus(t, db, tid, arangodb.TransactionAborted) + }) + }) + }) + }) + }) +} + +func Test_DatabaseTransactions_DocumentLock(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + d := document{ + basicDocument: basicDocument{Key: uuid.New().String()}, + Fields: "no1", + } + + ud := document{ + basicDocument: d.basicDocument, + Fields: "newNo", + } + _, err := col.CreateDocument(ctx, d) + require.NoError(t, err) + + t1, err := db.BeginTransaction(ctx, arangodb.TransactionCollections{Write: []string{col.Name()}}, &arangodb.BeginTransactionOptions{ + LockTimeoutDuration: 5 * time.Second, + }) + require.NoError(t, err) + defer abortTransaction(t, t1) + + col1, err := t1.Collection(ctx, col.Name()) + require.NoError(t, err) + + t2, err := db.BeginTransaction(ctx, arangodb.TransactionCollections{Write: []string{col.Name()}}, &arangodb.BeginTransactionOptions{ + LockTimeoutDuration: 1 * time.Second, + }) + require.NoError(t, err) + defer abortTransaction(t, t1) + + col2, err := t2.Collection(ctx, col.Name()) + require.NoError(t, err) + + _, err = col1.UpdateDocument(ctx, d.Key, ud) + require.NoError(t, err) + + _, err = col2.UpdateDocument(ctx, d.Key, ud) + require.Error(t, err) + require.True(t, shared.IsOperationTimeout(err)) + }) + }) + }) + }) +} + +func Test_DatabaseTransactions_List(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + withContextT(t, 30*time.Second, func(ctx context.Context, _ testing.TB) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + t.Run("List all transactions", func(t *testing.T) { + t1, err := db.BeginTransaction(ctx, arangodb.TransactionCollections{}, nil) + require.NoError(t, err) + t2, err := db.BeginTransaction(ctx, arangodb.TransactionCollections{}, nil) + require.NoError(t, err) + t3, err := db.BeginTransaction(ctx, arangodb.TransactionCollections{}, nil) + require.NoError(t, err) + + transactions, err := db.ListTransactions(ctx) + require.NoError(t, err) + + q := map[arangodb.TransactionID]arangodb.Transaction{} + for _, transaction := range transactions { + q[transaction.ID()] = transaction + } + + _, ok := q[t1.ID()] + require.True(t, ok) + + _, ok = q[t2.ID()] + require.True(t, ok) + + _, ok = q[t3.ID()] + require.True(t, ok) + }) + }) + }) + }) +} + +func ensureTransactionStatus(t testing.TB, db arangodb.Database, tid arangodb.TransactionID, status arangodb.TransactionStatus) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + transaction, err := db.Transaction(ctx, tid) + require.NoError(t, err) + + s, err := transaction.Status(ctx) + require.NoError(t, err) + + require.Equal(t, status, s.Status) + }) +} + +func abortTransaction(t testing.TB, transaction arangodb.Transaction) { + withContextT(t, 10*time.Second, func(ctx context.Context, t testing.TB) { + require.NoError(t, transaction.Abort(ctx, nil)) + }) +} diff --git a/v2/tests/docs_test.go b/v2/tests/docs_test.go index a9cc7f39..37addf15 100644 --- a/v2/tests/docs_test.go +++ b/v2/tests/docs_test.go @@ -22,6 +22,17 @@ package tests +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/arangodb/go-driver/v2/arangodb" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/stretchr/testify/require" +) + type UserDoc struct { Name string `json:"name"` Age int `json:"age"` @@ -77,3 +88,23 @@ type AccountEdge struct { To string `json:"_to,omitempty"` User *UserDoc `json:"user"` } + +func DocumentExists(t testing.TB, col arangodb.Collection, doc DocIDGetter) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + z := reflect.New(reflect.TypeOf(doc)) + + _, err := col.ReadDocument(ctx, doc.GetKey(), z.Interface()) + require.NoError(t, err) + require.Equal(t, doc, z.Elem().Interface()) + }) +} + +func DocumentNotExists(t testing.TB, col arangodb.Collection, doc DocIDGetter) { + withContextT(t, 30*time.Second, func(ctx context.Context, t testing.TB) { + z := reflect.New(reflect.TypeOf(doc)) + + _, err := col.ReadDocument(ctx, doc.GetKey(), z.Interface()) + require.Error(t, err) + require.True(t, shared.IsNotFound(err)) + }) +} diff --git a/v2/tests/documents_def_test.go b/v2/tests/documents_def_test.go new file mode 100644 index 00000000..2b0cddda --- /dev/null +++ b/v2/tests/documents_def_test.go @@ -0,0 +1,82 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package tests + +import "github.com/google/uuid" + +type DocIDGetter interface { + GetKey() string +} + +type basicDocuments []basicDocument + +func (b basicDocuments) getKeys() []string { + l := make([]string, len(b)) + + for i, g := range b { + l[i] = g.GetKey() + } + + return l +} + +type basicDocument struct { + Key string `json:"_key"` +} + +func newBasicDocument() basicDocument { + return basicDocument{ + Key: uuid.New().String(), + } +} + +func (d basicDocument) GetKey() string { + return d.Key +} + +type documents []document + +func (d documents) asBasic() basicDocuments { + z := make([]basicDocument, len(d)) + + for i, q := range d { + z[i] = q.basicDocument + } + + return z +} + +type document struct { + basicDocument `json:",inline"` + Fields interface{} `json:"data,omitempty"` +} + +func newDocs(c int) documents { + r := make([]document, c) + + for i := 0; i < c; i++ { + r[i].basicDocument = newBasicDocument() + } + + return r +} diff --git a/v2/tests/errors_test.go b/v2/tests/errors_test.go new file mode 100644 index 00000000..4a09fee9 --- /dev/null +++ b/v2/tests/errors_test.go @@ -0,0 +1,38 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func ExpectPanic(t testing.TB, f func(), expected interface{}) { + defer func() { + o := recover() + require.NotNil(t, o, "Panic did not occur") + require.Equal(t, expected, o) + }() + f() +} diff --git a/v2/tests/helper_test.go b/v2/tests/helper_test.go index 33c5f7d3..c29d3cd9 100644 --- a/v2/tests/helper_test.go +++ b/v2/tests/helper_test.go @@ -28,6 +28,8 @@ import ( "testing" "time" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/arangodb" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -70,7 +72,7 @@ func WithCollection(t testing.TB, db arangodb.Database, opts *arangodb.CreateCol return Interrupt{} } - if arangodb.IsNotFound(err) { + if shared.IsNotFound(err) { return nil } diff --git a/v2/tests/run_wrap_test.go b/v2/tests/run_wrap_test.go index 08841c62..15133c6e 100644 --- a/v2/tests/run_wrap_test.go +++ b/v2/tests/run_wrap_test.go @@ -217,3 +217,10 @@ func withContext(timeout time.Duration, f func(ctx context.Context) error) error return f(ctx) } + +func withContextT(t testing.TB, timeout time.Duration, f func(ctx context.Context, t testing.TB)) { + require.NoError(t, withContext(timeout, func(ctx context.Context) error { + f(ctx, t) + return nil + })) +}