diff --git a/client.go b/client.go new file mode 100644 index 0000000..62ebbbb --- /dev/null +++ b/client.go @@ -0,0 +1,154 @@ +package surrealdb + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// Client is a wrapper to more easily make HTTP calls to the SurrealDB engine. +// Any function that accepts a body of type interface{} will do one of two things depending on type. +// If it is of type string, the body will be the plaintext string, otherwise it will attempt to marshal +// it into JSON and send that +type Client struct { + // URL is the base URL in SurrealDB to be called + URL string + // Namespace that you want to connect to + NS string + // Database that you want to connect to + DB string + // The user to authenticate as + User string + // The password to authenticate with + Pass string +} + +type Response struct { + Time string `json:"time"` + Status string `json:"status"` + Result interface{} `json:"result"` +} + +// New creates a new instance of a Client +func NewClient(url, ns, db, user, pass string) Client { + return Client{ + URL: url, + NS: ns, + DB: db, + User: user, + Pass: pass, + } +} + +// Execute calls the endpoint POST /sql, executing whatever given statement +func (sc Client) Execute(query string) (Response, error) { + return sc.Request("/sql", "POST", query) +} + +// CreateOne calls the endpoint POST /key/:table/:id, executing the statement +// +// CREATE type::table($table) CONTENT $body; +func (sc Client) CreateOne(table, id, body interface{}) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s/%s", table, id), "POST", body) +} + +// CreateAll calls the endpoint POST /key/:table, executing the statement +// +// CREATE type::thing($table, $id) CONTENT $body; +func (sc Client) Create(table string, body interface{}) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s", table), "POST", body) +} + +// SelectAll calls the endpoint GET /key/:table, executing the statement +// +// SELECT * FROM type::table($table); +func (sc Client) SelectAll(table string) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s", table), "GET", "") +} + +// SelectOne calls the endpoint GET /key/:table/:id, executing the statement +// +// SELECT * FROM type::thing(:table, :id); +func (sc Client) SelectOne(table string, id string) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s/%s", table, id), "GET", "") +} + +// ReplaceOne calls the endpoint PUT /key/:table/:id, executing the statement +// +// UPDATE type::thing($table, $id) CONTENT $body; +func (sc Client) ReplaceOne(table, id string, body interface{}) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s/%s", table, id), "PUT", body) +} + +// UpsertOne calls the endpoint PUT /key/:table/:id, executing the statement +// +// UPDATE type::thing($table, $id) MERGE $body; +func (sc Client) UpsertOne(table, id string, body interface{}) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s/%s", table, id), "PATCH", body) +} + +// DeleteOne calls the endpoint DELETE /key/:table/:id, executing the statement +// +// DELETE FROM type::thing($table, $id); +func (sc Client) DeleteOne(table, id string) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s/%s", table, id), "DELETE", "") +} + +// DeleteAll calls the endpoint DELETE /key/:table/, executing the statement +// +// DELETE FROM type::table($table); +func (sc Client) DeleteAll(table string) (Response, error) { + return sc.Request(fmt.Sprintf("/key/%s", table), "DELETE", "") +} + +// Request makes a request to surrealdb to the given endpoint, with the given data. Responses returned from +// surrealdb vary, and this function will only return the first response +// TODO: have it return the array, or some other data type that more properly reflects the responses +func (sc Client) Request(endpoint string, requestType string, body interface{}) (Response, error) { + client := &http.Client{} + var bodyBytes []byte + var err error + + // If it is a string, send it directly though, otherwise try to unmarshal, throwing an error if it fails + switch v := body.(type) { + case string: + bodyBytes = []byte(v) + default: + bodyBytes, err = json.Marshal(v) + if err != nil { + return Response{}, err + } + } + + // TODO: verify its a valid requesttype + req, err := http.NewRequest(requestType, sc.URL+endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return Response{}, err + } + req.Header.Set("NS", sc.NS) + req.Header.Set("DB", sc.DB) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.SetBasicAuth(sc.User, sc.Pass) + + resp, err := client.Do(req) + if err != nil { + return Response{}, err + } + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return Response{}, err + } + + var realResp []Response + err = json.Unmarshal(data, &realResp) + if err != nil { + return Response{}, err + } + + return realResp[0], err +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..7cbf05b --- /dev/null +++ b/client_test.go @@ -0,0 +1,210 @@ +package surrealdb + +import ( + "bytes" + "fmt" + "reflect" + "testing" +) + +// TODO for testing: adding in more edge case detections, including NULLs and other weird scenarios. Most of these are just +// best case scenarios + +type person struct { + Child string `json:"child"` + Name string `json:"name"` + Age int `json:"age"` +} + +func Test_Nominal(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + resp, err := client.Execute("INFO FOR DB;") + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) +} + +func Test_Create(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + resp, err := client.Create("person", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + + fmt.Println(resp.Result) +} + +func Test_CreateOne(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + resp, err := client.CreateOne("person", "surrealcreate", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) +} + +func Test_SelectAll(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + resp, err := client.SelectAll("person") + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) +} + +func Test_SelectOne(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + expectedPerson := person{ + Child: "hello", + Name: "FooBar", + } + + resp, err := client.CreateOne("person", "surreal", `{child: "hello", name: "FooBar"}`) + IsEqual(t, nil, err) + fmt.Println(resp.Result) + + resp, err = client.SelectOne("person", "surreal") + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) + + var actualPerson person + err = Unmarshal(resp.Result, &actualPerson) + IsEqual(t, nil, err) + IsEqual(t, expectedPerson, actualPerson) +} + +func Test_ReplaceOne(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + resp, err := client.CreateOne("personreplace", "surreal", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + fmt.Println(resp.Result) + + resp, err = client.ReplaceOne("personreplace", "surreal", `{child: "DB", name: "FooBar", age: 1000}`) + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) +} + +func Test_ReplaceOne_PassInInterface(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + expectedPerson := person{ + Child: "interface", + Name: "interface", + Age: 100, + } + + resp, err := client.CreateOne("personreplace", "surrealinterface", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + fmt.Println(resp.Result) + + // Replace with the struct + resp, err = client.ReplaceOne("personreplace", "surrealinterface", expectedPerson) + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) + + // Now select it to ensure that it is correct still + resp, err = client.SelectOne("personreplace", "surrealinterface") + IsEqual(t, nil, err) + IsEqual(t, "OK", resp.Status) + fmt.Println(resp.Result) + + // Unmarshal and verify its the same + var actualPerson person + err = Unmarshal(resp.Result, &actualPerson) + IsEqual(t, nil, err) + IsEqual(t, expectedPerson, actualPerson) +} + +func Test_UpsertOne(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + expectedPerson := person{ + Child: "DB", + Name: "FooBar", + Age: 0, + } + + _, err := client.CreateOne("person", "surrealupsert", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + + resp, err := client.UpsertOne("person", "surrealupsert", `{child: "DB", name: "FooBar"}`) + IsEqual(t, nil, err) + + // Unmarshal and verify its the same + var actualPerson person + err = Unmarshal(resp.Result, &actualPerson) + IsEqual(t, nil, err) + IsEqual(t, expectedPerson, actualPerson) +} + +func Test_DeleteOne(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + _, err := client.CreateOne("person", "surrealdelete", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + + resp, err := client.DeleteOne("person", "surrealdelete") + IsEqual(t, nil, err) + IsEqual(t, []interface{}{}, resp.Result) + + resp2, err := client.SelectOne("person", "surrealdelete") + IsEqual(t, nil, err) + IsEqual(t, []interface{}{}, resp2.Result) +} + +func Test_DeleteAll(t *testing.T) { + client := NewClient("http://localhost:8000", "test", "test", "root", "root") + + _, err := client.CreateOne("person", "surrealdeleteall1", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + + _, err = client.CreateOne("person", "surrealdeleteall2", `{child: null, name: "FooBar"}`) + IsEqual(t, nil, err) + + resp, err := client.DeleteAll("person") + IsEqual(t, nil, err) + IsEqual(t, []interface{}{}, resp.Result) + + resp2, err := client.SelectAll("person") + IsEqual(t, nil, err) + IsEqual(t, []interface{}{}, resp2.Result) +} + +// Testing helpers brought in from testify, with extras removed + +func IsEqual(t *testing.T, expected, actual interface{}) bool { + if !ObjectsAreEqual(expected, actual) { + t.Errorf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s", expected, actual) + return false + } + return true +} + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +}