move profile datastore into workflow datastore instead

This commit is contained in:
Victor Vrantchan
2016-05-14 17:20:00 -04:00
parent 4e36e78575
commit 6e86a88ade
9 changed files with 165 additions and 457 deletions

View File

@@ -1,135 +0,0 @@
package profile
import (
"database/sql"
"fmt"
"strings"
"time"
kitlog "github.com/go-kit/kit/log"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq" // postgres driver
"github.com/pkg/errors"
)
// ErrExists is returned if a profile already exists
var ErrExists = errors.New("profile already exists. each profile must have a unique identifier")
// Datastore manages profiles in a database
type Datastore interface {
// Add adds a profile to the datastore,
// If a profile already exists, an error will be returned
Add(pr *Profile) (*Profile, error)
// Profiles can query the datastore for one or more profiles
// and accepts one or more params as filters
// Example: Profiles(Identifier{"com.example.id")}
Profiles(params ...interface{}) ([]Profile, error)
}
// whereer is for building args passed into Profiles()
type whereer interface {
where() string
}
// Identifier is a PayloadIdentifier filter which can be passed as a param to Profiles()
type Identifier struct{ PayloadIdentifier string }
func (p Identifier) where() string {
return fmt.Sprintf("identifier='%s'", p.PayloadIdentifier)
}
// UUID is a Profile UUID filter which can be passed as a param to Profiles()
type UUID struct{ UUID string }
func (p UUID) where() string {
return fmt.Sprintf("profile_uuid='%s'", p.UUID)
}
type pgStore struct {
*sqlx.DB
}
func (store pgStore) Add(prf *Profile) (*Profile, error) {
err := store.QueryRow(addProfileStmt, prf.PayloadIdentifier, prf.ProfileData).Scan(&prf.UUID)
if err == sql.ErrNoRows {
return nil, ErrExists
}
if err != nil {
return nil, errors.Wrap(err, "pgStore add profile")
}
return prf, nil
}
func (store pgStore) Profiles(params ...interface{}) ([]Profile, error) {
stmt := selectProfilesStmt
var where []string
for _, param := range params {
if f, ok := param.(whereer); ok {
where = append(where, f.where())
}
}
if len(where) != 0 {
whereFilter := strings.Join(where, ",")
stmt = fmt.Sprintf("%s WHERE %s", selectProfilesStmt, whereFilter)
}
var profiles []Profile
err := store.Select(&profiles, stmt)
if err != nil {
return nil, errors.Wrap(err, "pgStore Profiles")
}
return profiles, nil
}
//NewDB creates a Datastore
func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
switch driver {
case "postgres":
db, err := sqlx.Open(driver, conn)
if err != nil {
return nil, errors.Wrap(err, "profile datastore")
}
var dbError error
maxAttempts := 20
for attempts := 1; attempts <= maxAttempts; attempts++ {
dbError = db.Ping()
if dbError == nil {
break
}
logger.Log("msg", fmt.Sprintf("could not connect to postgres: %v", dbError))
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
return nil, errors.Wrap(dbError, "profile datastore")
}
migrate(db)
return pgStore{DB: db}, nil
default:
return nil, errors.New("unknown driver")
}
}
// sql statements
var (
addProfileStmt = `INSERT INTO profiles (identifier, profile_data) VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT profiles_identifier_key DO NOTHING
RETURNING profile_uuid;`
selectProfilesStmt = `SELECT profile_uuid, identifier FROM profiles`
)
func migrate(db *sqlx.DB) {
schema := `
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS profiles (
profile_uuid uuid PRIMARY KEY
DEFAULT uuid_generate_v4(),
identifier text UNIQUE NOT NULL,
profile_data bytea
);`
db.MustExec(schema)
}

View File

@@ -1,225 +0,0 @@
package profile
import (
"fmt"
"math/rand"
"os"
"reflect"
"testing"
"testing/quick"
"time"
"github.com/go-kit/kit/log"
"github.com/jmoiron/sqlx"
)
func TestNewDB(t *testing.T) {
defer teardown()
logger := log.NewLogfmtLogger(os.Stderr)
_, err := NewDB("postgres", testConn, logger)
if err != nil {
t.Fatal(err)
}
}
// TestAdd generates a bunch of new profiles
// and adds them to the database.
// The test checks that the profile stored is the same as the one returned.
func TestDatastoreAdd(t *testing.T) {
ds := datastore(t)
defer teardown()
assertion := func(pf Profile) bool {
newPrf, err := ds.Add(&pf)
if err != nil {
t.Fatal(err)
return false
}
if newPrf.UUID == "" || newPrf.PayloadIdentifier != pf.PayloadIdentifier {
return false
}
return true
}
if err := quick.Check(assertion, nil); err != nil {
t.Error(err)
}
}
func TestDatastoreProfiles(t *testing.T) {
ds := datastore(t)
defer teardown()
// test with empty table
testReturnNoArgs(t, ds, 0)
// insert some profiles and retrieve them
var profileCount = 5
addRandomProfiles(profileCount, ds, t)
profiles, err := ds.Profiles()
if err != nil {
t.Fatal(err)
}
returnedCount := len(profiles)
if returnedCount != profileCount {
t.Fatal("expected", profileCount, "got", returnedCount)
}
// build data for remaining tests
var allIdentifiers []string
var allUUIDs []string
for _, p := range profiles {
allIdentifiers = append(allIdentifiers, p.PayloadIdentifier)
allUUIDs = append(allUUIDs, p.UUID)
}
// return everything
testReturnNoArgs(t, ds, profileCount)
// test nonexisting profile
testReturnNone(t, ds)
// test returning a unique id
testReturnOneByIdentifier(t, ds, allIdentifiers[0])
// test returning a unique profile by UUID
testReturnOneByUUID(t, ds, allUUIDs[0])
}
func testReturnNoArgs(t *testing.T, ds Datastore, expectCount int) {
profiles, err := ds.Profiles()
if err != nil {
t.Fatal(err)
}
returnedCount := len(profiles)
if returnedCount != expectCount {
t.Fatal("expected", expectCount, "got", returnedCount)
}
}
func testReturnNone(t *testing.T, ds Datastore) {
identifier := "com.example.does.not.exist"
profiles, err := ds.Profiles(Identifier{identifier})
if err != nil {
t.Fatal(err)
}
returnedCount := len(profiles)
if returnedCount != 0 {
t.Fatal("expected", 0, "got", returnedCount)
}
}
func testReturnOneByIdentifier(t *testing.T, ds Datastore, id string) {
profiles, err := ds.Profiles(Identifier{id})
if err != nil {
t.Fatal(err)
}
returnedCount := len(profiles)
if returnedCount != 1 {
t.Fatal("expected", 1, "got", returnedCount)
}
returnedIdentifier := profiles[0].PayloadIdentifier
if returnedIdentifier != id {
t.Fatal("expected", id, "got", returnedIdentifier)
}
}
func testReturnOneByUUID(t *testing.T, ds Datastore, uuid string) {
profiles, err := ds.Profiles(UUID{uuid})
if err != nil {
t.Fatal(err)
}
returnedCount := len(profiles)
if returnedCount != 1 {
t.Fatal("expected", 1, "got", returnedCount)
}
returnedUUID := profiles[0].UUID
if returnedUUID != uuid {
t.Fatal("expected", uuid, "got", returnedUUID)
}
}
func addRandomProfiles(num int, ds Datastore, t *testing.T) {
for i := 1; i <= num; i++ {
pf := randomProfile()
_, err := ds.Add(&pf)
if err != nil {
t.Fatal(err)
}
}
}
// helpers
func randomProfile() Profile {
vrf, ok := quick.Value(reflect.TypeOf(Profile{}), rand.New(rand.NewSource(1)))
if !ok {
panic("randomProfile: no value")
}
if f, ok := vrf.Interface().(Profile); ok {
return f
}
return Profile{}
}
// implement quick.Generator for Profile
func (pf Profile) Generate(rand *rand.Rand, size int) reflect.Value {
a := RandomString(16)
b := RandomString(16)
c := RandomString(16)
randomIdnetifier := fmt.Sprintf("%v.%v.%v", a, b, c)
randomProfile := Profile{
PayloadIdentifier: randomIdnetifier,
ProfileData: `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KICAgIDxrZXk+UGF5bG9hZENvbnRlbnQ8L2tleT4KICAgIDxhcnJheT4KICAgICAgICA8ZGljdD4KICAgICAgICAgICAgPGtleT5QYXlsb2FkQ29udGVudDwva2V5PgogICAgICAgICAgICA8ZGljdD4KICAgICAgICAgICAgICAgIDxrZXk+Y29tLmFwcGxlLlNldHVwQXNzaXN0YW50PC9rZXk+CiAgICAgICAgICAgICAgICA8ZGljdD4KICAgICAgICAgICAgICAgICAgICA8a2V5PlNldC1PbmNlPC9rZXk+CiAgICAgICAgICAgICAgICAgICAgPGFycmF5PgogICAgICAgICAgICAgICAgICAgICAgICA8ZGljdD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrZXk+bWN4X2RhdGFfdGltZXN0YW1wPC9rZXk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGF0ZT4yMDE0LTEwLTI5VDE3OjIwOjEwWjwvZGF0ZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrZXk+bWN4X3ByZWZlcmVuY2Vfc2V0dGluZ3M8L2tleT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaWN0PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrZXk+RGlkU2VlQ2xvdWRTZXR1cDwva2V5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cnVlLz4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8a2V5Pkdlc3R1cmVNb3ZpZVNlZW48L2tleT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3RyaW5nPm5vbmU8L3N0cmluZz4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8a2V5Pkxhc3RTZWVuQ2xvdWRQcm9kdWN0VmVyc2lvbjwva2V5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxzdHJpbmc+MTAuMTEuMjwvc3RyaW5nPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrZXk+TGFzdFNlZW5CdWRkeUJ1aWxkVmVyc2lvbjwva2V5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxzdHJpbmc+MTVDNTA8L3N0cmluZz4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGljdD4KICAgICAgICAgICAgICAgICAgICAgICAgPC9kaWN0PgogICAgICAgICAgICAgICAgICAgIDwvYXJyYXk+CiAgICAgICAgICAgICAgICA8L2RpY3Q+CiAgICAgICAgICAgIDwvZGljdD4KICAgICAgICAgICAgPGtleT5QYXlsb2FkRW5hYmxlZDwva2V5PgogICAgICAgICAgICA8dHJ1ZS8+CiAgICAgICAgICAgIDxrZXk+UGF5bG9hZElkZW50aWZpZXI8L2tleT4KICAgICAgICAgICAgPHN0cmluZz5lZHUucHJhdHQuc3VwcHJlc3NfaWNsb3VkX2Fzc3Q8L3N0cmluZz4KICAgICAgICAgICAgPGtleT5QYXlsb2FkVHlwZTwva2V5PgogICAgICAgICAgICA8c3RyaW5nPmNvbS5hcHBsZS5NYW5hZ2VkQ2xpZW50LnByZWZlcmVuY2VzPC9zdHJpbmc+CiAgICAgICAgICAgIDxrZXk+UGF5bG9hZFVVSUQ8L2tleT4KICAgICAgICAgICAgPHN0cmluZz41ZTEwZjM0OC05MjNiLTQzOGEtOWI4Ny1mYTk3OGU4NmUxMWE8L3N0cmluZz4KICAgICAgICAgICAgPGtleT5QYXlsb2FkVmVyc2lvbjwva2V5PgogICAgICAgICAgICA8aW50ZWdlcj4xPC9pbnRlZ2VyPgogICAgICAgIDwvZGljdD4KICAgIDwvYXJyYXk+CiAgICA8a2V5PlBheWxvYWREZXNjcmlwdGlvbjwva2V5PgogICAgPHN0cmluZz5Db25maWd1cmVzIGNvbS5hcHBsZS5TZXR1cEFzc2lzdGFudDwvc3RyaW5nPgogICAgPGtleT5QYXlsb2FkRGlzcGxheU5hbWU8L2tleT4KICAgIDxzdHJpbmc+aUNsb3VkIFNldHVwQXNzaXN0YW50IENvbmZpZ3VyYXRpb248L3N0cmluZz4KICAgIDxrZXk+UGF5bG9hZElkZW50aWZpZXI8L2tleT4KICAgIDxzdHJpbmc+Y29tLmdpdGh1Yi5ncmVnbmVhZ2xlLnN1cHByZXNzX2ljbG91ZF9hc3N0PC9zdHJpbmc+CiAgICA8a2V5PlBheWxvYWRPcmdhbml6YXRpb248L2tleT4KICAgIDxzdHJpbmc+PC9zdHJpbmc+CiAgICA8a2V5PlBheWxvYWRSZW1vdmFsRGlzYWxsb3dlZDwva2V5PgogICAgPGZhbHNlLz4KICAgIDxrZXk+UGF5bG9hZFNjb3BlPC9rZXk+CiAgICA8c3RyaW5nPlN5c3RlbTwvc3RyaW5nPgogICAgPGtleT5QYXlsb2FkVHlwZTwva2V5PgogICAgPHN0cmluZz5Db25maWd1cmF0aW9uPC9zdHJpbmc+CiAgICA8a2V5PlBheWxvYWRVVUlEPC9rZXk+CiAgICA8c3RyaW5nPmU4MWY1MWMyLTExODAtNGRlMC05NGNkLTMxNTNhYTQxMzg3Njwvc3RyaW5nPgogICAgPGtleT5QYXlsb2FkVmVyc2lvbjwva2V5PgogICAgPGludGVnZXI+MTwvaW50ZWdlcj4KPC9kaWN0Pgo8L3BsaXN0Pgo=`,
}
return reflect.ValueOf(randomProfile)
}
func RandomString(strlen int) string {
rand.Seed(time.Now().UTC().UnixNano())
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
result := make([]byte, strlen)
for i := 0; i < strlen; i++ {
result[i] = chars[rand.Intn(len(chars))]
}
return string(result)
}
var (
testConn = "user=micromdm password=micromdm dbname=micromdm sslmode=disable"
)
func datastore(t *testing.T) Datastore {
logger := log.NewLogfmtLogger(os.Stderr)
ds, err := NewDB("postgres", testConn, logger)
if err != nil {
t.Fatal(err)
}
return ds
}
func setup() {
db, err := sqlx.Open("postgres", testConn)
if err != nil {
panic(err)
}
migrate(db)
defer db.Close()
}
func teardown() {
db, err := sqlx.Open("postgres", testConn)
if err != nil {
panic(err)
}
drop := `
DROP TABLE IF EXISTS profiles;
`
db.MustExec(drop)
defer db.Close()
}

View File

@@ -1,9 +1,7 @@
package workflow
import (
"database/sql"
"fmt"
"strings"
"time"
kitlog "github.com/go-kit/kit/log"
@@ -12,113 +10,32 @@ import (
"github.com/pkg/errors"
)
// ErrExists is returned if a workflow already exists
var ErrExists = errors.New("workflow already exists. each workflow must have a unique name")
// Profile is configuration profile in a workflow
type Profile struct {
UUID string
}
// Workflow describes a workflow that a device will execute
// A workflow contains a list of configuration profiles,
// Applications and included workflows
type Workflow struct {
UUID string `json:"uuid" db:"workflow_uuid"`
Name string `json:"name" db:"name"`
Profiles []Profile `json:"profiles"`
// Applications []application
// IncludedWorkflows []Workflow
}
// Datastore manages interactions of workflows in a database
type Datastore interface {
// Create adds a new workflow to the datastore
Create(wf *Workflow) (*Workflow, error)
CreateWorkflow(wf *Workflow) (*Workflow, error)
Workflows(params ...interface{}) ([]Workflow, error)
// CreateProfile adds a new profile to the datastore,
// If a profile already exists, an error will be returned
CreateProfile(pr *Profile) (*Profile, error)
// Profiles can query the datastore for one or more profiles
// and accepts one or more params as filters
// Example: Profiles(Identifier{"com.example.id")}
Profiles(params ...interface{}) ([]Profile, error)
}
type pgStore struct {
*sqlx.DB
}
// Create stores a new workflow in Postgres
func (store pgStore) Create(wf *Workflow) (*Workflow, error) {
err := store.QueryRow(createWorkflowStmt, wf.Name).Scan(&wf.UUID)
if err == sql.ErrNoRows {
return nil, ErrExists
}
if err != nil {
return nil, errors.Wrap(err, "pgStore create workflow")
}
profiles := wf.Profiles
if err := store.addProfiles(wf.UUID, profiles...); err != nil {
return nil, err
}
return wf, nil
}
func (store pgStore) addProfiles(wfUUID string, profiles ...Profile) error {
if len(profiles) == 0 {
return nil
}
for _, prf := range profiles {
if err := store.addProfile(wfUUID, prf.UUID); err != nil {
return err
}
}
return nil
}
func (store pgStore) addProfile(wfUUID, pfUUID string) error {
addProfileStmt := `INSERT INTO workflow_profile (workflow_uuid, profile_uuid) VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT workflow_profile_pkey DO NOTHING;`
_, err := store.Exec(addProfileStmt, wfUUID, pfUUID)
if err != nil {
return errors.Wrap(err, "pgStore add profile to workflow")
}
return nil
}
func (store pgStore) Workflows(params ...interface{}) ([]Workflow, error) {
stmt := selectWorkflowsStmt
var where []string
for _, param := range params {
if f, ok := param.(whereer); ok {
where = append(where, f.where())
}
}
if len(where) != 0 {
whereFilter := strings.Join(where, ",")
stmt = fmt.Sprintf("%s WHERE %s", selectWorkflowsStmt, whereFilter)
}
var workflows []Workflow
err := store.Select(&workflows, stmt)
if err != nil {
return nil, errors.Wrap(err, "pgStore Workflows")
}
return workflows, nil
}
// whereer is for building args passed into Profiles()
// whereer is for building args passed into a method which finds resources
type whereer interface {
where() string
}
// sql statements
var (
createWorkflowStmt = `INSERT INTO workflows (name) VALUES ($1)
ON CONFLICT ON CONSTRAINT workflows_name_key DO NOTHING
RETURNING workflow_uuid;`
selectWorkflowsStmt = `SELECT workflow_uuid, name FROM profiles`
)
//NewDB creates a Datastore
func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
switch driver {

View File

@@ -1,4 +1,4 @@
package profile
package workflow
// Profile is an Apple Configuration profile
type Profile struct {

52
workflow/profile_store.go Normal file
View File

@@ -0,0 +1,52 @@
package workflow
import (
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
)
// sql statements
var (
addProfileStmt = `INSERT INTO profiles (identifier, profile_data) VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT profiles_identifier_key DO NOTHING
RETURNING profile_uuid;`
selectProfilesStmt = `SELECT profile_uuid, identifier FROM profiles`
)
func (store pgStore) CreateProfile(prf *Profile) (*Profile, error) {
err := store.QueryRow(addProfileStmt, prf.PayloadIdentifier, prf.ProfileData).Scan(&prf.UUID)
if err == sql.ErrNoRows {
return nil, ErrExists
}
if err != nil {
return nil, errors.Wrap(err, "pgStore add profile")
}
return prf, nil
}
func (store pgStore) Profiles(params ...interface{}) ([]Profile, error) {
stmt := selectProfilesStmt
var where []string
for _, param := range params {
if f, ok := param.(whereer); ok {
where = append(where, f.where())
}
}
if len(where) != 0 {
whereFilter := strings.Join(where, ",")
stmt = fmt.Sprintf("%s WHERE %s", selectProfilesStmt, whereFilter)
}
var profiles []Profile
err := store.Select(&profiles, stmt)
if err != nil {
return nil, errors.Wrap(err, "pgStore Profiles")
}
return profiles, nil
}

View File

@@ -0,0 +1 @@
package workflow

17
workflow/workflow.go Normal file
View File

@@ -0,0 +1,17 @@
package workflow
import "errors"
// ErrExists is returned if a workflow already exists
var ErrExists = errors.New("workflow already exists. each workflow must have a unique name")
// Workflow describes a workflow that a device will execute
// A workflow contains a list of configuration profiles,
// Applications and included workflows
type Workflow struct {
UUID string `json:"uuid" db:"workflow_uuid"`
Name string `json:"name" db:"name"`
Profiles []Profile `json:"profiles"`
// Applications []application
// IncludedWorkflows []Workflow
}

View File

@@ -0,0 +1,81 @@
package workflow
import (
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
)
// sql statements
var (
createWorkflowStmt = `INSERT INTO workflows (name) VALUES ($1)
ON CONFLICT ON CONSTRAINT workflows_name_key DO NOTHING
RETURNING workflow_uuid;`
selectWorkflowsStmt = `SELECT workflow_uuid, name FROM profiles`
)
// Create stores a new workflow in Postgres
func (store pgStore) CreateWorkflow(wf *Workflow) (*Workflow, error) {
err := store.QueryRow(createWorkflowStmt, wf.Name).Scan(&wf.UUID)
if err == sql.ErrNoRows {
return nil, ErrExists
}
if err != nil {
return nil, errors.Wrap(err, "pgStore create workflow")
}
profiles := wf.Profiles
if err := store.addProfiles(wf.UUID, profiles...); err != nil {
return nil, err
}
return wf, nil
}
func (store pgStore) addProfiles(wfUUID string, profiles ...Profile) error {
if len(profiles) == 0 {
return nil
}
for _, prf := range profiles {
if err := store.addProfile(wfUUID, prf.UUID); err != nil {
return err
}
}
return nil
}
func (store pgStore) addProfile(wfUUID, pfUUID string) error {
addProfileStmt := `INSERT INTO workflow_profile (workflow_uuid, profile_uuid) VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT workflow_profile_pkey DO NOTHING;`
_, err := store.Exec(addProfileStmt, wfUUID, pfUUID)
if err != nil {
return errors.Wrap(err, "pgStore add profile to workflow")
}
return nil
}
func (store pgStore) Workflows(params ...interface{}) ([]Workflow, error) {
stmt := selectWorkflowsStmt
var where []string
for _, param := range params {
if f, ok := param.(whereer); ok {
where = append(where, f.where())
}
}
if len(where) != 0 {
whereFilter := strings.Join(where, ",")
stmt = fmt.Sprintf("%s WHERE %s", selectWorkflowsStmt, whereFilter)
}
var workflows []Workflow
err := store.Select(&workflows, stmt)
if err != nil {
return nil, errors.Wrap(err, "pgStore Workflows")
}
return workflows, nil
}

View File

@@ -84,7 +84,7 @@ func TestDatastoreCreate(t *testing.T) {
}
for _, tt := range createTests {
_, err := ds.Create(tt.in)
_, err := ds.CreateWorkflow(tt.in)
if !tt.shouldErr {
checkErr(err)
}