Skip to main content

Go Customization

TeaQL customization has two layers: generated entity-scoped hooks for domain behavior, and application-owned UserContext assembly for platform policy and infrastructure. Do not customize by editing generated Request or Entity files.

Choose the extension boundary​

RequirementPlace it here
Field or relation belongs to the domainKSML model, then regenerate
Entity validation or normalizationGenerated checker/behavior contract
Tenant filtering or page-size limitsRequest policy installed in context
Database selection and connectionContext initialization
Authenticated actor and permissionsTrusted server adapter into context
Immutable mutation evidenceRuntime row audit sink
Application outbox or custom auditApp audit/event sink
HTTP JSON field/operator allowlistApplication boundary before Request construction

Context factory​

Create one application function that translates authenticated server state into a fully initialized context. Handlers should receive this context, not a connection and not client-provided tenant metadata.

type TrustedRequest struct {
TenantID int64
ActorID string
Roles []string
}

func NewRequestContext(base *Infrastructure, trusted TrustedRequest) *runtime.UserContext {
context := runtime.NewUserContext()
// Register provider, generated metadata, policy and sinks with the released API.
// Install trusted.TenantID, trusted.ActorID and trusted.Roles here.
return context
}

Read the released runtime source for exact registration names. The invariant is more important than a guessed constructor: every terminal receives only context.

The current runtime customization shape is:

func ConfiguredRuntime(
base *runtime.UserContext,
requestPolicy runtime.RequestPolicy,
trustedTenant string,
appAuditSink runtime.AppAuditEventSink,
) (*runtime.UserContext, error) {
if base == nil {
return nil, fmt.Errorf("generated service runtime is required")
}
if strings.TrimSpace(trustedTenant) == "" {
return nil, fmt.Errorf("trusted tenant is required")
}
base.WithRequestPolicy(requestPolicy).
WithTrustedTenant(trustedTenant).
WithAppAuditEventSink(appAuditSink)
if err := base.RuntimeReadiness(); err != nil {
return nil, err
}
return base, nil
}

Attach telemetry with context.WithRuntimeTelemetry(telemetry). The composition root, not request JSON, constructs every dependency above.

Policy and dynamic input​

Map external JSON to generated calls through an explicit switch. Reject unknown fields, operators, sorts, deep paths, invalid ranges, negative offsets, excessive IN lists and page sizes. Do not reflect a JSON field name into a method name or SQL fragment.

switch filter.Field {
case "order_number":
req = req.WithOrderNumberContaining(requireString(filter.Value))
default:
return nil, fmt.Errorf("unsupported filter field %q", filter.Field)
}

Tenant, actor, permissions, provider, purpose policy, and request policy are not valid dynamic fields even for administrators.

Behavior and checker hooks​

Use generated interfaces and skeletons as the contract, but put durable handwritten implementations in an application package when the generator marks generated output as replaceable. Register them during context/module assembly. Tests should call the public generated API and prove the hook, not call the hook directly as the only test.

Two audit paths​

An audited save declares business reason through AuditAs. The runtime row/mutation event is immutable platform evidence. A separately registered application event sink can enrich a masked event for outbox, analytics, or observability. Do not substitute an application log line for the runtime event, and never log secrets or unmasked sensitive fields.

Provider customization​

Keep provider choice at startup. Qualify every driver/database pair with a real round-trip, optimistic lock, native aggregates, stable pagination, and exact relation Top-N before describing it as Feature PASS. A driver compiling is not provider support.

Regeneration rule​

When customization seems to require editing q.go, request.go, entity.go, or expression.go, stop. Change the model, generator template, or runtime extension point, then regenerate and rerun the first-verification suite.

Runtime infrastructure​

Go UserContext provides process-local cache and keyed locks plus optional Remote Cache/Remote Lock provider resources. Redis adapters are present, but the current lock uses a shared value and unconditional delete; use an owner-safe atomic provider for critical distributed work. Go also provides Nacos and Consul HTTP clients for registration, discovery, and health; Nacos adds configuration retrieval. The current Consul CloudStarter exposes only the registry even though the concrete client implements discovery. See Cache, Lock, and Cloud Runtime Infrastructure.

Worked Example: Local Cache and Lock​

func CurrencyView(
context *runtime.UserContext,
tenant string,
code string,
) (*CurrencyViewDTO, error) {
cacheKey := tenant + ":currency:" + code
if cached, ok := context.GetFromLocalCache(cacheKey).(*CurrencyViewDTO); ok {
return cached, nil
}

lockKey := tenant + ":currency-refresh:" + code
if !context.TryLocalLock(lockKey, 250, 5_000) {
return nil, fmt.Errorf("currency refresh is busy")
}
defer context.UnlockLocal(lockKey)

if cached, ok := context.GetFromLocalCache(cacheKey).(*CurrencyViewDTO); ok {
return cached, nil
}
value, err := LoadAuthorizedCurrencyView(context, code)
if err != nil { return nil, err }
context.PutToLocalCache(cacheKey, value, 300)
return value, nil
}

Invalidate with RemoveFromLocalCache after the audited save succeeds. Local Lock does not coordinate replicas; the current Go Redis lock adapter is also not owner-safe on release, so use an atomic provider for critical workflows.

Worked Example: Nacos Registration​

cloud := nacos.NewNacosCloud(&nacos.NacosConfig{
ServerAddrs: []string{"127.0.0.1:8848"},
NamespaceId: "production",
Group: "APP",
})
instance := &core.ServiceInstance{
ServiceId: "order-service",
Host: "10.0.0.7",
Port: 8080,
}
if err := cloud.Register(context, instance); err != nil { return err }
defer cloud.Deregister(context, instance)

instances, err := cloud.GetInstances(context, "payment-service")
if err != nil { return err }
_ = instances

Add Redis/database checks to application readiness. cloud.Health() checks Nacos availability, not the whole TeaQL application.