3
0

Base CRS definition

This commit is contained in:
Tomaž Jerman
2022-04-15 11:31:34 +02:00
parent 0f4628debf
commit faa2bd3e14
11 changed files with 1315 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package crs
import "github.com/cortezaproject/corteza-server/compose/crs/capabilities"
type (
crsConnectionWrap struct {
id uint64
dsn string
capabilities capabilities.Set
}
)
// CRSConnectionWrap is a utility to define generic store connections
func CRSConnectionWrap(id uint64, dsn string, cc ...capabilities.Capability) crsConnectionWrap {
return crsConnectionWrap{
id: id,
dsn: dsn,
capabilities: cc,
}
}
// Receivers to conform to interface
func (s crsConnectionWrap) ComposeRecordStoreID() uint64 {
return s.id
}
func (s crsConnectionWrap) StoreDSN() string {
return s.dsn
}
func (s crsConnectionWrap) Capabilities() capabilities.Set {
return s.capabilities
}
+173
View File
@@ -0,0 +1,173 @@
package crs
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/compose/crs/capabilities"
"github.com/cortezaproject/corteza-server/pkg/data"
)
type (
// the core struct that outlines the compose record store facility
composeRecordStore struct {
drivers []driver
stores map[uint64]*storeWrap
// Indexed by corresponding storeID
models map[uint64]data.ModelSet
primary *storeWrap
}
storeWrap struct {
store connFunc
driver driver
dsn string
capabilities capabilities.Set
}
connFunc func(ctx context.Context) (Store, error)
crsDefiner interface {
ComposeRecordStoreID() uint64
StoreDSN() string
Capabilities() capabilities.Set
}
)
const (
defaultStoreID uint64 = 0
)
// 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...)
crs := &composeRecordStore{
drivers: drivers,
stores: make(map[uint64]*storeWrap),
models: make(map[uint64]data.ModelSet),
primary: nil,
}
d = crs.getDriver(primary)
if d == nil {
return nil, fmt.Errorf("could not add default store: no supported driver found")
}
crs.primary = &storeWrap{
store: storeConnWrap(d, primary),
driver: d,
dsn: primary.StoreDSN(),
capabilities: primary.Capabilities(),
}
return crs, nil
}
// AddStore registers the given store definitions as compose record stores
func (crs *composeRecordStore) AddStore(ctx context.Context, definers ...crsDefiner) 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(),
}
}
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 {
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 {
return
}
// Remove from registry
delete(crs.stores, storeID)
}
return nil
}
// ---
func (crs *composeRecordStore) getModel(store uint64, ident string) *data.Model {
for _, model := range crs.models[store] {
if model.Ident == ident {
return model
}
}
return nil
}
// 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) {
err = func() error {
// get the requested store
var wrap *storeWrap
if storeID == defaultStoreID {
wrap = crs.primary
} else {
wrap = crs.stores[storeID]
}
if wrap == 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))
}
store, err = wrap.store(ctx)
can = wrap.capabilities
return nil
}()
if err != nil {
err = fmt.Errorf("could not connect to store %d: %v", storeID, err)
return
}
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())
}
}
+144
View File
@@ -0,0 +1,144 @@
package crs
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/compose/crs/capabilities"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/data"
)
func (crs *composeRecordStore) ComposeRecordCreate(ctx context.Context, module *types.Module, records ...*types.Record) (err error) {
// Determine required capabilities
requiredCap := capabilities.CreateCapabilities(module.Store.Capabilities...)
// Determine store
var s Store
if s, _, err = crs.getStore(ctx, module.Store.ComposeRecordStoreID, requiredCap...); err != nil {
return err
}
// Prepare data
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...)
}
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
}
// ---
func (crs *composeRecordStore) recFilterCapabilities(f *types.RecordFilter) (out capabilities.Set) {
if f == nil {
return
}
if f.PageCursor != nil {
out = append(out, capabilities.Paging)
}
if f.IncPageNavigation {
out = append(out, capabilities.Paging)
}
if f.IncTotal {
out = append(out, capabilities.Stats)
}
if f.Sort != nil {
out = append(out, capabilities.Sorting)
}
return
}
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
}
+68
View File
@@ -0,0 +1,68 @@
package crs
import (
"context"
"github.com/cortezaproject/corteza-server/compose/crs/capabilities"
"github.com/cortezaproject/corteza-server/pkg/data"
"github.com/cortezaproject/corteza-server/pkg/expr"
)
type (
driver interface {
// Capabilities returns all of the capabilities the driver is able to support
Capabilities() capabilities.Set
// Can determines if the driver is able to handle the connection
Can(dsn string, 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)
// -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
// - LookupRecord
// - UpdateRecords
// - DeleteRecords
// - TruncateRecords
// DDL
// Models returns all of the collections the given database already defines
Models(context.Context) (data.ModelSet, error)
// AddModel requests the driver to support the specified collections
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
// AlterModel requests the driver to alter the general collectio parameters
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
}
// probably somewhere else (on the db level?
Getter interface {
GetValue(name string, pos int) (expr.TypedValue, error)
}
Setter interface {
SetValue(name string, pos int, value expr.TypedValue) error
}
)
+94
View File
@@ -0,0 +1,94 @@
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",
}},
}
}
+473
View File
@@ -0,0 +1,473 @@
package crs
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/data"
)
type (
// cStore is a simplified interface so we can use the store.Storer to assert a valid schema
cStore interface {
SearchComposeModules(ctx context.Context, f types.ModuleFilter) (types.ModuleSet, types.ModuleFilter, error)
SearchComposeModuleFields(ctx context.Context, f types.ModuleFieldFilter) (types.ModuleFieldSet, types.ModuleFieldFilter, error)
SearchComposeNamespaces(ctx context.Context, f types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
}
)
const (
// https://www.rfc-editor.org/errata/eid1690
emailLength = 254
// Generally the upper most limit
urlLength = 2048
)
// 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)
if err != nil {
return
}
return crs.ReloadModules(ctx, modules...)
}
// ReloadModulesFromStore resets state based on the provided set of modules
func (crs *composeRecordStore) ReloadModules(ctx context.Context, modules ...*types.Module) (err error) {
// Clear up the old ones
// @todo profile if manually removing nested pointers makes it faster
crs.models = make(map[uint64]data.ModelSet)
return crs.AddModules(ctx, modules...)
}
// AddModules adds new modules without affecting existing ones
func (crs *composeRecordStore) AddModules(ctx context.Context, modules ...*types.Module) (err error) {
models, err := crs.modulesToModel(modules...)
if err != nil {
return
}
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)
}
err = crs.addModel(ctx, storeID, models)
if err != nil {
return
}
}
return
}
// RemoveModules removes the specified modules
func (crs *composeRecordStore) RemoveModules(ctx context.Context, modules ...*types.Module) (err error) {
models, err := crs.modulesToModel(modules...)
if err != nil {
return
}
// validation
for _, model := range models {
// Validate existence
old := crs.getModel(model.StoreID, model.Ident)
if old == nil {
return fmt.Errorf("cannot remove module %s: not registered", model.Ident)
}
// Validate no leftover references
// @todo we can probably expand on this quitea bit
for _, registered := range crs.models {
refs := registered.FilterByReferenced(model)
if len(refs) > 0 {
return fmt.Errorf("cannot remove module %s: referenced by other modules", model.Ident)
}
}
}
// Work
for _, model := range models {
oldModels := crs.models[model.StoreID]
crs.models[model.StoreID] = make(data.ModelSet, 0, len(oldModels))
for _, o := range oldModels {
if o.Ident == model.Ident {
continue
}
crs.models[model.StoreID] = append(crs.models[model.StoreID], o)
}
// @todo should the underlying store be notified about this?
}
return nil
}
// 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)
}
}
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)
}
// @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
}
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
}
// modelByStore maps the given models by their CRS
func (crs *composeRecordStore) modelByStore(models data.ModelSet) (out map[uint64]data.ModelSet) {
out = make(map[uint64]data.ModelSet)
for _, model := range models {
out[model.StoreID] = append(out[model.StoreID], model)
}
return
}
// loadModules is a utility to load available modules with all their metadata included
func (crs *composeRecordStore) loadModules(ctx context.Context, cs cStore) (mm types.ModuleSet, err error) {
var (
namespaces types.NamespaceSet
modules types.ModuleSet
fields types.ModuleFieldSet
)
namespaces, _, err = cs.SearchComposeNamespaces(ctx, types.NamespaceFilter{})
if err != nil {
return
}
for _, ns := range namespaces {
modules, _, err = cs.SearchComposeModules(ctx, types.ModuleFilter{
NamespaceID: ns.ID,
})
if err != nil {
return
}
for _, mod := range modules {
fields, _, err = cs.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
ModuleID: []uint64{mod.ID},
})
if err != nil {
return
}
mod.Fields = append(mod.Fields, fields...)
}
}
return
}
func moduleFieldCodec(f *types.ModuleField) (strat data.StoreStrategy) {
// Defaulting to alias
strat = data.StoreCodecAlias{
Ident: f.Name,
}
switch {
case f.Encoding.EncodingStrategyAlias != nil:
strat = data.StoreCodecAlias{
Ident: f.Encoding.EncodingStrategyAlias.Ident,
}
case f.Encoding.EncodingStrategyJSON != nil:
strat = data.StoreCodecJSON{
Ident: f.Encoding.EncodingStrategyJSON.Ident,
Path: f.Encoding.EncodingStrategyJSON.Path,
}
}
return
}
// ----------
func (crs *composeRecordStore) lookupModel(module *types.Module) (out *data.Model) {
for _, model := range crs.models[module.Store.ComposeRecordStoreID] {
if model.ResourceID == module.ID {
return model
}
}
return nil
}
func (crs *composeRecordStore) modulesToModel(modules ...*types.Module) (out data.ModelSet, err error) {
refIndex := make(map[uint64]*data.Model)
out = make(data.ModelSet, 0, len(modules))
// Initial pass to get everything we can
for _, module := range modules {
model := crs.moduleModelInit(module)
refIndex[module.ID] = model
out = append(out, model)
}
// Add stuff we already have
for _, models := range crs.models {
for _, model := range models {
refIndex[model.ResourceID] = model
}
}
// Build up fields
for i, mod := range modules {
out[i].Attributes, err = crs.moduleModelAttributes(mod, refIndex)
if err != nil {
return
}
}
return
}
func (crs *composeRecordStore) moduleModelInit(mod *types.Module) (out *data.Model) {
return &data.Model{
StoreID: mod.Store.ComposeRecordStoreID,
ResourceID: mod.ID,
ResourceType: types.ModuleResourceType,
// @todo this isn't exactly this
Ident: mod.Handle,
Attributes: make(data.AttributeSet, len(mod.Fields)),
}
}
func (crs *composeRecordStore) moduleModelAttributes(mod *types.Module, refIndex map[uint64]*data.Model) (out data.AttributeSet, err error) {
for _, f := range mod.Fields {
attr := &data.Attribute{
Ident: f.Name,
MultiValue: f.Multi,
Store: moduleFieldCodec(f),
}
out = append(out, attr)
switch strings.ToLower(f.Kind) {
case "bool":
attr.Type = data.TypeBoolean{}
case "datetime":
switch {
case f.IsDateOnly():
attr.Type = data.TypeDate{}
case f.IsTimeOnly():
attr.Type = data.TypeTime{}
default:
attr.Type = data.TypeTimestamp{}
}
case "email":
attr.Type = data.TypeText{Length: emailLength}
case "file":
attr.Type = data.TypeRef{
RefModel: &data.Model{Ident: "attachments"},
RefAttribute: &data.Attribute{Ident: "id"},
}
case "number":
attr.Type = data.TypeNumber{
Precision: f.Options.Precision(),
// Scale: ,
}
case "record":
var refModel *data.Model
mRefID := f.Options.UInt64("moduleID")
if mRefID > 0 {
refModel = refIndex[mRefID]
}
attr.Type = data.TypeRef{
RefModel: refModel,
RefAttribute: &data.Attribute{
Ident: "id",
},
}
case "select":
attr.Type = data.TypeEnum{
Values: f.SelectOptions(),
}
case "string":
attr.Type = data.TypeText{
Length: 0,
}
case "url":
attr.Type = data.TypeText{
Length: urlLength,
}
case "user":
attr.Type = data.TypeRef{
// @todo...
RefAttribute: &data.Attribute{
Ident: "id",
},
}
}
}
// System attrs
out = append(out,
&data.Attribute{
Ident: "recordID",
Store: moduleFieldCodec(&types.ModuleField{
Name: "recordID",
}),
Type: data.TypeID{},
},
&data.Attribute{
Ident: "createdAt",
Store: moduleFieldCodec(&types.ModuleField{
Name: "createdAt",
}),
Type: data.TypeTimestamp{},
},
&data.Attribute{
Ident: "updatedAt",
Store: moduleFieldCodec(&types.ModuleField{
Name: "updatedAt",
}),
Type: data.TypeTimestamp{},
},
&data.Attribute{
Ident: "deletedAt",
Store: moduleFieldCodec(&types.ModuleField{
Name: "deletedAt",
}),
Type: data.TypeTimestamp{},
},
&data.Attribute{
Ident: "ownedBy",
Store: moduleFieldCodec(&types.ModuleField{
Name: "ownedBy",
}),
Type: data.TypeRef{
RefAttribute: &data.Attribute{Ident: "id"},
},
},
&data.Attribute{
Ident: "createdBy",
Store: moduleFieldCodec(&types.ModuleField{
Name: "createdBy",
}),
Type: data.TypeRef{
RefAttribute: &data.Attribute{Ident: "id"},
},
},
&data.Attribute{
Ident: "updatedBy",
Store: moduleFieldCodec(&types.ModuleField{
Name: "updatedBy",
}),
Type: data.TypeRef{
RefAttribute: &data.Attribute{Ident: "id"},
},
},
&data.Attribute{
Ident: "deletedBy",
Store: moduleFieldCodec(&types.ModuleField{
Name: "deletedBy",
}),
Type: data.TypeRef{
RefAttribute: &data.Attribute{Ident: "id"},
},
},
)
return
}
func (crs *composeRecordStore) addModel(ctx context.Context, 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)
if err != nil {
return
}
crs.models[storeID] = append(crs.models[storeID], model)
}
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
}
available, err := s.Models(ctx)
if err != nil {
return err
}
// Check if already in there
if existing := available.FindByIdent(model.Ident); existing != nil {
// Assert validity
diff := existing.Diff(model)
if len(diff) > 0 {
return fmt.Errorf("model %s exists: model not compatible: %v", existing.Ident, diff)
}
return nil
}
// Try to add to store
err = s.AddModel(ctx, model)
if err != nil {
return
}
return nil
}
+77
View File
@@ -0,0 +1,77 @@
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))
})
}
+66
View File
@@ -0,0 +1,66 @@
// 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
}
+131
View File
@@ -0,0 +1,131 @@
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)
})
}
+11
View File
@@ -12,12 +12,23 @@ import (
)
type (
CRSDef struct {
ComposeRecordStoreID uint64
Capabilities capabilities.Set
Partitioned bool
PartitionFormat string
PartitionBase []any
}
Module struct {
ID uint64 `json:"moduleID,string"`
Handle string `json:"handle"`
Meta types.JSONText `json:"meta"`
Fields ModuleFieldSet `json:"fields"`
Store CRSDef
Labels map[string]string `json:"labels,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
+44
View File
@@ -57,6 +57,50 @@ var (
_ sort.Interface = &ModuleFieldSet{}
)
func (f *ModuleField) SelectOptions() (out []string) {
if f.Kind != "Select" {
return
}
var (
options, has = f.Options["options"]
)
if !has {
return
}
switch oo := options.(type) {
case []string:
out = oo
case []interface{}:
for _, o := range oo {
switch c := o.(type) {
case string:
out = append(out, c)
case map[string]string:
if value, has := c["value"]; has {
out = append(out, value)
}
case map[string]interface{}:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
out = append(out, value)
}
}
case ModuleFieldOptions:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
out = append(out, value)
}
}
}
}
}
return
}
func (f *ModuleField) decodeTranslationsExpressionValidatorValidatorIDError(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation