From 38a02535f01a1716447a332a8ceacf5351379d32 Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Wed, 5 Nov 2025 10:56:57 +0200 Subject: [PATCH] Make Attribute Profiles storable in MySQL and Postgres --- attributes/attributes_it_test.go | 4 +- config/config_defaults.go | 2 +- config/configsanity.go | 12 +- .../conf/samples/attr_test_mysql/cgrates.json | 3 +- .../samples/attr_test_postgres/cgrates.json | 148 ++++++++++++++++++ .../conf/samples/attr_test_redis/cgrates.json | 147 +++++++++++++++++ .../samples/attributes_mysql/cgrates.json | 6 +- .../samples/attributes_postgres/cgrates.json | 45 ++++++ .../samples/attributes_redis/cgrates.json | 43 +++++ data/storage/mysql/create_db_tables.sql | 13 +- data/storage/postgres/create_db_tables.sql | 11 ++ engine/models.go | 15 +- engine/storage_sql.go | 66 ++++++-- general_tests/attributes_it_test.go | 4 +- utils/attributes.go | 63 ++++++++ utils/consts.go | 47 +++--- utils/rsrparser.go | 25 +++ 17 files changed, 600 insertions(+), 54 deletions(-) create mode 100644 data/conf/samples/attr_test_postgres/cgrates.json create mode 100644 data/conf/samples/attr_test_redis/cgrates.json create mode 100644 data/conf/samples/attributes_postgres/cgrates.json create mode 100644 data/conf/samples/attributes_redis/cgrates.json diff --git a/attributes/attributes_it_test.go b/attributes/attributes_it_test.go index bd2d3d61a..834803c5d 100644 --- a/attributes/attributes_it_test.go +++ b/attributes/attributes_it_test.go @@ -109,11 +109,11 @@ func TestAttributesIT(t *testing.T) { case utils.MetaMongo: attrConfigDIR = "attributes_mongo" case utils.MetaRedis: - t.SkipNow() + attrConfigDIR = "attributes_redis" case utils.MetaMySQL: attrConfigDIR = "attributes_mysql" case utils.MetaPostgres: - t.SkipNow() + attrConfigDIR = "attributes_postgres" default: t.Fatal("Unknown Database type") } diff --git a/config/config_defaults.go b/config/config_defaults.go index 1b020482e..ef09cfbf5 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -132,6 +132,7 @@ const CGRATES_CFG_JSON = ` "*action_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*versions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, + "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, // compatible db types: <*internal|*redis|*mongo> "*actions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, @@ -143,7 +144,6 @@ const CGRATES_CFG_JSON = ` "*thresholds": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*filters": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*route_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, - "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*load_ids": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*resource_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, diff --git a/config/configsanity.go b/config/configsanity.go index 0b8a8da46..da257b18e 100644 --- a/config/configsanity.go +++ b/config/configsanity.go @@ -1026,6 +1026,14 @@ func (cfg *CGRConfig) checkConfigSanity() error { // DataDB sanity checks hasOneInternalDB := false // used to reutrn error in case more then 1 internaldb is found + allDBsItems := []string{ + utils.MetaAccounts, + utils.MetaIPProfiles, + utils.MetaIPAllocations, + utils.MetaActionProfiles, + utils.MetaChargerProfiles, + utils.MetaAttributeProfiles, + } for _, dbcfg := range cfg.dbCfg.DBConns { if dbcfg.Type == utils.MetaInternal { if hasOneInternalDB { @@ -1060,9 +1068,7 @@ func (cfg *CGRConfig) checkConfigSanity() error { dataDBTypes := []string{utils.MetaInternal, utils.MetaRedis, utils.MetaMongo, utils.Internal, utils.Redis, utils.Mongo} - if item != utils.MetaAccounts && item != utils.MetaIPProfiles && - item != utils.MetaIPAllocations && item != utils.MetaActionProfiles && - item != utils.MetaChargerProfiles { + if !slices.Contains(allDBsItems, item) { if item == utils.MetaCDRs { if !slices.Contains(storDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) { return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item, diff --git a/data/conf/samples/attr_test_mysql/cgrates.json b/data/conf/samples/attr_test_mysql/cgrates.json index 7803c1c22..0314b5599 100644 --- a/data/conf/samples/attr_test_mysql/cgrates.json +++ b/data/conf/samples/attr_test_mysql/cgrates.json @@ -36,7 +36,8 @@ } }, "items": { - "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} } }, "loaders": [ diff --git a/data/conf/samples/attr_test_postgres/cgrates.json b/data/conf/samples/attr_test_postgres/cgrates.json new file mode 100644 index 000000000..8a3a67473 --- /dev/null +++ b/data/conf/samples/attr_test_postgres/cgrates.json @@ -0,0 +1,148 @@ +{ +// CGRateS Configuration file +// + + +"general": { + "reply_timeout": "50s", +}, + +"logger": { + "level": 7 +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080", +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "postgres", + "db_host": "127.0.0.1", + "db_port": 5432, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, +"loaders": [ + { + "id": "*default", + "enabled": true, + "tenant": "cgrates.org", + "lockfile_path": ".cgr.lck", + "tp_in_dir": "/usr/share/cgrates/tariffplans/testit", + "tp_out_dir": "", + }, +], + +"cdrs": { + "enabled": true, + "chargers_conns":["*internal"], +}, + + +"attributes": { + "enabled": true, + "stats_conns": ["*localhost"], + "resources_conns": ["*localhost"], + "accounts_conns": ["*localhost"] +}, + + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"], +}, + + +"resources": { + "enabled": true, + "store_interval": "1s", + "thresholds_conns": ["*internal"] +}, + + +"stats": { + "enabled": true, + "store_interval": "1s", + "thresholds_conns": ["*internal"], +}, + + +"thresholds": { + "enabled": true, + "store_interval": "1s", +}, + + +"routes": { + "enabled": true, + "prefix_indexed_fields":["*req.Destination"], + "stats_conns": ["*internal"], + "resources_conns": ["*internal"], + "rates_conns": ["*internal"], +}, + + +"sessions": { + "enabled": true, + "routes_conns": ["*internal"], + "resources_conns": ["*internal"], + "attributes_conns": ["*internal"], + "rates_conns": ["*internal"], + "cdrs_conns": ["*internal"], + "chargers_conns": ["*internal"], +}, + + +"migrator":{ + "users_filters":["Account"], +}, + + +"admins": { + "enabled": true, +}, + + +"rates": { + "enabled": true +}, + + +"actions": { + "enabled": true, + "accounts_conns": ["*localhost"] +}, + + +"accounts": { + "enabled": true +}, + + +"filters": { + "stats_conns": ["*internal"], + "resources_conns": ["*internal"], + "accounts_conns": ["*internal"], +}, + + +} + diff --git a/data/conf/samples/attr_test_redis/cgrates.json b/data/conf/samples/attr_test_redis/cgrates.json new file mode 100644 index 000000000..7803c1c22 --- /dev/null +++ b/data/conf/samples/attr_test_redis/cgrates.json @@ -0,0 +1,147 @@ +{ +// CGRateS Configuration file +// + + +"general": { + "reply_timeout": "50s", +}, + +"logger": { + "level": 7 +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080", +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "mysql", + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, +"loaders": [ + { + "id": "*default", + "enabled": true, + "tenant": "cgrates.org", + "lockfile_path": ".cgr.lck", + "tp_in_dir": "/usr/share/cgrates/tariffplans/testit", + "tp_out_dir": "", + }, +], + +"cdrs": { + "enabled": true, + "chargers_conns":["*internal"], +}, + + +"attributes": { + "enabled": true, + "stats_conns": ["*localhost"], + "resources_conns": ["*localhost"], + "accounts_conns": ["*localhost"] +}, + + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"], +}, + + +"resources": { + "enabled": true, + "store_interval": "1s", + "thresholds_conns": ["*internal"] +}, + + +"stats": { + "enabled": true, + "store_interval": "1s", + "thresholds_conns": ["*internal"], +}, + + +"thresholds": { + "enabled": true, + "store_interval": "1s", +}, + + +"routes": { + "enabled": true, + "prefix_indexed_fields":["*req.Destination"], + "stats_conns": ["*internal"], + "resources_conns": ["*internal"], + "rates_conns": ["*internal"], +}, + + +"sessions": { + "enabled": true, + "routes_conns": ["*internal"], + "resources_conns": ["*internal"], + "attributes_conns": ["*internal"], + "rates_conns": ["*internal"], + "cdrs_conns": ["*internal"], + "chargers_conns": ["*internal"], +}, + + +"migrator":{ + "users_filters":["Account"], +}, + + +"admins": { + "enabled": true, +}, + + +"rates": { + "enabled": true +}, + + +"actions": { + "enabled": true, + "accounts_conns": ["*localhost"] +}, + + +"accounts": { + "enabled": true +}, + + +"filters": { + "stats_conns": ["*internal"], + "resources_conns": ["*internal"], + "accounts_conns": ["*internal"], +}, + + +} + diff --git a/data/conf/samples/attributes_mysql/cgrates.json b/data/conf/samples/attributes_mysql/cgrates.json index 632c04235..e9ca17c9f 100644 --- a/data/conf/samples/attributes_mysql/cgrates.json +++ b/data/conf/samples/attributes_mysql/cgrates.json @@ -25,12 +25,14 @@ } }, "items": { - "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, + "replicate":false, "dbConn": "StorDB"} } }, "attributes": { - "enabled": true, + "enabled": true, "prefix_indexed_fields": ["*req.Destination"], "exists_indexed_fields": ["*opts.*usage"], "notexists_indexed_fields": ["*req.ToR"], diff --git a/data/conf/samples/attributes_postgres/cgrates.json b/data/conf/samples/attributes_postgres/cgrates.json new file mode 100644 index 000000000..57414472d --- /dev/null +++ b/data/conf/samples/attributes_postgres/cgrates.json @@ -0,0 +1,45 @@ +{ + // CGRateS Configuration file + // will be used in apis/attributes_it_test.go + + "logger": { + "level": 7 + }, + + "db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { // The id of the DB connection + "db_type": "postgres", // db type: + "db_host": "127.0.0.1", + "db_port": 5432, // db port to reach the database + "db_name": "cgrates", // the host to connect to + "db_user": "cgrates", + "db_password": "CGRateS.org" // password to use when connecting to the database + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, + "replicate":false, "dbConn": "StorDB"} + } + }, + + "attributes": { + "enabled": true, + "prefix_indexed_fields": ["*req.Destination"], + "exists_indexed_fields": ["*opts.*usage"], + "notexists_indexed_fields": ["*req.ToR"], + }, + + "admins": { + "enabled": true, + } + +} \ No newline at end of file diff --git a/data/conf/samples/attributes_redis/cgrates.json b/data/conf/samples/attributes_redis/cgrates.json new file mode 100644 index 000000000..d227fa5e0 --- /dev/null +++ b/data/conf/samples/attributes_redis/cgrates.json @@ -0,0 +1,43 @@ +{ + // CGRateS Configuration file + // will be used in apis/attributes_it_test.go + + "logger": { + "level": 7 + }, + + "db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "mysql", + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } + }, + + "attributes": { + "enabled": true, + "prefix_indexed_fields": ["*req.Destination"], + "exists_indexed_fields": ["*opts.*usage"], + "notexists_indexed_fields": ["*req.ToR"], + }, + + "admins": { + "enabled": true, + } + +} \ No newline at end of file diff --git a/data/storage/mysql/create_db_tables.sql b/data/storage/mysql/create_db_tables.sql index 55d5396f0..5d24036de 100644 --- a/data/storage/mysql/create_db_tables.sql +++ b/data/storage/mysql/create_db_tables.sql @@ -55,4 +55,15 @@ CREATE TABLE charger_profiles ( PRIMARY KEY (`pk`), UNIQUE KEY unique_tenant_id (`tenant`, `id`) ); -CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles (`id`); \ No newline at end of file +CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles (`id`); + +DROP TABLE IF EXISTS attribute_profiles; +CREATE TABLE attribute_profiles ( + `pk` int(11) NOT NULL AUTO_INCREMENT, + `tenant` VARCHAR(40) NOT NULL, + `id` VARCHAR(64) NOT NULL, + `attribute_profile` JSON NOT NULL, + PRIMARY KEY (`pk`), + UNIQUE KEY unique_tenant_id (`tenant`, `id`) +); +CREATE UNIQUE INDEX attribute_profiles_idx ON attribute_profiles (`id`); \ No newline at end of file diff --git a/data/storage/postgres/create_db_tables.sql b/data/storage/postgres/create_db_tables.sql index b12166b3d..62c6344fe 100644 --- a/data/storage/postgres/create_db_tables.sql +++ b/data/storage/postgres/create_db_tables.sql @@ -54,3 +54,14 @@ CREATE TABLE charger_profiles ( UNIQUE (tenant, id) ); CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles ("id"); + + +DROP TABLE IF EXISTS attribute_profiles; +CREATE TABLE attribute_profiles ( + pk SERIAL PRIMARY KEY, + tenant VARCHAR(40) NOT NULL, + id VARCHAR(64) NOT NULL, + attribute_profile JSONB NOT NULL, + UNIQUE (tenant, id) +); +CREATE UNIQUE INDEX attribute_profiles_idx ON attribute_profiles ("id"); diff --git a/engine/models.go b/engine/models.go index 8152485f4..845f2c1ae 100644 --- a/engine/models.go +++ b/engine/models.go @@ -424,7 +424,7 @@ type ActionProfileJSONMdl struct { } func (ActionProfileJSONMdl) TableName() string { - return utils.TBLActionProfilesJSON + return utils.TBLActionProfiles } type ChargerProfileMdl struct { @@ -435,5 +435,16 @@ type ChargerProfileMdl struct { } func (ChargerProfileMdl) TableName() string { - return utils.TBLChargerProfilesJSON + return utils.TBLChargerProfiles +} + +type AttributeProfileMdl struct { + PK uint `gorm:"primary_key"` + Tenant string `index:"0" re:".*"` + ID string `index:"1" re:".*"` + AttributeProfile utils.JSONB `gorm:"type:jsonb" index:"2" re:".*"` +} + +func (AttributeProfileMdl) TableName() string { + return utils.TBLAttributeProfiles } diff --git a/engine/storage_sql.go b/engine/storage_sql.go index 37f646246..f4c119314 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -105,9 +105,11 @@ func (sqls *SQLStorage) GetKeysForPrefix(ctx *context.Context, prefix string) (k case utils.IPAllocationsPrefix: keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLIPAllocations, tntID) case utils.ActionProfilePrefix: - keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLActionProfilesJSON, tntID) + keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLActionProfiles, tntID) case utils.ChargerProfilePrefix: - keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLChargerProfilesJSON, tntID) + keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLChargerProfiles, tntID) + case utils.AttributeProfilePrefix: + keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLAttributeProfiles, tntID) default: err = fmt.Errorf("unsupported prefix in GetKeysForPrefix: %q", prefix) } @@ -616,6 +618,51 @@ func (sqls *SQLStorage) RemoveChargerProfileDrv(_ *context.Context, tenant, id s return nil } +func (sqls *SQLStorage) GetAttributeProfileDrv(ctx *context.Context, tenant, id string) (ap *utils.AttributeProfile, err error) { + var result []*AttributeProfileMdl + if err := sqls.db.Model(&AttributeProfileMdl{}).Where(&AttributeProfileMdl{Tenant: tenant, + ID: id}).Find(&result).Error; err != nil { + return nil, err + } + if len(result) == 0 { + return nil, utils.ErrNotFound + } + + return utils.MapStringInterfaceToAttributeProfile(result[0].AttributeProfile) +} + +func (sqls *SQLStorage) SetAttributeProfileDrv(ctx *context.Context, ap *utils.AttributeProfile) (err error) { + tx := sqls.db.Begin() + mdl := &AttributeProfileMdl{ + Tenant: ap.Tenant, + ID: ap.ID, + AttributeProfile: ap.AsMapStringInterface(), + } + if err = tx.Model(&AttributeProfileMdl{}).Where( + AttributeProfileMdl{Tenant: mdl.Tenant, ID: mdl.ID}).Delete( + AttributeProfileMdl{}).Error; err != nil { + tx.Rollback() + return + } + if err = tx.Save(mdl).Error; err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func (sqls *SQLStorage) RemoveAttributeProfileDrv(ctx *context.Context, tenant, id string) (err error) { + tx := sqls.db.Begin() + if err = tx.Model(&AttributeProfileMdl{}).Where(&AttributeProfileMdl{Tenant: tenant, ID: id}). + Delete(&AttributeProfileMdl{}).Error; err != nil { + tx.Rollback() + return err + } + tx.Commit() + return +} + // AddLoadHistory DataDB method not implemented yet func (sqls *SQLStorage) AddLoadHistory(ldInst *utils.LoadInstance, loadHistSize int, transactionID string) error { @@ -838,21 +885,6 @@ func (sqls *SQLStorage) RemoveRouteProfileDrv(ctx *context.Context, tenant, id s return utils.ErrNotImplemented } -// DataDB method not implemented yet -func (sqls *SQLStorage) GetAttributeProfileDrv(ctx *context.Context, tenant, id string) (r *utils.AttributeProfile, err error) { - return nil, utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) SetAttributeProfileDrv(ctx *context.Context, r *utils.AttributeProfile) (err error) { - return utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) RemoveAttributeProfileDrv(ctx *context.Context, tenant, id string) (err error) { - return utils.ErrNotImplemented -} - // GetStorageType returns the storage type that is being used func (sqls *SQLStorage) GetStorageType() string { return utils.MetaMySQL diff --git a/general_tests/attributes_it_test.go b/general_tests/attributes_it_test.go index 6dc85f6e3..0a87f407a 100644 --- a/general_tests/attributes_it_test.go +++ b/general_tests/attributes_it_test.go @@ -67,13 +67,13 @@ func TestAttributeSIT(t *testing.T) { case utils.MetaInternal: alsPrfConfigDIR = "attr_test_internal" case utils.MetaRedis: - t.SkipNow() + alsPrfConfigDIR = "attr_test_redis" case utils.MetaMySQL: alsPrfConfigDIR = "attr_test_mysql" case utils.MetaMongo: alsPrfConfigDIR = "attr_test_mongo" case utils.MetaPostgres: - t.SkipNow() + alsPrfConfigDIR = "attr_test_postgres" default: t.Fatal("Unknown Database type") } diff --git a/utils/attributes.go b/utils/attributes.go index f9a19b7eb..e78bc31c2 100644 --- a/utils/attributes.go +++ b/utils/attributes.go @@ -415,3 +415,66 @@ func (ext *APIAttributeProfile) AsAttributeProfile() (attr *AttributeProfile, er attr.Weights = ext.Weights return } + +// AsMapStringInterface converts AttributeProfile struct to map[string]any +func (ap *AttributeProfile) AsMapStringInterface() map[string]any { + if ap == nil { + return nil + } + return map[string]any{ + Tenant: ap.Tenant, + ID: ap.ID, + FilterIDs: ap.FilterIDs, + Weights: ap.Weights, + Blockers: ap.Blockers, + Attributes: ap.Attributes, + } +} + +// MapStringInterfaceToAttributeProfile converts map[string]any to AttributeProfile struct +func MapStringInterfaceToAttributeProfile(m map[string]any) (ap *AttributeProfile, err error) { + ap = &AttributeProfile{} + if v, ok := m[Tenant].(string); ok { + ap.Tenant = v + } + if v, ok := m[ID].(string); ok { + ap.ID = v + } + ap.FilterIDs = InterfaceToStringSlice(m[FilterIDs]) + ap.Weights = InterfaceToDynamicWeights(m[Weights]) + ap.Blockers = InterfaceToDynamicBlockers(m[Blockers]) + ap.Attributes = InterfaceToAttributes(m[Attributes]) + return +} + +// InterfaceToAttributes converts interface{} to []*Attribute +func InterfaceToAttributes(v any) []*Attribute { + if v == nil { + return nil + } + attrs, ok := v.([]any) + if !ok { + return nil + } + attributes := make([]*Attribute, 0, len(attrs)) + for _, attrIface := range attrs { + attrMap, ok := attrIface.(map[string]any) + if !ok { + continue + } + attr := &Attribute{} + attr.FilterIDs = InterfaceToStringSlice(attrMap[FilterIDs]) + attr.Blockers = InterfaceToDynamicBlockers(attrMap[Blockers]) + if path, ok := attrMap[Path].(string); ok { + attr.Path = path + } + if typ, ok := attrMap[Type].(string); ok { + attr.Type = typ + } + if valueIface, ok := attrMap[Value]; ok { + attr.Value = InterfaceToRSRParsers(valueIface) + } + attributes = append(attributes, attr) + } + return attributes +} diff --git a/utils/consts.go b/utils/consts.go index ff1b5dcab..0a4ee41d4 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1897,29 +1897,30 @@ const ( // Table Name const ( - TBLTPResources = "tp_resources" - TBLTPStats = "tp_stats" - TBLTPRankings = "tp_rankings" - TBLTPTrends = "tp_trends" - TBLTPThresholds = "tp_thresholds" - TBLTPFilters = "tp_filters" - SessionCostsTBL = "session_costs" - CDRsTBL = "cdrs" - TBLTPRoutes = "tp_routes" - TBLTPAttributes = "tp_attributes" - TBLTPChargers = "tp_chargers" - TBLVersions = "versions" - TBLAccounts = "accounts" - TBLIPProfiles = "ip_profiles" - TBLIPAllocations = "ip_allocations" - TBLActionProfilesJSON = "action_profiles" - TBLChargerProfilesJSON = "charger_profiles" - OldSMCosts = "sm_costs" - TBLTPDispatchers = "tp_dispatcher_profiles" - TBLTPDispatcherHosts = "tp_dispatcher_hosts" - TBLTPRateProfiles = "tp_rate_profiles" - TBLTPActionProfiles = "tp_action_profiles" - TBLTPAccounts = "tp_accounts" + TBLTPResources = "tp_resources" + TBLTPStats = "tp_stats" + TBLTPRankings = "tp_rankings" + TBLTPTrends = "tp_trends" + TBLTPThresholds = "tp_thresholds" + TBLTPFilters = "tp_filters" + SessionCostsTBL = "session_costs" + CDRsTBL = "cdrs" + TBLTPRoutes = "tp_routes" + TBLTPAttributes = "tp_attributes" + TBLTPChargers = "tp_chargers" + TBLVersions = "versions" + TBLAccounts = "accounts" + TBLIPProfiles = "ip_profiles" + TBLIPAllocations = "ip_allocations" + TBLActionProfiles = "action_profiles" + TBLChargerProfiles = "charger_profiles" + TBLAttributeProfiles = "attribute_profiles" + OldSMCosts = "sm_costs" + TBLTPDispatchers = "tp_dispatcher_profiles" + TBLTPDispatcherHosts = "tp_dispatcher_hosts" + TBLTPRateProfiles = "tp_rate_profiles" + TBLTPActionProfiles = "tp_action_profiles" + TBLTPAccounts = "tp_accounts" ) // Cache Name diff --git a/utils/rsrparser.go b/utils/rsrparser.go index 9b0362e7a..0cbdc317f 100644 --- a/utils/rsrparser.go +++ b/utils/rsrparser.go @@ -214,6 +214,31 @@ func (prsrs RSRParsers) AsStringSlice() (v []string) { return } +// InterfaceToRSRParsers converts interface to RSRParsers (unexported fields not included) +func InterfaceToRSRParsers(v any) RSRParsers { + if v == nil { + return nil + } + parsers := make(RSRParsers, 0) + switch val := v.(type) { + case []any: + for _, item := range val { + if parserMap, ok := item.(map[string]any); ok { + parser := &RSRParser{} + if rules, ok := parserMap[Rules].(string); ok { + parser.Rules = rules + } + if path, ok := parserMap[Path].(string); ok { + parser.Path = path + } + parsers = append(parsers, parser) + } + } + } + + return parsers +} + // NewRSRParser builds one RSRParser func NewRSRParser(parserRules string) (rsrParser *RSRParser, err error) { if len(parserRules) == 0 {