diff --git a/claim/claimstore.go b/claim/claimstore.go index 7967fbd4..82d51641 100644 --- a/claim/claimstore.go +++ b/claim/claimstore.go @@ -38,14 +38,14 @@ var ( // Store is a persistent store for claims. type Store struct { - backingStore *crud.BackingStore + backingStore crud.ManagedStore encrypt EncryptionHandler decrypt EncryptionHandler } // NewClaimStore creates a persistent store for claims using the specified -// backing key-blob store. -func NewClaimStore(store *crud.BackingStore, encrypt EncryptionHandler, decrypt EncryptionHandler) Store { +// backing datastore. +func NewClaimStore(store crud.ManagedStore, encrypt EncryptionHandler, decrypt EncryptionHandler) Store { if encrypt == nil { encrypt = noOpEncryptionHandler } @@ -81,7 +81,7 @@ var noOpEncryptionHandler = func(data []byte) ([]byte, error) { } // GetBackingStore returns the data store behind this claim store. -func (s Store) GetBackingStore() *crud.BackingStore { +func (s Store) GetBackingStore() crud.ManagedStore { return s.backingStore } diff --git a/credentials/credstore.go b/credentials/credstore.go index 6296b94c..d721052e 100644 --- a/credentials/credstore.go +++ b/credentials/credstore.go @@ -17,19 +17,19 @@ var ErrNotFound = errors.New("Credential set does not exist") // Store is a persistent store for credential sets. type Store struct { - backingStore *crud.BackingStore + backingStore crud.ManagedStore } // NewCredentialStore creates a persistent store for credential sets using the specified // backing key-blob store. -func NewCredentialStore(store *crud.BackingStore) Store { +func NewCredentialStore(store crud.ManagedStore) Store { return Store{ backingStore: store, } } // GetBackingStore returns the data store behind this credentials store. -func (s Store) GetBackingStore() *crud.BackingStore { +func (s Store) GetBackingStore() crud.ManagedStore { return s.backingStore } diff --git a/utils/crud/backingstore.go b/utils/crud/backingstore.go index eca69c5e..a6655091 100644 --- a/utils/crud/backingstore.go +++ b/utils/crud/backingstore.go @@ -1,13 +1,12 @@ package crud -var _ Store = &BackingStore{} +var _ ManagedStore = &BackingStore{} // BackingStore wraps another store that may have Connect/Close methods that // need to be called. // - Connect is called before a method when the connection is closed. // - Close is called after each method when AutoClose is true (default). type BackingStore struct { - // AutoClose specifies if the connection should be automatically // closed when done accessing the backing store. AutoClose bool @@ -75,6 +74,16 @@ func (s *BackingStore) autoClose() error { return nil } +func (s *BackingStore) Count(itemType string, group string) (int, error) { + handleClose, err := s.HandleConnect() + defer handleClose() + if err != nil { + return 0, err + } + + return s.datastore.Count(itemType, group) +} + func (s *BackingStore) List(itemType string, group string) ([]string, error) { handleClose, err := s.HandleConnect() defer handleClose() diff --git a/utils/crud/backingstore_test.go b/utils/crud/backingstore_test.go index c4f0ac5f..ad1ad36f 100644 --- a/utils/crud/backingstore_test.go +++ b/utils/crud/backingstore_test.go @@ -209,3 +209,25 @@ func TestBackingStore_ReadAll(t *testing.T) { }) } } + +func TestBackingStore_Count(t *testing.T) { + s := NewMockStore() + + count, err := s.Count(testItemType, testGroup) + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 for an empty datastore") + + err = s.Save(testItemType, testGroup, "key1", []byte("value1")) + require.NoError(t, err, "Save failed") + + count, err = s.Count(testItemType, testGroup) + require.NoError(t, err, "Count failed") + assert.Equal(t, 1, count, "Count should be 1 after adding an item") + + err = s.Delete(testItemType, "key1") + require.NoError(t, err, "Delete failed") + + count, err = s.Count(testItemType, testGroup) + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 after deleting the item") +} diff --git a/utils/crud/filesystem.go b/utils/crud/filesystem.go index 3eadc78c..4fef059b 100644 --- a/utils/crud/filesystem.go +++ b/utils/crud/filesystem.go @@ -31,6 +31,11 @@ type FileSystemStore struct { fileExtensions map[string]string } +func (s FileSystemStore) Count(itemType string, group string) (int, error) { + names, err := s.List(itemType, group) + return len(names), err +} + func (s FileSystemStore) List(itemType string, group string) ([]string, error) { if err := s.ensure(itemType); err != nil { return nil, err diff --git a/utils/crud/filesystem_test.go b/utils/crud/filesystem_test.go index 8d72709f..6aac7190 100644 --- a/utils/crud/filesystem_test.go +++ b/utils/crud/filesystem_test.go @@ -6,13 +6,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var _ Store = FileSystemStore{} func TestFilesystemStore(t *testing.T) { is := assert.New(t) - tmdir, err := ioutil.TempDir("", "duffle-test-") + tmdir, err := ioutil.TempDir("", "cnab-test-") is.NoError(err) defer os.RemoveAll(tmdir) s := NewFileSystemStore(tmdir, map[string]string{testItemType: ".json"}) @@ -30,3 +31,28 @@ func TestFilesystemStore(t *testing.T) { is.NoError(err) is.Len(list, 0) } + +func TestFileSystemStore_Count(t *testing.T) { + tmdir, err := ioutil.TempDir("", "cnab-test-") + require.NoError(t, err) + defer os.RemoveAll(tmdir) + s := NewFileSystemStore(tmdir, map[string]string{testItemType: ".json"}) + + count, err := s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 for an empty datastore") + + err = s.Save(testItemType, "", "key1", []byte("value1")) + require.NoError(t, err, "Save failed") + + count, err = s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 1, count, "Count should be 1 after adding an item") + + err = s.Delete(testItemType, "key1") + require.NoError(t, err, "Delete failed") + + count, err = s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 after deleting the item") +} diff --git a/utils/crud/mock_store.go b/utils/crud/mock_store.go index e816792e..748a3f7c 100644 --- a/utils/crud/mock_store.go +++ b/utils/crud/mock_store.go @@ -89,6 +89,11 @@ func (s MockStore) key(itemType string, id string) string { return path.Join(itemType, id) } +func (s MockStore) Count(itemType string, group string) (int, error) { + names, err := s.List(itemType, group) + return len(names), err +} + func (s MockStore) List(itemType string, group string) ([]string, error) { if s.ListMock != nil { return s.ListMock(itemType, group) diff --git a/utils/crud/mock_store_test.go b/utils/crud/mock_store_test.go index f4df78a8..f5f709e4 100644 --- a/utils/crud/mock_store_test.go +++ b/utils/crud/mock_store_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMockStoreWithGroups(t *testing.T) { @@ -49,3 +50,25 @@ func TestMockStoreWithoutGroups(t *testing.T) { is.NoError(err) is.Equal(groups, []string{"test"}) } + +func TestMockStore_Count(t *testing.T) { + s := NewMockStore() + + count, err := s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 for an empty datastore") + + err = s.Save(testItemType, "", "key1", []byte("value1")) + require.NoError(t, err, "Save failed") + + count, err = s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 1, count, "Count should be 1 after adding an item") + + err = s.Delete(testItemType, "key1") + require.NoError(t, err, "Delete failed") + + count, err = s.Count(testItemType, "") + require.NoError(t, err, "Count failed") + assert.Equal(t, 0, count, "Count should be 0 after deleting the item") +} diff --git a/utils/crud/mongodb.go b/utils/crud/mongodb.go index cd97b728..df92cc3f 100644 --- a/utils/crud/mongodb.go +++ b/utils/crud/mongodb.go @@ -69,6 +69,19 @@ func (s *mongoDBStore) getCollection(itemType string) *mgo.Collection { return c } +func (s *mongoDBStore) Count(itemType string, group string) (int, error) { + collection := s.getCollection(itemType) + + var query map[string]string + if group != "" { + query = map[string]string{ + "group": group, + } + } + n, err := collection.Find(query).Count() + return n, wrapErr(err) +} + func (s *mongoDBStore) List(itemType string, group string) ([]string, error) { collection := s.getCollection(itemType) diff --git a/utils/crud/store.go b/utils/crud/store.go index f1294c99..6758e105 100644 --- a/utils/crud/store.go +++ b/utils/crud/store.go @@ -2,12 +2,41 @@ package crud // Store is a simplified interface to a key-blob store supporting CRUD operations. type Store interface { + // Count the number of items of the optional type and group. + Count(itemType string, group string) (int, error) + + // List the names of the items of the optional type and group. List(itemType string, group string) ([]string, error) + + // Save an item's data using the specified name with optional metadata + // identifying it with an item type and group. Save(itemType string, group string, name string, data []byte) error + + // Read the data for a named item of the optional type. Read(itemType string, name string) ([]byte, error) + + // Delete a named item of the optional type. Delete(itemType string, name string) error } +// ManagedStore is a wrapped crud.Store with a managed connection lifecycle. +type ManagedStore interface { + // Store is the underlying datastore. + Store + + // ReadAll retrieves all the items with the optional item type. + ReadAll(itemType string, group string) ([][]byte, error) + + // GetDataStore returns the datastore managed by this instance. + GetDataStore() Store + + // HandleConnect connects if necessary, returning a function to close the + // connection. This close function may be a no-op when connection was + // already established and this call to Connect isn't managing the + // connection. + HandleConnect() (func() error, error) +} + // HasConnect indicates that a struct must be initialized using the Connect // method before the interface's methods are called. type HasConnect interface {