Skip to content

Commit

Permalink
Merge pull request #896 from go-kivik/createDoc
Browse files Browse the repository at this point in the history
Emulate CreateDoc if not implemented by backend
  • Loading branch information
flimzy authored Feb 24, 2024
2 parents b8d1f63 + 9a45baa commit 0388fde
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 143 deletions.
38 changes: 20 additions & 18 deletions bulk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,26 @@ func TestBulkDocs(t *testing.T) { // nolint: gocyclo
tests.Add("emulated BulkDocs support", tt{
db: &DB{
client: &Client{},
driverDB: &mock.DB{
PutFunc: func(_ context.Context, docID string, doc interface{}, options driver.Options) (string, error) {
if docID == "error" {
return "", errors.New("error")
}
if docID != "foo" { // nolint: goconst
return "", fmt.Errorf("Unexpected docID: %s", docID)
}
expectedDoc := map[string]string{"_id": "foo"}
if d := testy.DiffInterface(expectedDoc, doc); d != nil {
return "", fmt.Errorf("Unexpected doc:\n%s", d)
}
gotOpts := map[string]interface{}{}
options.Apply(gotOpts)
if d := testy.DiffInterface(testOptions, gotOpts); d != nil {
return "", fmt.Errorf("Unexpected opts:\n%s", d)
}
return "2-xxx", nil // nolint: goconst
driverDB: &mock.DocCreator{
DB: mock.DB{
PutFunc: func(_ context.Context, docID string, doc interface{}, options driver.Options) (string, error) {
if docID == "error" {
return "", errors.New("error")
}
if docID != "foo" { // nolint: goconst
return "", fmt.Errorf("Unexpected docID: %s", docID)
}
expectedDoc := map[string]string{"_id": "foo"}
if d := testy.DiffInterface(expectedDoc, doc); d != nil {
return "", fmt.Errorf("Unexpected doc:\n%s", d)
}
gotOpts := map[string]interface{}{}
options.Apply(gotOpts)
if d := testy.DiffInterface(testOptions, gotOpts); d != nil {
return "", fmt.Errorf("Unexpected opts:\n%s", d)
}
return "2-xxx", nil // nolint: goconst
},
},
CreateDocFunc: func(_ context.Context, doc interface{}, options driver.Options) (string, string, error) {
gotOpts := map[string]interface{}{}
Expand Down
21 changes: 16 additions & 5 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"strings"
"sync"

"github.com/google/uuid"

"github.com/go-kivik/kivik/v4/driver"
"github.com/go-kivik/kivik/v4/internal"
)
Expand Down Expand Up @@ -302,12 +304,21 @@ func (db *DB) CreateDoc(ctx context.Context, doc interface{}, options ...Option)
if db.err != nil {
return "", "", db.err
}
endQuery, err := db.startQuery()
if err != nil {
return "", "", err
if docCreator, ok := db.driverDB.(driver.DocCreator); ok {
endQuery, err := db.startQuery()
if err != nil {
return "", "", err
}
defer endQuery()
return docCreator.CreateDoc(ctx, doc, multiOptions(options))
}
defer endQuery()
return db.driverDB.CreateDoc(ctx, doc, multiOptions(options))
docID, ok := extractDocID(doc)
if !ok {
// TODO: Consider making uuid algorithm configurable
docID = uuid.NewString()
}
rev, err = db.Put(ctx, docID, doc, options...)
return docID, rev, err
}

// normalizeFromJSON unmarshals a []byte, json.RawMessage or io.Reader to a
Expand Down
32 changes: 30 additions & 2 deletions db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@ func TestCreateDoc(t *testing.T) {
name: "error",
db: &DB{
client: &Client{},
driverDB: &mock.DB{
driverDB: &mock.DocCreator{
CreateDocFunc: func(context.Context, interface{}, driver.Options) (string, string, error) {
return "", "", &internal.Error{Status: http.StatusBadRequest, Err: errors.New("create error")}
},
Expand All @@ -1873,7 +1873,7 @@ func TestCreateDoc(t *testing.T) {
name: "success",
db: &DB{
client: &Client{},
driverDB: &mock.DB{
driverDB: &mock.DocCreator{
CreateDocFunc: func(_ context.Context, doc interface{}, options driver.Options) (string, string, error) {
gotOpts := map[string]interface{}{}
options.Apply(gotOpts)
Expand All @@ -1899,10 +1899,38 @@ func TestCreateDoc(t *testing.T) {
client: &Client{
closed: true,
},
driverDB: &mock.DocCreator{},
},
status: http.StatusServiceUnavailable,
err: "kivik: client closed",
},
{
name: "emulated with docID",
db: &DB{
client: &Client{},
driverDB: &mock.DB{
PutFunc: func(_ context.Context, docID string, doc interface{}, options driver.Options) (string, error) {
gotOpts := map[string]interface{}{}
options.Apply(gotOpts)
expectedDoc := map[string]string{"_id": "foo", "type": "test"}
if docID != "foo" {
return "", fmt.Errorf("Unexpected docID: %s", docID)
}
if d := testy.DiffInterface(expectedDoc, doc); d != nil {
return "", fmt.Errorf("Unexpected doc:\n%s", d)
}
if d := testy.DiffInterface(testOptions, gotOpts); d != nil {
return "", fmt.Errorf("Unexpected options:\n%s", d)
}
return "1-xxx", nil
},
},
},
doc: map[string]string{"type": "test", "_id": "foo"},
options: Params(testOptions),
docID: "foo",
rev: "1-xxx",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
10 changes: 8 additions & 2 deletions driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ type DB interface {
// AllDocs returns all of the documents in the database, subject to the
// options provided.
AllDocs(ctx context.Context, options Options) (Rows, error)
// CreateDoc creates a new doc, with a server-generated ID.
CreateDoc(ctx context.Context, doc interface{}, options Options) (docID, rev string, err error)
// Put writes the document in the database.
Put(ctx context.Context, docID string, doc interface{}, options Options) (rev string, err error)
// Get fetches the requested document from the database.
Expand Down Expand Up @@ -231,6 +229,14 @@ type DB interface {
Close() error
}

// DocCreator is an optional interface that extends a [DB] to support the
// creation of new documents. If not implemented, [DB.Put] will be used to
// emulate the functionality, with missing document IDs generated as V4 UUIDs.
type DocCreator interface {
// CreateDoc creates a new doc, with a server-generated ID.
CreateDoc(ctx context.Context, doc interface{}, options Options) (docID, rev string, err error)
}

// OpenRever is an ptional interface that extends a [DB] to support the open_revs
// option of the CouchDB get document endpoint. It is used by the replicator.
// Drivers that don't support this endpoint may not be able to replicate as
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/go-chi/chi/v5 v5.0.10
github.com/go-playground/validator/v10 v10.16.0
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.6.0
github.com/gopherjs/gopherjs v1.17.2
github.com/gopherjs/jsbuiltin v0.0.0-20180426082241-50091555e127
github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0
Expand All @@ -33,7 +34,6 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/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/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
Expand Down
17 changes: 11 additions & 6 deletions internal/mock/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ type DB struct {
ID string
AllDocsFunc func(ctx context.Context, options driver.Options) (driver.Rows, error)
GetFunc func(ctx context.Context, docID string, options driver.Options) (*driver.Document, error)
CreateDocFunc func(ctx context.Context, doc interface{}, options driver.Options) (docID, rev string, err error)
PutFunc func(ctx context.Context, docID string, doc interface{}, options driver.Options) (rev string, err error)
DeleteFunc func(ctx context.Context, docID string, options driver.Options) (newRev string, err error)
StatsFunc func(ctx context.Context) (*driver.DBStats, error)
Expand All @@ -39,6 +38,17 @@ type DB struct {
CloseFunc func() error
}

// DocCreator is a stub for a [github.com/go-kivik/v4/driver.DocCreator].
type DocCreator struct {
DB
CreateDocFunc func(ctx context.Context, doc interface{}, options driver.Options) (docID, rev string, err error)
}

// CreateDoc calls db.CreateDocFunc
func (db *DocCreator) CreateDoc(ctx context.Context, doc interface{}, opts driver.Options) (string, string, error) {
return db.CreateDocFunc(ctx, doc, opts)
}

// SecurityDB is a stub for a driver.SecurityDB.
type SecurityDB struct {
DB
Expand All @@ -58,11 +68,6 @@ func (db *DB) AllDocs(ctx context.Context, options driver.Options) (driver.Rows,
return db.AllDocsFunc(ctx, options)
}

// CreateDoc calls db.CreateDocFunc
func (db *DB) CreateDoc(ctx context.Context, doc interface{}, opts driver.Options) (string, string, error) {
return db.CreateDocFunc(ctx, doc, opts)
}

// Put calls db.PutFunc
func (db *DB) Put(ctx context.Context, docID string, doc interface{}, opts driver.Options) (string, error) {
return db.PutFunc(ctx, docID, doc, opts)
Expand Down
1 change: 1 addition & 0 deletions mockdb/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func client() error {

type fullDB interface {
driver.DB
driver.DocCreator
driver.AttachmentMetaGetter
driver.BulkDocer
driver.BulkGetter
Expand Down
5 changes: 0 additions & 5 deletions x/fsdb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ func (d *db) Query(context.Context, string, string, driver.Options) (driver.Rows
return nil, notYetImplemented
}

func (d *db) CreateDoc(context.Context, interface{}, driver.Options) (docID, rev string, err error) {
// FIXME: Unimplemented
return "", "", notYetImplemented
}

func (d *db) Delete(context.Context, string, driver.Options) (newRev string, err error) {
// FIXME: Unimplemented
return "", notYetImplemented
Expand Down
17 changes: 0 additions & 17 deletions x/memorydb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,6 @@ func (d *db) Get(ctx context.Context, docID string, options driver.Options) (*dr
}, nil
}

func (d *db) CreateDoc(ctx context.Context, doc interface{}, _ driver.Options) (docID, rev string, err error) {
if exists, _ := d.client.DBExists(ctx, d.dbName, nil); !exists {
return "", "", statusError{status: http.StatusPreconditionFailed, error: errors.New("database does not exist")}
}
couchDoc, err := toCouchDoc(doc)
if err != nil {
return "", "", err
}
if id, ok := couchDoc["_id"].(string); ok {
docID = id
} else {
docID = randStr()
}
rev, err = d.Put(ctx, docID, doc, nil)
return docID, rev, err
}

func (d *db) Put(ctx context.Context, docID string, doc interface{}, _ driver.Options) (rev string, err error) {
if exists, _ := d.client.DBExists(ctx, d.dbName, nil); !exists {
return "", statusError{status: http.StatusPreconditionFailed, error: errors.New("database does not exist")}
Expand Down
83 changes: 0 additions & 83 deletions x/memorydb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,86 +558,3 @@ func TestDeleteDoc(t *testing.T) {
}(test)
}
}

func TestCreateDoc(t *testing.T) {
type cdTest struct {
Name string
DB *db
Doc interface{}
Expected map[string]interface{}
Error string
}
tests := []cdTest{
{
Name: "SimpleDoc",
Doc: map[string]interface{}{
"foo": "bar",
},
Expected: map[string]interface{}{
"_rev": "1-xxx",
"foo": "bar",
},
},
{
Name: "Deleted DB",
DB: func() *db {
c := setup(t, nil)
if err := c.CreateDB(context.Background(), "deleted0", nil); err != nil {
t.Fatal(err)
}
dbv, err := c.DB("deleted0", nil)
if err != nil {
t.Fatal(err)
}
if e := c.DestroyDB(context.Background(), "deleted0", nil); e != nil {
t.Fatal(e)
}
return dbv.(*db)
}(),
Doc: map[string]interface{}{
"foo": "bar",
},
Error: "database does not exist",
},
}
for _, test := range tests {
func(test cdTest) {
t.Run(test.Name, func(t *testing.T) {
db := test.DB
if db == nil {
db = setupDB(t)
}
docID, _, err := db.CreateDoc(context.Background(), test.Doc, nil)
var msg string
if err != nil {
msg = err.Error()
}
if msg != test.Error {
t.Errorf("Unexpected error: %s", msg)
}
if err != nil {
return
}
rows, err := db.Get(context.Background(), docID, kivik.Params(nil))
if err != nil {
t.Fatal(err)
}
var result map[string]interface{}
if e := json.NewDecoder(rows.Body).Decode(&result); e != nil {
t.Fatal(e)
}
if result["_id"].(string) != docID {
t.Errorf("Unexpected id. %s != %s", result["_id"].(string), docID)
}
delete(result, "_id")
if rev, ok := result["_rev"].(string); ok {
parts := strings.SplitN(rev, "-", 2)
result["_rev"] = parts[0] + "-xxx"
}
if d := testy.DiffInterface(test.Expected, result); d != nil {
t.Error(d)
}
})
}(test)
}
}
2 changes: 1 addition & 1 deletion x/memorydb/find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func TestFindDoc(t *testing.T) {
query: `{"selector":{}, "fields":["value","_rev"]}`,
db: func() *db {
db := setupDB(t)
if _, _, err := db.CreateDoc(context.Background(), map[string]string{"value": "foo"}, nil); err != nil {
if _, err := db.Put(context.Background(), "foo", map[string]string{"value": "foo"}, nil); err != nil {
t.Fatal(err)
}
return db
Expand Down
2 changes: 1 addition & 1 deletion x/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ func TestServer(t *testing.T) {
authUser: userAdmin,
wantStatus: http.StatusCreated,
target: &struct {
ID string `json:"id" validate:"required,hexadecimal,len=32"`
ID string `json:"id" validate:"required,uuid"`
Rev string `json:"rev" validate:"required,startswith=1-"`
OK bool `json:"ok" validate:"required,eq=true"`
}{},
Expand Down

0 comments on commit 0388fde

Please sign in to comment.