Documentation
¶
Overview ¶
Package data provides Space-level database isolation for multi-tenant applications.
Space Isolation Overview ¶
Space isolation allows each logical space (tenant) to operate within its own database namespace, providing data separation at the database level rather than relying solely on application-layer checks.
The isolation is implemented through the SpaceRouter interface. ncore provides the plumbing and contract; the routing strategy is application-defined and injected via WithSpaceRouter.
Isolation Strategies ¶
PostgreSQL – schema-per-space:
Each Space gets its own PostgreSQL schema. The search_path is set per-transaction using SET LOCAL, which is safe with connection pools because the setting is automatically reverted when the transaction ends. BEGIN; SET LOCAL search_path TO "space_acme", "public"; -- all queries run in the space_acme schema COMMIT; -- connection returned to pool with original search_path restored
MySQL – database-per-space:
Each Space gets its own MySQL database. Application-managed per-space connection pools are used, typically with LRU eviction to cap total pool count.
Security ¶
Schema and database names are constructed from space identifiers. Because these identifiers cannot be parameterized via SQL placeholders, ValidateSpaceID enforces a strict allowlist regex to prevent injection attacks. Always call it before incorporating a space identifier into any SQL string.
Backward Compatibility ¶
All existing APIs (Data.WithTx, Data.GetMasterDB, etc.) are unchanged. If no SpaceRouter is injected, Data.WithSpaceTx and Data.WithSpaceTxRead transparently fall back to the ordinary transaction methods.
Usage ¶
// 1. Implement SpaceRouter in your application layer.
router := myapp.NewPostgresSchemaRouter(masterDB, slaveDB)
// 2. Initialize Data and inject the router.
d, cleanup, err := data.New(cfg)
data.WithSpaceRouter(router)(d)
// 3. Set space ID in HTTP middleware.
ctx = data.SetSpaceID(ctx, space.Slug)
// 4. Execute within the space namespace in service / repository.
err = d.WithSpaceTx(ctx, func(ctx context.Context) error {
tx, _ := data.GetTx(ctx) // *sql.Tx scoped to the space's schema or database
return repo.Create(ctx, entity)
})
Index ¶
- Variables
- func BuildSchemaName(prefix, spaceID string) (string, error)
- func ExecInTx(ctx context.Context, db *sql.DB, opts *sql.TxOptions, setupSQL string, ...) error
- func GetSpaceID(ctx context.Context) (string, bool)
- func GetTx(ctx context.Context) (*sql.Tx, error)
- func ListRegisteredDrivers() map[string][]string
- func NewSearchClient(d *Data, collector ...metrics.Collector) *search.Client
- func QuotePostgresIdentifier(identifier string) (string, error)
- func RegisterCacheDriver(driver CacheDriver)
- func RegisterDatabaseDriver(driver DatabaseDriver)
- func RegisterMessageDriver(driver MessageDriver)
- func RegisterSearchDriver(driver SearchDriver)
- func RegisterStorageDriver(driver StorageDriver)
- func SetSpaceID(ctx context.Context, id string) context.Context
- func ValidateIdentifier(identifier string) error
- func ValidateSpaceID(id string) error
- type CacheDriver
- type ContextKey
- type Data
- func (d *Data) Close() []error
- func (d *Data) ConsumeFromKafka(ctx context.Context, topic, groupID string, handler func([]byte) error) error
- func (d *Data) ConsumeFromRabbitMQ(queue string, handler func([]byte) error) error
- func (d *Data) DB() *sql.DB
- func (d *Data) DBRead() (*sql.DB, error)
- func (d *Data) GetDBManager() *connection.DBManager
- func (d *Data) GetDatabaseNodes() (master *sql.DB, slaves []*sql.DB, err error)
- func (d *Data) GetElasticsearch() any
- func (d *Data) GetMasterDB() *sql.DB
- func (d *Data) GetMeilisearch() any
- func (d *Data) GetMetricsCollector() metrics.Collector
- func (d *Data) GetMongoCollection(dbName, collName string, readOnly bool) (any, error)
- func (d *Data) GetMongoDatabase(name string, readOnly bool) (any, error)
- func (d *Data) GetMongoManager() any
- func (d *Data) GetOpenSearch() any
- func (d *Data) GetRedis() any
- func (d *Data) GetSlaveDB() (*sql.DB, error)
- func (d *Data) GetSpaceRouter() SpaceRouter
- func (d *Data) GetStats() map[string]any
- func (d *Data) Health(ctx context.Context) map[string]any
- func (d *Data) IsMessagingAvailable() bool
- func (d *Data) IsMessagingEnabled() bool
- func (d *Data) IsQueueAvailable() bool
- func (d *Data) IsReadOnlyMode(ctx context.Context) bool
- func (d *Data) MongoHealthCheck(ctx context.Context) error
- func (d *Data) Ping(ctx context.Context) error
- func (d *Data) PublishToKafka(ctx context.Context, topic string, key, value []byte) error
- func (d *Data) PublishToRabbitMQ(exchange, routingKey string, body []byte) error
- func (d *Data) ShouldUseMemoryFallback() bool
- func (d *Data) WithMongoTransaction(ctx context.Context, fn func(any) error) error
- func (d *Data) WithSpaceTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (d *Data) WithSpaceTxRead(ctx context.Context, fn func(ctx context.Context) error) error
- func (d *Data) WithTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (d *Data) WithTxRead(ctx context.Context, fn func(ctx context.Context) error) error
- type DatabaseDriver
- type MessageDriver
- type Option
- type SearchCollectorAdapter
- type SearchDriver
- type SearchEngine
- type SpaceRouter
- type StorageDriver
Constants ¶
This section is empty.
Variables ¶
var ProviderSet = wire.NewSet(ProvideData)
ProviderSet is the wire provider set for the data package. It provides *Data with a cleanup function that closes all connections.
Usage:
wire.Build(
data.ProviderSet,
// ... other providers
)
Functions ¶
func BuildSchemaName ¶ added in v0.2.5
BuildSchemaName constructs a safe schema or database name by combining a validated prefix with a sanitized space identifier.
- prefix must be a safe identifier component (e.g. "space_").
- spaceID is validated via ValidateSpaceID, then lowercased, and hyphens are replaced with underscores to satisfy both PostgreSQL schema naming rules and MySQL database naming conventions.
Returns an error if prefix, spaceID, or the final name fails validation.
Example:
name, err := data.BuildSchemaName("space_", "Acme-Corp")
// name == "space_acme_corp"
func ExecInTx ¶ added in v0.2.5
func ExecInTx(ctx context.Context, db *sql.DB, opts *sql.TxOptions, setupSQL string, fn func(ctx context.Context) error) error
ExecInTx is a low-level helper intended for use inside SpaceRouter implementations. It:
- Begins a transaction on db using opts (pass nil for read-write defaults).
- If setupSQL is non-empty, executes it immediately after BEGIN. This is the hook for statements like: "SET LOCAL search_path TO space_acme, public"
- Stores the *sql.Tx in ctx under ContextKeyTransaction so that callers of fn can retrieve it with GetTx.
- Calls fn with the enriched context.
- Commits on success or rolls back on error.
The setupSQL string must NOT be constructed from untrusted user input. Always derive schema / database names through ValidateSpaceID first.
Example PostgreSQL schema router usage:
func (r *PostgresSchemaRouter) WithSpace(ctx context.Context, spaceID string, fn func(context.Context) error) error {
schema, err := r.schemaName(spaceID) // internally calls ValidateSpaceID
if err != nil {
return err
}
schemaIdent, err := data.QuotePostgresIdentifier(schema)
if err != nil {
return err
}
globalIdent, err := data.QuotePostgresIdentifier(r.globalSchema)
if err != nil {
return err
}
setup := fmt.Sprintf("SET LOCAL search_path TO %s, %s", schemaIdent, globalIdent)
return data.ExecInTx(ctx, r.masterDB, nil, setup, fn)
}
func GetSpaceID ¶ added in v0.2.5
GetSpaceID retrieves the Space identifier previously set by SetSpaceID. The boolean return value is false when no space ID is present or when it is the empty string.
func ListRegisteredDrivers ¶ added in v0.2.0
ListRegisteredDrivers returns a snapshot of all registered drivers. Useful for debugging and diagnostics.
func NewSearchClient ¶ added in v0.2.1
NewSearchClient creates a search client from ncore data layer. It automatically detects and creates adapters for available search engines.
Returns nil if no search engines are available. Applications should check if the returned client is nil to support optional search functionality.
func QuotePostgresIdentifier ¶ added in v0.2.5
QuotePostgresIdentifier quotes a PostgreSQL identifier after validating it.
func RegisterCacheDriver ¶ added in v0.2.0
func RegisterCacheDriver(driver CacheDriver)
RegisterCacheDriver makes a cache driver available by the provided name. It follows the same pattern as RegisterDatabaseDriver.
func RegisterDatabaseDriver ¶ added in v0.2.0
func RegisterDatabaseDriver(driver DatabaseDriver)
RegisterDatabaseDriver makes a database driver available by the provided name. It is intended to be called from the init function in driver packages.
Example usage in a driver package:
func init() {
data.RegisterDatabaseDriver(&postgresDriver{})
}
If RegisterDatabaseDriver is called twice with the same name or if driver is nil, it panics.
func RegisterMessageDriver ¶ added in v0.2.0
func RegisterMessageDriver(driver MessageDriver)
RegisterMessageDriver makes a message queue driver available by the provided name.
func RegisterSearchDriver ¶ added in v0.2.0
func RegisterSearchDriver(driver SearchDriver)
RegisterSearchDriver makes a search engine driver available by the provided name.
func RegisterStorageDriver ¶ added in v0.2.0
func RegisterStorageDriver(driver StorageDriver)
RegisterStorageDriver makes a storage driver available by the provided name.
func SetSpaceID ¶ added in v0.2.5
SetSpaceID stores the Space identifier in ctx and returns the new context. It is typically called from an HTTP middleware after the space has been validated for the authenticated user.
ctx = data.SetSpaceID(r.Context(), space.Slug)
func ValidateIdentifier ¶ added in v0.2.5
ValidateIdentifier checks that an identifier component controlled by the application, such as a schema prefix or global schema name, is safe for SQL identifier use.
func ValidateSpaceID ¶ added in v0.2.5
ValidateSpaceID checks that id is safe for use as, or as a component of, a PostgreSQL schema name or MySQL database name.
Because schema / database names cannot be parameterized in SQL, they must be sanitized before string interpolation. ValidateSpaceID enforces the allowlist regex [a-zA-Z0-9][a-zA-Z0-9_-]{0,62}.
Callers should additionally convert the identifier to lowercase and replace hyphens with underscores before building the final schema or database name, since some databases treat these characters specially.
if err := data.ValidateSpaceID(slug); err != nil {
return err
}
schema := "space_" + strings.ToLower(strings.ReplaceAll(slug, "-", "_"))
Types ¶
type CacheDriver ¶ added in v0.2.0
type CacheDriver interface {
// Name returns the driver identifier (e.g., "redis", "memcached")
Name() string
// Connect establishes a new cache connection.
Connect(ctx context.Context, cfg any) (any, error)
// Close terminates the cache connection.
Close(conn any) error
// Ping verifies the cache connection is alive.
Ping(ctx context.Context, conn any) error
}
CacheDriver defines the interface for cache/key-value store drivers.
func GetCacheDriver ¶ added in v0.2.0
func GetCacheDriver(name string) (CacheDriver, error)
GetCacheDriver retrieves a registered cache driver by name.
type ContextKey ¶
type ContextKey string
const ContextKeySpaceID ContextKey = "space_id"
ContextKeySpaceID is the context key used to store and retrieve the current Space identifier. Use SetSpaceID and GetSpaceID instead of reading this key directly.
const (
ContextKeyTransaction ContextKey = "tx"
)
type Data ¶
type Data struct {
Conn *connection.Connections
// contains filtered or unexported fields
}
func ProvideData ¶ added in v0.2.0
ProvideData initializes and returns the data layer with cleanup function. The cleanup function should be called when the application shuts down to properly close all database connections and release resources.
func (*Data) ConsumeFromKafka ¶
func (d *Data) ConsumeFromKafka(ctx context.Context, topic, groupID string, handler func([]byte) error) error
ConsumeFromKafka consumes messages from Kafka with metrics
func (*Data) ConsumeFromRabbitMQ ¶
ConsumeFromRabbitMQ consumes messages from RabbitMQ with metrics
func (*Data) GetDBManager ¶
func (d *Data) GetDBManager() *connection.DBManager
func (*Data) GetDatabaseNodes ¶
GetDatabaseNodes returns information about all database nodes (master and slaves)
func (*Data) GetElasticsearch ¶
func (*Data) GetMasterDB ¶
func (*Data) GetMeilisearch ¶
func (*Data) GetMetricsCollector ¶
GetMetricsCollector returns the metrics collector
func (*Data) GetMongoCollection ¶
func (*Data) GetMongoDatabase ¶
func (*Data) GetMongoManager ¶
func (*Data) GetOpenSearch ¶
func (*Data) GetSpaceRouter ¶ added in v0.2.5
func (d *Data) GetSpaceRouter() SpaceRouter
GetSpaceRouter returns the injected SpaceRouter, or nil if none was set. Useful for applications that need direct access to the router for lifecycle operations (e.g. schema provisioning).
func (*Data) IsMessagingAvailable ¶
IsMessagingAvailable checks if any messaging (queue or memory) is available Deprecated: Use IsMessagingEnabled() and IsQueueAvailable() separately
func (*Data) IsMessagingEnabled ¶
IsMessagingEnabled checks if messaging services
func (*Data) IsQueueAvailable ¶
IsQueueAvailable checks if external message queues are available
func (*Data) IsReadOnlyMode ¶
IsReadOnlyMode checks if the system is in read-only mode (only slaves available)
func (*Data) PublishToKafka ¶
PublishToKafka publishes message to Kafka with metrics
func (*Data) PublishToRabbitMQ ¶
PublishToRabbitMQ publishes message to RabbitMQ with metrics
func (*Data) ShouldUseMemoryFallback ¶
ShouldUseMemoryFallback checks if should fallback to memory when queue unavailable
func (*Data) WithMongoTransaction ¶
func (*Data) WithSpaceTx ¶ added in v0.2.5
WithSpaceTx executes fn inside a read-write transaction scoped to the Space identified in ctx.
Routing behaviour:
- ctx contains a Space ID AND a SpaceRouter has been injected → delegates to SpaceRouter.WithSpace; the transaction operates in the Space's dedicated schema or database.
- Otherwise → falls back to Data.WithTx (identical to the pre-isolation behaviour, preserving backward compatibility).
The *sql.Tx is available inside fn via GetTx regardless of which path is taken.
func (*Data) WithSpaceTxRead ¶ added in v0.2.5
WithSpaceTxRead executes fn inside a read-only transaction scoped to the Space identified in ctx. It follows the same routing logic as [WithSpaceTx] but uses SpaceRouter.WithSpaceRead and falls back to Data.WithTxRead.
type DatabaseDriver ¶ added in v0.2.0
type DatabaseDriver interface {
// Name returns the driver identifier (e.g., "postgres", "mysql", "sqlite")
Name() string
// Connect establishes a new database connection using the provided configuration.
// The returned connection should be ready for use or return an error.
Connect(ctx context.Context, cfg any) (any, error)
// Close terminates the database connection and releases resources.
Close(conn any) error
// Ping verifies the connection is alive and functional.
Ping(ctx context.Context, conn any) error
}
DatabaseDriver defines the interface for relational database drivers. Implementations should handle connection lifecycle and health checks.
func GetDatabaseDriver ¶ added in v0.2.0
func GetDatabaseDriver(name string) (DatabaseDriver, error)
GetDatabaseDriver retrieves a registered database driver by name. It returns an error with helpful instructions if the driver is not found.
type MessageDriver ¶ added in v0.2.0
type MessageDriver interface {
// Name returns the driver identifier (e.g., "kafka", "rabbitmq")
Name() string
// Connect establishes a new message broker connection.
Connect(ctx context.Context, cfg any) (any, error)
// Close terminates the message broker connection.
Close(conn any) error
}
MessageDriver defines the interface for message queue/broker drivers.
func GetMessageDriver ¶ added in v0.2.0
func GetMessageDriver(name string) (MessageDriver, error)
GetMessageDriver retrieves a registered message queue driver by name.
type Option ¶
type Option func(*Data)
func WithExtensionCollector ¶
func WithExtensionCollector(collector metrics.ExtensionCollector) Option
func WithIndexPrefix ¶
func WithMetricsCollector ¶
func WithSearchConfig ¶
func WithSpaceRouter ¶ added in v0.2.5
func WithSpaceRouter(router SpaceRouter) Option
WithSpaceRouter returns an Option that injects a SpaceRouter into Data. Apply it after constructing Data for projects that require Space-level isolation:
d, cleanup, err := data.New(cfg) data.WithSpaceRouter(myRouter)(d)
Projects that do not need multi-tenancy simply omit this option; all Space-aware methods fall back to ordinary transactions automatically.
type SearchCollectorAdapter ¶ added in v0.2.1
type SearchCollectorAdapter struct {
// contains filtered or unexported fields
}
SearchCollectorAdapter adapts data/metrics.Collector to data/search.Collector
func (*SearchCollectorAdapter) SearchIndex ¶ added in v0.2.1
func (a *SearchCollectorAdapter) SearchIndex(engine, operation string)
SearchIndex records search index operation metrics
func (*SearchCollectorAdapter) SearchQuery ¶ added in v0.2.1
func (a *SearchCollectorAdapter) SearchQuery(engine string, err error)
SearchQuery records search query metrics
type SearchDriver ¶ added in v0.2.0
type SearchDriver interface {
// Name returns the driver identifier (e.g., "elasticsearch", "meilisearch")
Name() string
// Connect establishes a new search engine connection.
Connect(ctx context.Context, cfg any) (any, error)
// Close terminates the search engine connection.
Close(conn any) error
}
SearchDriver defines the interface for search engine drivers.
func GetSearchDriver ¶ added in v0.2.0
func GetSearchDriver(name string) (SearchDriver, error)
GetSearchDriver retrieves a registered search engine driver by name.
type SearchEngine ¶ added in v0.2.0
type SearchEngine interface {
// Health checks if the search engine is available and responds
Health(ctx context.Context) error
// IndexDocument indexes a single document
IndexDocument(ctx context.Context, index, docID string, document any) error
// DeleteDocument deletes a document by ID
DeleteDocument(ctx context.Context, index, docID string) error
// IndexExists checks if an index exists
IndexExists(ctx context.Context, index string) (bool, error)
// CreateIndex creates a new index with optional settings
CreateIndex(ctx context.Context, index, settings string) error
}
SearchEngine defines the interface that all search engine client implementations must satisfy. This allows the search.Client to work with any search backend through type assertions.
type SpaceRouter ¶ added in v0.2.5
type SpaceRouter interface {
// WithSpace executes fn inside a read-write transaction scoped to the
// namespace of the given space. spaceID is the application-defined
// identifier (e.g. a slug or UUID) that the router maps to a schema or
// database name.
WithSpace(ctx context.Context, spaceID string, fn func(ctx context.Context) error) error
// WithSpaceRead executes fn inside a read-only transaction scoped to the
// namespace of the given space. Implementations may direct the query to a
// read replica for this variant.
WithSpaceRead(ctx context.Context, spaceID string, fn func(ctx context.Context) error) error
}
SpaceRouter is the core abstraction for Space-level database routing.
Application code implements this interface and injects it into Data via WithSpaceRouter. ncore ships no concrete implementation so that no extra dependencies are imposed on projects that do not need multi-tenancy.
Implementation contract ¶
- Space context MUST be scoped to the transaction (e.g. SET LOCAL in PostgreSQL). Never use a session-level SET, which would leak the context to the next caller that borrows the same connection from the pool.
- The context passed to fn MUST carry the active *sql.Tx so that callers can retrieve it with GetTx and integrate it with their ORM.
- The implementation owns the full Begin / Commit / Rollback lifecycle of the transaction.
See the package-level ExecInTx helper, which correctly implements this contract and can be composed into custom router implementations.
type StorageDriver ¶ added in v0.2.0
type StorageDriver interface {
// Name returns the driver identifier (e.g., "s3", "minio", "local")
Name() string
// Connect establishes a new storage connection.
Connect(ctx context.Context, cfg any) (any, error)
// Close terminates the storage connection.
Close(conn any) error
}
StorageDriver defines the interface for object storage drivers.
func GetStorageDriver ¶ added in v0.2.0
func GetStorageDriver(name string) (StorageDriver, error)
GetStorageDriver retrieves a registered storage driver by name.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
all
module
|
|
|
cache
module
|
|
|
elasticsearch
module
|
|
|
entgo
module
|
|
|
kafka
module
|
|
|
meilisearch
module
|
|
|
mongodb
module
|
|
|
mysql
module
|
|
|
neo4j
module
|
|
|
opensearch
module
|
|
|
postgres
module
|
|
|
rabbitmq
module
|
|
|
redis
module
|
|
|
sqlite
module
|