Handler
Source: data/app/lib/handler/handler.go
Meet the Handler — the generic database write layer that sits between your Maestro (MAE) business-logic steps and the lower-level sql_db abstraction. Every time a step needs to create, update, or delete a row, it builds a HandlerMod, configures it with the provided helpers, and passes it to the Handler. The Handler takes care of audit timestamps, spy logging, and field validation so your steps stay focused on business logic.
Table of Contents
Overview
The handler package provides a unified interface for Create, Update, and Delete operations with built-in audit-field management, field validation, spy logging on every mutation, and optional transaction support via HandlerMod.Tx. It is the single point through which all database writes flow in the application.
Package & Imports
The table below lists every import used by the handler package and explains its role.
| Import | Alias | Role |
|---|---|---|
| context | — | Standard Go context; passed through to sql_db for cancellation/timeouts |
| database/sql | — | Provides *sql.Tx for optional transaction wrapping |
| encoding/json | — | JSON marshaling used by HandlerMod.Printable() |
| src/lib/error | erro | Internal error utilities; used to format panic messages |
| src/lib/spy | spy | Internal observability/logging; emits structured records on every mutation |
| src/lib/sql_db | sql_db | Internal DB abstraction layer; executes the actual SQL statements |
| src/lib/time | time | Internal time utilities; provides a TimeInterface for deterministic Now() calls (testable) |
Data Structures
The two core types in this package are the service struct (Handler) and the data-transfer object (HandlerMod) that carries all information needed for a single database operation.
Handler
The main service struct. Holds all injected dependencies and exposes CRUD and utility methods.
type Handler struct {
SqlDb *sql_db.SqlDb
Spy *spy.Spy
TimeInterface time.TimeInterface
}
The following fields are wired at construction time through the DI container.
| Field | Type | Description |
|---|---|---|
| SqlDb | *sql_db.SqlDb |
Database abstraction that executes Create/Update/Delete SQL |
| Spy | *spy.Spy |
Observability logger; records every mutation with its kind and payload |
| TimeInterface | time.TimeInterface |
Abstracted clock; allows tests to inject deterministic timestamps |
HandlerMod
A data-transfer object (DTO) that carries everything needed to execute one database operation.
type HandlerMod struct {
Tx *sql.Tx
Id string
Table string
Fields []string
Columns []string
Values []interface{}
}
MAE steps populate a HandlerMod using the utility helpers before passing it to Create, Update, or Delete.
| Field | Type | Description |
|---|---|---|
| Tx | *sql.Tx |
Optional database transaction; pass nil for auto-commit behavior |
| Id | string |
Row identifier used by Update and Delete |
| Table | string |
Target database table name |
| Fields | []string |
Logical field names the caller wants to write (e.g. ["Name", "Status"]); empty means “all fields” |
| Columns | []string |
Physical column names populated by SetColumns; passed to sql_db |
| Values | []interface{} |
Column values in the same order as Columns; populated by SetColumns and SetUtilities |
Methods
HandlerMod
The HandlerMod type exposes one method for producing a human-readable snapshot of the operation.
func (this *HandlerMod) Printable() string
Serializes Id, Table, Fields, Columns, and Values to a JSON string. Used by spy logging so that every mutation has a human-readable record. The Tx field is intentionally excluded.
Handler (CRUD)
These three methods perform the actual database writes. Each one emits a spy record before touching the database, so every mutation is observable.
// Create
func (this *Handler) Create(ctx context.Context, mod *HandlerMod) (string, int, error)
Inserts a new row into mod.Table. Returns the newly assigned UUID for the row, the number of rows affected (normally 1), and any database error. Logs a RecordKindHandlerCreate spy record before executing.
// Update
func (this *Handler) Update(ctx context.Context, mod *HandlerMod) (int, error)
Updates an existing row identified by mod.Id in mod.Table. Returns the number of rows affected and any database error. Logs a RecordKindHandlerUpdate spy record before executing.
// Delete
func (this *Handler) Delete(ctx context.Context, mod *HandlerMod) (int, error)
Deletes the row identified by mod.Id from mod.Table. Returns the number of rows affected and any database error. Logs a RecordKindHandlerDelete spy record before executing.
Handler (Utilities)
These helpers are called by callers (typically MAE steps) to prepare a HandlerMod before passing it to Create, Update, or Delete. Call them in the order shown below: SetColumns first, then SetUtilities.
// IndexesFromFields
func (this *Handler) IndexesFromFields(fields []string, tableFields []string) []int
Given a list of requested logical field names and the full ordered list of table field names, returns the positional indexes of each requested field within the table definition. Drives the subsequent subset-extraction helpers.
// StringSubsetFromIndexes
func (this *Handler) StringSubsetFromIndexes(indexes []int, set []string) []string
Returns the elements of set at the positions given by indexes. Used to extract the physical column names that correspond to a set of indexes.
// InterfaceSubsetFromIndexes
func (this *Handler) InterfaceSubsetFromIndexes(indexes []int, set []interface{}) []interface{}
Same as StringSubsetFromIndexes but for the values slice ([]interface{}).
// ExcludeUtilitiesIndexes
func (this *Handler) ExcludeUtilitiesIndexes(indexes []int, fields []string) []int
Filters out the indexes that correspond to system-managed fields: Id, CreatedAt, UpdatedAt, UpdatedBy. This ensures callers cannot accidentally overwrite audit or identity columns — those are always handled by SetUtilities or the database itself.
// AppendColumnValueAtNow
func (this *Handler) AppendColumnValueAtNow(column string, mod *HandlerMod)
Appends column to mod.Columns and the current timestamp (TimeInterface.Now()) to mod.Values. Called internally by SetUtilities.
// CheckFieldsExists
func (this *Handler) CheckFieldsExists(handlerFields []string, tableFields []string)
Validates that every field in handlerFields exists in tableFields.
⚠️ Warning:
CheckFieldsExistspanics with anerro-formatted message if any field is missing. This is intentional — an unknown field is a programming error that must be caught immediately during development, not silently dropped at runtime.
// SetColumns
func (this *Handler) SetColumns(mod *HandlerMod, tableFields []string, tableColumns []string, values []interface{})
The primary setup helper. Populates mod.Columns and mod.Values by:
- Defaulting
mod.Fieldsto alltableFieldsif the caller passed an empty slice - Calling
CheckFieldsExiststo validate the requested fields - Resolving field names to positional indexes via
IndexesFromFields - Stripping system-field indexes via
ExcludeUtilitiesIndexes - Building
mod.Columnsandmod.Valuesvia the subset helpers
// SetUtilities
func (this *Handler) SetUtilities(mod *HandlerMod, action string)
Appends audit timestamp columns to mod based on the operation type. The following actions are recognized.
| action | Columns appended |
|---|---|
"create" |
created_at, updated_at |
"update" |
updated_at only |
| (any other value) | No columns appended |
Architectural Patterns
The Handler package applies several patterns that keep write logic predictable and testable. Each one is described below.
Dependency injection via struct fields: Handler receives
SqlDb,Spy, andTimeInterfaceat construction time (wired in the DI container). This makes each dependency swappable in tests without modifying business logic.System field protection:
ExcludeUtilitiesIndexesguarantees that callers cannot accidentally includeId,CreatedAt,UpdatedAt, orUpdatedByin the data they write. Audit fields are always appended bySetUtilitiesusing the injected clock.Spy logging on every mutation: Every
Create,Update, andDeletecall emits a structured log record through spy before touching the database. ThePrintable()payload provides a JSON snapshot of the full operation for debugging and audit trails.Transaction support:
HandlerMod.Txthreads an optional*sql.Txthrough tosql_db. Passingniluses auto-commit. MAE steps that need multi-table atomicity obtain a transaction from the connection and set it on the mod.
Usage Example
The following example shows how a MAE step builds a HandlerMod, configures it with the utility helpers, and then calls Create.
// Inside a MAE step
func (s *PersistThingStep) Execute(ctx context.Context, input *dat.CreateThingInput) error {
mod := &handler.HandlerMod{
Table: "things",
Fields: []string{"Name", "Status"}, // only write these two fields
}
// Populate Columns and Values from the entity's full field/column/value slices,
// automatically excluding system fields (Id, CreatedAt, UpdatedAt, UpdatedBy).
s.Handler.SetColumns(mod, ThingFields, ThingColumns, input.Thing.Values())
// Append created_at + updated_at with the current timestamp.
s.Handler.SetUtilities(mod, "create")
// Execute the insert; id holds the newly assigned UUID.
id, _, err := s.Handler.Create(ctx, mod)
if err != nil {
return err
}
input.NewId = id
return nil
}
Next Steps
- Hydrator — control which associations are loaded alongside entities
- Fetcher — build dynamic SELECT queries with filters, sorting, and pagination
- Appliers — understand the post-creation side effects that run after a Handler write
- Maestros (Controllers) — see how Handler calls fit within the full MAE step flow