Documentation ¶
Overview ¶
Import the package and use DataStore interface to interact with the data access layer. If you want DAL to use a Postgres database, ensure you have the following environment variables set to relevant values: [DB_ADMIN_USERNAME], [DB_PORT], [DB_NAME], [DB_ADMIN_PASSWORD], [DB_HOST], [SSL_MODE]. You can also set [LOG_LEVEL] environment variable to debug/trace, if you want logging at a specific level (default is [Info])
Define structs that will be persisted using datastore similar to any gorm Models, for reference https://gorm.io/docs/models.html
- At least one field must be a primary key with `gorm:"primaryKey"` tag
- For multi-tenancy support, add `gorm:"column:org_id"` as tag to a filed
- For revision support to block concurrent updates, add `gorm:"column:revision"` as tag
- For multi-instance support, add `gorm:"column:instance_id"` as tag
DataStore interface exposes basic methods like Find/FindAll/Upsert/Delete. For richer queries and performing a set of operations within a transaction, please, use GetTransaction() method. For more info, refer to Gorm's transactions page: https://gorm.io/docs/transactions.html
Index ¶
- Constants
- func DBCreate(cfg DBConfig) error
- func DBExists(cfg DBConfig) bool
- func GetFieldValue(record Record, fieldName, columnName string) (string, bool)
- func GetInstanceId(record Record) (string, bool)
- func GetOrgId(record Record) (string, bool)
- func GetTableName(x interface{}) (tableName string)
- func IsColumnPresent(x Record, tableName, columnName string) bool
- func IsMultiInstanced(x Record, tableName string, instancerConfigured bool) bool
- func IsMultiTenanted(x Record, tableName string) bool
- func IsPointerToStruct(x interface{}) (isPtrType bool)
- func IsRevisioned(x Record, tableName string) bool
- func IsRowLevelSecurityRequired(record Record, tableName string, instancerConfigured bool) bool
- func SetFieldValue(record Record, fieldName, columnName, value string) bool
- func SetInstanceId(record Record, value string) bool
- func TypeName(x interface{}) string
- type DBConfig
- type DataStore
- func FromConfig(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer, ...) (d DataStore, err error)
- func FromEnv(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer) (d DataStore, err error)
- func FromEnvWithDB(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer, ...) (d DataStore, err error)
- type Helper
- type Pagination
- type Record
- type TenancyInfo
- type TestHelper
Examples ¶
Constants ¶
const ( DbConfigOrgId = "multitenant.orgId" // Name of Postgres run-time config. parameter that will store current user's org. ID DbConfigInstanceId = "multitenant.instanceId" // Name of Postgres run-time config. parameter that will store current session's instance ID MaxIdleConns = 1 )
const ( // Env. variable names. DB_NAME_ENV_VAR = "DB_NAME" DB_PORT_ENV_VAR = "DB_PORT" DB_HOST_ENV_VAR = "DB_HOST" SSL_MODE_ENV_VAR = "SSL_MODE" DB_ADMIN_USERNAME_ENV_VAR = "DB_ADMIN_USERNAME" DB_ADMIN_PASSWORD_ENV_VAR = "DB_ADMIN_PASSWORD" // SQL Error Codes. ERROR_DUPLICATE_KEY = "SQLSTATE 23505" ERROR_DUPLICATE_DATABASE = "SQLSTATE 42P04" )
const ( DEFAULT_OFFSET = 0 DEFAULT_LIMIT = 1000 DEFAULT_SORTBY = "" )
const ( // Struct Field Names. FIELD_ORGID = "OrgId" FIELD_INSTANCEID = "InstanceId" // SQL Columns. COLUMN_ORGID = "org_id" COLUMN_INSTANCEID = "instance_id" COLUMN_REVISION = "revision" // Messages. REVISION_OUTDATED_MSG = "Invalid update - outdated " )
Variables ¶
This section is empty.
Functions ¶
func GetFieldValue ¶ added in v0.0.5
Returns the requested fields value from record, which is a pointer to a struct implementing Record interface. Uses a tag rather than field name to find the desired field. Returns an empty string and false if such a field is not present.
func GetInstanceId ¶ added in v0.0.5
Returns the requested InstanceId field's value from record, which is a pointer to a struct implementing Record interface. Uses a tag rather than field name to find the desired field. Returns an empty string and false if such a field is not present.
func GetOrgId ¶
Returns the requested OrgId field's value from record, which is a pointer to a struct implementing Record interface. Uses a tag rather than field name to find the desired field. Returns an empty string and false if such a field is not present.
func GetTableName ¶
func GetTableName(x interface{}) (tableName string)
Extracts struct's name, which will serve as DB table name, using reflection.
func IsColumnPresent ¶ added in v0.0.5
func IsMultiInstanced ¶ added in v0.0.5
Checks if multiple deployment instances are supported in the given table.
func IsMultiTenanted ¶ added in v0.0.5
Checks if multiple tenants are supported in the given table.
func IsPointerToStruct ¶
func IsPointerToStruct(x interface{}) (isPtrType bool)
func IsRevisioned ¶ added in v0.0.5
Checks if revisioning is supported in the given table.
func IsRowLevelSecurityRequired ¶ added in v0.0.5
Row Level Security to used to partition tables for multi-tenancy and multi-instance support.
func SetFieldValue ¶ added in v0.0.5
func SetInstanceId ¶ added in v0.0.5
Types ¶
type DBConfig ¶
type DBConfig struct {
// contains filtered or unexported fields
}
func ConfigFromEnv ¶ added in v0.0.6
Returns DBConfig constructed from the env variables. If dbName is set, it is used instead of DB_NAME env variable. All env variables are required and if not set, this method would panic.
type DataStore ¶
type DataStore interface { Find(ctx context.Context, record Record) error FindSoftDeleted(ctx context.Context, record Record) error FindAll(ctx context.Context, records interface{}, pagination *Pagination) error FindAllIncludingSoftDeleted(ctx context.Context, records interface{}, pagination *Pagination) error FindWithFilter(ctx context.Context, filter Record, records interface{}, pagination *Pagination) error FindWithFilterIncludingSoftDeleted(ctx context.Context, filter Record, records interface{}, pagination *Pagination) error Insert(ctx context.Context, record Record) (int64, error) SoftDelete(ctx context.Context, record Record) (int64, error) Delete(ctx context.Context, record Record) (int64, error) Update(ctx context.Context, record Record) (int64, error) Upsert(ctx context.Context, record Record) (int64, error) GetTransaction(ctx context.Context, record ...Record) (tx *gorm.DB, err error) // Create a DB table for the given struct. Enables RLS in it if it is multi-tenant. // Generates Postgres roles and policies based on the provided role mapping and applies them // to the created DB table. // roleMapping - maps service roles to DB roles to be used for the generated DB table // There are 4 possible DB roles to choose from: // - READER, which gives read access to all the records in the table // - WRITER, which gives read & write access to all the records in the table // - TENANT_READER, which gives read access to current tenant's records // - TENANT_WRITER, which gives read & write access to current tenant's records. Register(ctx context.Context, roleMapping map[string]dbrole.DbRole, records ...Record) error Reset() GetAuthorizer() authorizer.Authorizer GetInstancer() authorizer.Instancer Helper() Helper TestHelper() TestHelper }
Example (MultiInstance) ¶
Example for the multi-instance feature, illustrates one can create records for different instances and that each instance context can access the data that belongs to specific instance only.
package main import ( "context" "fmt" "log" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/authorizer" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/datastore" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/dbrole" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/logutils" ) type Person struct { Id string `gorm:"primaryKey"` Name string Age int InstanceId string `gorm:"primaryKey"` } func (p Person) String() string { return fmt.Sprintf("[%s/%s] %s: %d", p.InstanceId, p.Id, p.Name, p.Age) } // Example for the multi-instance feature, illustrates one can create records for different instances // and that each instance context can access the data that belongs to specific instance only. func main() { uId := "P1337" p1 := &Person{uId, "Bob", 31, "Dev"} p2 := &Person{uId, "John", 36, "Prod"} p3 := &Person{"P3", "Pat", 39, "Dev"} SERVICE_ADMIN := "service_admin" SERVICE_AUDITOR := "service_auditor" mdAuthorizer := &authorizer.MetadataBasedAuthorizer{} instancer := &authorizer.SimpleInstancer{} ServiceAdminCtx := mdAuthorizer.GetAuthContext("", SERVICE_ADMIN) DevInstanceCtx := instancer.WithInstanceId(ServiceAdminCtx, "Dev") ProdInstanceCtx := instancer.WithInstanceId(ServiceAdminCtx, "Prod") // Initializes the Datastore using the metadata authorizer and connection details obtained from the ENV variables. ds, err := datastore.FromEnvWithDB(logutils.GetCompLogger(), mdAuthorizer, instancer, "ExampleDataStore_multiInstance") defer ds.Reset() if err != nil { log.Fatalf("datastore initialization from env errored: %s", err) } // Registers the necessary structs with their corresponding role mappings. roleMapping := map[string]dbrole.DbRole{ SERVICE_AUDITOR: dbrole.INSTANCE_READER, SERVICE_ADMIN: dbrole.INSTANCE_WRITER, } if err = ds.Register(context.TODO(), roleMapping, &Person{}); err != nil { log.Fatalf("Failed to create DB tables: %+v", err) } // Inserts a record with a given Id (uId) using the context of the Dev instance. rowsAffected, err := ds.Insert(DevInstanceCtx, p1) fmt.Println(rowsAffected, err) // Inserts another record with the same uId using the context of the Prod instance. rowsAffected, err = ds.Insert(ProdInstanceCtx, p2) fmt.Println(rowsAffected, err) // Inserts a third record with a different uId using the context of the Dev instance. rowsAffected, err = ds.Insert(DevInstanceCtx, p3) fmt.Println(rowsAffected, err) // Finds a record using the context of the Dev instance and the specified uId. q1 := &Person{Id: uId} err = ds.Find(DevInstanceCtx, q1) fmt.Println(q1, err) // Finds a record using the context of the Prod instance and the same uId. q2 := &Person{Id: uId} err = ds.Find(ProdInstanceCtx, q2) fmt.Println(q2, err) // Finds a record using the correct context of the Dev instance. q3 := &Person{Id: "P3"} err = ds.Find(DevInstanceCtx, q3) fmt.Println(q3, err) // Attempts to find a record using the incorrect context of the Prod instance and should error out. q4 := &Person{Id: "P3"} err = ds.Find(ProdInstanceCtx, q4) fmt.Printf("err != nil - %t\n", err != nil) // Deletes a record using the context of the Dev instance and the specified uId. rowsAffected, err = ds.Delete(DevInstanceCtx, q1) fmt.Println(rowsAffected, err) // Deletes a record using the context of the Prod instance and the same uId. rowsAffected, err = ds.Delete(ProdInstanceCtx, q2) fmt.Println(rowsAffected, err) // Attempts to delete a record using an invalid context of the Prod instance, which should not affect the database. rowsAffected, err = ds.Delete(ProdInstanceCtx, q4) fmt.Println(rowsAffected, err) // Deletes a record using a valid context of the Dev instance. rowsAffected, err = ds.Delete(DevInstanceCtx, q3) fmt.Println(rowsAffected, err) }
Output: 1 <nil> 1 <nil> 1 <nil> [Dev/P1337] Bob: 31 <nil> [Prod/P1337] John: 36 <nil> [Dev/P3] Pat: 39 <nil> err != nil - true 1 <nil> 1 <nil> 0 <nil> 1 <nil>
Example (MultiTenancy) ¶
Example for the multi-tenancy feature, illustrates one can create records for different tenants and that each tenant context can access the data that belongs to specific tenant only.
package main import ( "context" "fmt" "log" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/authorizer" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/datastore" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/dbrole" "github.com/vmware-labs/multi-tenant-persistence-for-saas/pkg/logutils" ) type User struct { Id string `gorm:"primaryKey"` Name string Age int OrgId string `gorm:"primaryKey"` } func (p User) String() string { return fmt.Sprintf("[%s/%s] %s: %d", p.OrgId, p.Id, p.Name, p.Age) } // Example for the multi-tenancy feature, illustrates one can create records for different tenants // and that each tenant context can access the data that belongs to specific tenant only. func main() { uId := "P1337" p1 := &User{uId, "Bob", 31, "Coke"} p2 := &User{uId, "John", 36, "Pepsi"} p3 := &User{"P3", "Pat", 39, "Coke"} TENANT_ADMIN := "tenant_admin" TENANT_AUDITOR := "tenant_auditor" mdAuthorizer := &authorizer.MetadataBasedAuthorizer{} CokeOrgCtx := mdAuthorizer.GetAuthContext("Coke", TENANT_ADMIN) PepsiOrgCtx := mdAuthorizer.GetAuthContext("Pepsi", TENANT_ADMIN) // Initializes the Datastore using the metadata authorizer and connection details obtained from the ENV variables. ds, err := datastore.FromEnvWithDB(logutils.GetCompLogger(), mdAuthorizer, nil, "ExampleDataStore_multiTenancy") defer ds.Reset() if err != nil { log.Fatalf("datastore initialization from env errored: %s", err) } // Registers the necessary structs with their corresponding tenant role mappings. roleMapping := map[string]dbrole.DbRole{ TENANT_AUDITOR: dbrole.TENANT_READER, TENANT_ADMIN: dbrole.TENANT_WRITER, } if err = ds.Register(context.TODO(), roleMapping, &User{}); err != nil { log.Fatalf("Failed to create DB tables: %+v", err) } // Inserts a record with a given Id (uId) using the context of the Coke organization. rowsAffected, err := ds.Insert(CokeOrgCtx, p1) fmt.Println(rowsAffected, err) // Inserts another record with the same uId using the context of the Pepsi organization. rowsAffected, err = ds.Insert(PepsiOrgCtx, p2) fmt.Println(rowsAffected, err) // Inserts a third record with a different uId using the context of the Coke organization. rowsAffected, err = ds.Insert(CokeOrgCtx, p3) fmt.Println(rowsAffected, err) // Finds a record using the context of the Coke organization and the specified uId. q1 := &User{Id: uId} err = ds.Find(CokeOrgCtx, q1) fmt.Println(q1, err) // Finds a record using the context of the Pepsi organization and the same uId. q2 := &User{Id: uId} err = ds.Find(PepsiOrgCtx, q2) fmt.Println(q2, err) // Finds a record using the correct context of the Coke organization. q3 := &User{Id: "P3"} err = ds.Find(CokeOrgCtx, q3) fmt.Println(q3, err) // Attempts to find a record using the incorrect context of the Pepsi organization and should error out. q4 := &User{Id: "P3"} err = ds.Find(PepsiOrgCtx, q4) fmt.Printf("err != nil - %t\n", err != nil) // Deletes a record using the context of the Coke organization and the specified uId. rowsAffected, err = ds.Delete(CokeOrgCtx, q1) fmt.Println(rowsAffected, err) // Deletes a record using the context of the Pepsi organization and the same uId. rowsAffected, err = ds.Delete(PepsiOrgCtx, q2) fmt.Println(rowsAffected, err) // Attempts to delete a record using an invalid context of the Pepsi organization, which should not affect the database. rowsAffected, err = ds.Delete(PepsiOrgCtx, q4) fmt.Println(rowsAffected, err) // Deletes a record using a valid context of the Coke organization. rowsAffected, err = ds.Delete(CokeOrgCtx, q3) fmt.Println(rowsAffected, err) }
Output: 1 <nil> 1 <nil> 1 <nil> [Coke/P1337] Bob: 31 <nil> [Pepsi/P1337] John: 36 <nil> [Coke/P3] Pat: 39 <nil> err != nil - true 1 <nil> 1 <nil> 0 <nil> 1 <nil>
func FromConfig ¶
func FromConfig(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer, cfg DBConfig) (d DataStore, err error)
func FromEnv ¶
func FromEnv(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer) (d DataStore, err error)
func FromEnvWithDB ¶ added in v0.0.9
func FromEnvWithDB(l *logrus.Entry, a authorizer.Authorizer, instancer authorizer.Instancer, dbName string) (d DataStore, err error)
type Helper ¶
type Helper interface { FindInTable(ctx context.Context, tableName string, record Record, softDelete bool) (err error) FindAllInTable(ctx context.Context, tableName string, records interface{}, pagination *Pagination, softDelete bool) error FindWithFilterInTable(ctx context.Context, tableName string, record Record, records interface{}, pagination *Pagination, softDelete bool) (err error) GetDBTransaction(ctx context.Context, tableName string, record Record) (tx *gorm.DB, err error) RegisterHelper(ctx context.Context, roleMapping map[string]dbrole.DbRole, tableName string, record Record) error }
type Pagination ¶ added in v0.0.4
func DefaultPagination ¶ added in v0.0.4
func DefaultPagination() *Pagination
func GetPagination ¶ added in v0.0.4
func GetPagination(offset int, limit int, sortBy string) *Pagination
func NoPagination ¶ added in v0.0.4
func NoPagination() *Pagination
type Record ¶
type Record interface { }
func GetRecordInstanceFromSlice ¶
func GetRecordInstanceFromSlice(x interface{}) Record
type TenancyInfo ¶ added in v0.0.5
type TestHelper ¶
type TestHelper interface { DropTables(records ...Record) error // Drop DB tables by records Truncate(tableNames ...string) error // Truncates DB tables TruncateCascade(cascade bool, tableNames ...string) error // Truncates DB tables, with an option to truncate them in a cascading fashion HasTable(tableName string) (bool, error) // Checks if DB table exists }