diff --git a/.github/workflows/test-neo4j.yml b/.github/workflows/test-neo4j.yml
new file mode 100644
index 00000000..77b07df5
--- /dev/null
+++ b/.github/workflows/test-neo4j.yml
@@ -0,0 +1,27 @@
+on:
+ push:
+ branches:
+ - master
+ - main
+ paths:
+ - 'neo4j/**'
+ pull_request:
+ paths:
+ - 'neo4j/**'
+name: "Tests Neo4j"
+jobs:
+ Tests:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ go-version:
+ - 1.23.x
+ steps:
+ - name: Fetch Repository
+ uses: actions/checkout@v4
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '${{ matrix.go-version }}'
+ - name: Run Test
+ run: cd ./neo4j && go test ./... -v -race
diff --git a/README.md b/README.md
index 79d51567..8ec2fcb8 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,7 @@ type Storage interface {
- [MSSQL](./mssql/README.md)
- [MySQL](./mysql/README.md)
- [NATS](./nats/README.md)
+- [Neo4j](./neo4j/README.md)
- [Pebble](./pebble/README.md)
- [Postgres](./postgres/README.md)
- [Redis](./redis/README.md)
diff --git a/arangodb/README.md b/arangodb/README.md
index 8478d2c1..dee1cf9b 100644
--- a/arangodb/README.md
+++ b/arangodb/README.md
@@ -35,7 +35,7 @@ ArangoDB is tested on the 2 last (1.14/1.15) [Go versions](https://golang.org/dl
```bash
go mod init github.com//
```
-And then install the mysql implementation:
+And then install the arangodb implementation:
```bash
go get github.com/gofiber/storage/arangodb/v2
```
diff --git a/neo4j/README.md b/neo4j/README.md
new file mode 100644
index 00000000..a6136ae6
--- /dev/null
+++ b/neo4j/README.md
@@ -0,0 +1,156 @@
+---
+id: neo4j
+title: Neo4j
+---
+
+
+[](https://gofiber.io/discord)
+
+
+
+
+A Neo4j storage driver using [neo4j/neo4j-go-driver](https://github.com/neo4j/neo4j-go-driver).
+
+> **Note: Requires latest two releases of Golang**
+
+### 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() neo4j.DriverWithContext
+```
+
+### Installation
+
+Neo4j 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 neo4j implementation:
+
+```bash
+go get github.com/gofiber/storage/neo4j
+```
+
+### Examples
+
+Import the storage package.
+
+```go
+import "github.com/gofiber/storage/neo4j"
+```
+
+You can use the following possibilities to create a storage:
+
+> The `neo4j` package name used in this example is the package name (and default import name) for this storage driver. Feel free import it with a custom name to avoid confusing it with the neo4j-go-driver package which also uses `neo4j` as package name (and default import name).
+
+```go
+// Initialize default config
+store := neo4j.New()
+
+// Initialize custom config
+store := neo4j.New(neo4j.Config{
+ DB: driver,
+ Node: "fiber_storage",
+ Reset: false,
+ GCInterval: 10 * time.Second,
+})
+```
+
+### Config
+
+> The `neo4j`, `auth`, and `config` package names used here belong to the neo4j-go-driver package.
+
+```go
+// Config defines the config for storage.
+type Config struct {
+ // Connection pool
+ //
+ // DB neo4j.DriverWithContext object will override connection URI and other connection fields.
+ //
+ // Optional. Default is nil.
+ DB neo4j.DriverWithContext
+
+ // Target Server
+ //
+ // Optional. Default is "neo4j://localhost"
+ URI string
+
+ // Connection authentication
+ //
+ // Auth auth.TokenManager will override Username and Password fields
+ //
+ // Optional. Default is nil.
+ Auth auth.TokenManager
+
+ // Connection configurations
+ //
+ // Optional. Default is nil
+ Configurations []func(*config.Config)
+
+ // Server username
+ //
+ // Optional. Default is ""
+ Username string
+
+ // Server password
+ //
+ // Optional. Default is ""
+ Password string
+
+ // Node name
+ //
+ // Optional. Default is "fiber_storage"
+ Node string
+
+ // Reset clears any existing keys (Nodes)
+ //
+ // Optional. Default is false
+ Reset bool
+
+ // Time before deleting expired keys (Nodes)
+ //
+ // Optional. Default is 10 * time.Second
+ GCInterval time.Duration
+}
+```
+
+#### A note on Authentication
+
+If auth is enabled on your server, then authentication must be provided in one of the three ways (the previous overrides the next):
+
+- Via the connection pool, `neo4j.DriverWithContext`, provided on the `DB` field.
+- Via the `Auth` field: it must be an `auth.TokenManager` whose value is any one but `neo4j.NoAuth()`.
+- By setting both `Username` and `Password` fields: This will cause this storage driver to use Basic Auth.
+
+Otherwise, your neo4j driver will panic with authorization error.
+
+In contrast, if auth is disabled on your server, there's no need to provide any authentication parameter.
+
+### Default Config
+
+Used only for optional fields
+
+```go
+var ConfigDefault = Config{
+ URI: "neo4j://localhost",
+ Node: "fiber_storage",
+ Reset: false,
+ GCInterval: 10 * time.Second,
+}
+```
diff --git a/neo4j/config.go b/neo4j/config.go
new file mode 100644
index 00000000..31473ac5
--- /dev/null
+++ b/neo4j/config.go
@@ -0,0 +1,102 @@
+package neo4j
+
+import (
+ "time"
+
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j"
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j/auth"
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j/config"
+)
+
+type Config struct {
+ // Connection pool.
+ //
+ // DB neo4j.DriverWithContext object will override connection uri and other connection fields.
+ //
+ // Optional. Default is nil.
+ DB neo4j.DriverWithContext
+
+ // Target Server
+ //
+ // Optional. Default is "neo4j://localhost"
+ URI string
+
+ // Connection authentication.
+ //
+ // Auth auth.TokenManager will override Username and Password fields.
+ //
+ // Optional. Default is nil.
+ Auth auth.TokenManager
+
+ // Connection configurations
+ //
+ // Optional. Default is nil
+ Configurations []func(*config.Config)
+
+ // Server username
+ //
+ // Optional. Default is ""
+ Username string
+
+ // Server password
+ //
+ // Optional. Default is ""
+ Password string
+
+ // Node name
+ //
+ // Optional. Default is "fiber_storage"
+ Node string
+
+ // Reset clears any existing keys (Nodes)
+ //
+ // Optional. Default is false
+ Reset bool
+
+ // Time before deleting expired keys (Nodes)
+ //
+ // Optional. Default is 10 * time.Second
+ GCInterval time.Duration
+}
+
+var ConfigDefault = Config{
+ URI: "neo4j://localhost",
+ Node: "fiber_storage",
+ Reset: false,
+ GCInterval: 10 * time.Second,
+}
+
+// Helper function to set default values
+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.URI == "" {
+ cfg.URI = ConfigDefault.URI
+ }
+
+ if cfg.Auth == nil {
+ if cfg.Username != "" && cfg.Password != "" {
+ cfg.Auth = neo4j.BasicAuth(cfg.Username, cfg.Password, "")
+ } else {
+ cfg.Auth = neo4j.NoAuth()
+ }
+ }
+
+ if cfg.Node == "" {
+ cfg.Node = ConfigDefault.Node
+ }
+
+ if int(cfg.GCInterval.Seconds()) <= 0 {
+ cfg.GCInterval = ConfigDefault.GCInterval
+ }
+
+ return cfg
+}
diff --git a/neo4j/go.mod b/neo4j/go.mod
new file mode 100644
index 00000000..9e842be1
--- /dev/null
+++ b/neo4j/go.mod
@@ -0,0 +1,59 @@
+module github.com/gofiber/storage/neo4j
+
+go 1.23
+
+require (
+ github.com/neo4j/neo4j-go-driver/v5 v5.27.0
+ github.com/stretchr/testify v1.9.0
+ github.com/testcontainers/testcontainers-go/modules/neo4j v0.35.0
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/containerd/containerd v1.7.18 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/docker v27.1.1+incompatible // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.17.4 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.5.0 // indirect
+ github.com/moby/sys/user v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/shirou/gopsutil/v3 v3.23.12 // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/testcontainers/testcontainers-go v0.35.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/yusufpapurcu/wmi v1.2.3 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.24.0 // indirect
+ go.opentelemetry.io/otel/trace v1.24.0 // indirect
+ golang.org/x/crypto v0.31.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/neo4j/go.sum b/neo4j/go.sum
new file mode 100644
index 00000000..e59fbc87
--- /dev/null
+++ b/neo4j/go.sum
@@ -0,0 +1,196 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
+github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
+github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
+github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
+github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
+github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/neo4j/neo4j-go-driver/v5 v5.27.0 h1:YdsIxDjAQbjlP/4Ha9B/gF8Y39UdgdTwCyihSxy8qTw=
+github.com/neo4j/neo4j-go-driver/v5 v5.27.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
+github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
+github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
+github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
+github.com/testcontainers/testcontainers-go/modules/neo4j v0.35.0 h1:6ib9TgYgvpWSrxutvPDN1rCIACNmce39j3a3rPpfwKo=
+github.com/testcontainers/testcontainers-go/modules/neo4j v0.35.0/go.mod h1:rG86zSleWupj1x3nb1Pb4pMvanBybAkGCcQ5xQQJWP4=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
+github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
+go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
+google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
diff --git a/neo4j/neo4j.go b/neo4j/neo4j.go
new file mode 100644
index 00000000..9c0c47c3
--- /dev/null
+++ b/neo4j/neo4j.go
@@ -0,0 +1,216 @@
+package neo4j
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j"
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j/auth"
+ "github.com/neo4j/neo4j-go-driver/v5/neo4j/config"
+)
+
+// Storage interface that is implemented by storage providers
+type Storage struct {
+ db neo4j.DriverWithContext
+ gcInterval time.Duration
+ done chan struct{}
+
+ cypherMatch string
+ cypherMerge string
+ cypherDelete string
+ cypherReset string
+ cypherGC string
+}
+
+type model struct {
+ Key string `json:"k"`
+ Val []byte `json:"v"`
+ Exp int64 `json:"e"`
+}
+
+type neo4jConnConfig struct {
+ URI string
+ Auth auth.TokenManager
+ Configurations []func(*config.Config)
+}
+
+func newDriverWithContext(cfg neo4jConnConfig) (neo4j.DriverWithContext, error) {
+ return neo4j.NewDriverWithContext(cfg.URI, cfg.Auth, cfg.Configurations...)
+}
+
+// New creates a new storage
+func New(config ...Config) *Storage {
+ // Set default config
+ cfg := configDefault(config...)
+
+ // Select db connection
+ var err error
+ db := cfg.DB
+ if db == nil {
+ db, err = newDriverWithContext(neo4jConnConfig{
+ URI: cfg.URI,
+ Auth: cfg.Auth,
+ Configurations: cfg.Configurations,
+ })
+ if err != nil {
+ log.Panicf("Unable to create connection pool: %v\n", err)
+ }
+ }
+
+ ctx := context.Background()
+
+ if err := db.VerifyConnectivity(ctx); err != nil {
+ log.Panicf("Unable to verify connection: %v\n", err)
+ }
+
+ // delete all nodes if reset set to true
+ if cfg.Reset {
+ if _, err := neo4j.ExecuteQuery(ctx, db, fmt.Sprintf("MATCH (n:%s) DELETE n FINISH", cfg.Node), nil, neo4j.EagerResultTransformer); err != nil {
+ db.Close(ctx)
+ log.Panicf("Unable to reset storage: %v\n", err)
+ }
+ }
+
+ // create index on key
+ if _, err := neo4j.ExecuteQuery(ctx, db, fmt.Sprintf("CREATE INDEX neo4jstore_key_idx IF NOT EXISTS FOR (n:%s) ON (n.k)", cfg.Node), nil, neo4j.EagerResultTransformer); err != nil {
+ db.Close(ctx)
+ log.Panicf("Unable to create index on key: %v\n", err)
+ }
+
+ store := &Storage{
+ db: db,
+ gcInterval: cfg.GCInterval,
+ done: make(chan struct{}),
+
+ cypherMatch: fmt.Sprintf("OPTIONAL MATCH (n:%s{ k: $key }) RETURN n { .* } AS data", cfg.Node),
+ cypherMerge: fmt.Sprintf("MERGE (n:%s{ k: $key }) SET n.v = $val, n.e = $exp FINISH", cfg.Node),
+ cypherDelete: fmt.Sprintf("MATCH (n:%s{ k: $key }) DELETE n FINISH", cfg.Node),
+ cypherReset: fmt.Sprintf("MATCH (n:%s) DELETE n FINISH", cfg.Node),
+ cypherGC: fmt.Sprintf("MATCH (n:%s) WHERE n.e <= $exp AND n.e != 0 DELETE n FINISH", cfg.Node),
+ }
+
+ go store.gcTicker()
+
+ return store
+}
+
+func (s *Storage) Get(key string) ([]byte, error) {
+ if len(key) <= 0 {
+ return nil, nil
+ }
+
+ ctx := context.Background()
+
+ res, err := neo4j.ExecuteQuery(
+ ctx, s.db, s.cypherMatch,
+ map[string]any{"key": key},
+ neo4j.EagerResultTransformer,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ data, _, _ := neo4j.GetRecordValue[map[string]any](res.Records[0], "data")
+
+ if data == nil {
+ return nil, nil
+ }
+
+ // result model
+ var model model
+ bt, _ := json.Marshal(data)
+
+ if err := json.Unmarshal(bt, &model); err != nil {
+ return nil, fmt.Errorf("error parsing result data: %v", err)
+ }
+
+ // If the expiration time has already passed, then return nil
+ if model.Exp != 0 && model.Exp <= time.Now().Unix() {
+ return nil, nil
+ }
+
+ return model.Val, nil
+}
+
+// Set key with value
+func (s *Storage) Set(key string, val []byte, exp time.Duration) error {
+ if len(key) <= 0 || len(val) <= 0 {
+ return nil
+ }
+ var expireAt int64
+ if exp != 0 {
+ expireAt = time.Now().Add(exp).Unix()
+ }
+
+ // create the structure for the storage
+ data := model{
+ Key: key,
+ Val: val,
+ Exp: expireAt,
+ }
+
+ ctx := context.Background()
+
+ _, err := neo4j.ExecuteQuery(
+ ctx, s.db, s.cypherMerge,
+ map[string]any{"key": data.Key, "val": data.Val, "exp": data.Exp},
+ neo4j.EagerResultTransformer,
+ )
+
+ return err
+}
+
+// Delete value by key
+func (s *Storage) Delete(key string) error {
+ if len(key) <= 0 {
+ return nil
+ }
+
+ _, err := neo4j.ExecuteQuery(context.Background(), s.db, s.cypherDelete, map[string]any{"key": key}, neo4j.EagerResultTransformer)
+ return err
+}
+
+// Reset all keys. Remove all nodes
+func (s *Storage) Reset() error {
+ _, err := neo4j.ExecuteQuery(
+ context.Background(), s.db, s.cypherReset,
+ nil,
+ neo4j.EagerResultTransformer,
+ )
+
+ return err
+}
+
+// Close the database
+func (s *Storage) Close() error {
+ s.done <- struct{}{}
+
+ return s.db.Close(context.Background())
+}
+
+// Return database client
+func (s *Storage) Conn() neo4j.DriverWithContext {
+ return s.db
+}
+
+// gcTicker starts the gc ticker
+func (s *Storage) gcTicker() {
+ ticker := time.NewTicker(s.gcInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-s.done:
+ return
+ case t := <-ticker.C:
+ s.gc(t)
+ }
+ }
+}
+
+// gc deletes all expired entries
+func (s *Storage) gc(t time.Time) {
+ _, _ = neo4j.ExecuteQuery(context.Background(), s.db, s.cypherGC, map[string]any{"exp": t.Unix()}, neo4j.EagerResultTransformer)
+}
diff --git a/neo4j/neo4j_test.go b/neo4j/neo4j_test.go
new file mode 100644
index 00000000..7a6548bf
--- /dev/null
+++ b/neo4j/neo4j_test.go
@@ -0,0 +1,219 @@
+package neo4j
+
+import (
+ "context"
+ "log"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "github.com/testcontainers/testcontainers-go/modules/neo4j"
+)
+
+var testStore *Storage
+
+// TestMain sets up and tears down the test container
+func TestMain(m *testing.M) {
+ ctx := context.Background()
+
+ // Start a Neo4j test container
+ neo4jContainer, err := neo4j.Run(ctx,
+ "neo4j:5.26",
+ neo4j.WithAdminPassword("pass#w*#d"),
+ )
+ if err != nil {
+ log.Fatalf("Failed to start Neo4j container: %v", err)
+ }
+
+ // Get the Bolt URI
+ uri, err := neo4jContainer.BoltUrl(ctx)
+ if err != nil {
+ log.Fatalf("Failed to get connection string: %v", err)
+ }
+
+ // Initialize Neo4j store with test container credentials
+ store := New(Config{
+ Reset: true,
+ URI: uri,
+ Username: "neo4j",
+ Password: "pass#w*#d",
+ })
+
+ testStore = store
+
+ defer testStore.Close()
+ defer func() {
+ if err := neo4jContainer.Terminate(ctx); err != nil {
+ log.Printf("Failed to terminate Neo4j container: %v", err)
+ }
+ }()
+
+ code := m.Run()
+
+ os.Exit(code)
+}
+
+func Test_Neo4jStore_Set(t *testing.T) {
+ var (
+ key = "john"
+ val = []byte("doe")
+ )
+
+ err := testStore.Set(key, val, 0)
+ require.NoError(t, err)
+}
+
+func Test_Neo4jStore_Upsert(t *testing.T) {
+ var (
+ key = "john"
+ val = []byte("doe")
+ )
+
+ err := testStore.Set(key, val, 0)
+ require.NoError(t, err)
+
+ err = testStore.Set(key, val, 0)
+ require.NoError(t, err)
+}
+
+func Test_Neo4jStore_Get(t *testing.T) {
+ var (
+ key = "john"
+ val = []byte("doe")
+ )
+
+ err := testStore.Set(key, val, 0)
+ require.NoError(t, err)
+
+ result, err := testStore.Get(key)
+ require.NoError(t, err)
+ require.Equal(t, val, result)
+}
+
+func Test_Neo4jStore_Set_Expiration(t *testing.T) {
+ var (
+ key = "john"
+ val = []byte("doe")
+ exp = 100 * time.Millisecond
+ )
+
+ err := testStore.Set(key, val, exp)
+ require.NoError(t, err)
+
+ time.Sleep(200 * time.Millisecond)
+
+ result, err := testStore.Get(key)
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+}
+
+func Test_Neo4jStore_Get_Expired(t *testing.T) {
+ key := "john"
+
+ result, err := testStore.Get(key)
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+}
+
+func Test_Neo4jStore_Get_NotExist(t *testing.T) {
+ result, err := testStore.Get("notexist")
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+}
+
+func Test_Neo4jStore_Delete(t *testing.T) {
+ var (
+ key = "john"
+ val = []byte("doe")
+ )
+
+ err := testStore.Set(key, val, 0)
+ require.NoError(t, err)
+
+ err = testStore.Delete(key)
+ require.NoError(t, err)
+
+ result, err := testStore.Get(key)
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+}
+
+func Test_Neo4jStore_Reset(t *testing.T) {
+ val := []byte("doe")
+
+ err := testStore.Set("john1", val, 0)
+ require.NoError(t, err)
+
+ err = testStore.Set("john2", val, 0)
+ require.NoError(t, err)
+
+ err = testStore.Reset()
+ require.NoError(t, err)
+
+ result, err := testStore.Get("john1")
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+
+ result, err = testStore.Get("john2")
+ require.NoError(t, err)
+ require.Zero(t, len(result))
+}
+
+func Test_Neo4jStore_Non_UTF8(t *testing.T) {
+ val := []byte("0xF5")
+
+ err := testStore.Set("0xF6", val, 0)
+ require.NoError(t, err)
+
+ result, err := testStore.Get("0xF6")
+ require.NoError(t, err)
+ require.Equal(t, val, result)
+}
+
+func Test_Neo4jStore_Close(t *testing.T) {
+ require.Nil(t, testStore.Close())
+}
+
+func Test_Neo4jStore_Conn(t *testing.T) {
+ require.True(t, testStore.Conn() != nil)
+}
+
+func Benchmark_Neo4jStore_Set(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ err = testStore.Set("john", []byte("doe"), 0)
+ }
+
+ require.NoError(b, err)
+}
+
+func Benchmark_Neo4jStore_Get(b *testing.B) {
+ err := testStore.Set("john", []byte("doe"), 0)
+ require.NoError(b, err)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ _, err = testStore.Get("john")
+ }
+
+ require.NoError(b, err)
+}
+
+func Benchmark_Neo4jStore_SetAndDelete(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ var err error
+ for i := 0; i < b.N; i++ {
+ _ = testStore.Set("john", []byte("doe"), 0)
+ err = testStore.Delete("john")
+ }
+
+ require.NoError(b, err)
+}
diff --git a/postgres/config.go b/postgres/config.go
index f78650fc..aecbfe97 100644
--- a/postgres/config.go
+++ b/postgres/config.go
@@ -133,9 +133,6 @@ func configDefault(config ...Config) Config {
if cfg.Table == "" {
cfg.Table = ConfigDefault.Table
}
- if cfg.Table == "" {
- cfg.Table = ConfigDefault.Table
- }
if int(cfg.GCInterval.Seconds()) <= 0 {
cfg.GCInterval = ConfigDefault.GCInterval
}
diff --git a/redis/README.md b/redis/README.md
index aae925bc..b0b4eed9 100644
--- a/redis/README.md
+++ b/redis/README.md
@@ -33,6 +33,12 @@ func (s *Storage) Keys() ([][]byte, error)
```
### Installation
Redis 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:
+
+> **Note:** You can also use [DragonflyDB](https://dragonflydb.io/) as a Redis replacement.
+> Since DragonflyDB is fully compatible with the Redis API, you can use it exactly like Redis **without any code changes**.
+> [Example](#example-using-dragonflydb)
+
+
```bash
go mod init github.com//
```
@@ -194,3 +200,7 @@ var ConfigDefault = Config{
SentinelPassword: "",
}
```
+
+### Example: Using DragonflyDB
+> **Note:** You can use [DragonflyDB](https://dragonflydb.io/) in the same way as Redis.
+> Simply start a DragonflyDB server and configure it just like Redis. Then, call `New()` and use it exactly as you would with Redis.
diff --git a/redis/redis.go b/redis/redis.go
index f9bdc16a..3969dc4d 100644
--- a/redis/redis.go
+++ b/redis/redis.go
@@ -13,7 +13,7 @@ type Storage struct {
db redis.UniversalClient
}
-// New creates a new redis storage
+// New creates a new Redis storage instance.
func New(config ...Config) *Storage {
// Set default config
cfg := configDefault(config...)