Skip to main content

Generic CRUD

The crud package is one of the most important user-facing layers in VEF. It turns typed models and typed request structs into reusable API operations with built-in transactions, validation, data permissions, file promotion, and result formatting.

The Basic Pattern

You usually embed CRUD providers into a resource struct:

type UserResource struct {
api.Resource

crud.FindPage[User, UserSearch]
crud.Create[User, UserParams]
crud.Update[User, UserParams]
crud.Delete[User]
}

func NewUserResource() api.Resource {
return &UserResource{
Resource: api.NewRPCResource("sys/user"),
FindPage: crud.NewFindPage[User, UserSearch]().PermToken("sys:user:query"),
Create: crud.NewCreate[User, UserParams]().PermToken("sys:user:create"),
Update: crud.NewUpdate[User, UserParams]().PermToken("sys:user:update"),
Delete: crud.NewDelete[User]().PermToken("sys:user:delete"),
}
}

The framework collects embedded CRUD builders automatically because they implement api.OperationsProvider.

Generic Parameter Meanings

Most CRUD builders only use one of these generic shapes:

GenericMeaningTypical type
TModelpersistence model loaded from or written to the databaseUser, Role, Flow
TParamswrite-side params decoded from Request.ParamsUserParams, CreateUserParams
TSearchread-side search params decoded from Request.ParamsUserSearch, RoleSearch

Operation families use them like this:

Builder familyGeneric shapeMeaning
single-record write buildersCreate[TModel, TParams], Update[TModel, TParams]params are copied into a model before persistence
batch write buildersCreateMany[TModel, TParams], UpdateMany[TModel, TParams]framework wraps TParams into batch params types
read buildersFindOne[TModel, TSearch], FindPage[TModel, TSearch], and similarmodel defines the query target, search defines filters
delete buildersDelete[TModel], DeleteMany[TModel]deletion works from primary-key payloads, so no extra TParams type is needed
export builderExport[TModel, TSearch]export runs a read query and then renders the result into a file
import builderImport[TModel]imported rows are decoded directly into models

Prebuilt Builder Matrix

BuilderDefault RPC actionDefault REST actionInput contractOutput contractTypical use
NewCreate[TModel, TParams]createpost /TParams from paramsprimary-key mapcreate one record
NewUpdate[TModel, TParams]updateput /:idTParams from params, including PK fieldssuccess resultupdate one record
NewDelete[TModel]deletedelete /:idraw PK values from paramssuccess resultdelete one record
NewCreateMany[TModel, TParams]create_manypost /manyCreateManyParams[TParams] with listlist of primary-key mapsbatch create
NewUpdateMany[TModel, TParams]update_manyput /manyUpdateManyParams[TParams] with listsuccess resultbatch update
NewDeleteMany[TModel]delete_manydelete /manyDeleteManyParams with pkssuccess resultbatch delete
NewFindOne[TModel, TSearch]find_oneget /:idTSearch from paramsone modelsingle-record query
NewFindAll[TModel, TSearch]find_allget /TSearch from params[]TModelfiltered list without paging metadata
NewFindPage[TModel, TSearch]find_pageget /pageTSearch from params + page.Pageable from metapage.Page[T]admin list screen
NewFindOptions[TModel, TSearch]find_optionsget /optionsTSearch from params + DataOptionConfig from meta[]DataOptiondropdown options
NewFindTree[TModel, TSearch](treeBuilder)find_treeget /treeTSearch from paramshierarchical []TModeltree-structured data
NewFindTreeOptions[TModel, TSearch]find_tree_optionsget /tree/optionsTSearch from params + DataOptionConfig from meta[]TreeDataOptiontree options
NewExport[TModel, TSearch]exportget /exportTSearch from params + export format from metafile downloadExcel or CSV export
NewImport[TModel]importpost /importmultipart file upload + import format from meta{total: n}Excel or CSV import

Shared Builder Controls

Every CRUD builder inherits the common controls from Builder[T]:

MethodEffect
ResourceKind(kind)switches the builder between RPC and REST naming/validation rules
Action(action)overrides the default action name
Public()marks the operation as unauthenticated
PermToken(token)requires a permission token for access
Timeout(duration)sets the request timeout
EnableAudit()enables audit logging for the operation
RateLimit(max, period)applies per-operation rate limiting

Important detail:

  • Action(...) is validated according to the current ResourceKind(...)
  • if you are overriding a REST action, set ResourceKind(api.KindREST) first

Shared Find Controls

All read-oriented builders are built on top of Find[...], so they share a richer set of query-shaping options:

MethodPurpose
WithProcessor(...)post-processes the query result before response serialization
WithOptions(...)appends reusable low-level FindOperationOption values
WithSelect(column)adds a column to the select list
WithSelectAs(column, alias)adds a selected column with an alias
WithDefaultSort(...)sets fallback sorting when no dynamic sort is provided
WithCondition(...)adds a WHERE condition using orm.ConditionBuilder
DisableDataPerm()disables automatic data-permission filtering
WithRelation(...)adds relation joins through orm.RelationSpec
WithAuditUserNames(userModel, nameColumn...)joins audit user information to populate creator/updater names
WithQueryApplier(...)applies arbitrary query modifications with typed access to TSearch

Runtime defaults for most find-style builders:

  • search tags from TSearch are applied automatically
  • data permission filtering is enabled by default
  • sort defaults to primary key descending when the model has a single PK
  • if no single PK exists, the fallback sort is created_at DESC when available

Query Parts For Tree Builders

Tree builders use recursive CTEs, so some options can target different query stages:

Query partMeaning
QueryRootthe final outer query
QueryBasethe starting query inside the recursive CTE
QueryRecursivethe recursive branch of the CTE
QueryAllall query parts

For FindTree and FindTreeOptions, several methods intentionally change their defaults:

  • WithCondition(...) defaults to QueryBase
  • WithQueryApplier(...) defaults to QueryBase
  • WithSelect(...), WithSelectAs(...), and WithRelation(...) default to both QueryBase and QueryRecursive

Read Builders

FindOne[TModel, TSearch]

Use FindOne when the resource should return one record.

AspectDetails
GenericsTModel is the query target model, TSearch defines filters
InputTSearch from params, raw api.Meta from meta
Outputone TModel value after optional WithProcessor(...) transformation
Default behaviorruns a select with model columns and LIMIT 1
Common configurationshared find controls such as WithCondition, WithRelation, WithQueryApplier, WithAuditUserNames

Use this when the read still behaves like a query instead of a fixed metadata fetch.

FindAll[TModel, TSearch]

Use FindAll when you need a filtered list without paging metadata.

AspectDetails
GenericsTModel is the result model, TSearch defines filters
InputTSearch from params, api.Meta from meta
Output[]TModel or the processed slice returned by WithProcessor(...)
Default behaviorapplies a safety limit (maxQueryLimit) and returns an empty slice instead of nil
Common configurationshared find controls, especially WithDefaultSort, WithCondition, WithRelation, WithQueryApplier

FindPage[TModel, TSearch]

Use FindPage for most admin-style list screens.

AspectDetails
GenericsTModel is the item model, TSearch defines query filters
InputTSearch from params, page.Pageable from meta, plus any extra api.Meta
Outputpage.Page[T]
Default behaviorpaginates, counts total rows, and normalizes page settings
Special configurationWithDefaultPageSize(size) sets the fallback page size

Use this when the caller needs total, page number, page size, and item list together.

FindOptions[TModel, TSearch]

Use FindOptions for lightweight option lists such as select boxes.

AspectDetails
GenericsTModel is the source model, TSearch defines filters
InputTSearch from params, DataOptionConfig from meta
Output[]DataOption
Default behaviormaps data into label, value, description, and optional meta
Special configurationWithDefaultColumnMapping(mapping) sets fallback label/value/description/meta column mapping

DataOptionConfig comes from meta and can override:

FieldMeaning
labelColumnsource column for label
valueColumnsource column for value
descriptionColumnoptional source column for description
metaColumnsadditional columns to include in the option meta object

Defaults:

  • label column defaults to name
  • value column defaults to id

FindTree[TModel, TSearch]

Use FindTree when the domain is hierarchical and the response should contain nested model records.

Constructor shape:

crud.NewFindTree[Category, CategorySearch](tree.Build)
AspectDetails
GenericsTModel is the tree node model, TSearch defines filters
InputTSearch from params, api.Meta from meta
Outputhierarchical []TModel
Default behaviorbuilds a recursive CTE, loads flat rows, then runs the provided treeBuilder function
Special configurationWithIDColumn(name) and WithParentIDColumn(name) customize the tree columns

Defaults:

  • node ID column defaults to id
  • parent ID column defaults to parent_id

FindTreeOptions[TModel, TSearch]

Use FindTreeOptions when you need a hierarchical option tree instead of full model records.

AspectDetails
GenericsTModel is the source model, TSearch defines filters
InputTSearch from params, DataOptionConfig from meta
Output[]TreeDataOption
Default behaviorbuilds a recursive CTE and converts the result into nested TreeDataOption values
Special configurationWithDefaultColumnMapping(...), WithIDColumn(...), WithParentIDColumn(...)

Use this when the client needs label/value plus children, not the full persistence model.

Write Builders

Create[TModel, TParams]

Use Create for single-record creation.

AspectDetails
GenericsTModel is the persistence model, TParams is the write params type
InputTParams from params
Outputprimary-key map for the created record
Default behaviorcopies params into a new model, promotes storage references, runs inside a transaction, inserts the record
Special configurationWithPreCreate(...), WithPostCreate(...)

Hook responsibilities:

MethodRuns whenTypical use
WithPreCreatebefore insert, inside the same transactionnormalization, validation, derived fields, extra query shaping
WithPostCreateafter insert, inside the same transactionside effects that belong to the same transaction

Update[TModel, TParams]

Use Update for single-record update.

AspectDetails
GenericsTModel is the persistence model, TParams is the write params type
InputTParams from params, including primary-key fields
Outputsuccess result
Default behaviorcopies params into a temporary model, validates PK presence, loads the old model, applies data permissions, merges non-empty fields, updates in a transaction
Special configurationWithPreUpdate(...), WithPostUpdate(...), DisableDataPerm()

Important detail:

  • Update uses copier.WithIgnoreEmpty() when merging the incoming model into the loaded model

Delete[TModel]

Use Delete for single-record deletion.

AspectDetails
GenericsTModel is the persistence model
Inputprimary-key values from raw api.Params
Outputsuccess result
Default behaviorvalidates PK input, loads the model, applies data permissions, deletes in a transaction, then cleans up promoted files
Special configurationWithPreDelete(...), WithPostDelete(...), DisableDataPerm()

Batch Builders

CreateMany[TModel, TParams]

AspectDetails
Input contractCreateManyParams[TParams] with a list field
Outputlist of primary-key maps
Special configurationWithPreCreateMany(...), WithPostCreateMany(...)
Behaviorcopies each params item into a model, inserts all models in one transaction

UpdateMany[TModel, TParams]

AspectDetails
Input contractUpdateManyParams[TParams] with a list field
Outputsuccess result
Special configurationWithPreUpdateMany(...), WithPostUpdateMany(...), DisableDataPerm()
Behaviorvalidates PKs for every item, loads all old models, merges updates, and executes a bulk update in one transaction

DeleteMany[TModel]

AspectDetails
Input contractDeleteManyParams with a pks field
Outputsuccess result
Special configurationWithPreDeleteMany(...), WithPostDeleteMany(...), DisableDataPerm()
Behaviorsupports single-PK payloads as scalar values and composite-PK payloads as maps

DeleteManyParams.pks rules:

Model PK shapeAccepted payload shape
single primary key["id1", "id2"]
composite primary key[{"user_id":"u1","role_id":"r1"}]

Export And Import Builders

Export[TModel, TSearch]

Use Export when the caller should download a query result as an Excel or CSV file.

AspectDetails
GenericsTModel is the exported row model, TSearch defines query filters
InputTSearch from params, format from meta
Outputfile download
Default behaviorruns a find-style query, applies optional pre-export processing, and writes Excel or CSV to the response
Special configurationWithDefaultFormat(...), WithExcelOptions(...), WithCsvOptions(...), WithPreExport(...), WithFilenameBuilder(...)

format values:

FormatValue
Excelexcel
CSVcsv

Defaults:

  • export format defaults to excel
  • default filenames are data.xlsx and data.csv

Import[TModel]

Use Import when the caller uploads a CSV or Excel file that should be decoded into models and inserted.

AspectDetails
GenericsTModel is the model type imported from the file
Inputmultipart file upload in params.file, plus optional format in meta
Output{total: n} on success
Default behaviorrequires multipart input, parses rows into models, validates imported rows, inserts them in a transaction
Special configurationWithDefaultFormat(...), WithExcelOptions(...), WithCsvOptions(...), WithPreImport(...), WithPostImport(...)

Important details:

  • JSON requests are rejected for import
  • if row-level import validation fails, the response contains an errors payload instead of partial persistence
  • import format defaults to excel

Practical Advice

  • start with FindPage + Create + Update + Delete for admin resources
  • keep write params and search params separate
  • add permissions at the builder level
  • rely on default data permissions unless you have a specific reason to disable them
  • use FindOptions or FindTreeOptions for UI option payloads instead of overloading full model endpoints
  • prefer the standard CRUD vocabulary unless your business action has a stronger domain verb
  • it is normal for one resource to combine CRUD builders with a few custom actions when the UI needs both

Next Step

Read Custom Handlers when a resource needs operations that do not fit the generic CRUD model.