diff --git a/.github/dependabot.yml b/.github/dependabot.yml index af2f83a1..cae2be18 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,6 +26,12 @@ updates: - "๐Ÿค– Dependencies" schedule: interval: "daily" + - package-ecosystem: "gomod" + directory: "/couchbase/" # Location of package manifests + labels: + - "๐Ÿค– Dependencies" + schedule: + interval: "daily" - package-ecosystem: "gomod" directory: "/dynamodb/" # Location of package manifests labels: diff --git a/.github/release-drafter-couchbase.yml b/.github/release-drafter-couchbase.yml new file mode 100644 index 00000000..9e5306a0 --- /dev/null +++ b/.github/release-drafter-couchbase.yml @@ -0,0 +1,43 @@ +name-template: 'Couchbase - v$RESOLVED_VERSION' +tag-template: 'couchbase/v$RESOLVED_VERSION' +tag-prefix: couchbase/v +include-paths: + - couchbase +categories: + - title: '๐Ÿš€ New' + labels: + - 'โœ๏ธ Feature' + - title: '๐Ÿงน Updates' + labels: + - '๐Ÿงน Updates' + - '๐Ÿค– Dependencies' + - title: '๐Ÿ› Fixes' + labels: + - 'โ˜ข๏ธ Bug' + - title: '๐Ÿ“š Documentation' + labels: + - '๐Ÿ“’ Documentation' +change-template: '- $TITLE (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + - 'โœ๏ธ Feature' + patch: + labels: + - 'patch' + - '๐Ÿ“’ Documentation' + - 'โ˜ข๏ธ Bug' + - '๐Ÿค– Dependencies' + - '๐Ÿงน Updates' + default: patch +template: | + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...couchbase/v$RESOLVED_VERSION + + Thank you $CONTRIBUTORS for making this update possible. diff --git a/.github/workflows/release-drafter-couchbase.yml b/.github/workflows/release-drafter-couchbase.yml new file mode 100644 index 00000000..52159477 --- /dev/null +++ b/.github/workflows/release-drafter-couchbase.yml @@ -0,0 +1,19 @@ +name: Release Drafter Couchbase +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + - main + paths: + - 'couchbase/**' +jobs: + draft_release_couchbase: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter-couchbase.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/couchbase/README.md b/couchbase/README.md new file mode 100644 index 00000000..d90ca284 --- /dev/null +++ b/couchbase/README.md @@ -0,0 +1,83 @@ +# Couchbase + +A Couchbase storage driver using [couchbase/gocb](https://github.com/couchbase/gocb). + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +func New(config ...Config) Storage +func (s *Storage) Get(key string) ([]byte, error) +func (s *Storage) Set(key string, val []byte, exp time.Duration) error +func (s *Storage) Delete(key string) error +func (s *Storage) Reset() error +func (s *Storage) Close() error +func (s *Storage) Conn() *gocb.Cluster +``` +### Installation +Couchbase is tested on the 2 last [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: +```bash +go mod init github.com// +``` +And then install the Couchbase implementation: +```bash +go get github.com/gofiber/storage/couchbase +``` + +### Examples +Import the storage package. +```go +import "github.com/gofiber/storage/couchbase" +``` + +You can use the following possibilities to create a storage: +```go +// Initialize default config +store := couchbase.New() + +// Initialize Couchbase storage with custom config +store := couchbase.New(couchbase.Config{ + Host: "127.0.0.1:8091", + Username: "", + Password: "", + Bucket: 0, + ConnectionTimeout: 3* time.Second, + KVTimeout: 1* time.Second, +}) +``` + +### Config +```go +type Config struct { + // The application username to Connect to the Couchbase cluster + Username string + // The application password to Connect to the Couchbase cluster + Password string + // The connection string for the Couchbase cluster + Host string + // The name of the bucket to Connect to + Bucket string + // The timeout for connecting to the Couchbase cluster + ConnectionTimeout time.Duration + // The timeout for performing operations on the Couchbase cluster + KVTimeout time.Duration +} +``` + +### Default Config +```go +// ConfigDefault is the default config +var ConfigDefault = Config{ + Host: "127.0.0.1:8091", + Username: "admin", + Password: "123456", + Bucket: "fiber_storage", + ConnectionTimeout: 3 * time.Second, + KVTimeout: 1 * time.Second, +} +``` diff --git a/couchbase/config.go b/couchbase/config.go new file mode 100644 index 00000000..1f5dfe10 --- /dev/null +++ b/couchbase/config.go @@ -0,0 +1,62 @@ +package couchbase + +import ( + "time" +) + +type Config struct { + // The application username to Connect to the Couchbase cluster + Username string + // The application password to Connect to the Couchbase cluster + Password string + // The connection string for the Couchbase cluster + Host string + // The name of the bucket to Connect to + Bucket string + // The timeout for connecting to the Couchbase cluster + ConnectionTimeout time.Duration + // The timeout for performing operations on the Couchbase cluster + KVTimeout time.Duration +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Host: "127.0.0.1:8091", + Username: "admin", + Password: "123456", + Bucket: "fiber_storage", + ConnectionTimeout: 3 * time.Second, + KVTimeout: 1 * time.Second, +} + +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Username == "" { + cfg.Username = ConfigDefault.Username + } + if cfg.Password == "" { + cfg.Password = ConfigDefault.Password + } + if cfg.Host == "" { + cfg.Host = ConfigDefault.Host + } + if cfg.Bucket == "" { + cfg.Bucket = ConfigDefault.Bucket + } + if cfg.ConnectionTimeout == 0 { + cfg.ConnectionTimeout = ConfigDefault.ConnectionTimeout + } + if cfg.KVTimeout == 0 { + cfg.KVTimeout = ConfigDefault.KVTimeout + } + + return cfg +} diff --git a/couchbase/couchbase.go b/couchbase/couchbase.go new file mode 100644 index 00000000..c9e4da6c --- /dev/null +++ b/couchbase/couchbase.go @@ -0,0 +1,97 @@ +package couchbase + +import ( + "time" + + "github.com/couchbase/gocb/v2" +) + +type Storage struct { + cb *gocb.Cluster + bucket *gocb.Bucket +} + +func New(config ...Config) *Storage { + // Set default config + cfg := configDefault(config...) + + cb, err := gocb.Connect(cfg.Host, gocb.ClusterOptions{ + Authenticator: gocb.PasswordAuthenticator{ + Username: cfg.Username, + Password: cfg.Password, + }, + TimeoutsConfig: gocb.TimeoutsConfig{ + ConnectTimeout: cfg.ConnectionTimeout, + KVTimeout: cfg.KVTimeout, + }, + Transcoder: gocb.NewLegacyTranscoder(), + }) + + if err != nil { + panic(err) + } + + _, err = cb.Ping(&gocb.PingOptions{ + Timeout: cfg.ConnectionTimeout, + }) + + if err != nil { + panic(err) + } + + b := cb.Bucket(cfg.Bucket) + + return &Storage{cb: cb, bucket: b} +} + +func (s *Storage) Get(key string) ([]byte, error) { + out, err := s.bucket.DefaultCollection().Get(key, nil) + if err != nil { + switch e := err.(type) { + case *gocb.KeyValueError: + if e.InnerError.Error() == gocb.ErrDocumentNotFound.Error() { + return nil, nil + } + default: //*gocb.TimeoutError,... + return nil, err + } + + return nil, err + } + + var value []byte + if err := out.Content(&value); err != nil { + return nil, err + } + + return value, nil +} + +func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + if _, err := s.bucket.DefaultCollection().Upsert(key, val, &gocb.UpsertOptions{ + Expiry: exp, + }); err != nil { + return err + } + + return nil +} + +func (s *Storage) Delete(key string) error { + if _, err := s.bucket.DefaultCollection().Remove(key, nil); err != nil { + return err + } + return nil +} + +func (s *Storage) Reset() error { + return s.cb.Buckets().FlushBucket(s.bucket.Name(), nil) +} + +func (s *Storage) Close() error { + return s.cb.Close(nil) +} + +func (s *Storage) Conn() *gocb.Cluster { + return s.cb +} diff --git a/couchbase/couchbase_test.go b/couchbase/couchbase_test.go new file mode 100644 index 00000000..bdb6b1d9 --- /dev/null +++ b/couchbase/couchbase_test.go @@ -0,0 +1,135 @@ +package couchbase + +import ( + "strings" + "testing" + "time" + + "github.com/gofiber/utils" +) + +func TestSetCouchbase_ShouldReturnNoError(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Set("test", []byte("test"), 0) + + utils.AssertEqual(t, nil, err) +} + +func TestGetCouchbase_ShouldReturnNil_WhenDocumentNotFound(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + val, err := testStorage.Get("not_found_key") + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(val)) +} + +func TestSetAndGet_GetShouldReturn_SettedValueWithoutError(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Set("test", []byte("fiber_test_value"), 0) + utils.AssertEqual(t, nil, err) + + val, err := testStorage.Get("test") + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, []byte("fiber_test_value")) +} + +func TestSetAndGet_GetShouldReturnNil_WhenTTLExpired(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Set("test", []byte("fiber_test_value"), 3*time.Second) + utils.AssertEqual(t, nil, err) + + time.Sleep(6 * time.Second) + + val, err := testStorage.Get("test") + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(val)) +} + +func TestSetAndDelete_DeleteShouldReturn_NoError(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Set("test", []byte("fiber_test_value"), 0) + utils.AssertEqual(t, nil, err) + + err = testStorage.Delete("test") + utils.AssertEqual(t, nil, err) + + _, err = testStorage.Get("test") + errStr := err.Error() + + utils.AssertEqual(t, true, strings.Contains(errStr, "document not found")) +} + +func TestSetAndReset_ResetShouldReturn_NoError(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Set("test", []byte("fiber_test_value"), 0) + utils.AssertEqual(t, nil, err) + + err = testStorage.Reset() + utils.AssertEqual(t, nil, err) + + _, err = testStorage.Get("test") + errStr := err.Error() + + utils.AssertEqual(t, true, strings.Contains(errStr, "document not found")) +} + +func TestClose_CloseShouldReturn_NoError(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + + err := testStorage.Close() + utils.AssertEqual(t, nil, err) +} + +func TestGetConn_ReturnsNotNill(t *testing.T) { + testStorage := New(Config{ + Username: "admin", + Password: "123456", + Host: "127.0.0.1:8091", + Bucket: "fiber_storage", + }) + conn := testStorage.Conn() + utils.AssertEqual(t, true, conn != nil) +} diff --git a/couchbase/go.mod b/couchbase/go.mod new file mode 100644 index 00000000..8175a84a --- /dev/null +++ b/couchbase/go.mod @@ -0,0 +1,12 @@ +module github.com/gofiber/storage/couchbase + +go 1.18 + +require github.com/gofiber/utils v1.1.0 + +require ( + github.com/couchbase/gocb/v2 v2.6.3 // indirect + github.com/couchbase/gocbcore/v10 v10.2.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.0 // indirect +) diff --git a/couchbase/go.sum b/couchbase/go.sum new file mode 100644 index 00000000..fb5b8a28 --- /dev/null +++ b/couchbase/go.sum @@ -0,0 +1,24 @@ +github.com/couchbase/gocb/v2 v2.6.3 h1:5RsMo+RRfK0mVxHLAfpBz3/tHlgXZb1WBNItLk9Ab+c= +github.com/couchbase/gocb/v2 v2.6.3/go.mod h1:yF5F6BHTZ/ZowhEuZbySbXrlI4rHd1TIhm5azOaMbJU= +github.com/couchbase/gocbcore/v10 v10.2.3 h1:PEkRSNSkKjUBXx82Ucr094+anoiCG5GleOOQZOHo6D4= +github.com/couchbase/gocbcore/v10 v10.2.3/go.mod h1:lYQIIk+tzoMcwtwU5GzPbDdqEkwkH3isI2rkSpfL0oM= +github.com/couchbaselabs/gocaves/client v0.0.0-20230307083111-cc3960c624b1/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= +github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=