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
| Requirement | Place it here |
|---|---|
| Field or relation belongs to the domain | KSML model, then regenerate |
| Entity validation or normalization | Generated checker/behavior contract |
| Tenant filtering or page-size limits | Request policy installed in context |
| Database selection and connection | Context initialization |
| Authenticated actor and permissions | Trusted server adapter into context |
| Immutable mutation evidence | Runtime row audit sink |
| Application outbox or custom audit | App audit/event sink |
| HTTP JSON field/operator allowlist | Application 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.