diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 715684d3a..f90cfdb29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,7 @@ docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=t docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5 docker run --name gofr-zipkin -d -p 2005:9411 openzipkin/zipkin:2 docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack +docker run --name cassandra-node -d -p 9042:9042 -v cassandra_data:/var/lib/cassandra cassandra:latest docker run --name gofr-pgsql -d -e POSTGRES_DB=customers -e POSTGRES_PASSWORD=root123 -p 2006:5432 postgres:15.1 docker run --name gofr-mssql -d -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=reallyStrongPwd123' -p 2007:1433 mcr.microsoft.com/azure-sql-edge docker run --name kafka-1 -p 9092:9092 \ diff --git a/docs/advanced-guide/handling-data-migrations/page.md b/docs/advanced-guide/handling-data-migrations/page.md index ecec8eca3..6fe3d9ebe 100644 --- a/docs/advanced-guide/handling-data-migrations/page.md +++ b/docs/advanced-guide/handling-data-migrations/page.md @@ -1,7 +1,7 @@ # Handling Data Migrations Suppose you manually make changes to your database, and now it's your responsibility to inform other developers to execute them. Additionally, you need to keep track of which changes should be applied to production machines in the next deployment. -GoFr supports data migrations for MySQL, Postgres and Redis which allows altering the state of a database, be it adding a new column to existing table or modifying the data type of existing column or adding constraints to an existing table, setting and removing keys etc. +GoFr supports data migrations for MySQL, Postgres, Redis, Clickhouse & Cassandra which allows altering the state of a database, be it adding a new column to existing table or modifying the data type of existing column or adding constraints to an existing table, setting and removing keys etc. ## Usage @@ -156,4 +156,78 @@ Where, **Method** : It contains the method(UP/DOWN) in which migration ran. (For now only method UP is supported) -> ##### Check out the example to add and run migrations in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-migrations/main.go) \ No newline at end of file +### Migrations in Cassandra + +`GoFr` provides support for migrations in Cassandra but does not guarantee atomicity for individual Data Manipulation Language (DML) commands. To achieve atomicity during migrations, users can leverage batch operations using the `NewBatch`, `BatchQuery`, and `ExecuteBatch` methods. These methods allow multiple queries to be executed as a single atomic operation. + +Alternatively, users can construct their batch queries using the `BEGIN BATCH` and `APPLY BATCH` statements to ensure that all the commands within the batch are executed successfully or not at all. This is particularly useful for complex migrations involving multiple inserts, updates, or schema changes in a single transaction-like operation. + +When using batch operations, consider using a `LoggedBatch` for atomicity or an `UnloggedBatch` for improved performance where atomicity isn't required. This approach provides a way to maintain data consistency during complex migrations. + +> Note: The following example assumes that user has already created the `KEYSPACE` in cassandra. A `KEYSPACE` in Cassandra is a container for tables that defines data replication settings across the cluster. + + +```go +package migrations + +import ( + "gofr.dev/pkg/gofr/migration" +) + +const ( + createTableCassandra = `CREATE TABLE IF NOT EXISTS employee ( + id int PRIMARY KEY, + name text, + gender text, + number text + );` + + addCassandraRecords = `BEGIN BATCH + INSERT INTO employee (id, name, gender, number) VALUES (1, 'Alison', 'F', '1234567980'); + INSERT INTO employee (id, name, gender, number) VALUES (2, 'Alice', 'F', '9876543210'); + APPLY BATCH; + ` + + employeeDataCassandra = `INSERT INTO employee (id, name, gender, number) VALUES (?, ?, ?, ?);` +) + +func createTableEmployeeCassandra() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + // Execute the create table statement + if err := d.Cassandra.Exec(createTableCassandra); err != nil { + return err + } + + // Batch processes can also be executed in Exec as follows: + if err := d.Cassandra.Exec(addCassandraRecords); err != nil { + return err + } + + // Create a new batch operation + batchName := "employeeBatch" + if err := d.Cassandra.NewBatch(batchName, 0); err != nil { // 0 for LoggedBatch + return err + } + + // Add multiple queries to the batch + if err := d.Cassandra.BatchQuery(batchName, employeeDataCassandra, 1, "Harry", "M", "1234567980"); err != nil { + return err + } + + if err := d.Cassandra.BatchQuery(batchName, employeeDataCassandra, 2, "John", "M", "9876543210"); err != nil { + return err + } + + // Execute the batch operation + if err := d.Cassandra.ExecuteBatch(batchName); err != nil { + return err + } + + return nil + }, + } +} +``` + +> ##### Check out the example to add and run migrations in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-migrations/main.go) diff --git a/docs/advanced-guide/injecting-databases-drivers/page.md b/docs/advanced-guide/injecting-databases-drivers/page.md index 7b42419ec..ed91cfe57 100644 --- a/docs/advanced-guide/injecting-databases-drivers/page.md +++ b/docs/advanced-guide/injecting-databases-drivers/page.md @@ -181,8 +181,6 @@ type Cassandra interface { BatchQuery(stmt string, values ...any) error - ExecuteBatch() error - NewBatch(name string, batchType int) error CassandraBatch @@ -214,7 +212,8 @@ type Person struct { ID int `json:"id,omitempty"` Name string `json:"name"` Age int `json:"age"` - State string `json:"state"` + // db tag specifies the actual column name in the database + State string `json:"state" db:"location"` } func main() { @@ -240,7 +239,7 @@ func main() { return nil, err } - err = c.Cassandra.Exec(`INSERT INTO persons(id, name, age, state) VALUES(?, ?, ?, ?)`, + err = c.Cassandra.Exec(`INSERT INTO persons(id, name, age, location) VALUES(?, ?, ?, ?)`, person.ID, person.Name, person.Age, person.State) if err != nil { return nil, err @@ -252,7 +251,7 @@ func main() { app.GET("/user", func(c *gofr.Context) (interface{}, error) { persons := make([]Person, 0) - err := c.Cassandra.Query(&persons, `SELECT id, name, age, state FROM persons`) + err := c.Cassandra.Query(&persons, `SELECT id, name, age, location FROM persons`) return persons, err }) @@ -260,6 +259,133 @@ func main() { app.Run() } ``` +## DGraph +GoFr supports injecting Dgraph with an interface that defines the necessary methods for interacting with the Dgraph +database. Any driver that implements the following interface can be added using the app.AddDgraph() method. + +```go +// Dgraph defines the methods for interacting with a Dgraph database. +type Dgraph interface { + // Query executes a read-only query in the Dgraph database and returns the result. + Query(ctx context.Context, query string) (interface{}, error) + + // QueryWithVars executes a read-only query with variables in the Dgraph database. + QueryWithVars(ctx context.Context, query string, vars map[string]string) (interface{}, error) + + // Mutate executes a write operation (mutation) in the Dgraph database and returns the result. + Mutate(ctx context.Context, mu interface{}) (interface{}, error) + + // Alter applies schema or other changes to the Dgraph database. + Alter(ctx context.Context, op interface{}) error + + // NewTxn creates a new transaction (read-write) for interacting with the Dgraph database. + NewTxn() interface{} + + // NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database. + NewReadOnlyTxn() interface{} + + // HealthChecker checks the health of the Dgraph instance. + HealthChecker +} +``` + +Users can easily inject a driver that supports this interface, allowing for flexibility without compromising usability. +This structure supports both queries and mutations in Dgraph. + +### Example + +```go +package main + +import ( + "encoding/json" + "fmt" + + "github.com/dgraph-io/dgo/v210/protos/api" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/datasource/dgraph" +) + +func main() { + // Create a new application + app := gofr.New() + + db := dgraph.New(dgraph.Config{ + Host: "localhost", + Port: "8080", + }) + + // Connect to Dgraph running on localhost:9080 + app.AddDgraph(db) + + // Add routes for Dgraph operations + app.POST("/dgraph", DGraphInsertHandler) + app.GET("/dgraph", DGraphQueryHandler) + + // Run the application + app.Run() +} + +// DGraphInsertHandler handles POST requests to insert data into Dgraph +func DGraphInsertHandler(c *gofr.Context) (interface{}, error) { + // Example mutation data to insert into Dgraph + mutationData := ` + { + "set": [ + { + "name": "GoFr Dev" + }, + { + "name": "James Doe" + } + ] + } + ` + + // Create an api.Mutation object + mutation := &api.Mutation{ + SetJson: []byte(mutationData), // Set the JSON payload + CommitNow: true, // Auto-commit the transaction + } + + // Run the mutation in Dgraph + response, err := c.DGraph.Mutate(c, mutation) + if err != nil { + return nil, err + } + + return response, nil +} + +// DGraphQueryHandler handles GET requests to fetch data from Dgraph +func DGraphQueryHandler(c *gofr.Context) (interface{}, error) { + // A simple query to fetch all persons with a name in Dgraph + response, err := c.DGraph.Query(c, "{ persons(func: has(name)) { uid name } }") + if err != nil { + return nil, err + } + + // Cast response to *api.Response (the correct type returned by Dgraph Query) + resp, ok := response.(*api.Response) + if !ok { + return nil, fmt.Errorf("unexpected response type") + } + + // Parse the response JSON + var result map[string]interface{} + err = json.Unmarshal(resp.Json, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +``` + + + ## Solr GoFr supports injecting Solr database that supports the following interface. Any driver that implements the interface can be added diff --git a/docs/references/context/page.md b/docs/references/context/page.md index 5c1a5dead..fb0da0925 100644 --- a/docs/references/context/page.md +++ b/docs/references/context/page.md @@ -50,6 +50,24 @@ parts of the request. ctx.Bind(&p) // the Bind() method will map the incoming request to variable p ``` + +- `Binding multipart-form data` + - To bind multipart-form data, you can use the Bind method similarly. The struct fields should be tagged appropriately + to map the form fields to the struct fields. + + ```go + type Data struct { + Name string `form:"name"` + + Compressed file.Zip `file:"upload"` + + FileHeader *multipart.FileHeader `file:"file_upload"` + } + ``` + + - The `form` tag is used to bind non-file fields. + - The `file` tag is used to bind file fields. If the tag is not present, the field name is used as the key. + - `HostName()` - to access the host name for the incoming request ```go diff --git a/examples/using-file-bind/README.md b/examples/using-file-bind/README.md index ef74f96a5..0b10006b8 100644 --- a/examples/using-file-bind/README.md +++ b/examples/using-file-bind/README.md @@ -8,7 +8,7 @@ it to the fields of the struct. GoFr currently supports zip file type and also b type Data struct { Compressed file.Zip `file:"upload"` - FileHeader *multipart.FileHeader `file:"a"` + FileHeader *multipart.FileHeader `file:"file_upload"` } func Handler (c *gofr.Context) (interface{}, error) { diff --git a/examples/using-file-bind/main.go b/examples/using-file-bind/main.go index aa813504e..9ff76507b 100644 --- a/examples/using-file-bind/main.go +++ b/examples/using-file-bind/main.go @@ -30,7 +30,7 @@ type Data struct { // The FileHeader determines the generic file format that we can get // from the multipart form that gets parsed by the incoming HTTP request - FileHeader *multipart.FileHeader `file:"a"` + FileHeader *multipart.FileHeader `file:"file_upload"` } func UploadHandler(c *gofr.Context) (interface{}, error) { diff --git a/examples/using-file-bind/main_test.go b/examples/using-file-bind/main_test.go index 50a385a74..839e3277f 100644 --- a/examples/using-file-bind/main_test.go +++ b/examples/using-file-bind/main_test.go @@ -56,7 +56,7 @@ func generateMultiPartBody(t *testing.T) (*bytes.Buffer, string) { t.Fatalf("Failed to write file to form: %v", err) } - fileHeader, err := writer.CreateFormFile("a", "hello.txt") + fileHeader, err := writer.CreateFormFile("file_upload", "hello.txt") if err != nil { t.Fatalf("Failed to create form file: %v", err) } diff --git a/go.mod b/go.mod index 65e311451..1efab4ae8 100644 --- a/go.mod +++ b/go.mod @@ -24,25 +24,25 @@ require ( github.com/redis/go-redis/v9 v9.6.1 github.com/segmentio/kafka-go v0.4.47 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 - go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 + go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 - go.opentelemetry.io/otel/exporters/prometheus v0.51.0 - go.opentelemetry.io/otel/exporters/zipkin v1.29.0 - go.opentelemetry.io/otel/metric v1.29.0 - go.opentelemetry.io/otel/sdk v1.29.0 - go.opentelemetry.io/otel/sdk/metric v1.29.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/otel/exporters/prometheus v0.52.0 + go.opentelemetry.io/otel/exporters/zipkin v1.30.0 + go.opentelemetry.io/otel/metric v1.30.0 + go.opentelemetry.io/otel/sdk v1.30.0 + go.opentelemetry.io/otel/sdk/metric v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 go.uber.org/mock v0.4.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 golang.org/x/term v0.24.0 golang.org/x/text v0.18.0 - google.golang.org/api v0.195.0 - google.golang.org/grpc v1.66.1 + google.golang.org/api v0.197.0 + google.golang.org/grpc v1.66.2 google.golang.org/protobuf v1.34.2 - modernc.org/sqlite v1.33.0 + modernc.org/sqlite v1.33.1 ) require ( @@ -65,7 +65,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/s2a-go v0.1.8 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -77,7 +77,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -88,8 +88,8 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/time v0.6.0 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect @@ -97,6 +97,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/strutil v1.2.0 // indirect diff --git a/go.sum b/go.sum index 205761380..5d77c7ce2 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0= -github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -170,8 +170,8 @@ github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/j github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= @@ -220,28 +220,28 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= -go.opentelemetry.io/otel/exporters/prometheus v0.51.0 h1:G7uexXb/K3T+T9fNLCCKncweEtNEBMTO+46hKX5EdKw= -go.opentelemetry.io/otel/exporters/prometheus v0.51.0/go.mod h1:v0mFe5Kk7woIh938mrZBJBmENYquyA0IICrlYm4Y0t4= -go.opentelemetry.io/otel/exporters/zipkin v1.29.0 h1:rqaUJdM9ItWf6DGrelaShXnJpb8rd3HTbcZWptvcsWA= -go.opentelemetry.io/otel/exporters/zipkin v1.29.0/go.mod h1:wDIyU6DjrUYqUgnmzjWnh1HOQGZCJ6YXMIJCdMc+T9Y= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/exporters/prometheus v0.52.0 h1:kmU3H0b9ufFSi8IQCcxack+sWUblKkFbqWYs6YiACGQ= +go.opentelemetry.io/otel/exporters/prometheus v0.52.0/go.mod h1:+wsAp2+JhuGXX7YRkjlkx6hyWY3ogFPfNA4x3nyiAh0= +go.opentelemetry.io/otel/exporters/zipkin v1.30.0 h1:1uYaSfxiCLdJATlGEtYjQe4jZYfqCjVwxeSTMXe8VF4= +go.opentelemetry.io/otel/exporters/zipkin v1.30.0/go.mod h1:r/4BhMc3kiKxD61wGh9J3NVQ3/cZ45F2NHkQgVnql48= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= +go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -257,8 +257,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -284,8 +284,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -349,8 +349,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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/api v0.195.0 h1:Ude4N8FvTKnnQJHU48RFI40jOBgIrL8Zqr3/QeST6yU= -google.golang.org/api v0.195.0/go.mod h1:DOGRWuv3P8TU8Lnz7uQc4hyNqrBpMtD9ppW3wBJurgc= +google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ= +google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -369,8 +369,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= -google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -398,16 +398,28 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= -modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index ad933fd0c..abd611a3b 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -45,6 +45,7 @@ type Container struct { Clickhouse Clickhouse Mongo Mongo Solr Solr + DGraph Dgraph KVStore KVStore diff --git a/pkg/gofr/container/datasources.go b/pkg/gofr/container/datasources.go index 8ba60a943..aabdb5c42 100644 --- a/pkg/gofr/container/datasources.go +++ b/pkg/gofr/container/datasources.go @@ -13,15 +13,15 @@ import ( //go:generate go run go.uber.org/mock/mockgen -source=datasources.go -destination=mock_datasources.go -package=container type DB interface { - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row + Query(query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row - Exec(query string, args ...interface{}) (sql.Result, error) - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) Prepare(query string) (*sql.Stmt, error) Begin() (*gofrSQL.Tx, error) - Select(ctx context.Context, data interface{}, query string, args ...interface{}) + Select(ctx context.Context, data any, query string, args ...any) HealthCheck() *datasource.Health Dialect() string Close() error @@ -171,43 +171,43 @@ type ClickhouseProvider interface { type Mongo interface { // Find executes a query to find documents in a collection based on a filter and stores the results // into the provided results interface. - Find(ctx context.Context, collection string, filter interface{}, results interface{}) error + Find(ctx context.Context, collection string, filter any, results any) error // FindOne executes a query to find a single document in a collection based on a filter and stores the result // into the provided result interface. - FindOne(ctx context.Context, collection string, filter interface{}, result interface{}) error + FindOne(ctx context.Context, collection string, filter any, result any) error // InsertOne inserts a single document into a collection. // It returns the identifier of the inserted document and an error, if any. - InsertOne(ctx context.Context, collection string, document interface{}) (interface{}, error) + InsertOne(ctx context.Context, collection string, document any) (any, error) // InsertMany inserts multiple documents into a collection. // It returns the identifiers of the inserted documents and an error, if any. - InsertMany(ctx context.Context, collection string, documents []interface{}) ([]interface{}, error) + InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) // DeleteOne deletes a single document from a collection based on a filter. // It returns the number of documents deleted and an error, if any. - DeleteOne(ctx context.Context, collection string, filter interface{}) (int64, error) + DeleteOne(ctx context.Context, collection string, filter any) (int64, error) // DeleteMany deletes multiple documents from a collection based on a filter. // It returns the number of documents deleted and an error, if any. - DeleteMany(ctx context.Context, collection string, filter interface{}) (int64, error) + DeleteMany(ctx context.Context, collection string, filter any) (int64, error) // UpdateByID updates a document in a collection by its ID. // It returns the number of documents updated and an error if any. - UpdateByID(ctx context.Context, collection string, id interface{}, update interface{}) (int64, error) + UpdateByID(ctx context.Context, collection string, id any, update any) (int64, error) // UpdateOne updates a single document in a collection based on a filter. // It returns an error if any. - UpdateOne(ctx context.Context, collection string, filter interface{}, update interface{}) error + UpdateOne(ctx context.Context, collection string, filter any, update any) error // UpdateMany updates multiple documents in a collection based on a filter. // It returns the number of documents updated and an error if any. - UpdateMany(ctx context.Context, collection string, filter interface{}, update interface{}) (int64, error) + UpdateMany(ctx context.Context, collection string, filter any, update any) (int64, error) // CountDocuments counts the number of documents in a collection based on a filter. // It returns the count and an error if any. - CountDocuments(ctx context.Context, collection string, filter interface{}) (int64, error) + CountDocuments(ctx context.Context, collection string, filter any) (int64, error) // Drop an entire collection from the database. // It returns an error if any. @@ -217,7 +217,7 @@ type Mongo interface { CreateCollection(ctx context.Context, name string) error // StartSession starts a session and provide methods to run commands in a transaction. - StartSession() (interface{}, error) + StartSession() (any, error) HealthChecker } @@ -239,10 +239,10 @@ type MongoProvider interface { type provider interface { // UseLogger sets the logger for the Cassandra client. - UseLogger(logger interface{}) + UseLogger(logger any) // UseMetrics sets the metrics for the Cassandra client. - UseMetrics(metrics interface{}) + UseMetrics(metrics any) // Connect establishes a connection to Cassandra and registers metrics using the provided configuration when the client was Created. Connect() @@ -288,3 +288,63 @@ type SolrProvider interface { provider } + +// Dgraph defines the methods for interacting with a Dgraph database. +type Dgraph interface { + // Query executes a read-only query in the Dgraph database and returns the result. + // Parameters: + // - ctx: The context for the query, used for controlling timeouts, cancellation, etc. + // - query: The Dgraph query string in GraphQL+- format. + // Returns: + // - any: The result of the query, usually of type *api.Response. + // - error: An error if the query execution fails. + Query(ctx context.Context, query string) (any, error) + + // QueryWithVars executes a read-only query with variables in the Dgraph database. + // Parameters: + // - ctx: The context for the query. + // - query: The Dgraph query string in GraphQL+- format. + // - vars: A map of variables to be used within the query. + // Returns: + // - any: The result of the query with variables, usually of type *api.Response. + // - error: An error if the query execution fails. + QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) + + // Mutate executes a write operation (mutation) in the Dgraph database and returns the result. + // Parameters: + // - ctx: The context for the mutation. + // - mu: The mutation operation, usually of type *api.Mutation. + // Returns: + // - any: The result of the mutation, usually of type *api.Assigned. + // - error: An error if the mutation execution fails. + Mutate(ctx context.Context, mu any) (any, error) + + // Alter applies schema or other changes to the Dgraph database. + // Parameters: + // - ctx: The context for the alter operation. + // - op: The alter operation, usually of type *api.Operation. + // Returns: + // - error: An error if the operation fails. + Alter(ctx context.Context, op any) error + + // NewTxn creates a new transaction (read-write) for interacting with the Dgraph database. + // Returns: + // - any: A new transaction, usually of type *api.Txn. + NewTxn() any + + // NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database. + // Returns: + // - any: A new read-only transaction, usually of type *api.Txn. + NewReadOnlyTxn() any + + // HealthChecker checks the health of the Dgraph instance, ensuring it is up and running. + // Returns: + // - error: An error if the health check fails. + HealthChecker +} + +// DgraphProvider extends Dgraph with connection management capabilities. +type DgraphProvider interface { + Dgraph + provider +} diff --git a/pkg/gofr/container/health.go b/pkg/gofr/container/health.go index 307a24eec..dbed41f5a 100644 --- a/pkg/gofr/container/health.go +++ b/pkg/gofr/container/health.go @@ -57,40 +57,25 @@ func (c *Container) Health(ctx context.Context) interface{} { } func checkExternalDBHealth(ctx context.Context, c *Container, healthMap map[string]interface{}) (downCount int) { - if !isNil(c.Mongo) { - health, err := c.Mongo.HealthCheck(ctx) - if err != nil { - downCount++ - } - - healthMap["mongo"] = health - } - - if !isNil(c.Cassandra) { - health, err := c.Cassandra.HealthCheck(ctx) - if err != nil { - downCount++ - } - - healthMap["cassandra"] = health + services := map[string]interface { + HealthCheck(context.Context) (interface{}, error) + }{ + "mongo": c.Mongo, + "cassandra": c.Cassandra, + "clickHouse": c.Clickhouse, + "kv-store": c.KVStore, + "dgraph": c.DGraph, } - if !isNil(c.Clickhouse) { - health, err := c.Clickhouse.HealthCheck(ctx) - if err != nil { - downCount++ - } + for name, service := range services { + if !isNil(service) { + health, err := service.HealthCheck(ctx) + if err != nil { + downCount++ + } - healthMap["clickHouse"] = health - } - - if !isNil(c.KVStore) { - health, err := c.KVStore.HealthCheck(ctx) - if err != nil { - downCount++ + healthMap[name] = health } - - healthMap["kv-store"] = health } return downCount diff --git a/pkg/gofr/container/health_test.go b/pkg/gofr/container/health_test.go index 8e84afad9..34ba0bda1 100644 --- a/pkg/gofr/container/health_test.go +++ b/pkg/gofr/container/health_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "gofr.dev/pkg/gofr/datasource" "gofr.dev/pkg/gofr/datasource/sql" "gofr.dev/pkg/gofr/logging" @@ -74,6 +73,12 @@ func TestContainer_Health(t *testing.T) { }, }, }, + "dgraph": datasource.Health{ + Status: tc.datasourceHealth, Details: map[string]interface{}{ + "host": "localhost:8000", + "error": "dgraph not connected", + }, + }, "test-service": &service.Health{ Status: "UP", Details: map[string]interface{}{ "host": strings.TrimPrefix(srv.URL, "http://"), @@ -104,7 +109,7 @@ func TestContainer_Health(t *testing.T) { } } -func registerMocks(mocks Mocks, health string) { +func registerMocks(mocks *Mocks, health string) { mocks.SQL.ExpectHealthCheck().WillReturnHealthCheck(&datasource.Health{ Status: health, Details: map[string]interface{}{ @@ -155,4 +160,12 @@ func registerMocks(mocks Mocks, health string) { "error": "kv-store not connected", }, }, nil) + + mocks.DGraph.EXPECT().HealthCheck(context.Background()).Return(datasource.Health{ + Status: health, + Details: map[string]interface{}{ + "host": "localhost:8000", + "error": "dgraph not connected", + }, + }, nil) } diff --git a/pkg/gofr/container/mock_container.go b/pkg/gofr/container/mock_container.go index 104f927d7..8875ab594 100644 --- a/pkg/gofr/container/mock_container.go +++ b/pkg/gofr/container/mock_container.go @@ -23,8 +23,10 @@ type Mocks struct { Cassandra *MockCassandra Mongo *MockMongo KVStore *MockKVStore + DGraph *MockDgraph File *file.MockFileSystemProvider HTTPService *service.MockHTTP + Metrics *MockMetrics } type options func(c *Container, ctrl *gomock.Controller) any @@ -41,7 +43,7 @@ func WithMockHTTPService(httpServiceNames ...string) options { } } -func NewMockContainer(t *testing.T, options ...options) (*Container, Mocks) { +func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) { t.Helper() container := &Container{} @@ -78,6 +80,9 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, Mocks) { fileStoreMock := file.NewMockFileSystemProvider(ctrl) container.File = fileStoreMock + dgraphMock := NewMockDgraph(ctrl) + container.DGraph = dgraphMock + var httpMock *service.MockHTTP container.Services = make(map[string]service.HTTP) @@ -91,6 +96,11 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, Mocks) { } } + redisMock.EXPECT().Close().AnyTimes() + + mockMetrics := NewMockMetrics(ctrl) + container.metricsManager = mockMetrics + mocks := Mocks{ Redis: redisMock, SQL: sqlMockWrapper, @@ -100,17 +110,14 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, Mocks) { KVStore: kvStoreMock, File: fileStoreMock, HTTPService: httpMock, + DGraph: dgraphMock, + Metrics: mockMetrics, } - redisMock.EXPECT().Close().AnyTimes() - - mockMetrics := NewMockMetrics(ctrl) - container.metricsManager = mockMetrics - mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", gomock.Any(), "method", gomock.Any(), "status", fmt.Sprintf("%v", http.StatusInternalServerError)).AnyTimes() - return container, mocks + return container, &mocks } type MockPubSub struct { diff --git a/pkg/gofr/container/mock_datasources.go b/pkg/gofr/container/mock_datasources.go index efc416b53..b933a86a6 100644 --- a/pkg/gofr/container/mock_datasources.go +++ b/pkg/gofr/container/mock_datasources.go @@ -8869,3 +8869,289 @@ func (mr *MockKVStoreProviderMockRecorder) UseMetrics(metrics any) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockKVStoreProvider)(nil).UseMetrics), metrics) } + +// MockDgraph is a mock of Dgraph interface. +type MockDgraph struct { + ctrl *gomock.Controller + recorder *MockDgraphMockRecorder +} + +// MockDgraphMockRecorder is the mock recorder for MockDgraph. +type MockDgraphMockRecorder struct { + mock *MockDgraph +} + +// NewMockDgraph creates a new mock instance. +func NewMockDgraph(ctrl *gomock.Controller) *MockDgraph { + mock := &MockDgraph{ctrl: ctrl} + mock.recorder = &MockDgraphMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDgraph) EXPECT() *MockDgraphMockRecorder { + return m.recorder +} + +// Alter mocks base method. +func (m *MockDgraph) Alter(ctx context.Context, op any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Alter", ctx, op) + ret0, _ := ret[0].(error) + return ret0 +} + +// Alter indicates an expected call of Alter. +func (mr *MockDgraphMockRecorder) Alter(ctx, op any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraph)(nil).Alter), ctx, op) +} + +// HealthCheck mocks base method. +func (m *MockDgraph) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockDgraphMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDgraph)(nil).HealthCheck), arg0) +} + +// Mutate mocks base method. +func (m *MockDgraph) Mutate(ctx context.Context, mu any) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mutate", ctx, mu) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Mutate indicates an expected call of Mutate. +func (mr *MockDgraphMockRecorder) Mutate(ctx, mu any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockDgraph)(nil).Mutate), ctx, mu) +} + +// NewReadOnlyTxn mocks base method. +func (m *MockDgraph) NewReadOnlyTxn() any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewReadOnlyTxn") + ret0, _ := ret[0].(any) + return ret0 +} + +// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn. +func (mr *MockDgraphMockRecorder) NewReadOnlyTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraph)(nil).NewReadOnlyTxn)) +} + +// NewTxn mocks base method. +func (m *MockDgraph) NewTxn() any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTxn") + ret0, _ := ret[0].(any) + return ret0 +} + +// NewTxn indicates an expected call of NewTxn. +func (mr *MockDgraphMockRecorder) NewTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraph)(nil).NewTxn)) +} + +// Query mocks base method. +func (m *MockDgraph) Query(ctx context.Context, query string) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Query", ctx, query) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockDgraphMockRecorder) Query(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDgraph)(nil).Query), ctx, query) +} + +// QueryWithVars mocks base method. +func (m *MockDgraph) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryWithVars", ctx, query, vars) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryWithVars indicates an expected call of QueryWithVars. +func (mr *MockDgraphMockRecorder) QueryWithVars(ctx, query, vars any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockDgraph)(nil).QueryWithVars), ctx, query, vars) +} + +// MockDgraphProvider is a mock of DgraphProvider interface. +type MockDgraphProvider struct { + ctrl *gomock.Controller + recorder *MockDgraphProviderMockRecorder +} + +// MockDgraphProviderMockRecorder is the mock recorder for MockDgraphProvider. +type MockDgraphProviderMockRecorder struct { + mock *MockDgraphProvider +} + +// NewMockDgraphProvider creates a new mock instance. +func NewMockDgraphProvider(ctrl *gomock.Controller) *MockDgraphProvider { + mock := &MockDgraphProvider{ctrl: ctrl} + mock.recorder = &MockDgraphProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDgraphProvider) EXPECT() *MockDgraphProviderMockRecorder { + return m.recorder +} + +// Alter mocks base method. +func (m *MockDgraphProvider) Alter(ctx context.Context, op any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Alter", ctx, op) + ret0, _ := ret[0].(error) + return ret0 +} + +// Alter indicates an expected call of Alter. +func (mr *MockDgraphProviderMockRecorder) Alter(ctx, op any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraphProvider)(nil).Alter), ctx, op) +} + +// Connect mocks base method. +func (m *MockDgraphProvider) Connect() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Connect") +} + +// Connect indicates an expected call of Connect. +func (mr *MockDgraphProviderMockRecorder) Connect() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockDgraphProvider)(nil).Connect)) +} + +// HealthCheck mocks base method. +func (m *MockDgraphProvider) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockDgraphProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDgraphProvider)(nil).HealthCheck), arg0) +} + +// Mutate mocks base method. +func (m *MockDgraphProvider) Mutate(ctx context.Context, mu any) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mutate", ctx, mu) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Mutate indicates an expected call of Mutate. +func (mr *MockDgraphProviderMockRecorder) Mutate(ctx, mu any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockDgraphProvider)(nil).Mutate), ctx, mu) +} + +// NewReadOnlyTxn mocks base method. +func (m *MockDgraphProvider) NewReadOnlyTxn() any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewReadOnlyTxn") + ret0, _ := ret[0].(any) + return ret0 +} + +// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn. +func (mr *MockDgraphProviderMockRecorder) NewReadOnlyTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraphProvider)(nil).NewReadOnlyTxn)) +} + +// NewTxn mocks base method. +func (m *MockDgraphProvider) NewTxn() any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTxn") + ret0, _ := ret[0].(any) + return ret0 +} + +// NewTxn indicates an expected call of NewTxn. +func (mr *MockDgraphProviderMockRecorder) NewTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraphProvider)(nil).NewTxn)) +} + +// Query mocks base method. +func (m *MockDgraphProvider) Query(ctx context.Context, query string) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Query", ctx, query) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockDgraphProviderMockRecorder) Query(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDgraphProvider)(nil).Query), ctx, query) +} + +// QueryWithVars mocks base method. +func (m *MockDgraphProvider) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryWithVars", ctx, query, vars) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryWithVars indicates an expected call of QueryWithVars. +func (mr *MockDgraphProviderMockRecorder) QueryWithVars(ctx, query, vars any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockDgraphProvider)(nil).QueryWithVars), ctx, query, vars) +} + +// UseLogger mocks base method. +func (m *MockDgraphProvider) UseLogger(logger any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseLogger", logger) +} + +// UseLogger indicates an expected call of UseLogger. +func (mr *MockDgraphProviderMockRecorder) UseLogger(logger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockDgraphProvider)(nil).UseLogger), logger) +} + +// UseMetrics mocks base method. +func (m *MockDgraphProvider) UseMetrics(metrics any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseMetrics", metrics) +} + +// UseMetrics indicates an expected call of UseMetrics. +func (mr *MockDgraphProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockDgraphProvider)(nil).UseMetrics), metrics) +} diff --git a/pkg/gofr/datasource/README.md b/pkg/gofr/datasource/README.md index 2912121f1..a2f9b7c9a 100644 --- a/pkg/gofr/datasource/README.md +++ b/pkg/gofr/datasource/README.md @@ -82,3 +82,6 @@ Therefore, GoFr utilizes a pluggable approach for new datasources by separating | FTP | | ✅ | | | ✅ | | SFTP | | ✅ | | | ✅ | | Solr | | ✅ | ✅ | | ✅ | +| DGraph | ✅ | ✅ |✅ ||| + + diff --git a/pkg/gofr/datasource/dgraph/dgraph_test.go b/pkg/gofr/datasource/dgraph/dgraph_test.go new file mode 100644 index 000000000..ad0b70e92 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/dgraph_test.go @@ -0,0 +1,262 @@ +package dgraph + +import ( + "context" + "errors" + "testing" + + "github.com/dgraph-io/dgo/v210/protos/api" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func setupDB(t *testing.T) (*Client, *MockDgraphClient, *MockLogger, *MockMetrics) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := Config{Host: "localhost", Port: "9080"} + client := New(config) + client.UseLogger(mockLogger) + client.UseMetrics(mockMetrics) + + mockDgraphClient := NewMockDgraphClient(ctrl) + client.client = mockDgraphClient + + return client, mockDgraphClient, mockLogger, mockMetrics +} + +func TestClient_Connect_Success(t *testing.T) { + client, _, mockLogger, mockMetrics := setupDB(t) + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(2) + + // Mock Metric behavior + mockMetrics.EXPECT().NewHistogram(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + // Perform the connect operation + client.Connect() + + require.True(t, mockLogger.ctrl.Satisfied()) + require.True(t, mockMetrics.ctrl.Satisfied()) +} + +func Test_Query_Success(t *testing.T) { + client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil) + + mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Debugf("dgraph query succeeded in %dµs", gomock.Any()) + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_query_duration", gomock.Any()) + + resp, err := client.Query(context.Background(), "my query") + + require.NoError(t, err, "Test_Query_Success Failed!") + require.NotNil(t, resp, "Test_Query_Success Failed!") + require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "success"}`)}, "Test_Query_Success Failed!") +} + +func Test_Query_Error(t *testing.T) { + client, mockDgraphClient, mockLogger, _ := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(nil, errors.New("query failed")) + + mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + mockLogger.EXPECT().Error("dgraph query failed: ", errors.New("query failed")) + + resp, err := client.Query(context.Background(), "my query") + + require.EqualError(t, err, "query failed", "Test_Query_Error Failed!") + require.Nil(t, resp, "Test_Query_Error Failed!") +} + +func Test_QueryWithVars_Success(t *testing.T) { + client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + query := "my query with vars" + vars := map[string]string{"$var": "value"} + + mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil) + + mockLogger.EXPECT().Debugf("dgraph queryWithVars succeeded in %dµs", gomock.Any()) + mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_query_with_vars_duration", gomock.Any()) + + // Call the QueryWithVars method + resp, err := client.QueryWithVars(context.Background(), query, vars) + + require.NoError(t, err, "Test_QueryWithVars_Success Failed!") + require.NotNil(t, resp, "Test_QueryWithVars_Success Failed!") + require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "success"}`)}, "Test_QueryWithVars_Success Failed!") +} + +func Test_QueryWithVars_Error(t *testing.T) { + client, mockDgraphClient, mockLogger, _ := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + query := "my query with vars" + vars := map[string]string{"$var": "value"} + + mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(nil, errors.New("query failed")) + + mockLogger.EXPECT().Error("dgraph queryWithVars failed: ", errors.New("query failed")) + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + + // Call the QueryWithVars method + resp, err := client.QueryWithVars(context.Background(), query, vars) + + require.EqualError(t, err, "query failed", "Test_QueryWithVars_Error Failed!") + require.Nil(t, resp, "Test_QueryWithVars_Error Failed!") +} + +func Test_Mutate_Success(t *testing.T) { + client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + mutation := &api.Mutation{CommitNow: true} + + mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(&api.Response{Json: []byte(`{"result": "mutation success"}`)}, nil) + + mockLogger.EXPECT().Debug("executing dgraph mutation") + mockLogger.EXPECT().Debugf("dgraph mutation succeeded in %dµs", gomock.Any()) + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_mutate_duration", gomock.Any()) + + // Call the Mutate method + resp, err := client.Mutate(context.Background(), mutation) + + require.NoError(t, err, "Test_Mutate_Success Failed!") + require.NotNil(t, resp, "Test_Mutate_Success Failed!") + require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "mutation success"}`)}, "Test_Mutate_Success Failed!") +} + +func Test_Mutate_InvalidMutation(t *testing.T) { + client, _, _, _ := setupDB(t) + + // Call the Mutate method with an invalid type + resp, err := client.Mutate(context.Background(), "invalid mutation") + + require.EqualError(t, err, errInvalidMutation.Error(), "Test_Mutate_InvalidMutation Failed!") + require.Nil(t, resp, "Test_Mutate_InvalidMutation Failed!") +} + +func Test_Mutate_Error(t *testing.T) { + client, mockDgraphClient, mockLogger, _ := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + mutation := &api.Mutation{CommitNow: true} + + mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(nil, errors.New("mutation failed")) + + mockLogger.EXPECT().Debug("executing dgraph mutation") + mockLogger.EXPECT().Error("dgraph mutation failed: ", errors.New("mutation failed")) + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + + // Call the Mutate method + resp, err := client.Mutate(context.Background(), mutation) + + require.EqualError(t, err, "mutation failed", "Test_Mutate_Error Failed!") + require.Nil(t, resp, "Test_Mutate_Error Failed!") +} + +func Test_Alter_Success(t *testing.T) { + client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t) + + op := &api.Operation{} + mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(nil) + + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any()) + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any()) + + err := client.Alter(context.Background(), op) + + require.NoError(t, err, "Test_Alter_Success Failed!") +} + +func Test_Alter_Error(t *testing.T) { + client, mockDgraphClient, mockLogger, _ := setupDB(t) + + op := &api.Operation{} + mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(errors.New("alter failed")) + + mockLogger.EXPECT().Log(gomock.Any()).Times(1) + mockLogger.EXPECT().Error("dgraph alter failed: ", errors.New("alter failed")) + + err := client.Alter(context.Background(), op) + + require.EqualError(t, err, "alter failed", "Test_Alter_Error Failed!") +} + +func Test_Alter_InvalidOperation(t *testing.T) { + client, _, mockLogger, _ := setupDB(t) + + op := "invalid operation" + mockLogger.EXPECT().Error("invalid operation type provided to alter") + + err := client.Alter(context.Background(), op) + + require.EqualError(t, err, errInvalidOperation.Error(), "Test_Alter_InvalidOperation Failed!") +} + +func Test_NewTxn(t *testing.T) { + client, mockDgraphClient, _, _ := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + txn := client.NewTxn() + + require.NotNil(t, txn, "Test_NewTxn Failed!") +} + +func Test_NewReadOnlyTxn(t *testing.T) { + client, mockDgraphClient, _, _ := setupDB(t) + + mockReadOnlyTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewReadOnlyTxn().Return(mockReadOnlyTxn) + + txn := client.NewReadOnlyTxn() + + require.NotNil(t, txn, "Test_NewReadOnlyTxn Failed!") +} + +func Test_HealthCheck_Error(t *testing.T) { + client, mockDgraphClient, mockLogger, _ := setupDB(t) + + mockTxn := NewMockTxn(mockDgraphClient.ctrl) + mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) + + mockLogger.EXPECT().Error("dgraph health check failed: ", errors.New("query failed")) + + mockQueryResponse := &api.Response{} + mockTxn.EXPECT().Query(gomock.Any(), gomock.Any()).Return(mockQueryResponse, errors.New("query failed")) + + _, err := client.HealthCheck(context.Background()) + + require.EqualError(t, err, errHealthCheckFailed.Error(), "Test_HealthCheck_Error Failed!") +} diff --git a/pkg/gofr/datasource/dgraph/draph.go b/pkg/gofr/datasource/dgraph/draph.go new file mode 100644 index 000000000..92097f5e4 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/draph.go @@ -0,0 +1,294 @@ +package dgraph + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/dgraph-io/dgo/v210" + "github.com/dgraph-io/dgo/v210/protos/api" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// Config holds the configuration for connecting to Dgraph. +type Config struct { + Host string + Port string +} + +// Client represents the Dgraph client with logging and metrics. +type Client struct { + client DgraphClient + conn *grpc.ClientConn + logger Logger + metrics Metrics + config Config + tracer trace.Tracer +} + +type ( + Mutation = api.Mutation + Operation = api.Operation +) + +var ( + errInvalidMutation = errors.New("invalid mutation type") + errInvalidOperation = errors.New("invalid operation type") + errHealthCheckFailed = errors.New("dgraph health check failed") +) + +// New creates a new Dgraph client with the given configuration. +func New(config Config) *Client { + return &Client{ + config: config, + tracer: otel.GetTracerProvider().Tracer("gofr-dgraph"), + } +} + +// Connect connects to the Dgraph database using the provided configuration. +func (d *Client) Connect() { + address := fmt.Sprintf("%s:%s", d.config.Host, d.config.Port) + d.logger.Logf("connecting to dgraph at %v", address) + + conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + d.logger.Errorf("error connecting to Dgraph, err: %v", err) + return + } + + d.logger.Logf("connected to dgraph client at %v:%v", d.config.Host, d.config.Port) + + // Register metrics + // Register all metrics + d.metrics.NewHistogram("dgraph_query_duration", "Response time of Dgraph queries in milliseconds.", + 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10) + d.metrics.NewHistogram("dgraph_query_with_vars_duration", "Response time of Dgraph queries with variables in milliseconds.", + 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10) + d.metrics.NewHistogram("dgraph_mutate_duration", "Response time of Dgraph mutations in milliseconds.", + 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10) + d.metrics.NewHistogram("dgraph_alter_duration", "Response time of Dgraph alter operations in milliseconds.", + 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10) + + dg := dgo.NewDgraphClient(api.NewDgraphClient(conn)) + d.client = NewDgraphClient(dg) + + // Check connection by performing a basic health check + if _, err := d.HealthCheck(context.Background()); err != nil { + d.logger.Errorf("dgraph health check failed: %v", err) + return + } +} + +// UseLogger sets the logger for the Dgraph client which asserts the Logger interface. +func (d *Client) UseLogger(logger any) { + if l, ok := logger.(Logger); ok { + d.logger = l + } +} + +// UseMetrics sets the metrics for the Dgraph client which asserts the Metrics interface. +func (d *Client) UseMetrics(metrics any) { + if m, ok := metrics.(Metrics); ok { + d.metrics = m + } +} + +// Query executes a read-only query in the Dgraph database and returns the result. +func (d *Client) Query(ctx context.Context, query string) (any, error) { + start := time.Now() + + ctx, span := d.tracer.Start(ctx, "dgraph-query") + defer span.End() + + // Execute query + resp, err := d.client.NewTxn().Query(ctx, query) + duration := time.Since(start).Microseconds() + + // Create and log the query details + ql := &QueryLog{ + Type: "query", + URL: query, + Duration: duration, + } + + span.SetAttributes( + attribute.String("dgraph.query.query", query), + attribute.Int64("dgraph.query.duration", duration), + ) + + if err != nil { + d.logger.Error("dgraph query failed: ", err) + ql.PrettyPrint(d.logger) + return nil, err + } + + d.sendOperationStats(ctx, ql, "dgraph_query_duration") + + return resp, nil +} + +// QueryWithVars executes a read-only query with variables in the Dgraph database. +// QueryWithVars executes a read-only query with variables in the Dgraph database. +func (d *Client) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) { + start := time.Now() + + ctx, span := d.tracer.Start(ctx, "dgraph-query-with-vars") + defer span.End() + + // Execute the query with variables + resp, err := d.client.NewTxn().QueryWithVars(ctx, query, vars) + duration := time.Since(start).Microseconds() + + // Create and log the query details + ql := &QueryLog{ + Type: "queryWithVars", + URL: fmt.Sprintf("Query: %s, Vars: %v", query, vars), + Duration: duration, + } + + span.SetAttributes( + attribute.String("dgraph.query.query", query), + attribute.String("dgraph.query.vars", fmt.Sprintf("%v", vars)), + attribute.Int64("dgraph.query.duration", duration), + ) + + if err != nil { + d.logger.Error("dgraph queryWithVars failed: ", err) + ql.PrettyPrint(d.logger) + return nil, err + } + + d.sendOperationStats(ctx, ql, "dgraph_query_with_vars_duration") + + return resp, nil +} + +// Mutate executes a write operation (mutation) in the Dgraph database and returns the result. +func (d *Client) Mutate(ctx context.Context, mu any) (any, error) { + start := time.Now() + + ctx, span := d.tracer.Start(ctx, "dgraph-mutate") + defer span.End() + + // Cast to proper mutation type + mutation, ok := mu.(*api.Mutation) + if !ok { + return nil, errInvalidMutation + } + + // Execute mutation + resp, err := d.client.NewTxn().Mutate(ctx, mutation) + duration := time.Since(start).Microseconds() + + // Create and log the mutation details + ql := &QueryLog{ + Type: "mutation", + URL: mutationToString(mutation), + Duration: duration, + } + + span.SetAttributes( + attribute.String("dgraph.mutation.query", mutationToString(mutation)), + attribute.Int64("dgraph.mutation.duration", duration), + ) + + if err != nil { + d.logger.Error("dgraph mutation failed: ", err) + ql.PrettyPrint(d.logger) + return nil, err + } + + d.sendOperationStats(ctx, ql, "dgraph_mutate_duration") + + return resp, nil +} + +// Alter applies schema or other changes to the Dgraph database. +func (d *Client) Alter(ctx context.Context, op any) error { + start := time.Now() + + ctx, span := d.tracer.Start(ctx, "dgraph-alter") + defer span.End() + + // Cast to proper operation type + operation, ok := op.(*api.Operation) + if !ok { + d.logger.Error("invalid operation type provided to alter") + return errInvalidOperation + } + + // Apply the schema changes + err := d.client.Alter(ctx, operation) + duration := time.Since(start).Microseconds() + + // Create and log the operation details + ql := &QueryLog{ + Type: "Alter", + URL: operation.String(), + Duration: duration, + } + + span.SetAttributes( + attribute.String("dgraph.alter.operation", operation.String()), + attribute.Int64("dgraph.alter.duration", duration), + ) + + if err != nil { + d.logger.Error("dgraph alter failed: ", err) + ql.PrettyPrint(d.logger) + return err + } + + d.sendOperationStats(ctx, ql, "dgraph_alter_duration") + + return nil +} + +// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database. +func (d *Client) NewTxn() any { + return d.client.NewTxn() +} + +// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database. +func (d *Client) NewReadOnlyTxn() any { + return d.client.NewReadOnlyTxn() +} + +// HealthCheck performs a basic health check by pinging the Dgraph server. +func (d *Client) HealthCheck(ctx context.Context) (any, error) { + healthResponse, err := d.client.NewTxn().Query(ctx, `{ + health(func: has(dgraph.type)) { + status + } + }`) + + if err != nil || len(healthResponse.Json) == 0 { + d.logger.Error("dgraph health check failed: ", err) + return "DOWN", errHealthCheckFailed + } + + return "UP", nil +} + +func (d *Client) sendOperationStats(ctx context.Context, query *QueryLog, metricName string) { + query.PrettyPrint(d.logger) + d.metrics.RecordHistogram(ctx, metricName, float64(query.Duration)) +} + +func mutationToString(mutation *api.Mutation) string { + var compacted bytes.Buffer + if err := json.Compact(&compacted, mutation.SetJson); err != nil { + return "" + } + + return compacted.String() + +} diff --git a/pkg/gofr/datasource/dgraph/go.mod b/pkg/gofr/datasource/dgraph/go.mod new file mode 100644 index 000000000..63bc4c837 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/go.mod @@ -0,0 +1,37 @@ +module gofr.dev/pkg/gofr/datasource/dgraph + +require ( + github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d + github.com/prometheus/client_golang v1.20.3 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 + go.uber.org/mock v0.4.0 + google.golang.org/grpc v1.58.3 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.22.7 diff --git a/pkg/gofr/datasource/dgraph/go.sum b/pkg/gofr/datasource/dgraph/go.sum new file mode 100644 index 000000000..84250a80e --- /dev/null +++ b/pkg/gofr/datasource/dgraph/go.sum @@ -0,0 +1,102 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d h1:abDbP7XBVgwda+h0J5Qra5p2OQpidU2FdkXvzCKL+H8= +github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d/go.mod h1:wKFzULXAPj3U2BDAPWXhSbQQNC6FU1+1/5iika6IY7g= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +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/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +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/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/dgraph/interfaces.go b/pkg/gofr/datasource/dgraph/interfaces.go new file mode 100644 index 000000000..9e35ca140 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/interfaces.go @@ -0,0 +1,158 @@ +package dgraph + +import ( + "context" + + "github.com/dgraph-io/dgo/v210" + "github.com/dgraph-io/dgo/v210/protos/api" +) + +// Txn is an interface for Dgraph transactions. +type Txn interface { + // BestEffort sets the transaction to best-effort mode. + BestEffort() Txn + + // Query executes a query against the transaction. + Query(ctx context.Context, q string) (*api.Response, error) + + // QueryRDF executes an RDF query against the transaction. + QueryRDF(ctx context.Context, q string) (*api.Response, error) + + // QueryWithVars executes a query with variables against the transaction. + QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) + + // QueryRDFWithVars executes an RDF query with variables against the transaction. + QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) + + // Mutate applies a mutation to the transaction. + Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) + + // Do performs a raw request against the transaction. + Do(ctx context.Context, req *api.Request) (*api.Response, error) + + // Commit commits the transaction. + Commit(ctx context.Context) error + + // Discard discards the transaction. + Discard(ctx context.Context) error +} + +// DgraphClient is an interface that defines the methods for interacting with Dgraph. +type DgraphClient interface { + // NewTxn creates a new transaction (read-write) for interacting with the Dgraph database. + NewTxn() Txn + + // NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database. + NewReadOnlyTxn() Txn + + // Alter applies schema or other changes to the Dgraph database. + Alter(ctx context.Context, op *api.Operation) error + + // Login logs in to the Dgraph database. + Login(ctx context.Context, userid string, password string) error + + // LoginIntoNamespace logs in to the Dgraph database with a specific namespace. + LoginIntoNamespace(ctx context.Context, userid string, password string, namespace uint64) error + + // GetJwt returns the JWT token for the Dgraph client. + GetJwt() api.Jwt + + // Relogin relogs in to the Dgraph database. + Relogin(ctx context.Context) error +} + +// dgraphClientImpl is a struct that implements the DgraphClient interface. +type dgraphClientImpl struct { + client *dgo.Dgraph +} + +// NewDgraphClient returns a new Dgraph client. +func NewDgraphClient(client *dgo.Dgraph) DgraphClient { + return &dgraphClientImpl{client: client} +} + +// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database. +func (d *dgraphClientImpl) NewTxn() Txn { + return &txnImpl{d.client.NewTxn()} +} + +// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database. +func (d *dgraphClientImpl) NewReadOnlyTxn() Txn { + return &txnImpl{d.client.NewReadOnlyTxn()} +} + +// Alter applies schema or other changes to the Dgraph database. +func (d *dgraphClientImpl) Alter(ctx context.Context, op *api.Operation) error { + return d.client.Alter(ctx, op) +} + +// Login logs in to the Dgraph database. +func (d *dgraphClientImpl) Login(ctx context.Context, userid string, password string) error { + return d.client.Login(ctx, userid, password) +} + +// LoginIntoNamespace logs in to the Dgraph database with a specific namespace. +func (d *dgraphClientImpl) LoginIntoNamespace(ctx context.Context, userid string, password string, namespace uint64) error { + return d.client.LoginIntoNamespace(ctx, userid, password, namespace) +} + +// GetJwt returns the JWT token for the Dgraph client. +func (d *dgraphClientImpl) GetJwt() api.Jwt { + return d.client.GetJwt() +} + +// Relogin relogs in to the Dgraph database. +func (d *dgraphClientImpl) Relogin(ctx context.Context) error { + return d.client.Relogin(ctx) +} + +// txnImpl is the struct that implements the Txn interface by wrapping *dgo.Txn. +type txnImpl struct { + txn *dgo.Txn +} + +// BestEffort sets the transaction to best-effort mode. +func (t *txnImpl) BestEffort() Txn { + t.txn.BestEffort() + return t +} + +// Query executes a query against the transaction. +func (t *txnImpl) Query(ctx context.Context, q string) (*api.Response, error) { + return t.txn.Query(ctx, q) +} + +// QueryRDF executes an RDF query against the transaction. +func (t *txnImpl) QueryRDF(ctx context.Context, q string) (*api.Response, error) { + return t.txn.QueryRDF(ctx, q) +} + +// QueryWithVars executes a query with variables against the transaction. +func (t *txnImpl) QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) { + return t.txn.QueryWithVars(ctx, q, vars) +} + +// QueryRDFWithVars executes an RDF query with variables against the transaction. +func (t *txnImpl) QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) { + return t.txn.QueryRDFWithVars(ctx, q, vars) +} + +// Mutate applies a mutation to the transaction. +func (t *txnImpl) Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) { + return t.txn.Mutate(ctx, mu) +} + +// Do performs a raw request against the transaction. +func (t *txnImpl) Do(ctx context.Context, req *api.Request) (*api.Response, error) { + return t.txn.Do(ctx, req) +} + +// Commit commits the transaction. +func (t *txnImpl) Commit(ctx context.Context) error { + return t.txn.Commit(ctx) +} + +// Discard discards the transaction. +func (t *txnImpl) Discard(ctx context.Context) error { + return t.txn.Discard(ctx) +} diff --git a/pkg/gofr/datasource/dgraph/logger.go b/pkg/gofr/datasource/dgraph/logger.go new file mode 100644 index 000000000..a8d5034e9 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/logger.go @@ -0,0 +1,42 @@ +package dgraph + +import ( + "fmt" + "regexp" + "strings" +) + +// Logger interface with required methods +type Logger interface { + Debug(args ...interface{}) + Debugf(pattern string, args ...interface{}) + Log(args ...interface{}) + Logf(pattern string, args ...interface{}) + Error(args ...interface{}) + Errorf(pattern string, args ...interface{}) +} + +// QueryLog represents the structure for query logging +type QueryLog struct { + Type string `json:"type"` + URL string `json:"url"` + Duration int64 `json:"duration"` // Duration in microseconds +} + +// PrettyPrint logs the QueryLog in a structured format to the given writer +func (ql *QueryLog) PrettyPrint(logger Logger) { + // Format the log string + formattedLog := fmt.Sprintf( + "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s", + clean(ql.Type), "DGRAPH", ql.Duration, clean(ql.URL), + ) + + // Log the formatted string using the logger + logger.Debug(formattedLog) +} + +// clean replaces multiple consecutive whitespace characters with a single space and trims leading/trailing whitespace +func clean(query string) string { + query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ") + return strings.TrimSpace(query) +} diff --git a/pkg/gofr/datasource/dgraph/logger_test.go b/pkg/gofr/datasource/dgraph/logger_test.go new file mode 100644 index 000000000..e94204d0e --- /dev/null +++ b/pkg/gofr/datasource/dgraph/logger_test.go @@ -0,0 +1,23 @@ +package dgraph + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_PrettyPrint(t *testing.T) { + queryLog := QueryLog{ + Type: "GET", + Duration: 12345, + } + + logger := NewMockLogger(gomock.NewController(t)) + + logger.EXPECT().Log(gomock.Any()) + + queryLog.PrettyPrint(logger) + + require.True(t, logger.ctrl.Satisfied(), "Test_PrettyPrint Failed!") +} diff --git a/pkg/gofr/datasource/dgraph/metrics.go b/pkg/gofr/datasource/dgraph/metrics.go new file mode 100644 index 000000000..5373baea6 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/metrics.go @@ -0,0 +1,41 @@ +package dgraph + +import ( + "context" + "github.com/prometheus/client_golang/prometheus" +) + +type Metrics interface { + NewHistogram(name, desc string, buckets ...float64) + + RecordHistogram(ctx context.Context, name string, value float64, labels ...string) +} + +type PrometheusMetrics struct { + histograms map[string]*prometheus.HistogramVec +} + +// NewHistogram creates a new histogram metric with the given name, description, and optional bucket sizes. +func (p *PrometheusMetrics) NewHistogram(name, desc string, buckets ...float64) { + histogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: name, + Help: desc, + Buckets: buckets, + }, + []string{}, // labels can be added here if needed + ) + p.histograms[name] = histogram + prometheus.MustRegister(histogram) +} + +// RecordHistogram records a value to the specified histogram metric with optional labels. +func (p *PrometheusMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { + histogram, exists := p.histograms[name] + if !exists { + // Handle error: histogram not found + return + } + + histogram.WithLabelValues(labels...).Observe(value) +} diff --git a/pkg/gofr/datasource/dgraph/mock_interfaces.go b/pkg/gofr/datasource/dgraph/mock_interfaces.go new file mode 100644 index 000000000..1f3250bcb --- /dev/null +++ b/pkg/gofr/datasource/dgraph/mock_interfaces.go @@ -0,0 +1,294 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces.go +// +// Generated by this command: +// +// mockgen -source=interfaces.go -destination=mock_interfaces.go -package=dgraph +// + +// Package dgraph is a generated GoMock package. +package dgraph + +import ( + context "context" + reflect "reflect" + + api "github.com/dgraph-io/dgo/v210/protos/api" + gomock "go.uber.org/mock/gomock" +) + +// MockTxn is a mock of Txn interface. +type MockTxn struct { + ctrl *gomock.Controller + recorder *MockTxnMockRecorder +} + +// MockTxnMockRecorder is the mock recorder for MockTxn. +type MockTxnMockRecorder struct { + mock *MockTxn +} + +// NewMockTxn creates a new mock instance. +func NewMockTxn(ctrl *gomock.Controller) *MockTxn { + mock := &MockTxn{ctrl: ctrl} + mock.recorder = &MockTxnMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTxn) EXPECT() *MockTxnMockRecorder { + return m.recorder +} + +// BestEffort mocks base method. +func (m *MockTxn) BestEffort() Txn { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BestEffort") + ret0, _ := ret[0].(Txn) + return ret0 +} + +// BestEffort indicates an expected call of BestEffort. +func (mr *MockTxnMockRecorder) BestEffort() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BestEffort", reflect.TypeOf((*MockTxn)(nil).BestEffort)) +} + +// Commit mocks base method. +func (m *MockTxn) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockTxnMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTxn)(nil).Commit), ctx) +} + +// Discard mocks base method. +func (m *MockTxn) Discard(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Discard", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Discard indicates an expected call of Discard. +func (mr *MockTxnMockRecorder) Discard(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discard", reflect.TypeOf((*MockTxn)(nil).Discard), ctx) +} + +// Do mocks base method. +func (m *MockTxn) Do(ctx context.Context, req *api.Request) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", ctx, req) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Do indicates an expected call of Do. +func (mr *MockTxnMockRecorder) Do(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockTxn)(nil).Do), ctx, req) +} + +// Mutate mocks base method. +func (m *MockTxn) Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mutate", ctx, mu) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Mutate indicates an expected call of Mutate. +func (mr *MockTxnMockRecorder) Mutate(ctx, mu any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockTxn)(nil).Mutate), ctx, mu) +} + +// Query mocks base method. +func (m *MockTxn) Query(ctx context.Context, q string) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Query", ctx, q) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockTxnMockRecorder) Query(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockTxn)(nil).Query), ctx, q) +} + +// QueryRDF mocks base method. +func (m *MockTxn) QueryRDF(ctx context.Context, q string) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryRDF", ctx, q) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryRDF indicates an expected call of QueryRDF. +func (mr *MockTxnMockRecorder) QueryRDF(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRDF", reflect.TypeOf((*MockTxn)(nil).QueryRDF), ctx, q) +} + +// QueryRDFWithVars mocks base method. +func (m *MockTxn) QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryRDFWithVars", ctx, q, vars) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryRDFWithVars indicates an expected call of QueryRDFWithVars. +func (mr *MockTxnMockRecorder) QueryRDFWithVars(ctx, q, vars any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRDFWithVars", reflect.TypeOf((*MockTxn)(nil).QueryRDFWithVars), ctx, q, vars) +} + +// QueryWithVars mocks base method. +func (m *MockTxn) QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryWithVars", ctx, q, vars) + ret0, _ := ret[0].(*api.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryWithVars indicates an expected call of QueryWithVars. +func (mr *MockTxnMockRecorder) QueryWithVars(ctx, q, vars any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockTxn)(nil).QueryWithVars), ctx, q, vars) +} + +// MockDgraphClient is a mock of DgraphClient interface. +type MockDgraphClient struct { + ctrl *gomock.Controller + recorder *MockDgraphClientMockRecorder +} + +// MockDgraphClientMockRecorder is the mock recorder for MockDgraphClient. +type MockDgraphClientMockRecorder struct { + mock *MockDgraphClient +} + +// NewMockDgraphClient creates a new mock instance. +func NewMockDgraphClient(ctrl *gomock.Controller) *MockDgraphClient { + mock := &MockDgraphClient{ctrl: ctrl} + mock.recorder = &MockDgraphClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDgraphClient) EXPECT() *MockDgraphClientMockRecorder { + return m.recorder +} + +// Alter mocks base method. +func (m *MockDgraphClient) Alter(ctx context.Context, op *api.Operation) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Alter", ctx, op) + ret0, _ := ret[0].(error) + return ret0 +} + +// Alter indicates an expected call of Alter. +func (mr *MockDgraphClientMockRecorder) Alter(ctx, op any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraphClient)(nil).Alter), ctx, op) +} + +// GetJwt mocks base method. +func (m *MockDgraphClient) GetJwt() api.Jwt { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetJwt") + ret0, _ := ret[0].(api.Jwt) + return ret0 +} + +// GetJwt indicates an expected call of GetJwt. +func (mr *MockDgraphClientMockRecorder) GetJwt() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJwt", reflect.TypeOf((*MockDgraphClient)(nil).GetJwt)) +} + +// Login mocks base method. +func (m *MockDgraphClient) Login(ctx context.Context, userid, password string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", ctx, userid, password) + ret0, _ := ret[0].(error) + return ret0 +} + +// Login indicates an expected call of Login. +func (mr *MockDgraphClientMockRecorder) Login(ctx, userid, password any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockDgraphClient)(nil).Login), ctx, userid, password) +} + +// LoginIntoNamespace mocks base method. +func (m *MockDgraphClient) LoginIntoNamespace(ctx context.Context, userid, password string, namespace uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoginIntoNamespace", ctx, userid, password, namespace) + ret0, _ := ret[0].(error) + return ret0 +} + +// LoginIntoNamespace indicates an expected call of LoginIntoNamespace. +func (mr *MockDgraphClientMockRecorder) LoginIntoNamespace(ctx, userid, password, namespace any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginIntoNamespace", reflect.TypeOf((*MockDgraphClient)(nil).LoginIntoNamespace), ctx, userid, password, namespace) +} + +// NewReadOnlyTxn mocks base method. +func (m *MockDgraphClient) NewReadOnlyTxn() Txn { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewReadOnlyTxn") + ret0, _ := ret[0].(Txn) + return ret0 +} + +// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn. +func (mr *MockDgraphClientMockRecorder) NewReadOnlyTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraphClient)(nil).NewReadOnlyTxn)) +} + +// NewTxn mocks base method. +func (m *MockDgraphClient) NewTxn() Txn { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTxn") + ret0, _ := ret[0].(Txn) + return ret0 +} + +// NewTxn indicates an expected call of NewTxn. +func (mr *MockDgraphClientMockRecorder) NewTxn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraphClient)(nil).NewTxn)) +} + +// Relogin mocks base method. +func (m *MockDgraphClient) Relogin(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Relogin", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Relogin indicates an expected call of Relogin. +func (mr *MockDgraphClientMockRecorder) Relogin(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Relogin", reflect.TypeOf((*MockDgraphClient)(nil).Relogin), ctx) +} diff --git a/pkg/gofr/datasource/dgraph/mock_logger.go b/pkg/gofr/datasource/dgraph/mock_logger.go new file mode 100644 index 000000000..6ee9b2ef6 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/mock_logger.go @@ -0,0 +1,138 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: logger.go +// +// Generated by this command: +// +// mockgen -source=logger.go -destination=mock_logger.go -package=dgraph +// + +// Package dgraph is a generated GoMock package. +package dgraph + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debug mocks base method. +func (m *MockLogger) Debug(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Log mocks base method. +func (m *MockLogger) Log(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Log", varargs...) +} + +// Log indicates an expected call of Log. +func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...) +} + +// Logf mocks base method. +func (m *MockLogger) Logf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Logf", varargs...) +} + +// Logf indicates an expected call of Logf. +func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...) +} diff --git a/pkg/gofr/datasource/dgraph/mock_metrics.go b/pkg/gofr/datasource/dgraph/mock_metrics.go new file mode 100644 index 000000000..ee19f7e97 --- /dev/null +++ b/pkg/gofr/datasource/dgraph/mock_metrics.go @@ -0,0 +1,74 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: metrics.go +// +// Generated by this command: +// +// mockgen -source=metrics.go -destination=mock_metrics.go -package=dgraph +// + +// Package dgraph is a generated GoMock package. +package dgraph + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockMetrics is a mock of Metrics interface. +type MockMetrics struct { + ctrl *gomock.Controller + recorder *MockMetricsMockRecorder +} + +// MockMetricsMockRecorder is the mock recorder for MockMetrics. +type MockMetricsMockRecorder struct { + mock *MockMetrics +} + +// NewMockMetrics creates a new mock instance. +func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics { + mock := &MockMetrics{ctrl: ctrl} + mock.recorder = &MockMetricsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { + return m.recorder +} + +// NewHistogram mocks base method. +func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) { + m.ctrl.T.Helper() + varargs := []any{name, desc} + for _, a := range buckets { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewHistogram", varargs...) +} + +// NewHistogram indicates an expected call of NewHistogram. +func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{name, desc}, buckets...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...) +} + +// RecordHistogram mocks base method. +func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{ctx, name, value} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordHistogram", varargs...) +} + +// RecordHistogram indicates an expected call of RecordHistogram. +func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, name, value}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...) +} diff --git a/pkg/gofr/datasource/file/sftp/go.mod b/pkg/gofr/datasource/file/sftp/go.mod index 56803d4d6..784769eaa 100644 --- a/pkg/gofr/datasource/file/sftp/go.mod +++ b/pkg/gofr/datasource/file/sftp/go.mod @@ -1,6 +1,6 @@ module gofr.dev/pkg/gofr/datasource/file/sftp -go 1.22.3 +go 1.22 replace gofr.dev => ../../../../../../gofr @@ -9,7 +9,7 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.4.0 gofr.dev v0.19.0 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.27.0 ) require ( @@ -20,4 +20,4 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/pkg/gofr/datasource/file/sftp/go.sum b/pkg/gofr/datasource/file/sftp/go.sum index 41f95311c..6f14fcab7 100644 --- a/pkg/gofr/datasource/file/sftp/go.sum +++ b/pkg/gofr/datasource/file/sftp/go.sum @@ -25,6 +25,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/pkg/gofr/datasource/sql/db.go b/pkg/gofr/datasource/sql/db.go index 67383f5d6..e025f0b24 100644 --- a/pkg/gofr/datasource/sql/db.go +++ b/pkg/gofr/datasource/sql/db.go @@ -221,7 +221,6 @@ func (d *DB) Select(ctx context.Context, data interface{}, query string, args .. rvo := reflect.ValueOf(data) if rvo.Kind() != reflect.Ptr { d.logger.Error("we did not get a pointer. data is not settable.") - return } @@ -231,37 +230,59 @@ func (d *DB) Select(ctx context.Context, data interface{}, query string, args .. switch rv.Kind() { case reflect.Slice: - rows, err := d.QueryContext(ctx, query, args...) - if err != nil { - d.logger.Errorf("error running query: %v", err) + d.selectSlice(ctx, query, args, rvo, rv) - return - } + case reflect.Struct: + d.selectStruct(ctx, query, args, rv) - for rows.Next() { - val := reflect.New(rv.Type().Elem()) + default: + d.logger.Debugf("a pointer to %v was not expected.", rv.Kind().String()) + } +} - if rv.Type().Elem().Kind() == reflect.Struct { - d.rowsToStruct(rows, val) - } else { - _ = rows.Scan(val.Interface()) - } +func (d *DB) selectSlice(ctx context.Context, query string, args []interface{}, rvo, rv reflect.Value) { + rows, err := d.QueryContext(ctx, query, args...) + if err != nil { + d.logger.Errorf("error running query: %v", err) + return + } - rv = reflect.Append(rv, val.Elem()) - } + for rows.Next() { + val := reflect.New(rv.Type().Elem()) - if rvo.Elem().CanSet() { - rvo.Elem().Set(rv) + if rv.Type().Elem().Kind() == reflect.Struct { + d.rowsToStruct(rows, val) + } else { + _ = rows.Scan(val.Interface()) } - case reflect.Struct: - rows, _ := d.QueryContext(ctx, query, args...) - for rows.Next() { - d.rowsToStruct(rows, rv) - } + rv = reflect.Append(rv, val.Elem()) + } - default: - d.logger.Debugf("a pointer to %v was not expected.", rv.Kind().String()) + if rows.Err() != nil { + d.logger.Errorf("error parsing rows : %v", err) + return + } + + if rvo.Elem().CanSet() { + rvo.Elem().Set(rv) + } +} + +func (d *DB) selectStruct(ctx context.Context, query string, args []interface{}, rv reflect.Value) { + rows, err := d.QueryContext(ctx, query, args...) + if err != nil { + d.logger.Errorf("error running query: %v", err) + return + } + + for rows.Next() { + d.rowsToStruct(rows, rv) + } + + if rows.Err() != nil { + d.logger.Errorf("error parsing rows : %v", err) + return } } diff --git a/pkg/gofr/external_db.go b/pkg/gofr/external_db.go index 42de6699f..f5c64b4b5 100644 --- a/pkg/gofr/external_db.go +++ b/pkg/gofr/external_db.go @@ -82,3 +82,14 @@ func (a *App) AddSolr(db container.SolrProvider) { a.container.Solr = db } + +// AddDgraph sets the Dgraph datasource in the app's container. +func (a *App) AddDgraph(db container.DgraphProvider) { + // Create the Dgraph client with the provided configuration + db.UseLogger(a.Logger()) + db.UseMetrics(a.Metrics()) + + db.Connect() + + a.container.DGraph = db +} diff --git a/pkg/gofr/http/form_data_binder.go b/pkg/gofr/http/form_data_binder.go new file mode 100644 index 000000000..811cdfff4 --- /dev/null +++ b/pkg/gofr/http/form_data_binder.go @@ -0,0 +1,205 @@ +package http + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" +) + +func (*formData) setInterfaceValue(value reflect.Value, data any) (bool, error) { + if !value.CanSet() { + return false, fmt.Errorf("%w: %s", errUnsupportedInterfaceType, value.Kind()) + } + + value.Set(reflect.ValueOf(data)) + + return true, nil +} + +func (uf *formData) setSliceOrArrayValue(value reflect.Value, data string) (bool, error) { + if value.Kind() != reflect.Slice && value.Kind() != reflect.Array { + return false, fmt.Errorf("%w: %s", errUnsupportedKind, value.Kind()) + } + + elemType := value.Type().Elem() + + elements := strings.Split(data, ",") + + // Create a new slice/array with appropriate length and capacity + var newSlice reflect.Value + + if value.Kind() == reflect.Slice { + newSlice = reflect.MakeSlice(value.Type(), len(elements), len(elements)) + } else if len(elements) > value.Len() { + return false, errDataLengthExceeded + } else { + newSlice = reflect.New(value.Type()).Elem() + } + + // Create a reusable element value to avoid unnecessary allocations + elemValue := reflect.New(elemType).Elem() + + // Set the elements of the slice/array + for i, strVal := range elements { + // Update the reusable element value + if _, err := uf.setFieldValue(elemValue, strVal); err != nil { + return false, fmt.Errorf("%w %d: %w", errSettingValueFailure, i, err) + } + + newSlice.Index(i).Set(elemValue) + } + + value.Set(newSlice) + + return true, nil +} + +func (*formData) setStructValue(value reflect.Value, data string) (bool, error) { + if value.Kind() != reflect.Struct { + return false, errNotAStruct + } + + dataMap, err := parseStringToMap(data) + if err != nil { + return false, err + } + + if len(dataMap) == 0 { + return false, errFieldsNotSet + } + + numFieldsSet := 0 + + var multiErr error + + // Create a map for case-insensitive lookups + caseInsensitiveMap := make(map[string]interface{}) + for key, val := range dataMap { + caseInsensitiveMap[strings.ToLower(key)] = val + } + + for i := 0; i < value.NumField(); i++ { + fieldType := value.Type().Field(i) + fieldValue := value.Field(i) + fieldName := fieldType.Name + + // Perform case-insensitive lookup for the key in dataMap + val, exists := caseInsensitiveMap[strings.ToLower(fieldName)] + if !exists { + continue + } + + if !fieldValue.CanSet() { + multiErr = fmt.Errorf("%w: %s", errUnexportedField, fieldName) + continue + } + + if err := setFieldValueFromData(fieldValue, val); err != nil { + multiErr = fmt.Errorf("%w; %w", multiErr, err) + continue + } + + numFieldsSet++ + } + + if numFieldsSet == 0 { + return false, errFieldsNotSet + } + + return true, multiErr +} + +// setFieldValueFromData sets the field's value based on the provided data. +func setFieldValueFromData(field reflect.Value, data interface{}) error { + switch field.Kind() { + case reflect.String: + return setStringField(field, data) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return setIntField(field, data) + case reflect.Float32, reflect.Float64: + return setFloatField(field, data) + case reflect.Bool: + return setBoolField(field, data) + case reflect.Invalid, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer: + return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data) + default: + return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data) + } +} + +type customUnmarshaller struct { + dataMap map[string]interface{} +} + +// UnmarshalJSON is a custom unmarshaller because json package in Go unmarshal numbers to float64 by default. +func (c *customUnmarshaller) UnmarshalJSON(data []byte) error { + var rawData map[string]interface{} + + err := json.Unmarshal(data, &rawData) + if err != nil { + return err + } + + dataMap := make(map[string]any, len(rawData)) + + for key, val := range rawData { + if valFloat, ok := val.(float64); ok { + valInt := int(valFloat) + if valFloat == float64(valInt) { + val = valInt + } + } + + dataMap[key] = val + } + + *c = customUnmarshaller{dataMap} + + return nil +} + +func parseStringToMap(data string) (map[string]interface{}, error) { + var c customUnmarshaller + err := json.Unmarshal([]byte(data), &c) + + return c.dataMap, err +} + +func setStringField(field reflect.Value, data interface{}) error { + if val, ok := data.(string); ok { + field.SetString(val) + return nil + } + + return fmt.Errorf("%w: expected string but got %T", errUnsupportedFieldType, data) +} + +func setIntField(field reflect.Value, data interface{}) error { + if val, ok := data.(int); ok { + field.SetInt(int64(val)) + return nil + } + + return fmt.Errorf("%w: expected int but got %T", errUnsupportedFieldType, data) +} + +func setFloatField(field reflect.Value, data interface{}) error { + if val, ok := data.(float64); ok { + field.SetFloat(val) + return nil + } + + return fmt.Errorf("%w: expected float64 but got %T", errUnsupportedFieldType, data) +} + +func setBoolField(field reflect.Value, data interface{}) error { + if val, ok := data.(bool); ok { + field.SetBool(val) + return nil + } + + return fmt.Errorf("%w: expected bool but got %T", errUnsupportedFieldType, data) +} diff --git a/pkg/gofr/http/middleware/oauth.go b/pkg/gofr/http/middleware/oauth.go index 8894d4595..75344f65e 100644 --- a/pkg/gofr/http/middleware/oauth.go +++ b/pkg/gofr/http/middleware/oauth.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "math/big" @@ -15,6 +16,11 @@ import ( "github.com/golang-jwt/jwt/v5" ) +var ( + errAuthorizationHeaderRequired = errors.New("authorization header is required") + errInvalidAuthorizationHeader = errors.New("authorization header format must be Bearer {token}") +) + // JWTClaim represents a custom key used to store JWT claims within the request context. type JWTClaim string @@ -112,35 +118,15 @@ func OAuth(key PublicKeyProvider) func(inner http.Handler) http.Handler { return } - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Authorization header is required", http.StatusUnauthorized) - return - } - - headerParts := strings.Split(authHeader, " ") - if len(headerParts) != 2 || headerParts[0] != "Bearer" { - http.Error(w, "Authorization header format must be Bearer {token}", http.StatusUnauthorized) + tokenString, err := extractToken(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) return } - tokenString := headerParts[1] - - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - kid := token.Header["kid"] - - jwks := key.Get(fmt.Sprint(kid)) - if jwks == nil { - return nil, JWKNotFound{} - } - - return key.Get(fmt.Sprint(kid)), nil - }) - + token, err := parseToken(tokenString, key) if err != nil { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(err.Error())) - + http.Error(w, err.Error(), http.StatusUnauthorized) return } @@ -152,6 +138,36 @@ func OAuth(key PublicKeyProvider) func(inner http.Handler) http.Handler { } } +// ExtractToken validates the Authorization header and extracts the JWT token. +func extractToken(authHeader string) (string, error) { + if authHeader == "" { + return "", errAuthorizationHeaderRequired + } + + const bearerPrefix = "Bearer " + + token, ok := strings.CutPrefix(authHeader, bearerPrefix) + if !ok || token == "" { + return "", errInvalidAuthorizationHeader + } + + return token, nil +} + +// ParseToken parses the JWT token using the provided key provider. +func parseToken(tokenString string, key PublicKeyProvider) (*jwt.Token, error) { + return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + kid := token.Header["kid"] + jwks := key.Get(fmt.Sprint(kid)) + + if jwks == nil { + return nil, JWKNotFound{} + } + + return jwks, nil + }) +} + // JWKS represents a JSON Web Key Set. type JWKS struct { Keys []JSONWebKey `json:"keys"` diff --git a/pkg/gofr/http/middleware/oauth_test.go b/pkg/gofr/http/middleware/oauth_test.go index 86ae438b3..c4f2582ce 100644 --- a/pkg/gofr/http/middleware/oauth_test.go +++ b/pkg/gofr/http/middleware/oauth_test.go @@ -65,7 +65,7 @@ func TestOAuthInvalidTokenFormat(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assert.Contains(t, string(respBody), `Authorization header format must be Bearer {token}`) + assert.Contains(t, string(respBody), `authorization header format must be Bearer {token}`) resp.Body.Close() } @@ -89,7 +89,7 @@ func TestOAuthEmptyAuthHeader(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assert.Contains(t, string(respBody), `Authorization header is required`) + assert.Contains(t, string(respBody), `authorization header is required`) resp.Body.Close() } diff --git a/pkg/gofr/http/multipart_file_bind.go b/pkg/gofr/http/multipart_file_bind.go index c55025509..a4fe481f6 100644 --- a/pkg/gofr/http/multipart_file_bind.go +++ b/pkg/gofr/http/multipart_file_bind.go @@ -1,6 +1,7 @@ package http import ( + "errors" "io" "mime/multipart" "reflect" @@ -9,6 +10,17 @@ import ( "gofr.dev/pkg/gofr/file" ) +var ( + errUnsupportedInterfaceType = errors.New("unsupported interface value type") + errDataLengthExceeded = errors.New("data length exceeds array capacity") + errUnsupportedKind = errors.New("unsupported kind") + errSettingValueFailure = errors.New("error setting value at index") + errNotAStruct = errors.New("provided value is not a struct") + errUnexportedField = errors.New("cannot set field; it might be unexported") + errUnsupportedFieldType = errors.New("unsupported type for field") + errFieldsNotSet = errors.New("no fields were set") +) + type formData struct { fields map[string][]string files map[string][]*multipart.FileHeader @@ -134,6 +146,8 @@ func (*formData) setFile(value reflect.Value, header []*multipart.FileHeader) (b } func (uf *formData) setFieldValue(value reflect.Value, data string) (bool, error) { + value = dereferencePointerType(value) + kind := value.Kind() switch kind { case reflect.String: @@ -146,13 +160,31 @@ func (uf *formData) setFieldValue(value reflect.Value, data string) (bool, error return uf.setFloatValue(value, data) case reflect.Bool: return uf.setBoolValue(value, data) - case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, - reflect.Map, reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer: - // These types are not supported for setting via form data - return false, nil - default: + case reflect.Slice, reflect.Array: + return uf.setSliceOrArrayValue(value, data) + case reflect.Interface: + return uf.setInterfaceValue(value, data) + case reflect.Struct: + return uf.setStructValue(value, data) + case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, + reflect.Map, reflect.Pointer, reflect.UnsafePointer: return false, nil } + + return false, nil +} + +func dereferencePointerType(value reflect.Value) reflect.Value { + if value.Kind() == reflect.Ptr { + if value.IsNil() { + // Initialize the pointer to a new value if it's nil + value.Set(reflect.New(value.Type().Elem())) + } + + value = value.Elem() // Dereference the pointer + } + + return value } func (*formData) setStringValue(value reflect.Value, data string) (bool, error) { diff --git a/pkg/gofr/http/multipart_file_bind_test.go b/pkg/gofr/http/multipart_file_bind_test.go index 762836734..e69dd97bb 100644 --- a/pkg/gofr/http/multipart_file_bind_test.go +++ b/pkg/gofr/http/multipart_file_bind_test.go @@ -1,10 +1,18 @@ package http import ( + "errors" + "fmt" "reflect" "testing" + "unsafe" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + errUnsupportedType = errors.New("unsupported type for field: expected float64 but got bool") + errJSON = errors.New("unexpected end of JSON input") ) func TestGetFieldName(t *testing.T) { @@ -49,8 +57,209 @@ func TestGetFieldName(t *testing.T) { for i, tt := range tests { t.Run(tt.desc, func(t *testing.T) { result, gotOk := getFieldName(tt.field) - assert.Equal(t, tt.key, result, "TestGetFieldName[%d] : %v Failed!", i, tt.desc) - assert.Equal(t, tt.wantOk, gotOk, "TestGetFieldName[%d] : %v Failed!", i, tt.desc) + require.Equal(t, tt.key, result, "TestGetFieldName[%d] : %v Failed!", i, tt.desc) + require.Equal(t, tt.wantOk, gotOk, "TestGetFieldName[%d] : %v Failed!", i, tt.desc) + }) + } +} + +type testValue struct { + kind reflect.Kind + value interface{} +} + +func Test_SetFieldValue_Success(t *testing.T) { + testCases := []struct { + desc string + data string + expected bool + valueType testValue + }{ + {"String", "test", true, testValue{reflect.String, "string"}}, + {"Int", "10", true, testValue{reflect.Int, 0}}, + {"Uint", "10", true, testValue{reflect.Uint16, uint16(10)}}, + {"Float64", "3.14", true, testValue{reflect.Float64, 0.0}}, + {"Bool", "true", true, testValue{reflect.Bool, false}}, + {"Slice", "1,2,3,4,5", true, testValue{reflect.Slice, []int{}}}, + {"Array", "1,2,3,4,5", true, testValue{reflect.Array, [5]int{}}}, + {"Struct", `{"name": "John", "age": 30}`, true, testValue{reflect.Struct, struct { + Name string `json:"name"` + Age int `json:"age"` + }{}}}, + {"Interface", "test interface", true, testValue{reflect.Interface, new(any)}}, + } + + for _, tc := range testCases { + f := &formData{} + val := reflect.New(reflect.TypeOf(tc.valueType.value)).Elem() + + set, err := f.setFieldValue(val, tc.data) + + require.NoErrorf(t, err, "Unexpected error for value kind %v and data %q", val.Kind(), tc.data) + + require.Equalf(t, tc.expected, set, "Expected set to be %v for value kind %v and data %q", tc.expected, val.Kind(), tc.data) + } +} + +func TestSetFieldValue_InvalidKinds(t *testing.T) { + uf := &formData{} + + tests := []struct { + kind reflect.Kind + data string + typ reflect.Type + }{ + {reflect.Complex64, "foo", reflect.TypeOf(complex64(0))}, + {reflect.Complex128, "bar", reflect.TypeOf(complex128(0))}, + {reflect.Chan, "baz", reflect.TypeOf(make(chan int))}, + {reflect.Func, "qux", reflect.TypeOf(func() {})}, + {reflect.Map, "quux", reflect.TypeOf(map[string]int{})}, + {reflect.UnsafePointer, "grault", reflect.TypeOf(unsafe.Pointer(nil))}, + } + + for _, tt := range tests { + value := reflect.New(tt.typ).Elem() + ok, err := uf.setFieldValue(value, tt.data) + + require.False(t, ok, "expected false, got true for kind %v", tt.kind) + + require.NoError(t, err, "expected nil, got %v for kind %v", err, tt.kind) + } +} + +func TestSetSliceOrArrayValue(t *testing.T) { + type testStruct struct { + Slice []string + Array [3]string + } + + uf := &formData{} + + // Test with a slice + value := reflect.ValueOf(&testStruct{Slice: nil}).Elem().FieldByName("Slice") + + data := "a,b,c" + + ok, err := uf.setSliceOrArrayValue(value, data) + + require.True(t, ok, "setSliceOrArrayValue failed") + + require.NoError(t, err, "setSliceOrArrayValue failed: %v", err) + + require.Len(t, value.Interface().([]string), 3, "slice not set correctly") + + // Test with an array + value = reflect.ValueOf(&testStruct{Array: [3]string{}}).Elem().FieldByName("Array") + + data = "a,b,c" + + ok, err = uf.setSliceOrArrayValue(value, data) + + require.True(t, ok, "setSliceOrArrayValue failed") + + require.NoError(t, err, "setSliceOrArrayValue failed: %v", err) +} + +func TestSetStructValue_Success(t *testing.T) { + type testStruct struct { + Field1 string + Field2 int + } + + uf := &formData{} + + tests := []struct { + name string + data string + wantField1 string + wantField2 int + }{ + { + name: "Valid input with correct case", + data: `{"Field1":"value1","Field2":123}`, + wantField1: "value1", + wantField2: 123, + }, + { + name: "Valid input with case insensitive fields", + data: `{"field1":"value2","FIELD2":456}`, + wantField1: "value2", + wantField2: 456, + }, + { + name: "Mixed Case and invalid field names", + data: `{"FielD1":"value4", "invalidField":"ignored", "FiEld2":789}`, + wantField1: "value4", + wantField2: 789, + }, + { + name: "Case-insensitive field name but not in dataMap", + data: `{"fIeLd1":"value5", "not_in_dataMap": 123}`, + wantField1: "value5", + wantField2: 0, // Field2 should remain unset (default 0) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := reflect.ValueOf(&testStruct{}).Elem() + + ok, err := uf.setStructValue(value, tt.data) + + require.NoError(t, err, "TestSetStructValue_Success Failed.") + require.True(t, ok, "TestSetStructValue_Success Failed.") + require.Equal(t, tt.wantField1, value.FieldByName("Field1").String(), + "TestSetStructValue_Success Failed : Field1 not set correctly") + require.Equal(t, tt.wantField2, int(value.FieldByName("Field2").Int()), + "TestSetStructValue_Success Failed : Field2 not set correctly") + }) + } +} + +func TestSetStructValue_Errors(t *testing.T) { + type testStruct struct { + Field1 string + Field2 int + Field4 float64 + } + + uf := &formData{} + + tests := []struct { + name string + data string + err error + }{ + { + name: "Unexported field", + data: `{"field3":"value3"}`, + err: errFieldsNotSet, + }, + { + name: "Unsupported field type", + data: `{"field2":1,"Field4":true}`, + err: fmt.Errorf("%w; %w", nil, errUnsupportedType), + }, + { + name: "Invalid JSON", + data: `{"Field1":"value1", "Field2":123,`, + err: errJSON, // JSON parsing error + }, + { + name: "Field not settable", + data: `{"Field1":"value1", "Field2":123, "Field4": "not a float"}`, + err: errUnsupportedFieldType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := reflect.ValueOf(&testStruct{}).Elem() + + _, err := uf.setStructValue(value, tt.data) + + require.Error(t, err, "TestSetStructValue_Errors Failed.") + require.Contains(t, err.Error(), tt.err.Error(), "TestSetStructValue_Errors Failed.") }) } } diff --git a/pkg/gofr/migration/cassandra.go b/pkg/gofr/migration/cassandra.go new file mode 100644 index 000000000..e2dd4be0b --- /dev/null +++ b/pkg/gofr/migration/cassandra.go @@ -0,0 +1,94 @@ +package migration + +import ( + "time" + + "gofr.dev/pkg/gofr/container" +) + +type cassandraDS struct { + container.Cassandra +} + +type cassandraMigrator struct { + container.Cassandra + + migrator +} + +func (cs cassandraDS) apply(m migrator) migrator { + return cassandraMigrator{ + Cassandra: cs.Cassandra, + migrator: m, + } +} + +const ( + checkAndCreateCassandraMigrationTable = `CREATE TABLE IF NOT EXISTS gofr_migrations (version bigint, + method text, start_time timestamp, duration bigint, PRIMARY KEY (version, method));` + + getLastCassandraGoFrMigration = `SELECT version FROM gofr_migrations` + + insertCassandraGoFrMigrationRow = `INSERT INTO gofr_migrations (version, method, start_time, duration) VALUES (?, ?, ?, ?);` +) + +func (cs cassandraMigrator) checkAndCreateMigrationTable(c *container.Container) error { + if err := c.Cassandra.Exec(checkAndCreateCassandraMigrationTable); err != nil { + return err + } + + return cs.migrator.checkAndCreateMigrationTable(c) +} + +func (cs cassandraMigrator) getLastMigration(c *container.Container) int64 { + var lastMigration int64 // Default to 0 if no migrations found + + var lastMigrations []int64 + + err := c.Cassandra.Query(&lastMigrations, getLastCassandraGoFrMigration) + if err != nil { + return 0 + } + + for _, version := range lastMigrations { + if version > lastMigration { + lastMigration = version + } + } + + c.Debugf("cassandra last migration fetched value is: %v", lastMigration) + + lm2 := cs.migrator.getLastMigration(c) + + if lm2 > lastMigration { + return lm2 + } + + return lastMigration +} + +func (cs cassandraMigrator) beginTransaction(c *container.Container) transactionData { + cmt := cs.migrator.beginTransaction(c) + + c.Debug("cassandra migrator begin successfully") + + return cmt +} + +func (cs cassandraMigrator) commitMigration(c *container.Container, data transactionData) error { + err := cs.Cassandra.Exec(insertCassandraGoFrMigrationRow, data.MigrationNumber, + "UP", data.StartTime, time.Since(data.StartTime).Milliseconds()) + if err != nil { + return err + } + + c.Debugf("inserted record for migration %v in cassandra gofr_migrations table", data.MigrationNumber) + + return cs.migrator.commitMigration(c, data) +} + +func (cs cassandraMigrator) rollback(c *container.Container, data transactionData) { + cs.migrator.rollback(c, data) + + c.Fatalf("migration %v failed and rolled back", data.MigrationNumber) +} diff --git a/pkg/gofr/migration/cassandra_test.go b/pkg/gofr/migration/cassandra_test.go new file mode 100644 index 000000000..1ab1844d2 --- /dev/null +++ b/pkg/gofr/migration/cassandra_test.go @@ -0,0 +1,110 @@ +package migration + +import ( + "database/sql" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/testutil" +) + +func cassandraSetup(t *testing.T) (migrator, *container.MockCassandra, *container.Container) { + t.Helper() + + mockContainer, mocks := container.NewMockContainer(t) + + mockCassandra := mocks.Cassandra + + ds := Datasource{Cassandra: mockContainer.Cassandra} + + cassandraDB := cassandraDS{Cassandra: mockCassandra} + migratorWithCassandra := cassandraDB.apply(&ds) + + mockContainer.Cassandra = mockCassandra + + return migratorWithCassandra, mockCassandra, mockContainer +} + +func Test_CassandraCheckAndCreateMigrationTable(t *testing.T) { + migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t) + + testCases := []struct { + desc string + err error + }{ + {"no error", nil}, + {"connection failed", sql.ErrConnDone}, + } + + for i, tc := range testCases { + mockCassandra.EXPECT().Exec(checkAndCreateCassandraMigrationTable).Return(tc.err) + + err := migratorWithCassandra.checkAndCreateMigrationTable(mockContainer) + + assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc) + } +} + +func Test_CassandraGetLastMigration(t *testing.T) { + migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t) + + testCases := []struct { + desc string + err error + resp int64 + }{ + {"no error", nil, 0}, + {"connection failed", sql.ErrConnDone, 0}, + } + + var lastMigration []int64 + + for i, tc := range testCases { + mockCassandra.EXPECT().Query(&lastMigration, getLastCassandraGoFrMigration).Return(tc.err) + + resp := migratorWithCassandra.getLastMigration(mockContainer) + + assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc) + } +} + +func Test_CassandraCommitMigration(t *testing.T) { + migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t) + + testCases := []struct { + desc string + err error + }{ + {"no error", nil}, + {"connection failed", sql.ErrConnDone}, + } + + timeNow := time.Now() + + td := transactionData{ + StartTime: timeNow, + MigrationNumber: 10, + } + + for i, tc := range testCases { + mockCassandra.EXPECT().Exec(insertCassandraGoFrMigrationRow, td.MigrationNumber, + "UP", td.StartTime, gomock.Any()).Return(tc.err) + + err := migratorWithCassandra.commitMigration(mockContainer, td) + + assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc) + } +} + +func Test_CassandraBeginTransaction(t *testing.T) { + logs := testutil.StdoutOutputForFunc(func() { + migratorWithCassandra, _, mockContainer := cassandraSetup(t) + migratorWithCassandra.beginTransaction(mockContainer) + }) + + assert.Contains(t, logs, "cassandra migrator begin successfully") +} diff --git a/pkg/gofr/migration/clickhouse_test.go b/pkg/gofr/migration/clickhouse_test.go index 444883d82..246566880 100644 --- a/pkg/gofr/migration/clickhouse_test.go +++ b/pkg/gofr/migration/clickhouse_test.go @@ -24,7 +24,7 @@ func clickHouseSetup(t *testing.T) (migrator, *MockClickhouse, *container.Contai ds := Datasource{Clickhouse: mockClickhouse} ch := clickHouseDS{Clickhouse: mockClickhouse} - mg := ch.apply(ds) + mg := ch.apply(&ds) mockContainer.Clickhouse = mockClickhouse diff --git a/pkg/gofr/migration/datasource.go b/pkg/gofr/migration/datasource.go index f3c833ce6..edbc9c871 100644 --- a/pkg/gofr/migration/datasource.go +++ b/pkg/gofr/migration/datasource.go @@ -11,31 +11,27 @@ type Datasource struct { Redis Redis PubSub PubSub Clickhouse Clickhouse + Cassandra Cassandra } -// It is a base implementation for migration manger, on this other database drivers have been wrapped. +// It is a base implementation for migration manager, on this other database drivers have been wrapped. -// Datasource has to be changed to a pointer in a different PR. -func (Datasource) checkAndCreateMigrationTable(*container.Container) error { +func (*Datasource) checkAndCreateMigrationTable(*container.Container) error { return nil } -// Datasource has to be changed to a pointer in a different PR. -func (Datasource) getLastMigration(*container.Container) int64 { +func (*Datasource) getLastMigration(*container.Container) int64 { return 0 } -// Datasource has to be changed to a pointer in a different PR. -func (Datasource) beginTransaction(*container.Container) transactionData { +func (*Datasource) beginTransaction(*container.Container) transactionData { return transactionData{} } -// Datasource has to be changed to a pointer in a different PR. -func (Datasource) commitMigration(c *container.Container, data transactionData) error { +func (*Datasource) commitMigration(c *container.Container, data transactionData) error { c.Infof("Migration %v ran successfully", data.MigrationNumber) return nil } -// Datasource has to be changed to a pointer in a different PR. -func (Datasource) rollback(*container.Container, transactionData) {} +func (*Datasource) rollback(*container.Container, transactionData) {} diff --git a/pkg/gofr/migration/interface.go b/pkg/gofr/migration/interface.go index 7fa22109e..a253327f7 100644 --- a/pkg/gofr/migration/interface.go +++ b/pkg/gofr/migration/interface.go @@ -38,6 +38,15 @@ type Clickhouse interface { HealthCheck(ctx context.Context) (any, error) } +type Cassandra interface { + Exec(query string, args ...interface{}) error + NewBatch(name string, batchType int) error + BatchQuery(name, stmt string, values ...any) error + ExecuteBatch(name string) error + + HealthCheck(ctx context.Context) (any, error) +} + // keeping the migrator interface unexported as, right now it is not being implemented directly, by the externalDB drivers. // keeping the implementations for externalDB at one place such that if any change in migration logic, we would change directly here. type migrator interface { diff --git a/pkg/gofr/migration/migration.go b/pkg/gofr/migration/migration.go index c644a81ce..0628eb19b 100644 --- a/pkg/gofr/migration/migration.go +++ b/pkg/gofr/migration/migration.go @@ -112,7 +112,7 @@ func getMigrator(c *container.Container) (Datasource, migrator, bool) { var ( ok bool ds Datasource - mg migrator = ds + mg migrator = &ds ) if !isNil(c.SQL) { @@ -152,6 +152,16 @@ func getMigrator(c *container.Container) (Datasource, migrator, bool) { ds.PubSub = c.PubSub } + if !isNil(c.Cassandra) { + ok = true + + ds.Cassandra = cassandraDS{c.Cassandra} + + mg = cassandraDS{c.Cassandra}.apply(mg) + + c.Debug("initialized data source for Cassandra") + } + return ds, mg, ok } diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index d4314d73a..94547e5ec 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.20.0" +const Framework = "v1.21.0"