From 1d2e928fae92f1f16d3f184f32ca97af6c82549c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Thu, 28 Apr 2022 17:51:41 +0200 Subject: [PATCH] Connect external and internal CRS commponents --- compose/crs/crs.go | 109 ++++------- compose/crs/data.go | 166 ++++++++-------- compose/crs/driver.go | 145 ++++++++++---- compose/crs/main_test.go | 94 --------- compose/crs/model.go | 160 ++++++++------- compose/crs/module_management_test.go | 77 -------- compose/crs/noop/noop.go | 66 ------- compose/crs/store_management_test.go | 131 ------------- compose/types/compose_record_store.go | 1 + compose/types/module.go | 1 - store/adapters/rdbms/crs/connect.go | 118 +++++++++++ store/adapters/rdbms/drivers/mysql/connect.go | 30 +++ tests/crs/crs_test.go | 185 ++++++++++++++++++ 13 files changed, 649 insertions(+), 634 deletions(-) delete mode 100644 compose/crs/main_test.go delete mode 100644 compose/crs/module_management_test.go delete mode 100644 compose/crs/noop/noop.go delete mode 100644 compose/crs/store_management_test.go create mode 100644 store/adapters/rdbms/crs/connect.go create mode 100644 tests/crs/crs_test.go diff --git a/compose/crs/crs.go b/compose/crs/crs.go index 57dcf89dc..7ffbbefd2 100644 --- a/compose/crs/crs.go +++ b/compose/crs/crs.go @@ -6,29 +6,23 @@ import ( "github.com/cortezaproject/corteza-server/compose/crs/capabilities" "github.com/cortezaproject/corteza-server/pkg/data" + "go.uber.org/zap" ) type ( // the core struct that outlines the compose record store facility composeRecordStore struct { - drivers []driver - stores map[uint64]*storeWrap + stores map[uint64]StoreConnection // Indexed by corresponding storeID models map[uint64]data.ModelSet - primary *storeWrap - } + primary StoreConnection - storeWrap struct { - store connFunc - driver driver - dsn string - capabilities capabilities.Set + logger *zap.Logger + inDev bool } - connFunc func(ctx context.Context) (Store, error) - crsDefiner interface { ComposeRecordStoreID() uint64 StoreDSN() string @@ -41,64 +35,48 @@ const ( ) // ComposeRecordStore initializes a fresh record store where the given store serves as the default -func ComposeRecordStore(ctx context.Context, primary crsDefiner, d driver, drivers ...driver) (*composeRecordStore, error) { - drivers = append([]driver{d}, drivers...) - +func ComposeRecordStore(ctx context.Context, log *zap.Logger, inDev bool, primary crsDefiner, stores ...crsDefiner) (*composeRecordStore, error) { crs := &composeRecordStore{ - drivers: drivers, - stores: make(map[uint64]*storeWrap), + stores: make(map[uint64]StoreConnection), models: make(map[uint64]data.ModelSet), primary: nil, + + logger: log, + inDev: inDev, } - d = crs.getDriver(primary) - if d == nil { - return nil, fmt.Errorf("could not add default store: no supported driver found") + var err error + + crs.primary, err = connect(ctx, log, primary, inDev) + if err != nil { + return nil, err } - crs.primary = &storeWrap{ - store: storeConnWrap(d, primary), - driver: d, - dsn: primary.StoreDSN(), - capabilities: primary.Capabilities(), - } - - return crs, nil + return crs, crs.AddStore(ctx, stores...) } // AddStore registers the given store definitions as compose record stores -func (crs *composeRecordStore) AddStore(ctx context.Context, definers ...crsDefiner) error { +func (crs *composeRecordStore) AddStore(ctx context.Context, definers ...crsDefiner) (err error) { for _, definer := range definers { - if crs.stores[definer.ComposeRecordStoreID()] != nil { - return fmt.Errorf("can not add compose record store %d: already defined", definer.ComposeRecordStoreID()) - } - - d := crs.getDriver(definer) - if d == nil { - return fmt.Errorf("could not add store %d: no supported driver found", definer.ComposeRecordStoreID()) - } - - crs.stores[definer.ComposeRecordStoreID()] = &storeWrap{ - store: storeConnWrap(d, definer), - driver: d, - dsn: definer.StoreDSN(), - capabilities: definer.Capabilities(), + crs.stores[definer.ComposeRecordStoreID()], err = connect(ctx, crs.logger, definer, crs.inDev) + if err != nil { + return } } + return nil } // RemoveStore removes the given store definition as a compose record store -func (crs *composeRecordStore) RemoveStore(ctx context.Context, storeIDs ...uint64) (err error) { - for _, storeID := range storeIDs { +func (crs *composeRecordStore) RemoveStore(ctx context.Context, storeID uint64, storeIDs ...uint64) (err error) { + for _, storeID := range append(storeIDs, storeID) { s := crs.stores[storeID] if s == nil { return fmt.Errorf("can not remove compose record store %d: store does not exist", storeID) } - // Any potential driver cleanup - err = s.driver.Close(ctx, s.dsn) - if err != nil { + // Potential cleanups + if err = s.Close(ctx); err != nil { return } @@ -110,6 +88,7 @@ func (crs *composeRecordStore) RemoveStore(ctx context.Context, storeIDs ...uint } // --- +// Utilities func (crs *composeRecordStore) getModel(store uint64, ident string) *data.Model { for _, model := range crs.models[store] { @@ -122,26 +101,23 @@ func (crs *composeRecordStore) getModel(store uint64, ident string) *data.Model } // getStore returns a store for the given identifier/capabilities combination -func (crs *composeRecordStore) getStore(ctx context.Context, storeID uint64, cc ...capabilities.Capability) (store Store, can capabilities.Set, err error) { +func (crs *composeRecordStore) getStore(ctx context.Context, storeID uint64, cc ...capabilities.Capability) (store StoreConnection, can capabilities.Set, err error) { err = func() error { // get the requested store - var wrap *storeWrap if storeID == defaultStoreID { - wrap = crs.primary + store = crs.primary } else { - wrap = crs.stores[storeID] + store = crs.stores[storeID] } - if wrap == nil { + if store == nil { return fmt.Errorf("could not get store %d: store does not exist", storeID) } // check if store supports requested capabilities - if !wrap.capabilities.IsSuperset(cc...) { - return fmt.Errorf("store does not support requested capabilities: %v", capabilities.Set(cc).Diff(wrap.capabilities)) + if !store.Can(cc...) { + return fmt.Errorf("store does not support requested capabilities: %v", capabilities.Set(cc).Diff(store.Capabilities())) } - - store, err = wrap.store(ctx) - can = wrap.capabilities + can = store.Capabilities() return nil }() @@ -152,22 +128,3 @@ func (crs *composeRecordStore) getStore(ctx context.Context, storeID uint64, cc return } - -// getDriver returns a driver which can be used with the given store -func (crs *composeRecordStore) getDriver(def crsDefiner) driver { - for _, d := range crs.drivers { - if !d.Can(def.StoreDSN(), def.Capabilities()...) { - continue - } - - return d - } - - return nil -} - -func storeConnWrap(d driver, def crsDefiner) connFunc { - return func(ctx context.Context) (Store, error) { - return d.Store(ctx, def.StoreDSN()) - } -} diff --git a/compose/crs/data.go b/compose/crs/data.go index e34d2d9d0..2571941da 100644 --- a/compose/crs/data.go +++ b/compose/crs/data.go @@ -6,107 +6,110 @@ import ( "github.com/cortezaproject/corteza-server/compose/crs/capabilities" "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/pkg/data" ) +// ComposeRecordCreate creates the given records for the given module func (crs *composeRecordStore) ComposeRecordCreate(ctx context.Context, module *types.Module, records ...*types.Record) (err error) { + if !module.Store.Partitioned { + return fmt.Errorf("only partitioned modules work right now") + } + // Determine required capabilities requiredCap := capabilities.CreateCapabilities(module.Store.Capabilities...) // Determine store - var s Store + var s StoreConnection if s, _, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID, requiredCap...); err != nil { return err } - // Prepare data + // Get model model := crs.lookupModel(module) if model == nil { return crs.modelNotFoundErr(module) } - aux := make([]Getter, len(records)) - for i, r := range records { - aux[i] = Getter(r) - } - return s.CreateRecords(ctx, model, aux...) + return s.CreateRecords(ctx, model, records...) } +// @todo... func (crs *composeRecordStore) ComposeRecordSearch(ctx context.Context, module *types.Module, filter *types.RecordFilter) (records types.RecordSet, outFilter *types.RecordFilter, err error) { - // Determine requiredCap we'll need - requiredCap := capabilities.SearchCapabilities(module.Store.Capabilities...).Union(crs.recFilterCapabilities(filter)) - - // Connect to datasource - var s Store - var cc capabilities.Set - _ = cc - s, cc, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID, requiredCap...) - if err != nil { - return - } - - // Prepare data - model := crs.lookupModel(module) - if model == nil { - return nil, nil, crs.modelNotFoundErr(module) - } - - loader, err := s.SearchRecords(ctx, model, nil) - if err != nil { - return - } - - limit := int(filter.Limit) - if limit == 0 { - limit = 10 - } - - auxCC := make([]Setter, limit) - for i := range auxCC { - auxCC[i] = &types.Record{} - } - - var ok bool - _ = ok - for loader.More() && len(records) < int(limit) { - _, err = loader.Load(model, auxCC) - if err != nil { - return - } - - auxRecords, err := crs.extractRecords(model, auxCC...) - if err != nil { - return nil, nil, err - } - - if !capabilities.AccessControlCapabilities().IsSubset(cc...) && filter.Check != nil { - for _, r := range auxRecords { - if r == nil { - continue - } - if ok, err = filter.Check(r); err != nil { - return nil, nil, err - } else if !ok { - continue - } - - records = append(records, r) - } - } else { - for _, r := range auxRecords { - if r == nil { - break - } - records = append(records, r) - } - } - } - return + // // Determine requiredCap we'll need + // requiredCap := capabilities.SearchCapabilities(module.Store.Capabilities...).Union(crs.recFilterCapabilities(filter)) + + // // Connect to datasource + // var s Store + // var cc capabilities.Set + // _ = cc + // s, cc, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID, requiredCap...) + // if err != nil { + // return + // } + + // // Prepare data + // model := crs.lookupModel(module) + // if model == nil { + // return nil, nil, crs.modelNotFoundErr(module) + // } + + // loader, err := s.SearchRecords(ctx, model, nil) + // if err != nil { + // return + // } + + // limit := int(filter.Limit) + // if limit == 0 { + // limit = 10 + // } + + // auxCC := make([]Setter, limit) + // for i := range auxCC { + // auxCC[i] = &types.Record{} + // } + + // var ok bool + // _ = ok + // for loader.More() && len(records) < int(limit) { + // _, err = loader.Load(model, auxCC) + // if err != nil { + // return + // } + + // auxRecords, err := crs.extractRecords(model, auxCC...) + // if err != nil { + // return nil, nil, err + // } + + // if !capabilities.AccessControlCapabilities().IsSubset(cc...) && filter.Check != nil { + // for _, r := range auxRecords { + // if r == nil { + // continue + // } + // if ok, err = filter.Check(r); err != nil { + // return nil, nil, err + // } else if !ok { + // continue + // } + + // records = append(records, r) + // } + // } else { + // for _, r := range auxRecords { + // if r == nil { + // break + // } + // records = append(records, r) + // } + // } + // } + + // return } // --- +// recFilterCapabilities utility helps construct required filter capabilities based on the provided record filter func (crs *composeRecordStore) recFilterCapabilities(f *types.RecordFilter) (out capabilities.Set) { if f == nil { return @@ -133,12 +136,3 @@ func (crs *composeRecordStore) recFilterCapabilities(f *types.RecordFilter) (out func (crs composeRecordStore) modelNotFoundErr(module *types.Module) error { return fmt.Errorf("cannot create records for module %d: module not registered to crs", module.ID) } - -func (crs composeRecordStore) extractRecords(model *data.Model, gg ...Setter) (out types.RecordSet, err error) { - out = make(types.RecordSet, len(gg)) - for i, g := range gg { - out[i] = g.(*types.Record) - } - - return -} diff --git a/compose/crs/driver.go b/compose/crs/driver.go index 302ffb823..f8aee5a01 100644 --- a/compose/crs/driver.go +++ b/compose/crs/driver.go @@ -2,69 +2,103 @@ package crs import ( "context" + "fmt" + "os" + "regexp" + "strings" "github.com/cortezaproject/corteza-server/compose/crs/capabilities" + "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/data" "github.com/cortezaproject/corteza-server/pkg/expr" + "go.uber.org/zap" ) type ( - driver interface { - // Capabilities returns all of the capabilities the driver is able to support + StoreConnection interface { + // --- + + // Meta + // Capabilities returns all of the capabilities the given store supports Capabilities() capabilities.Set - // Can determines if the driver is able to handle the connection - Can(dsn string, capabilities ...capabilities.Capability) bool + // Can returns true if this store can handle the given capabilities + Can(capabilities ...capabilities.Capability) bool - // Store connects and returns a store we can use - Store(ctx context.Context, dsn string) (Store, error) - // Close closes the connection to specified - Close(ctx context.Context, dsn string) error - } + // --- - // Loader provides an interface for loading data from the underlying store - Loader interface { - More() bool - Load(*data.Model, []Setter) (coppied int, err error) + // Connection stuff + // Close closes the store connection allowing the driver to perform potential + // cleanup operations + Close(ctx context.Context) error - // -1 means unknown - Total() int - Cursor() any - // ... do we need anything else here? - } + // --- - // Store provides an interface which CRS uses to interact with the underlying database - Store interface { // DML - CreateRecords(ctx context.Context, sch *data.Model, cc ...Getter) error - // @note providing a model here would allow us to control what attributes we return; would we find this useful? - SearchRecords(ctx context.Context, sch *data.Model, filter any) (Loader, error) - // Rest are same difference + // CreateRecords stores the given records into the underlying database + // + // @todo rename to Create and replace types.Record with Getter + CreateRecords(ctx context.Context, m *data.Model, rr ...*types.Record) error + // SearchRecords returns an iterator we can use to load data + // + // @todo rename to Search and replace filter with xyz + SearchRecords(ctx context.Context, sch *data.Model, filter any) (Iterator, error) + // ... + // @todo other functions; they are same difference so omitting for now // - LookupRecord // - UpdateRecords // - DeleteRecords // - TruncateRecords + // --- + // DDL - // Models returns all of the collections the given database already defines + // Models returns all of the models the underlying database already supports + // + // This is useful when adding support for new modules since we can find out what + // can work out of the box. Models(context.Context) (data.ModelSet, error) - // returns all attribute types that driver supports - AttributeTypes() []data.AttributeType + // // returns all attribute types that driver supports + // AttributeTypes() []data.AttributeType - // AddModel requests the driver to support the specified collections + // AddModel adds support for the given models to the underlying database + // + // The operation returns an error if any of the models already exists. AddModel(context.Context, *data.Model, ...*data.Model) error - // // RemoveModel requests the driver to remove support for the specified collections - // RemoveModel(context.Context, *data.Model, ...*data.Model) error + // RemoveModel removes support for the given model from the underlying database + RemoveModel(context.Context, *data.Model, ...*data.Model) error - // AlterModel requests the driver to alter the general collectio parameters + // AlterModel requests for metadata changes to the existing model + // + // Only metadata (such as idents) are affected; attributes can not be changed here AlterModel(ctx context.Context, old *data.Model, new *data.Model) error - // AlterModelAttribute requests the driver to alter the specified attribute of the given collection - AlterModelAttribute(ctx context.Context, sch *data.Model, old data.Attribute, new data.Attribute, trans ...func(*data.Model, data.Attribute, expr.TypedValue) (expr.TypedValue, bool, error)) error + // AlterModelAttribute requests for the model attribute change + // + // Specific operations require data transformations (type change). + // Some basic ops. should be implemented on DB driver level, but greater controll can be + // achieved via the trans functions. + AlterModelAttribute(ctx context.Context, sch *data.Model, old data.Attribute, new data.Attribute, trans ...TransformationFunction) error } - // probably somewhere else (on the db level? + TransformationFunction func(*data.Model, data.Attribute, expr.TypedValue) (expr.TypedValue, bool, error) + + // Iterator provides an interface for loading data from the underlying store + Iterator interface { + Next(ctx context.Context) bool + Scan(r *types.Record) (err error) + Err() error + Close() error + + // // -1 means unknown + // Total() int + // Cursor() any + // // ... do we need anything else here? + } + + // Store provides an interface which CRS uses to interact with the underlying database + Getter interface { GetValue(name string, pos int) (any, error) } @@ -72,4 +106,47 @@ type ( Setter interface { SetValue(name string, pos int, value any) error } + + ConnectorFn func(ctx context.Context, dsn string, cc ...capabilities.Capability) (StoreConnection, error) ) + +var ( + registered = make(map[string]ConnectorFn) +) + +// Register registers a new connector for the given DSN schema +// +// In case of a duplicate schema the latter overwrites the prior +func Register(fn ConnectorFn, tt ...string) { + for _, t := range tt { + registered[t] = fn + } +} + +// connect opens a new StoreConnection for the given CRS +func connect(ctx context.Context, log *zap.Logger, def crsDefiner, isDevelopment bool) (StoreConnection, error) { + dsn := def.StoreDSN() + + if isDevelopment { + if strings.Contains(dsn, "{version}") { + log.Warn("You're using DB_DSN with {version}, It is still in EXPERIMENTAL phase") + log.Warn("Should be used only for development mode") + log.Warn("You may experience instability") + } + expr := regexp.MustCompile(`[.\-]+`) + version := expr.ReplaceAllString(os.Getenv("BUILD_VERSION"), "_") + dsn = strings.Replace(dsn, "{version}", version, 1) + } + + var storeType = strings.SplitN(dsn, "://", 2)[0] + if storeType == "" { + // Backward compatibility + storeType = "mysql" + } + + if conn, ok := registered[storeType]; ok { + return conn(ctx, dsn, def.Capabilities()...) + } else { + return nil, fmt.Errorf("unknown store type used: %q (check your storage configuration)", storeType) + } +} diff --git a/compose/crs/main_test.go b/compose/crs/main_test.go deleted file mode 100644 index 8fcb206ec..000000000 --- a/compose/crs/main_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package crs - -import ( - "time" - - "context" - "strings" - - "github.com/cortezaproject/corteza-server/compose/crs/capabilities" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/pkg/data" - "github.com/cortezaproject/corteza-server/pkg/expr" - "github.com/cortezaproject/corteza-server/pkg/id" -) - -type ( - testDriver struct{} - - testStore map[string]*types.Record -) - -var ( - gStore = make(testStore) - - // wrapper around time.Now() that will aid service testing - now = func() *time.Time { - c := time.Now().Round(time.Second) - return &c - } - - // wrapper around nextID that will aid service testing - nextID = func() uint64 { - return id.Next() - } -) - -func (d testDriver) Capabilities() capabilities.Set { - return capabilities.FullCapabilities() -} - -func (d testDriver) Can(uri string, cc ...capabilities.Capability) bool { - return strings.HasPrefix(uri, "noop://") && capabilities.Set(cc).IsSubset(d.Capabilities()...) -} - -func (d testDriver) Store(ctx context.Context, uri string) (str Store, err error) { - return &testStore{}, nil -} - -func (d testDriver) Close(ctx context.Context, uri string) (err error) { - return nil -} - -// --- - -func (ds *testStore) CreateRecords(ctx context.Context, sch *data.Model, cc ...Getter) error { - return nil -} - -func (ds *testStore) SearchRecords(ctx context.Context, sch *data.Model, filter any) (Loader, error) { - return nil, nil -} - -func (ds *testStore) Models(context.Context) (data.ModelSet, error) { - return nil, nil -} - -func (ds *testStore) AddModel(context.Context, *data.Model, ...*data.Model) error { - return nil -} - -func (ds *testStore) AlterModel(ctx context.Context, old *data.Model, new *data.Model) error { - return nil -} - -func (ds *testStore) AlterModelAttribute(ctx context.Context, sch *data.Model, old data.Attribute, new data.Attribute, trans ...func(*data.Model, data.Attribute, expr.TypedValue) (expr.TypedValue, bool, error)) error { - return nil -} - -func initCRS(ctx context.Context) *composeRecordStore { - crs, _ := ComposeRecordStore(ctx, CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), testDriver{}) - crs.AddStore(ctx, CRSConnectionWrap(defaultExternalCRS, "noop://external", capabilities.FullCapabilities()...)) - return crs -} - -func defaultModule() *types.Module { - return &types.Module{ - ID: nextID(), - Store: types.CRSDef{ComposeRecordStoreID: defaultExternalCRS}, - Fields: types.ModuleFieldSet{{ - Name: "first_name", - Kind: "String", - }}, - } -} diff --git a/compose/crs/model.go b/compose/crs/model.go index 6f5a9b9d8..a1d2f6668 100644 --- a/compose/crs/model.go +++ b/compose/crs/model.go @@ -26,6 +26,24 @@ const ( urlLength = 2048 ) +// ---------------------- +// @todo move this out and unify with RDBMS one + +const ( + sysID = "ID" + sysNamespaceID = "namespaceID" + sysModuleID = "moduleID" + sysCreatedAt = "createdAt" + sysCreatedBy = "createdBy" + sysUpdatedAt = "updatedAt" + sysUpdatedBy = "updatedBy" + sysDeletedAt = "deletedAt" + sysDeletedBy = "deletedBy" + sysOwnedBy = "ownedBy" +) + +// ---------------------- + // ReloadModulesFromStore resets state based on the provided cStore func (crs *composeRecordStore) ReloadModulesFromStore(ctx context.Context, cs cStore) (err error) { modules, err := crs.loadModules(ctx, cs) @@ -52,12 +70,17 @@ func (crs *composeRecordStore) AddModules(ctx context.Context, modules ...*types return } + var ( + s StoreConnection + ) + for storeID, models := range crs.modelByStore(models) { - if crs.stores[storeID] == nil { - return fmt.Errorf("can not add module to store %d: store does not exist", storeID) + s, _, err = crs.getStore(ctx, storeID) + if err != nil { + return err } - err = crs.addModel(ctx, storeID, models) + err = crs.addModel(ctx, s, storeID, models) if err != nil { return } @@ -112,54 +135,45 @@ func (crs *composeRecordStore) RemoveModules(ctx context.Context, modules ...*ty // AlterModule updates the old module with the new one func (crs *composeRecordStore) AlterModule(ctx context.Context, oldMod, newMod *types.Module) (err error) { - // validation - { - if oldMod.Store.ComposeRecordStoreID != newMod.Store.ComposeRecordStoreID { - return fmt.Errorf("cannot alter module stored in different record stores: old: %d, new: %d", oldMod.Store.ComposeRecordStoreID, newMod.Store.ComposeRecordStoreID) - } - } + return + // // validation + // { + // if oldMod.Store.ComposeRecordStoreID != newMod.Store.ComposeRecordStoreID { + // return fmt.Errorf("cannot alter module stored in different record stores: old: %d, new: %d", oldMod.Store.ComposeRecordStoreID, newMod.Store.ComposeRecordStoreID) + // } + // } - store, oldModel, err := crs.prepModuleDDL(ctx, oldMod) - if err != nil { - return - } - // store is same so we omit - _, newModel, err := crs.prepModuleDDL(ctx, newMod) - if err != nil { - return - } + // store, oldModel, err := crs.prepModuleDDL(ctx, oldMod) + // if err != nil { + // return + // } + // // store is same so we omit + // _, newModel, err := crs.prepModuleDDL(ctx, newMod) + // if err != nil { + // return + // } - return store.AlterModel(ctx, oldModel, newModel) + // return store.AlterModel(ctx, oldModel, newModel) } // @todo other ddl manipupations... // --- -func (crs *composeRecordStore) prepModuleDDL(ctx context.Context, module *types.Module) (s Store, model *data.Model, err error) { - s, _, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID) - if err != nil { - return - } +// func (crs *composeRecordStore) prepModuleDDL(ctx context.Context, module *types.Module) (s Store, model *data.Model, err error) { +// s, _, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID) +// if err != nil { +// return +// } - models, err := crs.modulesToModel(module) - if err != nil { - return - } - model = models[0] +// models, err := crs.modulesToModel(module) +// if err != nil { +// return +// } +// model = models[0] - return -} - -// modulesByStore maps the given module set by their CRS -func (crs *composeRecordStore) modulesByStore(modules types.ModuleSet) (out map[uint64]types.ModuleSet) { - out = make(map[uint64]types.ModuleSet) - for _, mod := range modules { - out[mod.Store.ComposeRecordStoreID] = append(out[mod.Store.ComposeRecordStoreID], mod) - } - - return -} +// return +// } // modelByStore maps the given models by their CRS func (crs *composeRecordStore) modelByStore(models data.ModelSet) (out map[uint64]data.ModelSet) { @@ -208,6 +222,7 @@ func (crs *composeRecordStore) loadModules(ctx context.Context, cs cStore) (mm t return } +// moduleFieldCodec is a little utility to construct the store codec we need func moduleFieldCodec(f *types.ModuleField) (strat data.StoreCodec) { // Defaulting to alias strat = data.StoreCodecAlias{ @@ -220,9 +235,8 @@ func moduleFieldCodec(f *types.ModuleField) (strat data.StoreCodec) { Ident: f.Encoding.EncodingStrategyAlias.Ident, } case f.Encoding.EncodingStrategyJSON != nil: - strat = data.StoreCodecJSON{ + strat = data.StoreCodecStdRecordValueJSON{ Ident: f.Encoding.EncodingStrategyJSON.Ident, - Path: f.Encoding.EncodingStrategyJSON.Path, } } @@ -276,8 +290,7 @@ func (crs *composeRecordStore) moduleModelInit(mod *types.Module) (out *data.Mod ResourceID: mod.ID, ResourceType: types.ModuleResourceType, - // @todo this isn't exactly this - Ident: mod.Handle, + Ident: formatPartitionIdent(mod), Attributes: make(data.AttributeSet, len(mod.Fields)), } } @@ -354,65 +367,65 @@ func (crs *composeRecordStore) moduleModelAttributes(mod *types.Module, refIndex // System attrs out = append(out, &data.Attribute{ - Ident: "recordID", + Ident: sysID, Store: moduleFieldCodec(&types.ModuleField{ - Name: "recordID", + Name: sysID, }), Type: data.TypeID{}, }, &data.Attribute{ - Ident: "createdAt", + Ident: sysCreatedAt, Store: moduleFieldCodec(&types.ModuleField{ - Name: "createdAt", + Name: sysCreatedAt, }), Type: data.TypeTimestamp{}, }, &data.Attribute{ - Ident: "updatedAt", + Ident: sysUpdatedAt, Store: moduleFieldCodec(&types.ModuleField{ - Name: "updatedAt", + Name: sysUpdatedAt, }), Type: data.TypeTimestamp{}, }, &data.Attribute{ - Ident: "deletedAt", + Ident: sysDeletedAt, Store: moduleFieldCodec(&types.ModuleField{ - Name: "deletedAt", + Name: sysDeletedAt, }), Type: data.TypeTimestamp{}, }, &data.Attribute{ - Ident: "ownedBy", + Ident: sysOwnedBy, Store: moduleFieldCodec(&types.ModuleField{ - Name: "ownedBy", + Name: sysOwnedBy, }), Type: data.TypeRef{ RefAttribute: &data.Attribute{Ident: "id"}, }, }, &data.Attribute{ - Ident: "createdBy", + Ident: sysCreatedBy, Store: moduleFieldCodec(&types.ModuleField{ - Name: "createdBy", + Name: sysCreatedBy, }), Type: data.TypeRef{ RefAttribute: &data.Attribute{Ident: "id"}, }, }, &data.Attribute{ - Ident: "updatedBy", + Ident: sysUpdatedBy, Store: moduleFieldCodec(&types.ModuleField{ - Name: "updatedBy", + Name: sysUpdatedBy, }), Type: data.TypeRef{ RefAttribute: &data.Attribute{Ident: "id"}, }, }, &data.Attribute{ - Ident: "deletedBy", + Ident: sysDeletedBy, Store: moduleFieldCodec(&types.ModuleField{ - Name: "deletedBy", + Name: sysDeletedBy, }), Type: data.TypeRef{ RefAttribute: &data.Attribute{Ident: "id"}, @@ -423,14 +436,14 @@ func (crs *composeRecordStore) moduleModelAttributes(mod *types.Module, refIndex return } -func (crs *composeRecordStore) addModel(ctx context.Context, storeID uint64, models data.ModelSet) (err error) { +func (crs *composeRecordStore) addModel(ctx context.Context, s StoreConnection, storeID uint64, models data.ModelSet) (err error) { for _, model := range models { existing := crs.getModel(storeID, model.Ident) if existing != nil { return fmt.Errorf("cannot add model %s to store %d: already exists", model.Ident, storeID) } - err = crs.addModelToStore(ctx, storeID, model) + err = crs.addModelToStore(ctx, s, model) if err != nil { return } @@ -441,12 +454,7 @@ func (crs *composeRecordStore) addModel(ctx context.Context, storeID uint64, mod return } -func (crs *composeRecordStore) addModelToStore(ctx context.Context, storeID uint64, model *data.Model) (err error) { - s, _, err := crs.getStore(ctx, storeID) - if err != nil { - return err - } - +func (crs *composeRecordStore) addModelToStore(ctx context.Context, s StoreConnection, model *data.Model) (err error) { available, err := s.Models(ctx) if err != nil { return err @@ -471,3 +479,17 @@ func (crs *composeRecordStore) addModelToStore(ctx context.Context, storeID uint return nil } + +// --- + +func formatPartitionIdent(mod *types.Module) string { + rpl := strings.NewReplacer( + "{{module}}", mod.Handle, + ) + + if mod.Store.PartitionFormat == "" { + return mod.Handle + } + + return rpl.Replace(mod.Store.PartitionFormat) +} diff --git a/compose/crs/module_management_test.go b/compose/crs/module_management_test.go deleted file mode 100644 index a9d5c07b4..000000000 --- a/compose/crs/module_management_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package crs - -import ( - "context" - "testing" - - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/stretchr/testify/require" -) - -var ( - defaultExternalCRS uint64 = nextID() -) - -func TestLoadModules(t *testing.T) { - ctx := context.Background() - - t.Run("no supporting CRS", func(t *testing.T) { - crs := initCRS(ctx) - - err := crs.ReloadModules(ctx, &types.Module{ - ID: nextID(), - Store: types.CRSDef{ComposeRecordStoreID: 999}, - }) - require.Error(t, err) - }) - - t.Run("module added", func(t *testing.T) { - crs := initCRS(ctx) - - mod := defaultModule() - - err := crs.ReloadModules(ctx, mod) - require.NoError(t, err) - - model := crs.lookupModel(mod) - require.NotNil(t, model) - - // 8 sys, 1 custom - require.Len(t, model.Attributes, 8+1) - - require.Equal(t, defaultExternalCRS, model.StoreID) - }) - - t.Run("duplicate module added", func(t *testing.T) { - crs := initCRS(ctx) - - mod := defaultModule() - - err := crs.ReloadModules(ctx, mod, mod) - require.Error(t, err) - }) -} - -func TestRemoveModules(t *testing.T) { - ctx := context.Background() - - t.Run("module not found", func(t *testing.T) { - crs := initCRS(ctx) - - err := crs.RemoveModules(ctx, defaultModule()) - require.Error(t, err) - }) - - t.Run("module removed", func(t *testing.T) { - crs := initCRS(ctx) - mod := defaultModule() - - err := crs.ReloadModules(ctx, mod) - require.NoError(t, err) - - err = crs.RemoveModules(ctx, mod) - require.NoError(t, err) - - require.Nil(t, crs.lookupModel(mod)) - }) -} diff --git a/compose/crs/noop/noop.go b/compose/crs/noop/noop.go deleted file mode 100644 index 0bda091bb..000000000 --- a/compose/crs/noop/noop.go +++ /dev/null @@ -1,66 +0,0 @@ -// This package is just so that I have an interface conforming driver - -package noop - -import ( - "context" - "strings" - - "github.com/cortezaproject/corteza-server/compose/crs" - "github.com/cortezaproject/corteza-server/compose/crs/capabilities" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/pkg/data" - "github.com/cortezaproject/corteza-server/pkg/expr" -) - -type ( - noopDriver struct{} - - noopStore map[string]*types.Record -) - -var ( - gStore = make(noopStore) -) - -func Driver() *noopDriver { - return &noopDriver{} -} - -func (d noopDriver) Capabilities() capabilities.Set { - return capabilities.FullCapabilities() -} - -func (d noopDriver) Can(uri string, cc ...capabilities.Capability) bool { - return strings.HasPrefix(uri, "noop://") && capabilities.Set(cc).IsSubset(d.Capabilities()...) -} - -func (d noopDriver) Store(ctx context.Context, uri string) (str crs.Store, err error) { - return &noopStore{}, nil -} - -// --- - -func (ds *noopStore) CreateRecords(ctx context.Context, sch *data.Model, cc ...crs.Getter) error { - return nil -} - -func (ds *noopStore) SearchRecords(ctx context.Context, sch *data.Model, filter any) (crs.Loader, error) { - return nil, nil -} - -func (ds *noopStore) Models(context.Context) (data.ModelSet, error) { - return nil, nil -} - -func (ds *noopStore) AddModel(context.Context, *data.Model, ...*data.Model) error { - return nil -} - -func (ds *noopStore) AlterModel(ctx context.Context, old *data.Model, new *data.Model) error { - return nil -} - -func (ds *noopStore) AlterModelAttribute(ctx context.Context, sch *data.Model, old data.Attribute, new data.Attribute, trans ...func(*data.Model, data.Attribute, expr.TypedValue) (expr.TypedValue, bool, error)) error { - return nil -} diff --git a/compose/crs/store_management_test.go b/compose/crs/store_management_test.go deleted file mode 100644 index c6177cd5d..000000000 --- a/compose/crs/store_management_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package crs - -import ( - "context" - "testing" - - "github.com/cortezaproject/corteza-server/compose/crs/capabilities" - "github.com/stretchr/testify/require" -) - -func TestInit(t *testing.T) { - ctx := context.Background() - - t.Run("invalid primary store", func(t *testing.T) { - _, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "invalid://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.Error(t, err) - }) - - t.Run("valid primary store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - require.Len(t, crs.stores, 0) - require.NotNil(t, crs.primary) - - }) -} - -func TestStoreAdd(t *testing.T) { - ctx := context.Background() - - t.Run("invalid external store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - sID := nextID() - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "invalid://external", capabilities.FullCapabilities()...)) - require.Error(t, err) - - require.NotNil(t, crs.primary) - require.Len(t, crs.stores, 0) - }) - - t.Run("valid external store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - sID := nextID() - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "noop://external", capabilities.FullCapabilities()...)) - require.NoError(t, err) - - require.NotNil(t, crs.primary) - require.Len(t, crs.stores, 1) - require.NotNil(t, crs.stores[sID]) - }) - - t.Run("duplicated external store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - sID := nextID() - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "noop://external", capabilities.FullCapabilities()...)) - require.NoError(t, err) - - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "noop://external", capabilities.FullCapabilities()...)) - require.Error(t, err) - - require.NotNil(t, crs.primary) - require.Len(t, crs.stores, 1) - require.NotNil(t, crs.stores[sID]) - }) -} - -func TestStoreRemove(t *testing.T) { - ctx := context.Background() - - t.Run("removing missing store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - sID := nextID() - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "noop://external", capabilities.FullCapabilities()...)) - require.NoError(t, err) - - err = crs.RemoveStore(ctx, 123) - require.Error(t, err) - require.Len(t, crs.stores, 1) - }) - - t.Run("removing store", func(t *testing.T) { - crs, err := ComposeRecordStore( - ctx, - CRSConnectionWrap(0, "noop://primary", capabilities.FullCapabilities()...), - testDriver{}, - ) - require.NoError(t, err) - - sID := nextID() - err = crs.AddStore(ctx, CRSConnectionWrap(sID, "noop://external", capabilities.FullCapabilities()...)) - require.NoError(t, err) - - err = crs.RemoveStore(ctx, sID) - require.NoError(t, err) - require.Len(t, crs.stores, 0) - require.NotNil(t, crs.primary) - }) -} diff --git a/compose/types/compose_record_store.go b/compose/types/compose_record_store.go index 31f942157..4b562b3b0 100644 --- a/compose/types/compose_record_store.go +++ b/compose/types/compose_record_store.go @@ -8,6 +8,7 @@ type ( Handle string DSN string Location string + // ... // @todo IMO having it like so (instead of in a struct) allows for more diff --git a/compose/types/module.go b/compose/types/module.go index 4a0d394d6..f86baf92d 100644 --- a/compose/types/module.go +++ b/compose/types/module.go @@ -20,7 +20,6 @@ type ( Partitioned bool PartitionFormat string - PartitionBase []any } Module struct { diff --git a/store/adapters/rdbms/crs/connect.go b/store/adapters/rdbms/crs/connect.go new file mode 100644 index 000000000..1c2323e98 --- /dev/null +++ b/store/adapters/rdbms/crs/connect.go @@ -0,0 +1,118 @@ +package crs + +import ( + "context" + "fmt" + + ccrs "github.com/cortezaproject/corteza-server/compose/crs" + "github.com/cortezaproject/corteza-server/compose/crs/capabilities" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/data" + "github.com/doug-martin/goqu/v9" + "go.uber.org/zap" +) + +type ( + Connector struct { + DB connection + + // Logger for connection + Logger *zap.Logger + + Dialect goqu.DialectWrapper + // --- + + modelHandlers map[uint64]*crs + capabilities capabilities.Set + } +) + +func CRSConnector(db connection, logger *zap.Logger, di goqu.DialectWrapper, cc capabilities.Set) *Connector { + return &Connector{ + DB: db, + Logger: logger, + Dialect: di, + modelHandlers: make(map[uint64]*crs), + capabilities: cc, + } +} + +func (c *Connector) Capabilities() capabilities.Set { + return c.capabilities +} + +func (c *Connector) Can(capabilities ...capabilities.Capability) bool { + return c.Capabilities().IsSuperset(capabilities...) +} + +func (c *Connector) Close(ctx context.Context) error { + return nil +} + +func (c *Connector) CreateRecords(ctx context.Context, m *data.Model, rr ...*types.Record) error { + h, err := c.getModelHandlerE(m) + if err != nil { + return err + } + + return h.Create(ctx, rr...) +} + +func (c *Connector) SearchRecords(ctx context.Context, sch *data.Model, filter any) (ccrs.Iterator, error) { + return nil, nil +} + +// --- + +func (c *Connector) RemoveModel(context.Context, *data.Model, ...*data.Model) error { + return nil +} + +func (c *Connector) AlterModel(ctx context.Context, old *data.Model, new *data.Model) error { + return nil +} + +func (c *Connector) AlterModelAttribute(ctx context.Context, sch *data.Model, old data.Attribute, new data.Attribute, trans ...ccrs.TransformationFunction) error { + return nil +} + +func (c *Connector) Models(ctx context.Context) (data.ModelSet, error) { + // @todo... + return nil, nil +} + +func (c *Connector) AddModel(ctx context.Context, m *data.Model, mm ...*data.Model) error { + for _, model := range append(mm, m) { + if h := c.getModelHandler(model); h != nil { + return fmt.Errorf("cannot add model: already exists") + } + + _, err := c.addModelHandler(ctx, model) + if err != nil { + return err + } + } + + return nil +} + +// --- + +func (c *Connector) getModelHandler(m *data.Model) *crs { + return c.modelHandlers[m.ResourceID] +} + +func (c *Connector) getModelHandlerE(m *data.Model) (*crs, error) { + h := c.modelHandlers[m.ResourceID] + if h == nil { + return nil, fmt.Errorf("cannot process model for resource %d: not supported", m.ResourceID) + } + return h, nil +} + +func (c *Connector) addModelHandler(ctx context.Context, m *data.Model) (*crs, error) { + h := CRS(m, c.DB) + c.modelHandlers[m.ResourceID] = h + + return h, nil +} diff --git a/store/adapters/rdbms/drivers/mysql/connect.go b/store/adapters/rdbms/drivers/mysql/connect.go index 4457e14f3..67af4cf8e 100644 --- a/store/adapters/rdbms/drivers/mysql/connect.go +++ b/store/adapters/rdbms/drivers/mysql/connect.go @@ -6,10 +6,14 @@ import ( "fmt" "strings" + "github.com/cortezaproject/corteza-server/compose/crs" + "github.com/cortezaproject/corteza-server/compose/crs/capabilities" + "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/store/adapters/rdbms" + rdbmsCRS "github.com/cortezaproject/corteza-server/store/adapters/rdbms/crs" "github.com/cortezaproject/corteza-server/store/adapters/rdbms/instrumentation" "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/mysql" @@ -20,9 +24,35 @@ import ( func init() { store.Register(Connect, "mysql", "mysql+debug") + crs.Register(ConnectCRS, "mysql", "mysql+debug") + sql.Register("mysql+debug", sqlmw.Driver(new(mysql.MySQLDriver), instrumentation.Debug())) } +// @todo cleanup; unify common bits and stuff + +func ConnectCRS(ctx context.Context, dsn string, cc ...capabilities.Capability) (_ crs.StoreConnection, err error) { + var ( + db *sqlx.DB + cfg *rdbms.ConnConfig + ) + + if cfg, err = NewConfig(dsn); err != nil { + return + } + + if db, err = rdbms.Connect(ctx, logger.Default(), cfg); err != nil { + return + } + + // See https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_ansi for details + if _, err = db.ExecContext(ctx, `SET SESSION sql_mode = 'ANSI'`); err != nil { + return + } + + return rdbmsCRS.CRSConnector(db, logger.Default(), goqu.Dialect("mysql"), cc), nil +} + func Connect(ctx context.Context, dsn string) (_ store.Storer, err error) { var ( db *sqlx.DB diff --git a/tests/crs/crs_test.go b/tests/crs/crs_test.go new file mode 100644 index 000000000..d3306b6c7 --- /dev/null +++ b/tests/crs/crs_test.go @@ -0,0 +1,185 @@ +package crs + +import ( + "context" + "testing" + "time" + + "github.com/cortezaproject/corteza-server/compose/crs" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/id" + + "github.com/stretchr/testify/require" + + "github.com/cortezaproject/corteza-server/compose/crs/capabilities" + _ "github.com/cortezaproject/corteza-server/store/adapters/rdbms/drivers/mysql" +) + +/* +See here for my table definitions + +CREATE TABLE `the_cake` ( + `id` bigint unsigned NOT NULL, + `name` VARCHAR(45), + `want` BOOLEAN, + `ownedBy` bigint unsigned NOT NULL, + `createdAt` datetime NOT NULL, + `updatedAt` datetime DEFAULT NULL, + `deletedAt` datetime DEFAULT NULL, + `createdBy` bigint unsigned NOT NULL, + `updatedBy` bigint unsigned NOT NULL DEFAULT '0', + `deletedBy` bigint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `compose_record_owner` (`ownedBy`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3; + +CREATE TABLE `the_cookie` ( + `id` bigint unsigned NOT NULL, + `name` VARCHAR(45), + `is_good` BOOLEAN, + `ownedBy` bigint unsigned NOT NULL, + `createdAt` datetime NOT NULL, + `updatedAt` datetime DEFAULT NULL, + `deletedAt` datetime DEFAULT NULL, + `createdBy` bigint unsigned NOT NULL, + `updatedBy` bigint unsigned NOT NULL DEFAULT '0', + `deletedBy` bigint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `compose_record_owner` (`ownedBy`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3; +*/ + +func TestHello(t *testing.T) { + ctx := context.Background() + + c, err := crs.ComposeRecordStore( + ctx, + nil, + false, + // Primary... + crs.CRSConnectionWrap(0, "mysql://envoy:envoy@tcp(localhost:3306)/crs?collation=utf8mb4_general_ci", capabilities.FullCapabilities()...), + + // Others... + crs.CRSConnectionWrap(1, "mysql://envoy:envoy@tcp(localhost:3306)/crs?collation=utf8mb4_general_ci", capabilities.FullCapabilities()...), + ) + require.NoError(t, err) + _ = c + + // --- + + cookieModule := &types.Module{ + ID: 10001, + Handle: "cookie", + Store: types.CRSDef{ + ComposeRecordStoreID: 0, + Capabilities: capabilities.FullCapabilities(), + Partitioned: true, + PartitionFormat: "the_{{module}}", + }, + Fields: types.ModuleFieldSet{&types.ModuleField{ + ModuleID: 10001, + ID: 20001, + Name: "name", + Encoding: types.EncodingStrategy{ + EncodingStrategyAlias: &types.EncodingStrategyAlias{ + Ident: "name", + }, + }, + }, &types.ModuleField{ + ModuleID: 10001, + ID: 20002, + Name: "is_good", + Kind: "Bool", + Encoding: types.EncodingStrategy{ + EncodingStrategyAlias: &types.EncodingStrategyAlias{ + Ident: "is_good", + }, + }, + }}, + } + + cakeModule := &types.Module{ + ID: 10002, + Handle: "cake", + Store: types.CRSDef{ + ComposeRecordStoreID: 1, + Capabilities: capabilities.FullCapabilities(), + Partitioned: true, + PartitionFormat: "the_{{module}}", + }, + Fields: types.ModuleFieldSet{&types.ModuleField{ + ModuleID: 10002, + ID: 20003, + Name: "name", + Encoding: types.EncodingStrategy{ + EncodingStrategyAlias: &types.EncodingStrategyAlias{ + // this doesn't work + Ident: "name", + }, + }, + }, &types.ModuleField{ + ModuleID: 10002, + ID: 20004, + Name: "want", + Kind: "Bool", + Encoding: types.EncodingStrategy{ + EncodingStrategyAlias: &types.EncodingStrategyAlias{ + Ident: "want", + }, + }, + }}, + } + + err = c.ReloadModules(ctx, cookieModule, cakeModule) + require.NoError(t, err) + + // --- + + a := time.Now() + + cookieRecord := &types.Record{ + ID: id.Next(), + ModuleID: 10001, + Values: types.RecordValueSet{{ + Name: "name", + Value: "SOME COOKIE HERE", + }, { + Name: "is_good", + Value: "1", + }}, + CreatedAt: time.Now(), + UpdatedAt: &a, + DeletedAt: &a, + OwnedBy: 20003, + CreatedBy: 20003, + UpdatedBy: 20003, + DeletedBy: 20003, + } + + cakeRecord := &types.Record{ + ID: id.Next(), + ModuleID: 10002, + Values: types.RecordValueSet{{ + Name: "name", + Value: "DOME CAKE HERE", + }, { + Name: "want", + Value: "1", + }}, + CreatedAt: time.Now(), + UpdatedAt: &a, + DeletedAt: &a, + OwnedBy: 20003, + CreatedBy: 20003, + UpdatedBy: 20003, + DeletedBy: 20003, + } + + err = c.ComposeRecordCreate(ctx, cookieModule, cookieRecord) + require.NoError(t, err) + + err = c.ComposeRecordCreate(ctx, cakeModule, cakeRecord) + require.NoError(t, err) + + require.FailNow(t, "the test dies a failNow so I can see logs!!!") +}