Documentation
¶
Overview ¶
Package flagsconfig owns feature flags: the flag definitions themselves and their per-environment values. Unlike appsettings and localization, a flag value is environment-wide rather than per-service, so every consumer in an environment watches every flag in it.
The package is the fullest worked example of the shape all three config domains share — model, sentinel errors, a Postgres repository and its SQLite mirror, a service that validates and publishes write-through to KV, and a handler that maps each sentinel to a status. CONTRIBUTING.md points here as the pattern to copy.
Flag keys are validated on the way in because they become part of a KV key ("{envID}.{flagKey}"): a key containing a dot would be ambiguous to a consumer parsing it, and would be republished by every sweep thereafter.
Index ¶
- Variables
- type DeletedFlagValue
- type Flag
- type FlagFilter
- type FlagValue
- type FlagValueFilter
- type FlagValueRow
- type Handler
- func (h *Handler) CreateFlag(w http.ResponseWriter, r *http.Request)
- func (h *Handler) CreateFlagValue(w http.ResponseWriter, r *http.Request)
- func (h *Handler) DeleteFlag(w http.ResponseWriter, r *http.Request)
- func (h *Handler) DeleteFlagValue(w http.ResponseWriter, r *http.Request)
- func (h *Handler) GetFlagsByID(w http.ResponseWriter, r *http.Request)
- func (h *Handler) GetFlagsValueByID(w http.ResponseWriter, r *http.Request)
- func (h *Handler) ListFlagValues(w http.ResponseWriter, r *http.Request)
- func (h *Handler) ListFlags(w http.ResponseWriter, r *http.Request)
- func (h *Handler) UpdateFlagValue(w http.ResponseWriter, r *http.Request)
- type Page
- type PostgresRepository
- func (r *PostgresRepository) CreateFlag(ctx context.Context, input Flag) (*Flag, error)
- func (r *PostgresRepository) CreateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
- func (r *PostgresRepository) DeleteFlag(ctx context.Context, id int64) ([]DeletedFlagValue, error)
- func (r *PostgresRepository) DeleteFlagValue(ctx context.Context, id int64) (*DeletedFlagValue, error)
- func (r *PostgresRepository) GetFlagValueByID(ctx context.Context, id int64) (*FlagValue, error)
- func (r *PostgresRepository) GetFlagsByID(ctx context.Context, id int64) (*Flag, error)
- func (r *PostgresRepository) ListAllForReconcile(ctx context.Context, since time.Time) ([]ReconcileRow, error)
- func (r *PostgresRepository) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
- func (r *PostgresRepository) ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
- func (r *PostgresRepository) UpdateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
- type ReconcileRow
- type Repository
- type SQLiteRepository
- func (r *SQLiteRepository) CreateFlag(ctx context.Context, input Flag) (*Flag, error)
- func (r *SQLiteRepository) CreateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
- func (r *SQLiteRepository) DeleteFlag(ctx context.Context, id int64) ([]DeletedFlagValue, error)
- func (r *SQLiteRepository) DeleteFlagValue(ctx context.Context, id int64) (*DeletedFlagValue, error)
- func (r *SQLiteRepository) GetFlagValueByID(ctx context.Context, id int64) (*FlagValue, error)
- func (r *SQLiteRepository) GetFlagsByID(ctx context.Context, id int64) (*Flag, error)
- func (r *SQLiteRepository) ListAllForReconcile(ctx context.Context, since time.Time) ([]ReconcileRow, error)
- func (r *SQLiteRepository) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
- func (r *SQLiteRepository) ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
- func (r *SQLiteRepository) UpdateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
- type Service
- func (s *Service) CreateFlag(ctx context.Context, req Flag) (*Flag, error)
- func (s *Service) CreateFlagValue(ctx context.Context, req FlagValue) (*FlagValue, error)
- func (s *Service) DeleteFlag(ctx context.Context, id int64) error
- func (s *Service) DeleteFlagValue(ctx context.Context, id int64) error
- func (s *Service) GetFlagByID(ctx context.Context, id int64) (*Flag, error)
- func (s *Service) GetFlagValueByID(ctx context.Context, id int64) (*FlagValue, error)
- func (s *Service) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
- func (s *Service) ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
- func (s *Service) UpdateFlagValue(ctx context.Context, req FlagValue) (*FlagValue, error)
Constants ¶
This section is empty.
Variables ¶
var ErrConflict = errors.New("flag value was modified by someone else")
ErrConflict is returned when an update carried an expected UPDATED_AT that no longer matches the stored row: somebody else edited it first. Distinct so a handler can map it to HTTP 409.
var ErrEnvironmentNotFound = errors.New("environment not found")
ErrEnvironmentNotFound guards a flag value against pointing at an environment that does not exist — the referential integrity check that keeps a foreign-key violation from surfacing as a 500.
var ErrFlagExists = errors.New("flag with this key already exists")
Creation collides with the natural unique keys: FLAG_KEY on CONFIG_FLAG, and (ENVIRONMENT_ID, FLAG_ID) on CONFIG_FLAG_VALUE. Distinct so a handler can map them to HTTP 409.
var ErrFlagNotFound = errors.New("flag not found")
var ErrFlagValueExists = errors.New("flag value for this environment already exists")
var ErrFlagValueTooLarge = errors.New("flag value exceeds the maximum length")
ErrFlagValueTooLarge rejects a value longer than the VALUE column. Left to the column it arrives as an opaque driver error, which writeErr can only map to a 500 — a server fault for what is ordinary caller input.
var ErrInvalidEnabled = errors.New("enabled must be 0 or 1")
ENABLED and IS_ACTIVE are SMALLINT columns carrying a boolean the API spells as 1/0 (migrations/004 explains why the column is not BOOLEAN). The int/bool split is the contract, not a range: only the KV payload is a real boolean, and it collapses everything non-zero to true, so a stored 7 is a row the API reads back as 7 while consumers see the same true a 1 would have given them. The bound is the meaning rather than the column — 2 fits a SMALLINT and says nothing — and it keeps an out-of-range SMALLINT from surfacing as a 500.
var ErrInvalidEnvironmentID = errors.New("invalid environment id")
var ErrInvalidFlagID = errors.New("invalid flag id")
var ErrInvalidFlagKey = errors.New("invalid flag key")
ErrInvalidFlagKey rejects a key that cannot be part of a KV key. The FLAGS bucket key is "{environmentID}.{flagKey}", so a key containing a dot or a space would either be ambiguous or refused by JetStream at publish time.
var ErrInvalidFlagValue = errors.New("flag value must not be empty")
ErrInvalidFlagValue rejects an empty value. VALUE is NOT NULL in migrations/005, but PostgreSQL keeps an empty string and NULL apart, so the constraint closes only half the hole and this check is the other half — the migration says so in as many words. It matters because the value is republished to every consumer in the environment and read as a rollout percentage, a variant name or a threshold: "" arrives at the far end indistinguishable from a parse bug, so a caller who means "no value" has to write a real placeholder.
var ErrInvalidIsActive = errors.New("isActive must be 0 or 1")
Functions ¶
This section is empty.
Types ¶
type DeletedFlagValue ¶
DeletedFlagValue carries just enough of a removed row to purge its KV key. Deleting a flag removes many of them at once.
type FlagFilter ¶
FlagFilter narrows the flag list. An empty FlagKey matches every flag.
type FlagValue ¶
type FlagValue struct {
ID int64 `json:"id"`
EnvironmentID int64 `json:"environmentId"`
FlagId int64 `json:"flagId"`
Value string `json:"value"`
Enabled int64 `json:"enabled"`
UpdatedAt time.Time `json:"updatedAt"`
// ExpectedUpdatedAt opts an update into optimistic concurrency: the write
// only applies while the stored row still carries this timestamp, so two
// admins editing the same flag no longer silently overwrite each other.
// Absent (nil) keeps the original last-write-wins behaviour.
ExpectedUpdatedAt *time.Time `json:"expectedUpdatedAt,omitempty"`
}
type FlagValueFilter ¶
FlagValueFilter narrows the flag-value list. Zero values match everything.
type FlagValueRow ¶
type FlagValueRow struct {
ID int64 `json:"id"`
EnvironmentID int64 `json:"environmentId"`
FlagId int64 `json:"flagId"`
FlagKey string `json:"flagKey"`
Value string `json:"value"`
Enabled int64 `json:"enabled"`
UpdatedAt time.Time `json:"updatedAt"`
}
FlagValueRow is a flag value with its flag's key already resolved — without it a list caller cannot tell which flag a row belongs to without a second request per row.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
func NewHandler ¶
func (*Handler) CreateFlag ¶
func (h *Handler) CreateFlag(w http.ResponseWriter, r *http.Request)
POST /flags
func (*Handler) CreateFlagValue ¶
func (h *Handler) CreateFlagValue(w http.ResponseWriter, r *http.Request)
POST /flags/values
func (*Handler) DeleteFlag ¶
func (h *Handler) DeleteFlag(w http.ResponseWriter, r *http.Request)
DELETE /flags/{id}
func (*Handler) DeleteFlagValue ¶
func (h *Handler) DeleteFlagValue(w http.ResponseWriter, r *http.Request)
DELETE /flags/values/{id}
func (*Handler) GetFlagsByID ¶
func (h *Handler) GetFlagsByID(w http.ResponseWriter, r *http.Request)
GET /flags/{id}
func (*Handler) GetFlagsValueByID ¶
func (h *Handler) GetFlagsValueByID(w http.ResponseWriter, r *http.Request)
GET /flags/values/{id}
func (*Handler) ListFlagValues ¶
func (h *Handler) ListFlagValues(w http.ResponseWriter, r *http.Request)
GET /flags/values?environmentId=&flagKey=&limit=&offset=
func (*Handler) ListFlags ¶
func (h *Handler) ListFlags(w http.ResponseWriter, r *http.Request)
GET /flags?flagKey=&limit=&offset=
func (*Handler) UpdateFlagValue ¶
func (h *Handler) UpdateFlagValue(w http.ResponseWriter, r *http.Request)
PUT /flags/values
type Page ¶
Page bounds a list query. The service normalises it, so a repository can bind both values as given.
type PostgresRepository ¶
type PostgresRepository struct {
// contains filtered or unexported fields
}
func NewPostgresRepository ¶
func NewPostgresRepository(db *sql.DB) *PostgresRepository
func (*PostgresRepository) CreateFlag ¶
func (*PostgresRepository) CreateFlagValue ¶
func (*PostgresRepository) DeleteFlag ¶
func (r *PostgresRepository) DeleteFlag(ctx context.Context, id int64) ([]DeletedFlagValue, error)
DeleteFlag drops the flag and its values in one transaction. Doing it in two statements outside a transaction would leave orphaned value rows behind if the second one failed.
func (*PostgresRepository) DeleteFlagValue ¶
func (r *PostgresRepository) DeleteFlagValue(ctx context.Context, id int64) (*DeletedFlagValue, error)
func (*PostgresRepository) GetFlagValueByID ¶
func (*PostgresRepository) GetFlagsByID ¶
func (*PostgresRepository) ListAllForReconcile ¶
func (r *PostgresRepository) ListAllForReconcile(ctx context.Context, since time.Time) ([]ReconcileRow, error)
ListAllForReconcile returns flag values joined with their flag key. A zero `since` sweeps every row; otherwise only rows changed at or after `since`, which keeps the periodic reconcile from re-reading the whole table.
func (*PostgresRepository) ListFlagValues ¶
func (r *PostgresRepository) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
func (*PostgresRepository) ListFlags ¶
func (r *PostgresRepository) ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
func (*PostgresRepository) UpdateFlagValue ¶
type ReconcileRow ¶
ReconcileRow is a flat flag-value + its human-stable key, used to republish every flag to KV during reconciliation. ID is the CONFIG_FLAG_VALUE id — the value an admin update targets.
type Repository ¶
type Repository interface {
GetFlagsByID(ctx context.Context, id int64) (*Flag, error)
GetFlagValueByID(ctx context.Context, id int64) (*FlagValue, error)
// UpdateFlagValue returns the stored row plus its flag's human-stable key,
// so the caller can publish without a second lookup.
UpdateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
CreateFlag(ctx context.Context, input Flag) (*Flag, error)
// DeleteFlag removes the flag and every value row hanging off it, returning
// what went away so the caller can purge the matching KV keys.
DeleteFlag(ctx context.Context, id int64) ([]DeletedFlagValue, error)
ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
// CreateFlagValue returns the stored row plus its flag key, on the same
// terms as UpdateFlagValue.
CreateFlagValue(ctx context.Context, input FlagValue) (*FlagValue, string, error)
DeleteFlagValue(ctx context.Context, id int64) (*DeletedFlagValue, error)
}
type SQLiteRepository ¶
type SQLiteRepository struct {
// contains filtered or unexported fields
}
SQLiteRepository backs the local test stack (see internal/database/sqlite.go). Same tables and columns as PostgreSQL; only the bind syntax differs (? vs $1).
func NewSQLiteRepository ¶
func NewSQLiteRepository(db *sql.DB) *SQLiteRepository
func (*SQLiteRepository) CreateFlag ¶
func (*SQLiteRepository) CreateFlagValue ¶
func (*SQLiteRepository) DeleteFlag ¶
func (r *SQLiteRepository) DeleteFlag(ctx context.Context, id int64) ([]DeletedFlagValue, error)
func (*SQLiteRepository) DeleteFlagValue ¶
func (r *SQLiteRepository) DeleteFlagValue(ctx context.Context, id int64) (*DeletedFlagValue, error)
func (*SQLiteRepository) GetFlagValueByID ¶
func (*SQLiteRepository) GetFlagsByID ¶
func (*SQLiteRepository) ListAllForReconcile ¶
func (r *SQLiteRepository) ListAllForReconcile(ctx context.Context, since time.Time) ([]ReconcileRow, error)
func (*SQLiteRepository) ListFlagValues ¶
func (r *SQLiteRepository) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)
func (*SQLiteRepository) ListFlags ¶
func (r *SQLiteRepository) ListFlags(ctx context.Context, filter FlagFilter) ([]Flag, error)
func (*SQLiteRepository) UpdateFlagValue ¶
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func NewService ¶
func NewService(repo Repository, pub messaging.ConfigPublisher) *Service
func (*Service) CreateFlag ¶
func (*Service) CreateFlagValue ¶
func (*Service) DeleteFlag ¶
DeleteFlag removes the flag, its values, and the KV keys those values fed. Waiting for the next full reconcile to prune them would leave consumers serving a deleted flag for up to a whole interval.
func (*Service) DeleteFlagValue ¶
func (*Service) GetFlagByID ¶
func (*Service) GetFlagValueByID ¶
func (*Service) ListFlagValues ¶
func (s *Service) ListFlagValues(ctx context.Context, filter FlagValueFilter) ([]FlagValueRow, error)