Repository: cloudflare/cloudflare-go
Pull request #1417 (2023-10-07 16:23:32)

Add support for D1
## Description

Adds API support for D1 Databases
## Has your change been tested?

Added new unit tests and they pass

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [X] I have added tests to cover my changes.
- [X] All new and existing tests passed.
- [X] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.



Patch:
diff --git a/.changelog/1417.txt b/.changelog/1417.txt
new file mode 100644
index 000000000..03e478798
--- /dev/null
+++ b/.changelog/1417.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+d1: adds support for d1
+```
diff --git a/d1.go b/d1.go
new file mode 100644
index 000000000..08a3ecc51
--- /dev/null
+++ b/d1.go
@@ -0,0 +1,199 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+var (
+	ErrMissingDatabaseID = fmt.Errorf("required missing database ID")
+)
+
+type D1Database struct {
+	Name      string     `json:"name"`
+	NumTables int        `json:"num_tables"`
+	UUID      string     `json:"uuid"`
+	Version   string     `json:"version"`
+	CreatedAt *time.Time `json:"created_at"`
+	FileSize  int64      `json:"file_size"`
+}
+
+type ListD1DatabasesParams struct {
+	Name string `url:"name,omitempty"`
+	ResultInfo
+}
+
+type ListD1Response struct {
+	Result []D1Database `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+type CreateD1DatabaseParams struct {
+	Name string `json:"name"`
+}
+
+type D1DatabaseResponse struct {
+	Result D1Database `json:"result"`
+	Response
+}
+
+type QueryD1DatabaseParams struct {
+	DatabaseID string   `json:"-"`
+	SQL        string   `json:"sql"`
+	Parameters []string `json:"params"`
+}
+
+type D1DatabaseMetadata struct {
+	ChangedDB   *bool   `json:"changed_db,omitempty"`
+	Changes     int     `json:"changes"`
+	Duration    float64 `json:"duration"`
+	LastRowID   int     `json:"last_row_id"`
+	RowsRead    int     `json:"rows_read"`
+	RowsWritten int     `json:"rows_written"`
+	SizeAfter   int     `json:"size_after"`
+}
+
+type D1Result struct {
+	Success *bool              `json:"success"`
+	Results []map[string]any   `json:"results"`
+	Meta    D1DatabaseMetadata `json:"meta"`
+}
+
+type QueryD1Response struct {
+	Result []D1Result `json:"result"`
+	Response
+}
+
+// ListD1Databases returns all databases for an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/cloudflare-d1-list-databases
+func (api *API) ListD1Databases(ctx context.Context, rc *ResourceContainer, params ListD1DatabasesParams) ([]D1Database, *ResultInfo, error) {
+	if rc.Identifier == "" {
+		return []D1Database{}, &ResultInfo{}, ErrMissingAccountID
+	}
+	baseURL := fmt.Sprintf("/accounts/%s/d1/database", rc.Identifier)
+	autoPaginate := true
+	if params.PerPage >= 1 || params.Page >= 1 {
+		autoPaginate = false
+	}
+
+	if params.PerPage < 1 {
+		params.PerPage = 100
+	}
+
+	if params.Page < 1 {
+		params.Page = 1
+	}
+	var databases []D1Database
+	var r ListD1Response
+	for {
+		uri := buildURI(baseURL, params)
+		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+		if err != nil {
+			return []D1Database{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+		}
+
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []D1Database{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+		}
+		databases = append(databases, r.Result...)
+		params.ResultInfo = r.ResultInfo.Next()
+		if params.ResultInfo.Done() || !autoPaginate {
+			break
+		}
+	}
+	return databases, &r.ResultInfo, nil
+}
+
+// CreateD1Database creates a new database for an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/cloudflare-d1-create-database
+func (api *API) CreateD1Database(ctx context.Context, rc *ResourceContainer, params CreateD1DatabaseParams) (D1Database, error) {
+	if rc.Identifier == "" {
+		return D1Database{}, ErrMissingAccountID
+	}
+	uri := fmt.Sprintf("/accounts/%s/d1/database", rc.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return D1Database{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r D1DatabaseResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return D1Database{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}
+
+// DeleteD1Database deletes a database for an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/cloudflare-d1-delete-database
+func (api *API) DeleteD1Database(ctx context.Context, rc *ResourceContainer, databaseID string) error {
+	if rc.Identifier == "" {
+		return ErrMissingAccountID
+	}
+	if databaseID == "" {
+		return ErrMissingDatabaseID
+	}
+	uri := fmt.Sprintf("/accounts/%s/d1/database/%s", rc.Identifier, databaseID)
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+	return nil
+}
+
+// GetD1Database returns a database for an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/cloudflare-d1-get-database
+func (api *API) GetD1Database(ctx context.Context, rc *ResourceContainer, databaseID string) (D1Database, error) {
+	if rc.Identifier == "" {
+		return D1Database{}, ErrMissingAccountID
+	}
+	uri := fmt.Sprintf("/accounts/%s/d1/database/%s", rc.Identifier, databaseID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return D1Database{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r D1DatabaseResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return D1Database{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}
+
+// QueryD1Database queries a database for an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/cloudflare-d1-query-database
+func (api *API) QueryD1Database(ctx context.Context, rc *ResourceContainer, params QueryD1DatabaseParams) ([]D1Result, error) {
+	if rc.Identifier == "" {
+		return []D1Result{}, ErrMissingAccountID
+	}
+	if params.DatabaseID == "" {
+		return []D1Result{}, ErrMissingDatabaseID
+	}
+	uri := fmt.Sprintf("/accounts/%s/d1/database/%s/query", rc.Identifier, params.DatabaseID)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return []D1Result{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r QueryD1Response
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []D1Result{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}


Test patch:
diff --git a/d1_test.go b/d1_test.go
new file mode 100644
index 000000000..1f3c5399a
--- /dev/null
+++ b/d1_test.go
@@ -0,0 +1,238 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	testD1DatabaseID = "480f4f691a284fdd92401ed29f0ac1df"
+	testD1Result     = `{
+		  "created_at": "2014-01-01T05:20:00.12345Z",
+		  "name": "my-database",
+		  "uuid": "480f4f691a284fdd92401ed29f0ac1df",
+		  "version": "beta",
+		  "num_tables": 1,
+		  "file_size": 100
+		}
+`
+)
+
+var (
+	d1CreatedAt, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	testD1Database = D1Database{
+		Name:      "my-database",
+		NumTables: 1,
+		UUID:      testD1DatabaseID,
+		Version:   "beta",
+		CreatedAt: &d1CreatedAt,
+		FileSize:  100,
+	}
+)
+
+func TestListD1Databases(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/d1/database", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": [
+                %s
+            ],
+            "result_info": {
+                "page": 1,
+                "per_page": 100,
+                "count": 1,
+                "total_count": 1
+            }
+        }`, testD1Result)
+	})
+
+	_, _, err := client.ListD1Databases(context.Background(), AccountIdentifier(""), ListD1DatabasesParams{})
+	if assert.Error(t, err, "Didn't get error for missing Account ID get listing D1 Database") {
+		assert.Equal(t, err.Error(), errMissingAccountID)
+	}
+	actual, _, err := client.ListD1Databases(context.Background(), testAccountRC, ListD1DatabasesParams{})
+	if assert.NoError(t, err, "ListD1Databases returned error: %v", err) {
+		expected := []D1Database{testD1Database}
+		if !assert.Equal(t, expected, actual) {
+			t.Errorf("ListD1Databases returned %+v, expected %+v", actual, expected)
+		}
+	}
+}
+
+func TestGetD1Databases(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/d1/database/"+testD1DatabaseID, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": %s
+        }`, testD1Result)
+	})
+
+	_, err := client.GetD1Database(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err, "Didn't get error for missing Account ID get getting D1 Database") {
+		assert.Equal(t, err.Error(), errMissingAccountID)
+	}
+	actual, err := client.GetD1Database(context.Background(), testAccountRC, testD1DatabaseID)
+	if assert.NoError(t, err, "GetD1Database returned error: %v", err) {
+		if !assert.Equal(t, testD1Database, actual) {
+			t.Errorf("GetD1Database returned %+v, expected %+v", actual, testD1Database)
+		}
+	}
+}
+
+func TestCreateD1Database(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/d1/database", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": %s
+		}`, testD1Result)
+	})
+
+	_, err := client.CreateD1Database(context.Background(), AccountIdentifier(""), CreateD1DatabaseParams{})
+	if assert.Error(t, err, "Didn't get error for missing Account ID get creating D1 Database") {
+		assert.Equal(t, err.Error(), errMissingAccountID)
+	}
+	actual, err := client.CreateD1Database(context.Background(), testAccountRC, CreateD1DatabaseParams{
+		Name: "my-database",
+	})
+	if assert.NoError(t, err, "CreateD1Database returned error: %v", err) {
+		if !assert.Equal(t, testD1Database, actual) {
+			t.Errorf("CreateD1Database returned %+v, expected %+v", actual, testD1Database)
+		}
+	}
+}
+
+func TestDeleteD1Database(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/d1/database/"+testD1DatabaseID, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": []
+		}`)
+	})
+
+	err := client.DeleteD1Database(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err, "Didn't get error for missing Account ID get deleting D1 Database") {
+		assert.Equal(t, err.Error(), errMissingAccountID)
+	}
+	err = client.DeleteD1Database(context.Background(), testAccountRC, testD1DatabaseID)
+	assert.NoError(t, err, "DeleteD1Database returned error: %v", err)
+}
+
+func TestQueryD1Database(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/d1/database/"+testD1DatabaseID+"/query", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		b, err := io.ReadAll(r.Body)
+		if err != nil {
+			t.Errorf("Error reading request body: %v", err)
+		}
+		if got := string(b); got != `{"sql":"SELECT * FROM my-database","params":["param1","param2"]}` {
+			t.Errorf("request Body is %s, want %s", got, `{"sql":"SELECT * FROM my-database","params":["param1","param2"]}`)
+		}
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"success": true,
+					"meta": {
+						"changed_db": false,
+						"changes": 0,
+						"duration": 3.3,
+						"last_row_id": 3,
+						"rows_read": 3,
+						"rows_written": 0,
+						"size_after": 10
+					},
+					"results": [
+						{
+							"id": 1,
+							"name": "test user"
+						},
+						{
+							"id": 2,
+							"name": "test user 2"
+						}
+					]
+				}
+			]
+		}`)
+	})
+
+	_, err := client.QueryD1Database(context.Background(), AccountIdentifier(""), QueryD1DatabaseParams{})
+	if assert.Error(t, err, "Didn't get error for missing Account ID get querying D1 Database") {
+		assert.Equal(t, err, ErrMissingAccountID)
+	}
+	_, err = client.QueryD1Database(context.Background(), testAccountRC, QueryD1DatabaseParams{})
+	if assert.Error(t, err, "Didn't get error for missing D1 Database ID get querying D1 Database") {
+		assert.Equal(t, err, ErrMissingDatabaseID)
+	}
+	actual, err := client.QueryD1Database(context.Background(), testAccountRC, QueryD1DatabaseParams{
+		DatabaseID: testD1DatabaseID,
+		SQL:        "SELECT * FROM my-database",
+		Parameters: []string{"param1", "param2"},
+	})
+	if assert.NoError(t, err, "QueryD1Database returned error: %v", err) {
+		expected := D1Result{
+			Success: BoolPtr(true),
+			Meta: D1DatabaseMetadata{
+				ChangedDB:   BoolPtr(false),
+				Changes:     0,
+				Duration:    3.3,
+				LastRowID:   3,
+				RowsRead:    3,
+				RowsWritten: 0,
+				SizeAfter:   10,
+			},
+			Results: []map[string]any{
+				{
+					"id":   float64(1),
+					"name": "test user",
+				},
+				{
+					"id":   float64(2),
+					"name": "test user 2",
+				},
+			},
+		}
+		if !assert.Equal(t, expected, actual[0]) {
+			t.Errorf("QueryD1Database returned %+v, expected %+v", actual, expected)
+		}
+	}
+}