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: CheckFieldsExists panics with an erro-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:

  1. Defaulting mod.Fields to all tableFields if the caller passed an empty slice
  2. Calling CheckFieldsExists to validate the requested fields
  3. Resolving field names to positional indexes via IndexesFromFields
  4. Stripping system-field indexes via ExcludeUtilitiesIndexes
  5. Building mod.Columns and mod.Values via 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, and TimeInterface at construction time (wired in the DI container). This makes each dependency swappable in tests without modifying business logic.

  • System field protection: ExcludeUtilitiesIndexes guarantees that callers cannot accidentally include Id, CreatedAt, UpdatedAt, or UpdatedBy in the data they write. Audit fields are always appended by SetUtilities using the injected clock.

  • Spy logging on every mutation: Every Create, Update, and Delete call emits a structured log record through spy before touching the database. The Printable() payload provides a JSON snapshot of the full operation for debugging and audit trails.

  • Transaction support: HandlerMod.Tx threads an optional *sql.Tx through to sql_db. Passing nil uses 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