crud

package module
v0.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 14, 2026 License: MIT Imports: 23 Imported by: 0

README

Go CRUD Controller

A Go package that provides a generic CRUD controller for REST APIs using Fiber and Bun ORM.

Installation

go get github.com/goliatone/go-crud

Quick Start

package main

import (
	"database/sql"
	"log"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/goliatone/go-crud"
	"github.com/goliatone/go-repository-bun"
	"github.com/google/uuid"
	"github.com/mattn/go-sqlite3"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
)

type User struct {
	bun.BaseModel `bun:"table:users,alias:cmp"`
	ID            *uuid.UUID `bun:"id,pk,nullzero,type:uuid" json:"id"`
	Name          string     `bun:"name,notnull" json:"name"`
	Email         string     `bun:"email,notnull" json:"email"`
	Password      string     `bun:"password,notnull" json:"password" crud:"-"`
	DeletedAt     *time.Time `bun:"deleted_at,soft_delete,nullzero" json:"deleted_at,omitempty"`
	CreatedAt     *time.Time `bun:"created_at,nullzero,default:current_timestamp" json:"created_at"`
	UpdatedAt     *time.Time `bun:"updated_at,nullzero,default:current_timestamp" json:"updated_at"`
}


func NewUserRepository(db bun.IDB) repository.Repository[*User] {
	handlers := repository.ModelHandlers[*User]{
		NewRecord: func() *User {
			return &User{}
		},
		GetID: func(record *User) uuid.UUID {
			return *record.ID
		},
		SetID: func(record *User, id uuid.UUID) {
			record.ID = &id
		},
		GetIdentifier: func() string {
			return "email"
		},
	}
	return repository.NewRepository[*User](db, handlers)
}

func main() {
	_ = sqlite3.Version() // ensure driver is imported
	sqldb, err := sql.Open("sqlite3", "file::memory:?cache=shared")
	if err != nil {
		panic(err)
	}
	db := bun.NewDB(sqldb, sqlitedialect.New())

	server := fiber.New()
	api := server.Group("/api/v1")

	userRepo := NewUserRepository(db)
	crud.NewController[*User](userRepo).RegisterRoutes(api)

log.Fatal(server.Listen(":3000"))
}

Virtual Attributes (virtual fields)

Virtual attributes let you expose computed or denormalized fields as top-level API fields while storing them in a backing map (e.g., metadata). Tag the virtual field with crud:"virtual:<mapField>" and bun:"-"; the VirtualFieldHandler automatically moves values between the struct field and the map on save/load.

type Article struct {
    bun.BaseModel `bun:"table:articles"`
    ID            uuid.UUID      `bun:"id,pk,type:uuid" json:"id"`
    Title         string         `bun:"title" json:"title"`
    Metadata      map[string]any `bun:"metadata,type:jsonb" json:"metadata,omitempty"` // storage

    ReadTime *int `bun:"-" json:"read_time,omitempty" crud:"virtual:metadata"` // virtual
}

vf := crud.NewVirtualFieldHandler[*Article]() // discovers virtuals via tags

// Attach via controller or service factory (REST + GraphQL share the same handler)
crud.NewController(articleRepo, crud.WithVirtualFields(vf))
// or:
crud.NewService(crud.ServiceConfig[*Article]{Repository: articleRepo, VirtualFields: vf})

Pointers are recommended for virtual fields so presence can be distinguished from zero values; unknown metadata keys remain intact.

GraphQL package (shared service layer)

The gql module reuses the same service layer as REST so hooks, scope guards, field policies, validation, activity, and virtual attributes all apply uniformly.

  • gql/helpers.GraphQLToCrudContext adapts gqlgen contexts to crud.Context.
  • gql/internal/templates + gql/cmd/graphqlgen generate schema, gqlgen config, resolvers, and dataloaders. Auth is opt‑in via --auth-package/--auth-guard, which emits a GraphQLContext helper that bridges go-auth actors into crud.ActorContext.
  • Example regeneration:
    go run ./gql/cmd/graphqlgen \
      --schema-package ./examples/relationships-gql/registrar \
      --out ./examples/relationships-gql/graph \
      --config ./examples/relationships-gql/gqlgen.yml \
      --emit-subscriptions --emit-dataloader \
      --auth-package github.com/goliatone/go-auth \
      --auth-guard "auth.FromContext(ctx)"
    
Generated Routes

For a User struct, the following routes are automatically created:

GET    /user/schema       - Get the OpenAPI bundle for this resource
GET    /user/:id          - Get a single user
GET    /users             - List users (with pagination, filtering, ordering)
POST   /user              - Create a user
POST   /user/batch        - Create multiple users
PUT    /user/:id          - Update a user
PUT    /user/batch        - Update multiple users
DELETE /user/:id          - Delete a user
DELETE /user/batch        - Delete multiple users
Resource Naming Convention

The controller automatically generates resource names following these rules:

  1. If a crud:"resource:name" tag is present, it uses that name:

    type Company struct {
        bun.BaseModel `bun:"table:companies" crud:"resource:organization"`
        // This generates /organization and /organizations endpoints
    }
    
  2. Otherwise, it converts the struct name to kebab-case and handles pluralization:

    • UserProfile becomes /user-profile (singular) and /user-profiles (plural)
    • Company becomes /company and /companies
    • BusinessAddress becomes /business-address and /business-addresses

The package uses proper pluralization rules, handling common irregular cases correctly:

  • Person/person and /people
  • Category/category and /categories
  • Bus/bus and /buses
Schema Endpoint Output

Each controller exposes a /resource/schema endpoint that returns a self-contained OpenAPI 3.0 document for that entity. The payload mirrors the format produced by go-router's MetadataAggregator, including the controller's paths, tags, and component schema:

{
  "openapi": "3.0.3",
  "paths": {
    "/user": {"post": { "summary": "Create User" }},
    "/users": {"get": { "summary": "List Users" }},
    "/user/{id}": {"get": { "summary": "Get User" }}
  },
  "tags": [
    { "name": "User" }
  ],
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "required": ["id", "email"],
        "properties": {
          "id": {"type": "string", "format": "uuid"},
          "email": {"type": "string"}
        }
      }
    }
  }
}

You can feed this JSON directly into Swagger UI, Stoplight Elements, or any OpenAPI tooling to visualize or validate the resource contract. Relationship metadata is embedded automatically via the generated schema.

Schema Metadata Hints

The generated OpenAPI includes vendor extensions that help downstream form builders choose sensible defaults:

  • Display label – mark the property that should appear in option lists with crud:"label" (or crud:"label:alternate" if the JSON name differs). The schema will include x-formgen-label-field with the resolved field name.
  • Relation includes – define Bun relations (e.g. bun:"rel:has-many,join:id=user_id") so x-formgen-relations can expose valid include paths, fields, and filter hints.
  • Shared parameters – collection routes reuse #/components/parameters/{Limit|Offset|Select|Include|Order} so clients can pull defaults (limit 25, offset 0) straight from the spec.
  • Pruning hooks – register crud.WithRelationFilter (or use router.RegisterRelationFilter) to hide sensitive relations from both the schema extension and runtime responses.
  • Admin extensions – the same schema object also carries:
    • x-admin-scope (from WithAdminScopeMetadata) summarizing the expected tenant/org level, required claims, or descriptive notes.
    • x-admin-actions (populated automatically from WithActions) describing the custom endpoints, HTTP methods, and paths available.
    • x-admin-menu (via WithAdminMenuMetadata) hinting how CMS navigation should categorize the resource (group, label, icon, order, custom path, hidden flag).
    • x-admin-row-filters (via WithRowFilterHints) documenting guard/policy criteria so operators know why records are filtered.

Example:

type User struct {
    bun.BaseModel `bun:"table:users"`

    ID       uuid.UUID         `bun:"id,pk,notnull" json:"id"`
    Name     string            `bun:"name,notnull" json:"name" crud:"label"`
    Email    string            `bun:"email,notnull,unique" json:"email"`
    Profiles []*UserProfile    `bun:"rel:has-many,join:id=user_id" json:"profiles,omitempty"`
}

The corresponding schema fragment contains:

components:
  schemas:
    user:
      x-formgen-label-field: "name"
      x-formgen-relations:
        includes:
          - profiles
      x-admin-scope:
        level: tenant
        claims: ["users:write"]
      x-admin-actions:
        - name: Deactivate
          slug: deactivate
          method: POST
          target: resource
      x-admin-menu:
        group: Directory
        label: Users
        order: 10
      x-admin-row-filters:
        - field: tenant_id
          operator: "="
          description: Matches actor tenant

Expose these hints with:

- `WithAdminScopeMetadata` – describe the default enforcement level/claims.
- `WithAdminMenuMetadata` – provide menu grouping, ordering, or icons for go-cms/go-admin ingestion.
- `WithRowFilterHints` – advertise guard/policy criteria (e.g., owner filters, tenant locking).
- `WithActions` – declare the custom endpoints so they show up in both routing and `x-admin-actions`.

### Schema Registry & Aggregation

Every controller registers itself with the in-memory schema registry when routes are mounted. This gives admin services a single discovery point for building `/admin/schemas` without crawling each `/{resource}/schema` endpoint:

```go
entries := crud.ListSchemas() // snapshot of every registered controller

if users, ok := crud.GetSchema("user"); ok {
	log.Println(users.Document["openapi"])
}

crud.RegisterSchemaListener(func(entry crud.SchemaEntry) {
	log.Printf("schema updated: %s at %s", entry.Resource, entry.UpdatedAt)
	// e.g., push the document to go-cms
})

Each SchemaEntry carries the compiled OpenAPI document (including all vendor extensions), so you can expose the aggregated payload directly or enrich it with service-specific metadata before returning it to go-admin/go-cms consumers. tree: name: user children: profiles: name: profiles parameters: Limit: description: Maximum number of records to return (default 25)


These hints eliminate duplicated configuration in consumers such as `go-formgen`.

### Shared Query Parameters

List endpoints in the generated OpenAPI document reference reusable query components built into `go-router`:

- `#/components/parameters/Limit` – caps the result size (defaults to `25`).
- `#/components/parameters/Offset` – skips records before pagination begins (defaults to `0`).
- `#/components/parameters/Include` – comma-separated relations to join (e.g. `profiles,company`).
- `#/components/parameters/Select` – comma-separated fields to project (e.g. `id,name,email`).

## Scope Guards & Request Metadata

Controllers can opt into multi-tenant enforcement by registering a guard adapter via `crud.WithScopeGuard`. The adapter receives the incoming request, resolves the actor, and returns a `ScopeFilter` that injects `WHERE` clauses before the repository executes.

```go
controller := crud.NewController(userRepo,
	crud.WithScopeGuard(userScopeGuard(scopeGuard)),
)

func userScopeGuard(g scope.Guard) crud.ScopeGuardFunc[*User] {
	actionMap := map[crud.CrudOperation]types.PolicyAction{
		crud.OpList: types.PolicyActionRead,
		crud.OpRead: types.PolicyActionRead,
		crud.OpCreate: types.PolicyActionCreate,
		crud.OpUpdate: types.PolicyActionUpdate,
		crud.OpDelete: types.PolicyActionDelete,
	}

	return func(ctx crud.Context, op crud.CrudOperation) (crud.ActorContext, crud.ScopeFilter, error) {
		tenantID := strings.TrimSpace(ctx.Query("tenant_id"))
		actor := crud.ActorContext{
			ActorID:  ctx.Query("actor_id", "system"),
			TenantID: tenantID,
		}

		requested := crud.ScopeFilter{}
		if tenantID != "" {
			requested.AddColumnFilter("tenant_id", "=", tenantID)
		}

		resolved, err := g.Enforce(
			ctx.UserContext(),
			toTypesActor(actor),
			toTypesScope(requested),
			actionMap[op],
			uuid.Nil,
		)
		if err != nil {
			return actor, crud.ScopeFilter{}, err
		}

		return actor, fromTypesScope(resolved), nil
	}
}

Helper functions (not shown) simply convert between crud.ActorContext/crud.ScopeFilter and the types.ActorRef/types.ScopeFilter structs used by go-users.

ActorContext mirrors the payload emitted by go-auth middleware (ID, tenant/org IDs, resource roles, impersonation flags). ScopeFilter collects guard-enforced column filters, and helper methods like AddColumnFilter make it easy to append tenant_id = ? clauses without touching Bun primitives. Column filters are applied automatically to Index, Show, and the Show read performed before Update/Delete.

Once the guard runs, go-crud stores the resolved metadata on the standard context so downstream services and repositories can reuse it:

  • crud.ContextWithActor / crud.ActorFromContext
  • crud.ContextWithScope / crud.ScopeFromContext
  • crud.ContextWithRequestID / crud.RequestIDFromContext
  • crud.ContextWithCorrelationID / crud.CorrelationIDFromContext

Lifecycle hooks now receive the same information via HookContext.Actor, HookContext.Scope, HookContext.RequestID, and HookContext.CorrelationID, so emitters can log activity without reparsing headers. Request IDs are inferred automatically from X-Request-ID/Request-ID headers (or can be pre-populated by middleware using the helpers above).

  • #/components/parameters/Order – comma-separated ordering with optional direction (e.g. name asc,created_at desc).

Additional filter parameters follow the {field}__{operator} convention emitted by the spec (for example: [email protected], ?age__gte=21, ?status__or=active,pending). These placeholders in the OpenAPI document are a reminder that any model field can be paired with the supported operators (eq, ne, gt, lt, gte, lte, like, ilike, and, or) to build expressive queries.

Options Response Shortcut

When a client appends ?format=options to the list endpoint (e.g. GET /users?format=options), the controller returns a simplified payload:

[
  {"value": "f5c5…", "label": "Jane Doe"},
  {"value": "23b9…", "label": "John Smith"}
]
  • The value comes from the repository handler’s GetID (or GetIdentifierValue fallback).
  • The label prefers the schema label field (crud:"label"), then falls back to the identifier or value.
  • Existing pagination parameters (limit, offset, order, etc.) still apply before the projection occurs—fetch a page, then the controller trims each record into {value,label}.
  • Batch create/update endpoints honour the same query parameter if callers need the refreshed options immediately after a mutation.

Omit the query parameter to receive the default envelope ({"data":[...],"$meta":{...}}).

Features

  • Service Layer Delegation – plug domain logic between the controller and repository without rewriting handlers. Supply a full Service[T] or override selected operations with helpers like WithServiceFuncs.
  • Lifecycle Hooks – register before/after callbacks for single and batch create/update/delete operations to weave in auditing, validation, or side effects.
  • Route/Operation Toggles – enable/disable or remap individual HTTP verbs when registering routes (e.g., prefer PATCH over PUT, drop batch operations).
  • Field Policies – restrict/deny/mask columns per actor and append row-level filters after guard enforcement while emitting structured policy logs.
  • Custom Actions – mount guard-aware resource or collection endpoints (e.g., “Deactivate user”) without leaving the controller by using WithActions.
  • Schema Registry – aggregate every controller’s OpenAPI document via ListSchemas, GetSchema, or RegisterSchemaListener to power /admin/schemas endpoints.
  • Advanced Query Builder – field-mapped filtering with AND/OR operators, pagination, ordering, and nested relation includes.
  • OpenAPI integration – automatic schema and path generation, with metadata propagated from struct tags and route definitions.
  • Batch Operations & Soft Deletes – first-class support for bulk create/update/delete and Bun’s soft-delete conventions.
  • Flexible Responses & Logging – swap response handlers (JSON API, HAL, etc.) and wire custom loggers to trace query building.
  • Router Adapters – ships with Fiber adapter and can be extended for other router implementations.

The repository also ships with a web demo (examples/web) that shows a combined API + HTML interface, complete with OpenAPI docs and frontend routes annotated with metadata. Run go run ./examples/web to explore the UI and generated documentation.

Configuration

Field Visibility

Use crud:"-" to exclude fields from API responses:

type User struct {
    bun.BaseModel `bun:"table:users"`
    ID       uuid.UUID `bun:"id,pk,notnull" json:"id"`
    Password string    `bun:"password,notnull" json:"-" crud:"-"`
}
Custom Response Handlers

The controller supports custom response handlers to control how data and errors are formatted. Here are some examples:

Default Response Format
// Default responses
GET /users/123
{
    "success": true,
    "data": {
        "id": "...",
        "name": "John Doe",
        "email": "[email protected]"
    }
}

GET /users
{
    "success": true,
    "data": [...],
    "$meta": {
        "count": 10
    }
}

// Error response (problem+json via go-errors)
HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
    "error": {
        "category": "not_found",
        "code": 404,
        "text_code": "NOT_FOUND",
        "message": "Record not found",
        "metadata": {
            "operation": "read"
        }
    }
}
Custom Response Handler Example
// JSONAPI style response handler
type JSONAPIResponseHandler[T any] struct{}

func (h JSONAPIResponseHandler[T]) OnData(c *fiber.Ctx, data T, op CrudOperation) error {
    c.Set("Content-type", "application/vnd.api+json")
    return c.JSON(fiber.Map{
        "data": map[string]any{
            "type":       "users",
            "id":         getId(data),
            "attributes": data,
        },
    })
}

func (h JSONAPIResponseHandler[T]) OnList(c *fiber.Ctx, data []T, op CrudOperation, total int) error {
    items := make([]map[string]any, len(data))
    for i, item := range data {
        items[i] = map[string]any{
            "type":       "users",
            "id":         getId(item),
            "attributes": item,
        }
    }
    c.Set("Content-type", "application/vnd.api+json")
    return c.JSON(fiber.Map{
        "data": items,
        "meta": map[string]any{
            "total": total,
        },
    })
}

func (h JSONAPIResponseHandler[T]) OnEmpty(c *fiber.Ctx, op CrudOperation) error {
    c.Set("Content-type", "application/vnd.api+json")
    return c.SendStatus(fiber.StatusNoContent)
}

func (h JSONAPIResponseHandler[T]) OnError(c *fiber.Ctx, err error, op CrudOperation) error {
    status := fiber.StatusInternalServerError
    if _, isNotFound := err.(*NotFoundError); isNotFound {
        status = fiber.StatusNotFound
    }
    c.Set("Content-type", "application/vnd.api+json")
    return c.Status(status).JSON(fiber.Map{
        "errors": []map[string]any{
            {
                "status": status,
                "title":  "Error",
                "detail": err.Error(),
            },
        },
    })
}

#### Error Encoders

go-crud now emits [RFC‑7807](https://datatracker.ietf.org/doc/html/rfc7807) problem+json payloads by default using [github.com/goliatone/go-errors](https://github.com/goliatone/go-errors). This keeps error categories, codes, text codes, timestamps, and metadata consistent across all controllers.

If you have existing clients that still expect the legacy `{success:false,error:string}` envelope, use the new `WithErrorEncoder` option to swap encoders without rewriting your response handler:

```go
controller := crud.NewController(repo,
    crud.WithScopeGuard(demoScopeGuard()),
    crud.WithErrorEncoder[*User](crud.LegacyJSONErrorEncoder()),
)

You can also build encoders with crud.ProblemJSONErrorEncoder(...) to set custom mappers, stack-trace behavior, or status resolvers while keeping the go-errors schema.


### Delegating to a Service Layer

Controllers can delegate every CRUD operation to a domain service. Provide a complete implementation via `WithService` or override specific operations with `WithServiceFuncs`, which layers on top of the default repository-backed service.

```go
type UserService struct {
	repo repository.Repository[*User]
}

func (s *UserService) Create(ctx crud.Context, record *User) (*User, error) {
	record.CreatedAt = timePtr(time.Now())
	return s.repo.Create(ctx.UserContext(), record)
}

// ...implement remaining methods or embed crud.NewRepositoryService(...)

controller := crud.NewController(
	userRepo,
	crud.WithServiceFuncs[*User](crud.ServiceFuncs[*User]{
		Create: func(ctx crud.Context, record *User) (*User, error) {
			now := time.Now()
			record.CreatedAt = &now
			return crud.NewRepositoryService(userRepo).Create(ctx, record)
		},
	}),
)

### Custom Actions

Expose admin-only commands (approve, deactivate, sync, etc.) without forking controllers by registering custom actions. Handlers receive `ActionContext`, which embeds the router context plus actor/scope metadata resolved by your guard:

```go
controller := crud.NewController(
	userRepo,
	crud.WithActions(crud.Action[*User]{
		Name:        "Deactivate",
		Target:      crud.ActionTargetResource, // or crud.ActionTargetCollection
		Summary:     "Deactivate a user account",
		Description: "Marks the account as inactive and emits notifications",
		Handler: func(actx crud.ActionContext[*User]) error {
			id := actx.Params("id")
			if err := service.Deactivate(actx.UserContext(), id, actx.Actor); err != nil {
				return err
			}
			return actx.Status(http.StatusAccepted).JSON(fiber.Map{"ok": true})
		},
	}),
)

Record actions mount under /{singular}/:id/actions/{slug} while collection actions use /{plural}/actions/{slug}. Each action automatically appears in the generated OpenAPI (including the x-admin-actions vendor extension) so admin clients can discover the extra endpoints.

// or wrap the default repository service with command adapters: controller := crud.NewController( userRepo, crud.WithCommandService(func(defaults crud.Service[*User]) crud.Service[*User] { return crud.ComposeService(defaults, crud.ServiceFuncs[*User]{ Create: func(ctx crud.Context, user *User) (*User, error) { if err := lifecycleCmd.Execute(ctx.UserContext(), user); err != nil { return nil, err } return defaults.Show(ctx, user.ID.String(), nil) }, }) }), )


### Lifecycle Hooks

Register before/after callbacks without implementing a full service:

```go
controller := crud.NewController(
	userRepo,
	crud.WithLifecycleHooks(crud.LifecycleHooks[*User]{
		BeforeCreate: func(hctx crud.HookContext, user *User) error {
			user.CreatedBy = hctx.Context.UserContext().Value(authKey).(string)
			return nil
		},
		AfterDelete: func(_ crud.HookContext, user *User) error {
			audit.LogDeletion(user.ID)
			return nil
		},
	}),
)

Upgrading from legacy hooks that accepted crud.Context? Wrap them with crud.HookFromContext (or crud.HookBatchFromContext) so they keep compiling while gaining access to the enriched metadata:

crud.WithLifecycleHooks(crud.LifecycleHooks[*User]{
	BeforeCreate: crud.HookFromContext(func(ctx crud.Context, user *User) error {
		user.CreatedBy = ctx.Query("actor_id", "system")
		return nil
	}),
})
Activity & Notification Emitters

Configure crud.WithActivityHooks to emit structured activity for every CRUD success/failure using the shared pkg/activity module. The controller handles emission automatically (including batch events and failures), defaulting the channel to crud unless you override it.

import (
	"context"

	crudactivity "github.com/goliatone/go-crud/pkg/activity"
	crudusersink "github.com/goliatone/go-crud/pkg/activity/usersink"
	usertypes "github.com/goliatone/go-users/pkg/types"
)

sink := &myUsersActivitySink{} // implements usertypes.ActivitySink
hooks := crudactivity.Hooks{
	crudusersink.Hook{Sink: sink}, // maps to go-users ActivityRecord
	crudactivity.HookFunc(func(ctx context.Context, evt crudactivity.Event) error {
		metrics.Count("crud", evt.Verb)
		return nil
	}),
}

controller := crud.NewController(
	userRepo,
	crud.WithActivityHooks(hooks, crudactivity.Config{
		Enabled: true,
		Channel: "crud", // optional; defaults to "crud"
	}),
)

Emitted events follow the crud.<resource>.<op> verb convention (append .batch for batch routes and .failed on errors) and include route_name, route_path, method, request_id, correlation_id, actor IDs/roles, scope labels/raw, and error on failures. Batch emissions carry batch_size, batch_index, and batch_ids when available. Timestamps default when missing, and the go-users adapter stores definition_code/recipients inside the Data map.

Migration from legacy helpers:

  • EmitActivity, ActivityEvent, and WithActivityEmitter were removed; the controller now emits automatically when WithActivityHooks is configured.
  • Drop manual helper calls in lifecycle hooks. If you need to enrich or transform events, wrap that logic inside an activity.Hook before forwarding to sinks.

Notifications still use SendNotification with WithNotificationEmitter:

controller := crud.NewController(
	userRepo,
	crud.WithNotificationEmitter[*User](emitter),
	crud.WithLifecycleHooks(crud.LifecycleHooks[*User]{
		AfterUpdate: func(hctx crud.HookContext, user *User) error {
			return crud.SendNotification(hctx, crud.ActivityPhaseAfter, user,
				crud.WithNotificationChannel("email"),
				crud.WithNotificationTemplate("user-updated"),
				crud.WithNotificationRecipients("[email protected]"))
		},
	}),
)

SendNotification no-ops when the emitter isn’t configured, so shared hooks can run across services that opt out of notifications.

Field Policies

Controllers can enforce per-actor column visibility by wiring a FieldPolicyProvider. The provider receives the current operation, actor, scope, and resource metadata, then returns allow/deny lists, mask functions, and optional row filters:

policy := func(req crud.FieldPolicyRequest[*User]) (crud.FieldPolicy, error) {
	if req.Actor.Role == "support" {
		filter := crud.ScopeFilter{}
		filter.AddColumnFilter("tenant_id", "=", req.Actor.TenantID)

		return crud.FieldPolicy{
			Name:      "support:limited",
			Allow:     []string{"id", "name", "email"},
			Deny:      []string{"password", "ssn"},
			Mask:      map[string]crud.FieldMaskFunc{"email": func(v any) any { return "[email protected]" }},
			RowFilter: filter, // appended after guard criteria
		}, nil
	}
	return crud.FieldPolicy{}, nil
}

controller := crud.NewController(
	userRepo,
	crud.WithFieldPolicyProvider(policy),
)

Key behaviors:

  • Allow/Deny determine which JSON fields can be selected, filtered, or ordered. The query builder drops disallowed fields automatically.
  • Mask runs before the response is serialized so secrets can be obfuscated without mutating the record in storage.
  • RowFilter appends additional criteria (e.g., owner_id = actor_id) after guard-enforced tenant/org filters.
  • Every resolved policy is logged via LogFieldPolicyDecision, which attaches operation/resource/allow/deny/mask metadata to your logger implementation for auditing.
Route/Operation Toggles

Fine-tune which routes get registered and which HTTP verbs they use:

controller := crud.NewController(
	userRepo,
	crud.WithRouteConfig(crud.RouteConfig{
		Operations: map[crud.CrudOperation]crud.RouteOptions{
			crud.OpUpdate:      {Method: http.MethodPatch}, // use PATCH instead of PUT
			crud.OpDeleteBatch: {Enabled: crud.BoolPtr(false)}, // disable batch delete
		},
	}),
)

// Using the custom handler
controller := crud.NewController[*User](
    repo,
    crud.WithResponseHandler[*User](JSONAPIResponseHandler[*User]{}),
)

The above handler would produce responses in JSONAPI format:

{
    "data": {
        "type": "users",
        "id": "123",
        "attributes": {
            "name": "John Doe",
            "email": "[email protected]"
        }
    }
}
Query Parameters

The List endpoint supports:

  • Pagination: ?limit=10&offset=20 (default limit: 25, offset: 0)
  • Ordering: ?order=name asc,created_at desc
  • Field selection: ?select=id,name,email
  • Relations: ?include=Company,Profile (supports filtering: ?include=Profile.status=outdated)
  • Nested relations & filters: ?include=Blocks.Translations.locale__eq=es (any depth, multiple clauses)
  • Filtering:
    • Basic: ?name=John
    • Operators: ?age__gte=30, ?name__ilike=john%
    • Available operators: eq, ne, gt, lt, gte, lte, like, ilike, and, or
    • Multiple values: ?name__or=John,Jack

License

MIT

Copyright (c) 2024 goliatone

Documentation

Index

Constants

View Source
const (
	TAG_CRUD         = "crud"
	TAG_BUN          = "bun"
	TAG_JSON         = "json"
	TAG_KEY_RESOURCE = "resource"
)
View Source
const (
	VirtualDialectPostgres = persistence.VirtualDialectPostgres
	VirtualDialectSQLite   = persistence.VirtualDialectSQLite
)

Variables

View Source
var DefaultLimit = 25
View Source
var DefaultOffset = 0
View Source
var LoggerEnabled = false

Functions

func ActivityEmitterFromContext added in v0.14.0

func ActivityEmitterFromContext(ctx context.Context) *activity.Emitter

ActivityEmitterFromContext returns the hook activity emitter stored on the context.

func BoolPtr added in v0.5.0

func BoolPtr(v bool) *bool

func ContextWithActivityEmitter added in v0.14.0

func ContextWithActivityEmitter(ctx context.Context, emitter *activity.Emitter) context.Context

ContextWithActivityEmitter stores the activity emitter used by hooks.

func ContextWithActor added in v0.12.0

func ContextWithActor(ctx context.Context, actor ActorContext) context.Context

ContextWithActor stores the provided actor metadata on the standard context.

func ContextWithCorrelationID added in v0.12.0

func ContextWithCorrelationID(ctx context.Context, correlationID string) context.Context

ContextWithCorrelationID stores the correlation ID on the context.

func ContextWithHookMetadata added in v0.14.0

func ContextWithHookMetadata(ctx context.Context, meta HookMetadata) context.Context

ContextWithHookMetadata attaches hook metadata to the context.

func ContextWithNotificationEmitter added in v0.14.0

func ContextWithNotificationEmitter(ctx context.Context, emitter NotificationEmitter) context.Context

ContextWithNotificationEmitter stores the notification emitter for hooks.

func ContextWithRequestID added in v0.12.0

func ContextWithRequestID(ctx context.Context, requestID string) context.Context

ContextWithRequestID stores the current request identifier on the context.

func ContextWithScope added in v0.12.0

func ContextWithScope(ctx context.Context, scope ScopeFilter) context.Context

ContextWithScope stores the guard scope filter on the context.

func CorrelationIDFromContext added in v0.12.0

func CorrelationIDFromContext(ctx context.Context) string

CorrelationIDFromContext extracts the stored correlation ID.

func DefaultDeserializer

func DefaultDeserializer[T any](op CrudOperation, ctx Context) (T, error)

DefaultDeserializer provides a generic deserializer.

func DefaultDeserializerMany added in v0.1.0

func DefaultDeserializerMany[T any](op CrudOperation, ctx Context) ([]T, error)

DefaultDeserializerMany provides a generic deserializer.

func DefaultOperatorMap

func DefaultOperatorMap() map[string]string

func GetResourceName

func GetResourceName(typ reflect.Type) (string, string)

GetResourceName returns the singular and plural resource names for type T. It first checks for a 'crud:"resource:..."' tag on any embedded fields. If found, it uses the specified resource name. Otherwise, it derives the name from the type's name.

func GetResourceTitle added in v0.1.1

func GetResourceTitle(typ reflect.Type) (string, string)

func LogFieldPolicyDecision added in v0.12.0

func LogFieldPolicyDecision(logger Logger, audit FieldPolicyAudit)

LogFieldPolicyDecision writes the audit entry using the provided logger.

func RegisterSchemaListener added in v0.12.0

func RegisterSchemaListener(listener SchemaListener)

RegisterSchemaListener subscribes to schema updates.

func RequestIDFromContext added in v0.12.0

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request identifier stored in the context.

func SendNotification added in v0.12.0

func SendNotification[T any](hctx HookContext, phase ActivityPhase, record T, opts ...NotificationEventOption) error

SendNotification emits a notification event for a single record.

func SendNotificationBatch added in v0.12.0

func SendNotificationBatch[T any](hctx HookContext, phase ActivityPhase, records []T, opts ...NotificationEventOption) error

SendNotificationBatch emits a notification event for multiple records.

func SetOperatorMap

func SetOperatorMap(om map[string]string)

func StringPtr added in v0.14.0

func StringPtr(v string) *string

StringPtr returns a pointer to a string value.

func VirtualFieldExpr added in v0.14.0

func VirtualFieldExpr(dialect, sourceField, key string, asJSON bool) string

VirtualFieldExpr returns a SQL snippet for the given dialect to access a virtual field. When asJSON is false, text extraction is used (suitable for comparisons/order-by). When asJSON is true, the raw JSON value is returned.

func WithProblemJSONContentType added in v0.12.0

func WithProblemJSONContentType(contentType string) problemJSONEncoderOption

WithProblemJSONContentType overrides the response content type (defaults to application/problem+json).

func WithProblemJSONErrorMappers added in v0.12.0

func WithProblemJSONErrorMappers(mappers ...goerrors.ErrorMapper) problemJSONEncoderOption

WithProblemJSONErrorMappers appends additional error mappers.

func WithProblemJSONIncludeStack added in v0.12.0

func WithProblemJSONIncludeStack(include bool) problemJSONEncoderOption

WithProblemJSONIncludeStack configures whether stack traces should be serialized.

func WithProblemJSONStatusResolver added in v0.12.0

func WithProblemJSONStatusResolver(resolver ErrorStatusResolver) problemJSONEncoderOption

WithProblemJSONStatusResolver overrides the status resolver used by the encoder.

Types

type APIListResponse added in v0.0.2

type APIListResponse[T any] struct {
	Success bool     `json:"success"`
	Data    []T      `json:"data"`
	Meta    *Filters `json:"$meta"`
}

type APIResponse added in v0.0.2

type APIResponse[T any] struct {
	Success bool   `json:"success"`
	Data    T      `json:"data,omitempty"`
	Error   string `json:"error,omitempty"`
}

type Action added in v0.12.0

type Action[T any] struct {
	Name        string
	Method      string
	Target      ActionTarget
	Path        string
	Summary     string
	Description string
	Tags        []string
	Parameters  []router.Parameter
	RequestBody *router.RequestBody
	Responses   []router.Response
	Security    []string
	Handler     ActionHandler[T]
}

Action describes a custom endpoint registered alongside the CRUD routes.

type ActionContext added in v0.12.0

type ActionContext[T any] struct {
	Context
	Actor         ActorContext
	Scope         ScopeFilter
	RequestID     string
	CorrelationID string
	Action        ActionDescriptor
	Operation     CrudOperation
}

ActionContext extends the base Context with actor/scope metadata for convenience.

type ActionDescriptor added in v0.12.0

type ActionDescriptor struct {
	Name        string       `json:"name"`
	Slug        string       `json:"slug"`
	Method      string       `json:"method"`
	Target      ActionTarget `json:"target"`
	Path        string       `json:"path"`
	Summary     string       `json:"summary,omitempty"`
	Description string       `json:"description,omitempty"`
	Tags        []string     `json:"tags,omitempty"`
}

ActionDescriptor is a normalized view of the action exposed via metadata.

type ActionHandler added in v0.12.0

type ActionHandler[T any] func(ActionContext[T]) error

ActionHandler executes the custom action logic. Use the embedded Context to write responses.

type ActionTarget added in v0.12.0

type ActionTarget string

ActionTarget indicates whether the action targets the collection or a specific record.

const (
	ActionTargetCollection ActionTarget = "collection"
	ActionTargetResource   ActionTarget = "resource"
)

type ActivityPhase added in v0.12.0

type ActivityPhase string

ActivityPhase indicates the lifecycle moment that triggered the emitter.

const (
	ActivityPhaseBefore ActivityPhase = "before"
	ActivityPhaseAfter  ActivityPhase = "after"
	ActivityPhaseError  ActivityPhase = "error"
)

type ActorContext added in v0.12.0

type ActorContext struct {
	ActorID        string
	Subject        string
	Role           string
	ResourceRoles  map[string]string
	TenantID       string
	OrganizationID string
	Metadata       map[string]any
	ImpersonatorID string
	IsImpersonated bool
}

ActorContext captures normalized actor metadata attached to a request. It mirrors the payload emitted by go-auth middleware so guard adapters can run without importing that package.

func ActorFromContext added in v0.12.0

func ActorFromContext(ctx context.Context) ActorContext

ActorFromContext retrieves the ActorContext previously stored on the context.

func (ActorContext) Clone added in v0.12.0

func (a ActorContext) Clone() ActorContext

Clone returns a shallow copy guarding internal maps from mutation.

func (ActorContext) IsZero added in v0.12.0

func (a ActorContext) IsZero() bool

IsZero reports whether the actor context carries any meaningful identifier.

type AdminMenuMetadata added in v0.12.0

type AdminMenuMetadata struct {
	Group  string `json:"group,omitempty"`
	Label  string `json:"label,omitempty"`
	Icon   string `json:"icon,omitempty"`
	Order  int    `json:"order,omitempty"`
	Path   string `json:"path,omitempty"`
	Hidden bool   `json:"hidden,omitempty"`
}

AdminMenuMetadata hints how the CMS should organize the resource.

type AdminScopeMetadata added in v0.12.0

type AdminScopeMetadata struct {
	Level       string   `json:"level,omitempty"`
	Description string   `json:"description,omitempty"`
	Claims      []string `json:"claims,omitempty"`
	Roles       []string `json:"roles,omitempty"`
	Labels      []string `json:"labels,omitempty"`
}

AdminScopeMetadata documents the required scope/claims for a resource.

type CommandServiceFactory added in v0.12.0

type CommandServiceFactory[T any] func(defaults Service[T]) Service[T]

CommandServiceFactory builds a Service implementation that can wrap the controller's default service (repository-backed) with command adapters.

func CommandServiceFromFuncs added in v0.12.0

func CommandServiceFromFuncs[T any](overrides ServiceFuncs[T]) CommandServiceFactory[T]

CommandServiceFromFuncs returns a CommandServiceFactory that applies the given overrides on top of the provided defaults. It is useful when command adapters only need to intercept a subset of operations.

type Context added in v0.1.0

type Context interface {
	Request
	Response
}

type Controller

type Controller[T any] struct {
	Repo repository.Repository[T]
	// contains filtered or unexported fields
}

Controller handles CRUD operations for a given model.

func NewController

func NewController[T any](repo repository.Repository[T], opts ...Option[T]) *Controller[T]

NewController creates a new Controller with functional options.

func NewControllerWithService added in v0.14.0

func NewControllerWithService[T any](repo repository.Repository[T], service Service[T], opts ...Option[T]) *Controller[T]

NewControllerWithService builds a controller using the provided service while still honoring controller options (deserializer, response handler, hooks, etc.).

func (*Controller[T]) Create

func (c *Controller[T]) Create(ctx Context) error

func (*Controller[T]) CreateBatch added in v0.1.0

func (c *Controller[T]) CreateBatch(ctx Context) error

func (*Controller[T]) Delete

func (c *Controller[T]) Delete(ctx Context) error

func (*Controller[T]) DeleteBatch added in v0.1.0

func (c *Controller[T]) DeleteBatch(ctx Context) error

func (*Controller[T]) GetMetadata added in v0.1.1

func (c *Controller[T]) GetMetadata() router.ResourceMetadata

GetMetadata implements router.MetadataProvider, we use it to generate the required info that will be used to create a OpenAPI spec or something similar

func (*Controller[T]) Index added in v0.1.0

func (c *Controller[T]) Index(ctx Context) error

Index supports different query string parameters: GET /users?limit=10&offset=20 GET /users?order=name asc,created_at desc GET /users?select=id,name,email GET /users?include=company,profile GET /users?name__ilike=John&age__gte=30 GET /users?name__and=John,Jack GET /users?name__or=John,Jack

func (*Controller[T]) RegisterRoutes

func (c *Controller[T]) RegisterRoutes(r Router)

func (*Controller[T]) Schema added in v0.2.0

func (c *Controller[T]) Schema(ctx Context) error

func (*Controller[T]) Show added in v0.1.0

func (c *Controller[T]) Show(ctx Context) error

Show supports different query string parameters: GET /user?include=Company,Profile GET /user?select=id,age,email

func (*Controller[T]) Update

func (c *Controller[T]) Update(ctx Context) error

func (*Controller[T]) UpdateBatch added in v0.1.0

func (c *Controller[T]) UpdateBatch(ctx Context) error

type CrudOperation

type CrudOperation string

CrudOperation defines the type for CRUD operations.

const (
	OpCreate      CrudOperation = "create"
	OpCreateBatch CrudOperation = "create:batch"
	OpRead        CrudOperation = "read"
	OpList        CrudOperation = "list"
	OpUpdate      CrudOperation = "update"
	OpUpdateBatch CrudOperation = "update:batch"
	OpDelete      CrudOperation = "delete"
	OpDeleteBatch CrudOperation = "delete:batch"
)

type DefaultResponseHandler added in v0.0.2

type DefaultResponseHandler[T any] struct {
	// contains filtered or unexported fields
}

func (*DefaultResponseHandler[T]) OnData added in v0.0.2

func (h *DefaultResponseHandler[T]) OnData(c Context, data T, op CrudOperation, filters ...*Filters) error

func (*DefaultResponseHandler[T]) OnEmpty added in v0.0.2

func (h *DefaultResponseHandler[T]) OnEmpty(c Context, op CrudOperation) error

func (*DefaultResponseHandler[T]) OnError added in v0.0.2

func (h *DefaultResponseHandler[T]) OnError(c Context, err error, op CrudOperation) error

func (*DefaultResponseHandler[T]) OnList added in v0.0.2

func (h *DefaultResponseHandler[T]) OnList(c Context, data []T, op CrudOperation, filters *Filters) error

type ErrorEncoder added in v0.12.0

type ErrorEncoder func(ctx Context, err error, op CrudOperation) error

ErrorEncoder serializes controller errors into HTTP responses.

func LegacyJSONErrorEncoder added in v0.12.0

func LegacyJSONErrorEncoder() ErrorEncoder

LegacyJSONErrorEncoder preserves the previous {success:false,error:string} payloads.

func ProblemJSONErrorEncoder added in v0.12.0

func ProblemJSONErrorEncoder(opts ...problemJSONEncoderOption) ErrorEncoder

ProblemJSONErrorEncoder returns an encoder that emits go-errors compatible RFC-7807/problem+json responses. The encoder inspects known error categories, maps them to HTTP status codes, and writes go-errors.ErrorResponse bodies.

type ErrorStatusResolver added in v0.12.0

type ErrorStatusResolver func(err *goerrors.Error, op CrudOperation) int

ErrorStatusResolver resolves the HTTP status code for a go-errors error.

type FieldMapProvider added in v0.4.0

type FieldMapProvider func(reflect.Type) map[string]string

func NewVirtualFieldMapProvider added in v0.14.0

func NewVirtualFieldMapProvider(cfg VirtualFieldHandlerConfig, base FieldMapProvider) FieldMapProvider

NewVirtualFieldMapProvider builds a FieldMapProvider that merges the base provider with virtual field expressions. It inspects model tags to discover virtual fields and emits dialect-aware expressions.

type FieldMaskFunc added in v0.12.0

type FieldMaskFunc func(value any) any

FieldMaskFunc receives the current field value and returns the masked version. Returning nil will zero the field when serialized.

type FieldPolicy added in v0.12.0

type FieldPolicy struct {
	// Name helps auditors identify which policy executed (e.g., "tenant-admin").
	Name string
	// Allow restricts responses to the listed JSON field names. Empty means inherit defaults.
	Allow []string
	// Deny removes the listed JSON field names from the response/queryable set.
	Deny []string
	// Mask applies field-level transformations before encoding the response.
	Mask map[string]FieldMaskFunc
	// RowFilter appends additional column filters after guard enforcement.
	RowFilter ScopeFilter
	// Labels stores arbitrary metadata surfaced in audit logs.
	Labels map[string]string
}

FieldPolicy describes per-request column visibility rules returned by FieldPolicyProvider.

type FieldPolicyAudit added in v0.12.0

type FieldPolicyAudit struct {
	Policy    string
	Resource  string
	Operation CrudOperation
	Allowed   []string
	Denied    []string
	Masked    []string
	RowFilter []ScopeColumnFilter
	Labels    map[string]string
}

FieldPolicyAudit captures the normalized details emitted to logs.

type FieldPolicyProvider added in v0.12.0

type FieldPolicyProvider[T any] func(FieldPolicyRequest[T]) (FieldPolicy, error)

FieldPolicyProvider returns the policy governing the given request.

type FieldPolicyRequest added in v0.12.0

type FieldPolicyRequest[T any] struct {
	Context     Context
	Operation   CrudOperation
	Actor       ActorContext
	Scope       ScopeFilter
	Resource    string
	ResourceTyp reflect.Type
}

FieldPolicyRequest conveys the context supplied to FieldPolicyProvider.

type Fields added in v0.4.0

type Fields map[string]any

type Filters added in v0.1.0

type Filters struct {
	Operation string         `json:"operation,omitempty"`
	Limit     int            `json:"limit,omitempty"`
	Offset    int            `json:"offset,omitempty"`
	Page      int            `json:"page,omitempty"`
	Adjusted  bool           `json:"adjusted,omitempty"`
	Count     int            `json:"count,omitempty"`
	Order     []Order        `json:"order,omitempty"`
	Fields    []string       `json:"fields,omitempty"`
	Include   []string       `json:"include,omitempty"`
	Relations []RelationInfo `json:"relations,omitempty"`
}

func BuildQueryCriteria added in v0.1.0

func BuildQueryCriteria[T any](ctx Context, op CrudOperation, opts ...QueryBuilderOption) ([]repository.SelectCriteria, *Filters, error)

func BuildQueryCriteriaWithLogger added in v0.4.0

func BuildQueryCriteriaWithLogger[T any](ctx Context, op CrudOperation, logger Logger, enableTrace bool, opts ...QueryBuilderOption) ([]repository.SelectCriteria, *Filters, error)

type HookBatchFunc added in v0.5.0

type HookBatchFunc[T any] func(HookContext, []T) error

HookBatchFunc represents a lifecycle hook for multiple records.

func ChainBatchHooks added in v0.14.0

func ChainBatchHooks[T any](hooks ...HookBatchFunc[T]) HookBatchFunc[T]

ChainBatchHooks composes multiple batch hooks in order.

func HookBatchFromContext added in v0.12.0

func HookBatchFromContext[T any](hook func(Context, []T) error) HookBatchFunc[T]

HookBatchFromContext adapts a legacy batch hook (Context + []T) into the new HookBatchFunc form that receives HookContext metadata.

type HookContext added in v0.5.0

type HookContext struct {
	Context       Context
	Metadata      HookMetadata
	Actor         ActorContext
	Scope         ScopeFilter
	RequestID     string
	CorrelationID string
	// contains filtered or unexported fields
}

HookContext bundles the request context with hook metadata.

func (HookContext) ActivityHooks added in v0.13.0

func (h HookContext) ActivityHooks() *activity.Emitter

ActivityHooks returns the v2 activity emitter constructed from pkg/activity.

func (HookContext) HasNotificationEmitter added in v0.12.0

func (h HookContext) HasNotificationEmitter() bool

HasNotificationEmitter reports whether the controller configured a NotificationEmitter.

func (HookContext) NotificationEmitter added in v0.12.0

func (h HookContext) NotificationEmitter() NotificationEmitter

NotificationEmitter returns the configured NotificationEmitter (if any).

type HookFunc added in v0.5.0

type HookFunc[T any] func(HookContext, T) error

HookFunc represents a lifecycle hook for a single record.

func ChainHooks added in v0.14.0

func ChainHooks[T any](hooks ...HookFunc[T]) HookFunc[T]

ChainHooks composes multiple single-record hooks in order.

func HookFromContext added in v0.12.0

func HookFromContext[T any](hook func(Context, T) error) HookFunc[T]

HookFromContext adapts a legacy hook that only expected crud.Context into a HookFunc that receives the enriched HookContext. Nil hooks return nil.

type HookMetadata added in v0.5.0

type HookMetadata struct {
	Operation CrudOperation
	Resource  string
	RouteName string
	Method    string
	Path      string
}

HookMetadata carries operational attributes for lifecycle hooks.

func HookMetadataFromContext added in v0.14.0

func HookMetadataFromContext(ctx context.Context) HookMetadata

HookMetadataFromContext retrieves hook metadata stored on the context.

type LifecycleHooks added in v0.5.0

type LifecycleHooks[T any] struct {
	BeforeCreate      []HookFunc[T]
	AfterCreate       []HookFunc[T]
	BeforeCreateBatch []HookBatchFunc[T]
	AfterCreateBatch  []HookBatchFunc[T]

	BeforeUpdate      []HookFunc[T]
	AfterUpdate       []HookFunc[T]
	BeforeUpdateBatch []HookBatchFunc[T]
	AfterUpdateBatch  []HookBatchFunc[T]

	AfterRead []HookFunc[T]
	AfterList []HookBatchFunc[T]

	BeforeDelete      []HookFunc[T]
	AfterDelete       []HookFunc[T]
	BeforeDeleteBatch []HookBatchFunc[T]
	AfterDeleteBatch  []HookBatchFunc[T]
}

LifecycleHooks groups all supported CRUD lifecycle hooks.

type Logger added in v0.2.0

type Logger interface {
	Debug(format string, args ...any)
	Info(format string, args ...any)
	Error(format string, args ...any)
}

type MergePolicy added in v0.14.0

type MergePolicy struct {
	PutReplace     bool
	PatchMerge     bool
	DeleteWithNull bool
	// Per-field overrides keyed by source map name (e.g., "Metadata") and merge strategy ("deep", "shallow", "replace").
	FieldMergeStrategy map[string]string
}

MergePolicy controls how partial updates are interpreted.

type MetadataRouterRouteInfo added in v0.4.0

type MetadataRouterRouteInfo interface {
	RouterRouteInfo
	Description(string) MetadataRouterRouteInfo
	Summary(string) MetadataRouterRouteInfo
	Tags(...string) MetadataRouterRouteInfo
	Parameter(name, in string, required bool, schema map[string]any) MetadataRouterRouteInfo
	RequestBody(desc string, required bool, content map[string]any) MetadataRouterRouteInfo
	Response(code int, desc string, content map[string]any) MetadataRouterRouteInfo
}

MetadataRouterRouteInfo exposes optional metadata configuration methods

type NotFoundError added in v0.0.2

type NotFoundError struct {
	// contains filtered or unexported fields
}

type NotificationEmitter added in v0.12.0

type NotificationEmitter interface {
	SendNotification(ctx context.Context, event NotificationEvent) error
}

NotificationEmitter sends user-facing notifications derived from lifecycle events.

func NotificationEmitterFromContext added in v0.14.0

func NotificationEmitterFromContext(ctx context.Context) NotificationEmitter

NotificationEmitterFromContext extracts the notification emitter from the context.

type NotificationEvent added in v0.12.0

type NotificationEvent struct {
	Operation     CrudOperation
	Phase         ActivityPhase
	Resource      string
	RouteName     string
	Method        string
	Path          string
	Actor         ActorContext
	Scope         ScopeFilter
	RequestID     string
	CorrelationID string
	Records       []any
	Channel       string
	Template      string
	Recipients    []string
	Metadata      map[string]any
}

NotificationEvent describes a notification occurrence.

type NotificationEventOption added in v0.12.0

type NotificationEventOption func(*NotificationEvent)

NotificationEventOption configures optional notification fields.

func WithNotificationChannel added in v0.12.0

func WithNotificationChannel(channel string) NotificationEventOption

WithNotificationChannel sets the outbound channel (e.g., email, webhook).

func WithNotificationMetadata added in v0.12.0

func WithNotificationMetadata(metadata map[string]any) NotificationEventOption

WithNotificationMetadata merges arbitrary metadata into the event.

func WithNotificationRecipients added in v0.12.0

func WithNotificationRecipients(recipients ...string) NotificationEventOption

WithNotificationRecipients declares the intended recipients (emails, IDs, etc.).

func WithNotificationTemplate added in v0.12.0

func WithNotificationTemplate(template string) NotificationEventOption

WithNotificationTemplate stores the downstream template identifier.

type Option

type Option[T any] func(*Controller[T])

func WithActions added in v0.12.0

func WithActions[T any](actions ...Action[T]) Option[T]

func WithActivityHooks added in v0.13.0

func WithActivityHooks[T any](hooks activity.Hooks, cfg activity.Config) Option[T]

WithActivityHooks configures the controller with the shared activity emitter built from pkg/activity hooks and config. Defaults to no-op when hooks are empty.

func WithAdminMenuMetadata added in v0.12.0

func WithAdminMenuMetadata[T any](meta AdminMenuMetadata) Option[T]

func WithAdminScopeMetadata added in v0.12.0

func WithAdminScopeMetadata[T any](meta AdminScopeMetadata) Option[T]

func WithBatchReturnOrderByID added in v0.16.0

func WithBatchReturnOrderByID[T any](enabled bool) Option[T]

WithBatchReturnOrderByID enables ordered batch returns for CreateBatch/UpdateBatch.

func WithBatchRouteSegment added in v0.16.0

func WithBatchRouteSegment[T any](segment string) Option[T]

WithBatchRouteSegment sets the path segment used for batch routes (default "batch").

func WithCommandService added in v0.12.0

func WithCommandService[T any](factory CommandServiceFactory[T]) Option[T]

WithCommandService composes the default repository-backed service with the provided command adapter factory. When the factory is nil the option is a no-op.

func WithDeserializer

func WithDeserializer[T any](d func(CrudOperation, Context) (T, error)) Option[T]

WithDeserializer sets a custom deserializer for the Controller.

func WithErrorEncoder added in v0.12.0

func WithErrorEncoder[T any](encoder ErrorEncoder) Option[T]

WithErrorEncoder overrides the encoder used when controllers serialize errors. When paired with the default response handler, this switches between the problem+json encoder and the legacy {success:false,error:string} payloads.

func WithFieldMapProvider added in v0.4.0

func WithFieldMapProvider[T any](provider FieldMapProvider) Option[T]

func WithFieldPolicyProvider added in v0.12.0

func WithFieldPolicyProvider[T any](provider FieldPolicyProvider[T]) Option[T]

func WithLifecycleHooks added in v0.5.0

func WithLifecycleHooks[T any](hooks LifecycleHooks[T]) Option[T]

func WithLogger added in v0.2.0

func WithLogger[T any](logger Logger) Option[T]

func WithNotificationEmitter added in v0.12.0

func WithNotificationEmitter[T any](emitter NotificationEmitter) Option[T]

func WithQueryLogging added in v0.4.0

func WithQueryLogging[T any](enabled bool) Option[T]

func WithRelationFilter added in v0.8.0

func WithRelationFilter[T any](filter router.RelationFilterFunc) Option[T]

func WithRelationMetadataProvider added in v0.8.0

func WithRelationMetadataProvider[T any](provider router.RelationMetadataProvider) Option[T]

func WithResponseHandler added in v0.0.2

func WithResponseHandler[T any](handler ResponseHandler[T]) Option[T]

func WithRouteConfig added in v0.5.0

func WithRouteConfig[T any](config RouteConfig) Option[T]

func WithRowFilterHints added in v0.12.0

func WithRowFilterHints[T any](hints ...RowFilterHint) Option[T]

func WithScopeGuard added in v0.12.0

func WithScopeGuard[T any](guard ScopeGuardFunc[T]) Option[T]

func WithService added in v0.6.0

func WithService[T any](service Service[T]) Option[T]

func WithServiceFuncs added in v0.6.0

func WithServiceFuncs[T any](overrides ServiceFuncs[T]) Option[T]

func WithVirtualFields added in v0.14.0

func WithVirtualFields[T any](cfg ...VirtualFieldHandlerConfig) Option[T]

WithVirtualFields enables virtual field handling with optional config.

type Order added in v0.1.0

type Order struct {
	Field string `json:"field"`
	Dir   string `json:"dir"`
}

type QueryBuilderOption added in v0.12.0

type QueryBuilderOption func(*queryBuilderConfig)

func WithAllowedFields added in v0.12.0

func WithAllowedFields(fields map[string]string) QueryBuilderOption

type RelationFilter added in v0.1.0

type RelationFilter struct {
	Field    string `json:"field"`
	Operator string `json:"operator"`
	Value    string `json:"value"`
}

type RelationInfo added in v0.1.0

type RelationInfo struct {
	Name    string           `json:"name"`
	Filters []RelationFilter `json:"filters,omitempty"`
}

type RepositoryServiceOptions added in v0.16.0

type RepositoryServiceOptions struct {
	BatchInsertCriteria []repository.InsertCriteria
	BatchUpdateCriteria []repository.UpdateCriteria
}

RepositoryServiceOptions configures repository-backed service behavior.

type Request added in v0.1.0

type Request interface {
	UserContext() context.Context
	Params(key string, defaultValue ...string) string
	BodyParser(out any) error
	Query(key string, defaultValue ...string) string
	QueryValues(key string) []string
	QueryInt(key string, defaultValue ...int) int
	Queries() map[string]string
	Body() []byte
}

type ResourceController added in v0.1.0

type ResourceController[T any] interface {
	RegisterRoutes(r Router)
}

ResourceController defines an interface for registering CRUD routes

type ResourceHandler added in v0.1.0

type ResourceHandler interface {
	// Index fetches all records
	Index(Context) error
	// Show fetches a single record, usually by ID
	Show(Context) error
	Create(Context) error
	CreateBatch(Context) error
	Update(Context) error
	UpdateBatch(Context) error
	Delete(Context) error
	DeleteBatch(Context) error
}

type Response added in v0.1.0

type Response interface {
	Status(status int) Response
	JSON(data any, ctype ...string) error
	SendStatus(status int) error
}

type ResponseHandler added in v0.0.2

type ResponseHandler[T any] interface {
	OnError(ctx Context, err error, op CrudOperation) error
	OnData(ctx Context, data T, op CrudOperation, filters ...*Filters) error
	OnEmpty(ctx Context, op CrudOperation) error
	OnList(ctx Context, data []T, op CrudOperation, filters *Filters) error
}

ResponseHandler defines how controller responses are handled

func NewDefaultResponseHandler added in v0.0.2

func NewDefaultResponseHandler[T any]() ResponseHandler[T]

type RouteConfig added in v0.5.0

type RouteConfig struct {
	Operations map[CrudOperation]RouteOptions
}

func DefaultRouteConfig added in v0.5.0

func DefaultRouteConfig() RouteConfig

type RouteOptions added in v0.5.0

type RouteOptions struct {
	Enabled *bool
	Method  string
}

type Router added in v0.1.0

type Router interface {
	Get(path string, handler func(Context) error) RouterRouteInfo
	Post(path string, handler func(Context) error) RouterRouteInfo
	Put(path string, handler func(Context) error) RouterRouteInfo
	Patch(path string, handler func(Context) error) RouterRouteInfo
	Delete(path string, handler func(Context) error) RouterRouteInfo
}

Router is a simplified interface from the crud package perspective, referencing the generic router

func NewFiberAdapter added in v0.1.0

func NewFiberAdapter(r fiber.Router) Router

NewFiberAdapter creates a new crud.Router that uses a router.Router[any]

func NewGoRouterAdapter added in v0.1.1

func NewGoRouterAdapter[T any](r router.Router[T]) Router

NewGoRouterAdapter creates a new crud.Router that uses a router.Router[T] This follows the same pattern as the existing NewFiberAdapter

type RouterRouteInfo added in v0.1.0

type RouterRouteInfo interface {
	Name(string) RouterRouteInfo
}

RouterRouteInfo is a simplified interface for route info

type RowFilterHint added in v0.12.0

type RowFilterHint struct {
	Field       string `json:"field"`
	Operator    string `json:"operator,omitempty"`
	Description string `json:"description,omitempty"`
}

RowFilterHint documents guard/policy criteria applied to the resource.

type SchemaEntry added in v0.12.0

type SchemaEntry struct {
	Resource  string         `json:"resource"`
	Plural    string         `json:"plural"`
	Document  map[string]any `json:"document"`
	UpdatedAt time.Time      `json:"updated_at"`
}

SchemaEntry holds the cached OpenAPI document for a controller.

func GetSchema added in v0.12.0

func GetSchema(resource string) (SchemaEntry, bool)

GetSchema retrieves the schema for the given resource name.

func ListSchemas added in v0.12.0

func ListSchemas() []SchemaEntry

ListSchemas returns all registered schema documents.

type SchemaListener added in v0.12.0

type SchemaListener func(SchemaEntry)

SchemaListener receives notifications whenever a schema entry changes.

type ScopeColumnFilter added in v0.12.0

type ScopeColumnFilter struct {
	Column   string
	Operator string
	Values   []string
}

ScopeColumnFilter captures a single column restriction enforced by the guard.

type ScopeFilter added in v0.12.0

type ScopeFilter struct {
	// ColumnFilters expresses equality/IN restrictions (guard authoritative).
	ColumnFilters []ScopeColumnFilter
	// Bypass instructs controllers to skip automatic filter application.
	// Guard implementations must set this explicitly; it never defaults to true.
	Bypass bool
	// Labels provide optional structured metadata for logging/auditing.
	Labels map[string]string
	// Raw stores arbitrary data that higher layers may need for observability.
	Raw map[string]any
}

ScopeFilter describes the row-level restrictions returned by a guard. Controllers apply these filters before executing repository calls.

func ScopeFromContext added in v0.12.0

func ScopeFromContext(ctx context.Context) ScopeFilter

ScopeFromContext extracts the guard scope filter from the context.

func (*ScopeFilter) AddColumnFilter added in v0.12.0

func (sf *ScopeFilter) AddColumnFilter(column string, operator string, values ...string)

AddColumnFilter appends a new filter, ignoring empty columns/values.

func (ScopeFilter) HasFilters added in v0.12.0

func (sf ScopeFilter) HasFilters() bool

HasFilters reports whether the guard produced any column filters.

type ScopeGuardFunc added in v0.12.0

type ScopeGuardFunc[T any] func(ctx Context, op CrudOperation) (ActorContext, ScopeFilter, error)

ScopeGuardFunc resolves the actor + scope restrictions for a CRUD operation.

type Service added in v0.6.0

type Service[T any] interface {
	Create(ctx Context, record T) (T, error)
	CreateBatch(ctx Context, records []T) ([]T, error)

	Update(ctx Context, record T) (T, error)
	UpdateBatch(ctx Context, records []T) ([]T, error)

	Delete(ctx Context, record T) error
	DeleteBatch(ctx Context, records []T) error

	Index(ctx Context, criteria []repository.SelectCriteria) ([]T, int, error)
	Show(ctx Context, id string, criteria []repository.SelectCriteria) (T, error)
}

Service defines pluggable CRUD behaviours that the controller can delegate to.

func ComposeService added in v0.6.0

func ComposeService[T any](defaults Service[T], funcs ServiceFuncs[T]) Service[T]

ComposeService returns a Service implementation that uses the given defaults and overrides any operation provided in funcs.

func NewRepositoryService added in v0.6.0

func NewRepositoryService[T any](repo repository.Repository[T]) Service[T]

NewRepositoryService returns a Service[T] that delegates to repository.Repository[T].

func NewRepositoryServiceWithOptions added in v0.16.0

func NewRepositoryServiceWithOptions[T any](repo repository.Repository[T], opts RepositoryServiceOptions) Service[T]

NewRepositoryServiceWithOptions returns a Service[T] that delegates to repository.Repository[T].

func NewService added in v0.14.0

func NewService[T any](cfg ServiceConfig[T]) Service[T]

NewService composes the repository-backed service with optional layers in the default order (inner → outer): repo → virtual fields → validation → hooks → scope guard → field policy → activity/notifications. Alternate orderings should be implemented as custom wrappers by callers.

func NewServiceFromFuncs added in v0.6.0

func NewServiceFromFuncs[T any](repo repository.Repository[T], funcs ServiceFuncs[T]) Service[T]

NewServiceFromFuncs builds a service backed by the repository and applying overrides.

type ServiceConfig added in v0.14.0

type ServiceConfig[T any] struct {
	Repository           repository.Repository[T]
	Hooks                LifecycleHooks[T]
	ScopeGuard           ScopeGuardFunc[T]
	FieldPolicy          FieldPolicyProvider[T]
	VirtualFields        VirtualFieldProcessor[T]
	Validator            ValidatorFunc[T]
	ActivityHooks        activity.Hooks
	ActivityConfig       activity.Config
	NotificationEmitter  NotificationEmitter
	ResourceName         string
	ResourceType         reflect.Type
	BatchReturnOrderByID bool
}

ServiceConfig holds all optional business logic layers applied by NewService. ResourceName/ResourceType help field policies resolve friendly names when reflection on the model type is not sufficient or needs overriding.

type ServiceFuncs added in v0.6.0

type ServiceFuncs[T any] struct {
	Create      func(ctx Context, record T) (T, error)
	CreateBatch func(ctx Context, records []T) ([]T, error)

	Update      func(ctx Context, record T) (T, error)
	UpdateBatch func(ctx Context, records []T) ([]T, error)

	Delete      func(ctx Context, record T) error
	DeleteBatch func(ctx Context, records []T) error

	Index func(ctx Context, criteria []repository.SelectCriteria) ([]T, int, error)
	Show  func(ctx Context, id string, criteria []repository.SelectCriteria) (T, error)
}

ServiceFuncs allows callers to override specific service operations.

type ValidationError added in v0.0.2

type ValidationError struct {
	// contains filtered or unexported fields
}

type ValidatorFunc added in v0.14.0

type ValidatorFunc[T any] func(ctx Context, record T) error

ValidatorFunc validates a single record before persistence/update.

type VirtualFieldDef added in v0.14.0

type VirtualFieldDef struct {
	FieldName     string
	JSONName      string
	SourceField   string
	FieldType     reflect.Type
	FieldIndex    int
	AllowZero     bool
	MergeStrategy string // e.g. deep|shallow|replace (used by service merge semantics)
}

VirtualFieldDef describes a virtual field extracted from struct tags.

type VirtualFieldHandler added in v0.14.0

type VirtualFieldHandler[T any] struct {
	// contains filtered or unexported fields
}

VirtualFieldHandler moves virtual fields into/out of a map field (e.g. metadata).

func NewVirtualFieldHandler added in v0.14.0

func NewVirtualFieldHandler[T any]() *VirtualFieldHandler[T]

NewVirtualFieldHandler builds a handler using defaults.

func NewVirtualFieldHandlerWithConfig added in v0.14.0

func NewVirtualFieldHandlerWithConfig[T any](cfg VirtualFieldHandlerConfig) *VirtualFieldHandler[T]

NewVirtualFieldHandlerWithConfig builds a handler with custom config.

func (*VirtualFieldHandler[T]) AfterLoad added in v0.14.0

func (h *VirtualFieldHandler[T]) AfterLoad(ctx HookContext, model T) error

AfterLoad hydrates virtual fields from the backing map, preserving or removing keys based on config.

func (*VirtualFieldHandler[T]) AfterLoadBatch added in v0.14.0

func (h *VirtualFieldHandler[T]) AfterLoadBatch(ctx HookContext, models []T) error

AfterLoadBatch hydrates virtuals for a slice of models.

func (*VirtualFieldHandler[T]) BeforeSave added in v0.14.0

func (h *VirtualFieldHandler[T]) BeforeSave(ctx HookContext, model T) error

BeforeSave moves virtual values into the backing map and optionally clears the fields.

func (*VirtualFieldHandler[T]) FieldDefs added in v0.14.0

func (h *VirtualFieldHandler[T]) FieldDefs() []VirtualFieldDef

FieldDefs returns a copy of detected virtual field definitions.

type VirtualFieldHandlerConfig added in v0.14.0

type VirtualFieldHandlerConfig struct {
	PreserveVirtualKeys *bool  // keep virtual keys in the backing map after load (default: true)
	CopyMetadata        bool   // defensive copy of map before mutation
	ClearVirtualValues  *bool  // clear virtual field after BeforeSave (default: true)
	AllowZeroTag        string // tag option name to opt-in zero moves for value fields
	Dialect             string // virtual field SQL dialect (default: postgres)
}

VirtualFieldHandlerConfig controls extraction/injection behavior.

type VirtualFieldProcessor added in v0.14.0

type VirtualFieldProcessor[T any] interface {
	BeforeSave(HookContext, T) error
	AfterLoad(HookContext, T) error
	AfterLoadBatch(HookContext, []T) error
}

VirtualFieldProcessor is implemented by VirtualFieldHandler and test doubles.

Directories

Path Synopsis
examples
virtual-fields command
gql module
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL