composable

package
v0.0.0-...-2dafbf0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: LGPL-2.1 Imports: 36 Imported by: 0

Documentation

Overview

Package composable is the successor of package engine. It offers the same dynamic-resource capabilities (schema-driven CRUD, org scoping, permission, computed fields, the built-in REST route table and its param binders) but split into the onion layers a feature module already has: a repository, a domain service, an application service and a REST handler, each with a default implementation a module embeds and extends.

The split is what lets a resource expose an action as REST plus service API, or as service API only: a custom action is a method on the module's application service, and a REST route for it is an optional AddRoute call. Package engine forced every action through one DynamicActionDefinition that carried both concerns.

Nothing here imports package engine or package interfaces: the code was duplicated on purpose so that engine can be decommissioned once every module has migrated.

Index

Constants

View Source
const (
	ErrRowValidation          = "err_import_validation"
	ErrRowDuplicateExternalId = "err_import_duplicate_external_id"
	ErrRowExternalIdOtherOrg  = "err_import_external_id_in_other_org"
)

Row-level error codes shared by bulk create and import. They are translation keys.

View Source
const (
	// ParamComputeField names the field to compute; the REST engine fills it from the path.
	ParamComputeField = "field"
	// ParamComputeModel is the request body's unsaved model.
	ParamComputeModel = "model"
	// ParamComputeArgs is the request body's caller-supplied extras.
	ParamComputeArgs = "args"
)
View Source
const (
	ImportFormFile    = "file"
	ImportFormMapping = "mapping"
)

Multipart form parts of POST /{resource}/import.

View Source
const (
	ErrImportFileRequired       = "err_import_file_required"
	ErrImportFileUnreadable     = "err_import_file_unreadable"
	ErrImportTooManyRows        = "err_import_too_many_rows"
	ErrImportMappingInvalid     = "err_import_mapping_invalid"
	ErrImportMappingUnknown     = "err_import_mapping_unknown_field"
	ErrImportMappingDuplicate   = "err_import_mapping_duplicate_target"
	ErrImportHeaderNotFound     = "err_import_header_not_found"
	ErrImportMandatoryUnmapped  = "err_import_mandatory_unmapped"
	ErrImportStorageUnavailable = "err_import_storage_unavailable"
)

Request-level error keys: the caller can fix these by changing the request.

View Source
const (
	ErrRowCellInvalid           = "err_import_cell_invalid"
	ErrRowReferenceNotFound     = "err_import_reference_not_found"
	ErrRowReferenceAmbiguous    = "err_import_reference_ambiguous"
	ErrRowReferenceCreateFailed = "err_import_reference_create_failed"
)

Row-level error keys specific to import (the shared ones live in bulk_create.go).

View Source
const (
	CrudActionCreate       = CrudAction("create")
	CrudActionUpdate       = CrudAction("update")
	CrudActionDelete       = CrudAction("delete")
	CrudActionSetArchived  = CrudAction("set_archived")
	CrudActionGetById      = CrudAction("get_by_id")
	CrudActionGetByUnique  = CrudAction("get_by_unique")
	CrudActionSearch       = CrudAction("search")
	CrudActionExists       = CrudAction("exists")
	CrudActionGetSchema    = CrudAction("get_schema")
	CrudActionComputeField = CrudAction("compute_field")
	CrudActionBulkCreate   = CrudAction("bulk_create")
	CrudActionImport       = CrudAction("import")
)
View Source
const (
	ActionTypeCreate        = ActionType("Create")
	ActionTypeDelete        = ActionType("Delete")
	ActionTypeRead          = ActionType("Read")
	ActionTypeUpdatePatch   = ActionType("UpdatePatch")
	ActionTypeUpdateReplace = ActionType("UpdateReplace")

	// ActionTypeGeneric is for actions whose semantics are none of the CRUD verbs, such as
	// "exists" or "suspend". It maps to POST so the action may carry a request body.
	ActionTypeGeneric = ActionType("Generic")

	// ActionTypeUpload is a multipart POST. The REST engine does not bind its body: the handler
	// reads the form itself and receives a nil payload.
	ActionTypeUpload = ActionType("Upload")

	// ActionTypeUploadPatch is ActionTypeUpload against an existing record: a multipart PATCH.
	//
	// It exists because a resource whose create accepts a file must be able to accept one on
	// update too, at the same ":id" path its JSON update uses. Serving that through
	// ActionTypeUpdatePatch instead would make the engine attempt a JSON bind of a multipart body,
	// which bindPayload reports as a 400 before the handler ever runs.
	ActionTypeUploadPatch = ActionType("UploadPatch")
)
View Source
const (
	PermissionCreate      = "create"
	PermissionRead        = "read"
	PermissionUpdate      = "update"
	PermissionDelete      = "delete"
	PermissionSetArchived = "set_archived"
)

Permission action codes, matching what the IAM application services assert.

View Source
const (
	FieldSourceSystem = "source_system"
	FieldExternalId   = "external_id"

	SourceSystemManual = "manual"
	SourceSystemImport = "import"
)

Deduplication fields. A schema declaring both gets insert-or-update semantics in bulk create.

View Source
const DefaultImportLanguage = "en-US"

DefaultImportLanguage fills a mapping that names no language, so a multilingual cell still lands under a real language key.

View Source
const EngineNamePrefix = "dynengine_"

EngineNamePrefix distinguishes composable engines from package engine's "engine_" names in the dependency container, so both generations coexist while modules migrate.

View Source
const RecordIdParam = basemodel.FieldId

RecordIdParam is the path parameter every single-row built-in route carries.

Variables

View Source
var RestPathRegex = regexp.MustCompile(`^:?[a-zA-Z0-9_]+(/:?[a-zA-Z0-9_]+)*$`)

RestPathRegex accepts slash-separated segments of [a-zA-Z0-9_], where a segment may be an Echo path param (":name"). Hyphens are deliberately excluded: the word separator is "_".

Functions

func AssertComputedFunctionsDefined

func AssertComputedFunctionsDefined() error

AssertComputedFunctionsDefined checks every built onion's "function"-kind computed fields against the functions registered on it. Every onion is reported rather than only the first, so one boot surfaces the whole gap.

func BuiltSchemaNames

func BuiltSchemaNames() []string

BuiltSchemaNames lists every schema whose onion has been built so far, sorted. Onions are dig constructors, so one nobody resolved is absent; by OnAppStarted every routed onion is built.

func EngineDependencyName

func EngineDependencyName(schemaName string) string

EngineDependencyName is the container name of the onion serving the given schema.

func Identity

func Identity[TData any](data TData) any

Identity passes the result data through as the response body.

func MutateResponse

func MutateResponse(mutation dyn.MutateResultData) any

MutateResponse shapes a mutation result the way the built-in update and delete answer.

func ParamBool

func ParamBool(params dmodel.DynamicFields, field string) (bool, bool)

ParamBool reads a bool or *bool param. The second value reports whether one was present.

func ParamId

func ParamId(params dmodel.DynamicFields) model.Id

ParamId reads the record id out of a command or query. Empty when absent.

func ParamOrgId

func ParamOrgId(params dmodel.DynamicFields) *model.Id

ParamOrgId reads the org the caller named, or nil when the params carry none.

func ParamString

func ParamString(params dmodel.DynamicFields, field string) string

ParamString reads a string-typed param. model.Id and model.Etag are string types, so the string case covers them too. Anything else answers empty.

func SchemaSupportsDedup

func SchemaSupportsDedup(schema *dmodel.ModelSchema) bool

SchemaSupportsDedup reports whether a schema carries the two deduplication fields, which is what switches bulk create from "insert every row" to "insert or update by external key".

func SearchRepositoryRows

func SearchRepositoryRows(
	ctx corectx.Context, repo CrudRepository, schemaName string, keyColumn string, keys []any, fields []string,
) ([]dmodel.DynamicFields, error)

SearchRepositoryRows runs the IN-keyed projection read a computed field needs against one repository. Exported so the legacy registry can serve a composable source the same way.

func ServeAction

func ServeAction[TData any](
	echoCtx *echo.Context,
	action string,
	payload map[string]any,
	call func(ctx corectx.Context, params dmodel.DynamicFields) (*dyn.OpResult[TData], error),
	shape func(data TData) any,
) (err error)

ServeAction is the body of a custom route's handler: it hands the bound payload to an application-service method and answers the way the built-in handlers do. A client error answers 400, a result without data answers 400 with a not-found payload, and a Go error bubbles up as 500.

func (this *WarehouseRest) Suspend(echoCtx *echo.Context, payload map[string]any) error {
	return composable.ServeAction(echoCtx, "suspend warehouse", payload, this.warehouseSvc.Suspend, composable.MutateResponse)
}

func SetLegacySourceResolver

func SetLegacySourceResolver(fn SourceSearchFn)

SetLegacySourceResolver installs the fallback used when a computed field names a schema no composable onion serves, so a migrated resource may still compute over one that package engine serves. Package dynamicresource installs it at Init.

func WithTransaction

func WithTransaction(
	ctx corectx.Context, repo CrudRepository, body func(tranxCtx corectx.Context) error,
) error

WithTransaction runs body inside one database transaction opened on repo, committing when body returns nil and rolling back otherwise. The body receives a cloned request context that carries the transaction, so every repository call made through it joins the same unit.

Types

type ActionType

type ActionType string

ActionType classifies a route for the REST engine, which maps it to an HTTP method and decides how the request payload is bound.

func (ActionType) HasRequestBody

func (this ActionType) HasRequestBody() bool

HasRequestBody reports whether the REST engine reads a JSON body for this type. Upload is a multipart form, which the handler consumes itself.

func (ActionType) HttpMethod

func (this ActionType) HttpMethod() string

HttpMethod returns an empty string for an invalid type; callers validate before registering.

func (ActionType) IsUpload

func (this ActionType) IsUpload() bool

IsUpload reports whether the engine leaves this action's body to the handler. Both multipart types do; every other type is bound before the handler is called.

func (ActionType) IsValid

func (this ActionType) IsValid() bool

func (ActionType) String

func (this ActionType) String() string

type AfterValidationFn

type AfterValidationFn = corecrud.AfterValidationSuccessFn[*DynamicEntity]

AfterValidationFn runs after successful schema validation.

type BeforeValidationFn

type BeforeValidationFn = corecrud.BeforeValidationFn[*DynamicEntity]

BeforeValidationFn may sanitize or enrich the model before schema validation.

type BuildParam

type BuildParam struct {
	dig.In

	Client        orm.DbClient
	ConfigSvc     config.ConfigService
	QueryBuilder  orm.QueryBuilder
	Logger        logging.LoggerService
	NewBaseRepoFn dyn.NewBaseDynamicRepositoryFn

	// Storage holds the transient file of an import. Optional so a binary without object storage
	// still builds every onion; import then answers a configuration error instead.
	Storage filestorage.FileStorageAdapter `optional:"true"`
}

BuildParam carries the core services every repository needs. A module's registration closure declares it as a constructor argument, so the container fills it:

deps.RegisterNamed(composable.EngineDependencyName(schema), func(param composable.BuildParam) composable.DynamicResourceEngineOnion {
	return composable.MustBuild(&composable.DynamicResourceEngineOnionImpl{SchemaName: schema, ...}, param)
})

type BulkCreateCommand

type BulkCreateCommand = dmodel.DynamicFields

BulkCreateCommand is the wire form of POST /{resource}/bulk: {org_id?, items: [{...}, ...]}.

type BulkCreateResult

type BulkCreateResult = dyn.OpResult[BulkCreateResultData]

func RunBulkCreate

func RunBulkCreate(
	ctx corectx.Context, domSvc CrudDomainService, rows []BulkRow, rejected []RowError,
) (*BulkCreateResult, error)

RunBulkCreate writes rows through domSvc inside one transaction and reports the outcome.

domSvc must be the outermost domain service of the resource (the module's own, when it has one) so that its Create/Update overrides and hooks run for every row exactly as they would for a single call. A row the schema or a hook rejects is reported in the result and skipped; a Go error - including a database refusal such as the unique-key last defence - aborts and rolls back every row.

rejected are rows the caller already refused (import conversion, unresolved references); they count toward TotalRows and are echoed in the errors list.

type BulkCreateResultData

type BulkCreateResultData struct {
	AffectedCount int                 `json:"affected_count"`
	CreatedCount  int                 `json:"created_count"`
	UpdatedCount  int                 `json:"updated_count"`
	AffectedAt    model.ModelDateTime `json:"affected_at"`
	TotalRows     int                 `json:"total_rows"`
	ErrorCount    int                 `json:"error_count"`
	Errors        []RowError          `json:"errors"`
}

BulkCreateResultData answers bulk_create and import alike: what was written, and which rows were skipped and why. Skipped rows are data, not client errors, because the caller cannot fix them by changing the request - only by changing the file.

type BulkRow

type BulkRow struct {
	Number int
	Fields dmodel.DynamicFields
}

BulkRow is one record to write, tagged with its 1-based position so a rejection can be pointed back at the caller's list or spreadsheet.

type ClientErrorSignal

type ClientErrorSignal struct {
	Errors ft.ClientErrors
}

ClientErrorSignal carries a rule violation out through a transaction body, so a rejected operation rolls back rather than committing half of itself. It is an error only in the plumbing sense: the caller unwraps it back into client errors, which answer 400 rather than 500.

func AsClientErrorSignal

func AsClientErrorSignal(err error) (ClientErrorSignal, bool)

func (ClientErrorSignal) Error

func (this ClientErrorSignal) Error() string

type ColumnMapping

type ColumnMapping struct {
	Source string `json:"source"`
	Target string `json:"target"`
}

ColumnMapping pairs a file header with a target field name. The referenced schema of an edge field is derived from the schema relations, never sent by the client.

type ComputeFieldCommand

type ComputeFieldCommand = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type ComputeFieldResult

type ComputeFieldResult = dyn.OpResult[ComputeFieldResultData]

type ComputeFieldResultData

type ComputeFieldResultData struct {
	DataType string `json:"data_type"`
	IsArray  bool   `json:"is_array"`
	Value    any    `json:"value"`
}

ComputeFieldResultData is the answer of a meta/compute call: the evaluated value with the declared type of the field, so a client can render it without knowing the function.

type ComputeFnRequest

type ComputeFnRequest struct {
	// SchemaName and FieldName identify what is being computed, so one function can serve several
	// fields or several schemas.
	SchemaName string
	FieldName  string

	// Models are the rows to compute over: a page of rows on a read, exactly one on a
	// meta/compute call, where it is the unsaved model the client posted.
	Models []dmodel.DynamicFields

	// Args carries the caller-supplied extras of a meta/compute call. Nil on a read.
	Args map[string]any
}

ComputeFnRequest is what a computed-field function is given.

type ComputedFieldFn

type ComputedFieldFn func(ctx corectx.Context, req ComputeFnRequest) ([]any, error)

ComputedFieldFn produces the value of a "function"-kind computed field.

It receives the whole page at once rather than one row at a time: a search returns up to a page of rows, and a per-row signature would turn any lookup the function performs into an N+1. It must return exactly one value per row in Models, in the same order; a length mismatch is an error, never a partial fill.

The function may resolve services from the dependency container. Resolve them once, when the module registers the function, and close over them.

type CreateCommand

type CreateCommand = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type CreateOptions

type CreateOptions struct {
	BeforeValidation       BeforeValidationFn
	AfterValidationSuccess AfterValidationFn
	ValidateExtra          ValidateExtraFn
}

CreateOptions are what a derived domain service passes when it calls the embedded default's Create. They replace package engine's ModifyAction: instead of a definition the service reads at call time, the override hands the hooks over on each call.

type CreateResult

type CreateResult = dyn.OpResult[dmodel.DynamicFields]

type CrudAction

type CrudAction string

CrudAction names one built-in action. The values double as action labels in error messages and as the allow-list vocabulary of NewDomainServiceParam.CrudActions.

func AllCrudActions

func AllCrudActions() []CrudAction

AllCrudActions lists every built-in action.

func ReadCrudActions

func ReadCrudActions() []CrudAction

func (CrudAction) String

func (this CrudAction) String() string

type CrudApplicationService

type CrudApplicationService interface {
	Create(ctx corectx.Context, cmd CreateCommand) (*CreateResult, error)
	Update(ctx corectx.Context, cmd UpdateCommand) (*MutateResult, error)
	Delete(ctx corectx.Context, cmd DeleteCommand) (*MutateResult, error)
	SetArchived(ctx corectx.Context, cmd SetArchivedCommand) (*MutateResult, error)
	GetById(ctx corectx.Context, query GetByIdQuery) (*GetOneResult, error)
	GetOne(ctx corectx.Context, query GetOneQuery) (*GetOneResult, error)
	Search(ctx corectx.Context, query SearchQuery) (*SearchResult, error)
	Exists(ctx corectx.Context, query ExistsQuery) (*ExistsResult, error)
	GetSchema(ctx corectx.Context, query GetSchemaQuery) (*GetSchemaResult, error)
	ComputeField(ctx corectx.Context, cmd ComputeFieldCommand) (*ComputeFieldResult, error)

	// CreateBulk writes {items: [...]} under one permission check and one transaction; a row
	// the schema rejects is reported in the result rather than failing the call.
	CreateBulk(ctx corectx.Context, cmd BulkCreateCommand) (*BulkCreateResult, error)

	// Import parses an uploaded xlsx/csv through a column mapping and writes it like CreateBulk.
	Import(ctx corectx.Context, cmd ImportCommand) (*BulkCreateResult, error)

	DomainService() CrudDomainService
	Schema() *dmodel.ModelSchema

	// ResourceCode is the permission resource code, which is the schema name.
	ResourceCode() string

	// The authorization machinery, reusable by a derived service's custom actions.
	AssertAction(ctx corectx.Context, actionCode string, params dmodel.DynamicFields) (*model.Id, *ft.ClientErrors)
	AssertPermission(ctx corectx.Context, actionCode string, orgId *model.Id) *ft.ClientErrors
	ResolveOrgScope(ctx corectx.Context, params dmodel.DynamicFields) (*model.Id, *ft.ClientErrors)
	ConstrainSearchToOrg(params dmodel.DynamicFields, orgId model.Id) error
	AssertRecordInOrg(ctx corectx.Context, params dmodel.DynamicFields, orgId model.Id) (*ft.ClientErrors, error)
	FetchByKeys(ctx corectx.Context, keys dmodel.DynamicFields, vErrs *ft.ClientErrors) (dmodel.DynamicFields, error)
}

CrudApplicationService is the layer that authorizes. Every built-in method resolves the org scope, asserts the permission and only then delegates to the domain service, which is what a hand-written {entity}_appservice.go does.

A module extends it by embedding the default implementation into its own {Resource}ApplicationServiceImpl. A custom action is a method on that struct which calls AssertAction (and AssertRecordInOrg for a single-row action) before delegating. Whether the action also gets a REST route is decided in the transport layer with RestEngine.AddRoute; a method without a route is the resource's service-only API.

func NewDefaultApplicationService

func NewDefaultApplicationService(param NewAppServiceParam) CrudApplicationService

type CrudDomainService

type CrudDomainService interface {
	Create(ctx corectx.Context, cmd CreateCommand, options ...CreateOptions) (*CreateResult, error)
	Update(ctx corectx.Context, cmd UpdateCommand, options ...UpdateOptions) (*MutateResult, error)
	Delete(ctx corectx.Context, cmd DeleteCommand, options ...DeleteOptions) (*MutateResult, error)
	SetArchived(ctx corectx.Context, cmd SetArchivedCommand) (*MutateResult, error)

	// GetById fetches one record by primary key. The query carries "id" and optional "fields".
	GetById(ctx corectx.Context, query GetByIdQuery) (*GetOneResult, error)

	// GetOne fetches one record by any unique key carried in the query. It has no REST route.
	GetOne(ctx corectx.Context, query GetOneQuery) (*GetOneResult, error)

	Search(ctx corectx.Context, query SearchQuery, options ...corecrud.ServiceSearchOptions) (*SearchResult, error)
	Exists(ctx corectx.Context, query ExistsQuery) (*ExistsResult, error)

	// ComputeField evaluates one "function"-kind computed field against the unsaved model the
	// command carries under "model", with optional "args".
	ComputeField(ctx corectx.Context, cmd ComputeFieldCommand) (*ComputeFieldResult, error)

	Schema() *dmodel.ModelSchema
	Repository() CrudRepository
}

CrudDomainService holds the business processing of a resource. It performs validation and orchestration, and invokes the repository when it needs to touch the database. It performs no permission check: that belongs to the application service, which sits above it.

A module extends it by embedding the default implementation into its own {Resource}DomainServiceImpl. An override of Create/Update/Delete adds its rules by passing options to the embedded default, so the rules run inside the crud helper and fire on a direct service call as well as on a REST call.

func NewDefaultDomainService

func NewDefaultDomainService(param NewDomainServiceParam) CrudDomainService

func WithComputedFields

func WithComputedFields(
	base CrudDomainService, sourceSearch SourceSearchFn, invokeFunction FunctionInvokeFn,
	defaultSearchFields []string,
) CrudDomainService

WithComputedFields decorates a domain service so declared computed fields evaluate on every read and reject every write. Wrapping is unconditional and costs nothing for a schema without computed fields: the eval planner returns nil and every call passes straight through.

The onion wraps the default before handing it to NewDomainServiceFn, so a module's derived service embeds the wrapped one and its overrides keep layering on top.

defaultSearchFields must be the same list the wrapped service falls back to when a search names no fields, otherwise a defaulted listing evaluates the wrong set of computed fields and reads operands the projection never selected.

type CrudRepository

type CrudRepository interface {
	// Embedding this interface lets the repository be passed directly to the generic helpers
	// of modules/core/dynamicmodel/crud, which expect a dyn.DynamicModelRepository.
	dyn.DynamicModelRepository

	BeginTransaction(ctx corectx.Context) (database.DbTransaction, error)

	Insert(ctx corectx.Context, data dmodel.DynamicFields) (*dyn.OpResult[int], error)
	Update(ctx corectx.Context, data dmodel.DynamicFields) (*MutateResult, error)
	DeleteOne(ctx corectx.Context, keys dmodel.DynamicFields) (*MutateResult, error)

	// FindByKeys fetches the single record identified by the given primary or unique keys.
	FindByKeys(ctx corectx.Context, keys dmodel.DynamicFields) (*dyn.OpResult[dmodel.DynamicFields], error)

	GetOne(ctx corectx.Context, param dyn.RepoGetOneParam) (*dyn.OpResult[dmodel.DynamicFields], error)
	Search(ctx corectx.Context, param dyn.RepoSearchParam) (*SearchResult, error)
	Exists(ctx corectx.Context, keys []dmodel.DynamicFields) (*dyn.OpResult[dyn.RepoExistsResult], error)

	Schema() *dmodel.ModelSchema
}

CrudRepository reads and writes the resource records through the SQL query builder.

A module extends it by embedding the default implementation into its own {Resource}RepositoryImpl and handing that struct to DynamicResourceEngineOnionImpl.NewRepositoryFn.

func LookupSourceRepository

func LookupSourceRepository(schemaName string) (CrudRepository, bool)

LookupSourceRepository returns the repository of a schema served by a composable onion. The legacy registry uses it as a fallback so a legacy resource may compute over a migrated one.

func NewDefaultCrudRepository

func NewDefaultCrudRepository(param NewRepositoryParam) CrudRepository

type CrudRestBase

type CrudRestBase struct {
	httpserver.RestBase
	// contains filtered or unexported fields
}

CrudRestBase serves the built-in actions of one resource over HTTP. Every handler binds the request, calls the application service (which authorizes) and shapes the response the way the hand-written REST handlers in modules/core/httpserver do.

type UomRest struct{ composable.CrudRestBase }

func NewUomRest(param struct {
	dig.In
	Engine composable.DynamicResourceEngineOnion `name:"dynengine_essential_uom"`
}) *UomRest {
	rest := &UomRest{}
	rest.SetApplicationService(param.Engine.ApplicationService())
	return rest
}

func (*CrudRestBase) ApplicationService

func (this *CrudRestBase) ApplicationService() CrudApplicationService

func (*CrudRestBase) ComputeField

func (this *CrudRestBase) ComputeField(echoCtx *echo.Context) error

ComputeField serves POST {resource}/meta/compute/:field with a body of {model, args}.

func (*CrudRestBase) Create

func (this *CrudRestBase) Create(echoCtx *echo.Context) error

func (*CrudRestBase) CreateBulk

func (this *CrudRestBase) CreateBulk(echoCtx *echo.Context) error

CreateBulk serves POST {resource}/bulk with a body of {org_id?, items: [...]}. The body is bound raw: "items" is not a schema field, and each item is filtered by the create path.

func (*CrudRestBase) Delete

func (this *CrudRestBase) Delete(echoCtx *echo.Context) error

func (*CrudRestBase) Exists

func (this *CrudRestBase) Exists(echoCtx *echo.Context) error

func (*CrudRestBase) GetById

func (this *CrudRestBase) GetById(echoCtx *echo.Context) error

func (*CrudRestBase) GetSchema

func (this *CrudRestBase) GetSchema(echoCtx *echo.Context) error

func (*CrudRestBase) Import

func (this *CrudRestBase) Import(echoCtx *echo.Context) (err error)

Import serves POST {resource}/import: a multipart form with exactly two parts, "file" and "mapping" (a JSON document). org_id travels in the query string, as on every read route.

func (*CrudRestBase) Search

func (this *CrudRestBase) Search(echoCtx *echo.Context) error

Search answers 200 with an empty item list for a filter matching nothing, or a page past the end: HasData=false is a successful result there, not a missing record.

func (*CrudRestBase) SetApplicationService

func (this *CrudRestBase) SetApplicationService(svc CrudApplicationService)

func (*CrudRestBase) SetArchived

func (this *CrudRestBase) SetArchived(echoCtx *echo.Context) error

func (*CrudRestBase) Update

func (this *CrudRestBase) Update(echoCtx *echo.Context) error

type CrudRestHandlers

type CrudRestHandlers interface {
	Create(echoCtx *echo.Context) error
	Update(echoCtx *echo.Context) error
	Delete(echoCtx *echo.Context) error
	SetArchived(echoCtx *echo.Context) error
	GetById(echoCtx *echo.Context) error
	Search(echoCtx *echo.Context) error
	Exists(echoCtx *echo.Context) error
	GetSchema(echoCtx *echo.Context) error
	ComputeField(echoCtx *echo.Context) error
	CreateBulk(echoCtx *echo.Context) error
	Import(echoCtx *echo.Context) error

	ApplicationService() CrudApplicationService
}

CrudRestHandlers is what RestEngine.AddCrudRoutes binds. CrudRestBase implements it; a module's {Resource}Rest embeds CrudRestBase and adds its own handlers for AddRoute.

type DefaultApplicationServiceImpl

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

func (*DefaultApplicationServiceImpl) AssertAction

func (this *DefaultApplicationServiceImpl) AssertAction(
	ctx corectx.Context, actionCode string, params dmodel.DynamicFields,
) (*model.Id, *ft.ClientErrors)

AssertAction is the one call a built-in or custom action makes before touching data: it resolves the org the request is confined to, then asserts the caller holds actionCode on this resource inside that org. A nil org means the action is not org-scoped.

func (*DefaultApplicationServiceImpl) AssertPermission

func (this *DefaultApplicationServiceImpl) AssertPermission(
	ctx corectx.Context, actionCode string, orgId *model.Id,
) *ft.ClientErrors

AssertPermission checks the caller's entitlement for actionCode on this resource.

orgId names the org the record belongs to, and is nil for an action that is not org-scoped. Passing it through InOrg is what lets an org-scoped grant match: a Perm with no OrgId can only ever match an exact or domain grant. An empty actionCode skips the check.

func (*DefaultApplicationServiceImpl) AssertRecordInOrg

func (this *DefaultApplicationServiceImpl) AssertRecordInOrg(
	ctx corectx.Context, params dmodel.DynamicFields, orgId model.Id,
) (*ft.ClientErrors, error)

AssertRecordInOrg refuses a single-row action whose target row belongs to another org.

The crud commands behind get_by_id, update, delete and set_archived take an id and nothing else, so without this check a caller holding a grant in their own org could read or mutate any row whose id they knew, simply by naming their own org. A row in another org answers "not found" rather than "forbidden": the caller is not entitled to learn that the id exists. Params without an id are left to the action's own validation.

func (*DefaultApplicationServiceImpl) ComputeField

ComputeField evaluates a function-kind computed field over an unsaved model. The model travels in the command and there is no stored row to confine to an org, so only the permission is checked: computing a derived value discloses no more than reading the record would.

func (*DefaultApplicationServiceImpl) ConstrainSearchToOrg

func (this *DefaultApplicationServiceImpl) ConstrainSearchToOrg(
	params dmodel.DynamicFields, orgId model.Id,
) error

ConstrainSearchToOrg ANDs an org_id equality onto whatever graph the caller sent, so the org the caller named decides which rows a search can reach, not just which rows it asks for.

The caller's graph arrives as a decoded map, so it is re-marshalled through SearchNode rather than manipulated in place: SearchNode owns the graph's JSON shape.

func (*DefaultApplicationServiceImpl) Create

Create defers a missing org_id to schema validation rather than reporting it here.

org_id is a declared required-for-create field on an org-scoped resource, so a create that omits it is a missing required field like any other, and must answer the same err_missing_required_field the rest of the payload would. Reporting err_org_id_required first would make one required field explain itself differently from its neighbours. An org_id that IS present still resolves here, so a caller naming an org they do not belong to is refused before the record is written.

func (*DefaultApplicationServiceImpl) CreateBulk

CreateBulk writes many records under one permission check and one transaction. It lives on the application service rather than the domain service on purpose: the loop must call the OUTERMOST domain service so a module's Create/Update overrides run per row, and only the application layer holds that reference.

func (*DefaultApplicationServiceImpl) Delete

func (*DefaultApplicationServiceImpl) DomainService

func (this *DefaultApplicationServiceImpl) DomainService() CrudDomainService

func (*DefaultApplicationServiceImpl) Exists

func (*DefaultApplicationServiceImpl) FetchByKeys

FetchByKeys loads the record a custom action wants to act on. A missing record is a client error reported through vErrs with a nil map, not a Go error.

A key of the wrong shape can never match a row, and reporting it as "not found" hides the caller's actual mistake, so each key is first checked against its declared data type.

func (*DefaultApplicationServiceImpl) GetById

func (*DefaultApplicationServiceImpl) GetOne

GetOne is org-confined through the normalized org_id itself: the domain service turns every column-named param, org_id included, into an equality condition.

func (*DefaultApplicationServiceImpl) GetSchema

GetSchema serves the resource schema in the simplified shape the clients cache. Schema metadata belongs to no org, so only the permission is checked.

func (*DefaultApplicationServiceImpl) Import

Import is POST {resource}/import: after the same permission check as create, the file is bounded by the configured limits, parked in object storage under a random name, parsed from there and written through bulk create. The object is removed whatever the outcome.

The order is deliberate - nothing touches storage before the permission check - for the reason coremart's vending-machine upload states: a stored file is a side effect a refused caller must not be able to cause.

func (*DefaultApplicationServiceImpl) ResolveOrgScope

func (this *DefaultApplicationServiceImpl) ResolveOrgScope(
	ctx corectx.Context, params dmodel.DynamicFields,
) (*model.Id, *ft.ClientErrors)

ResolveOrgScope enforces the resource's org scoping and returns the org the request is confined to. A nil org means the resource is not org-scoped, and nothing downstream filters by org. The authoritative org_id is normalized back into params so that every downstream consumer - the search graph, the single-row key set, the create payload - reads one value.

func (*DefaultApplicationServiceImpl) ResourceCode

func (this *DefaultApplicationServiceImpl) ResourceCode() string

func (*DefaultApplicationServiceImpl) Schema

func (*DefaultApplicationServiceImpl) Search

func (*DefaultApplicationServiceImpl) SetArchived

func (*DefaultApplicationServiceImpl) Update

type DefaultCrudRepositoryImpl

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

DefaultCrudRepositoryImpl is the schema-agnostic counterpart of the hand-written repositories such as iam's UserDynamicRepository: it delegates to the same baserepo helpers, but speaks DynamicFields instead of a typed domain model.

func (*DefaultCrudRepositoryImpl) BeginTransaction

func (this *DefaultCrudRepositoryImpl) BeginTransaction(ctx corectx.Context) (database.DbTransaction, error)

func (*DefaultCrudRepositoryImpl) DeleteOne

func (*DefaultCrudRepositoryImpl) Exists

func (*DefaultCrudRepositoryImpl) FindByKeys

FindByKeys reads every column of the schema.

func (*DefaultCrudRepositoryImpl) GetBaseRepo

func (*DefaultCrudRepositoryImpl) GetOne

func (*DefaultCrudRepositoryImpl) Insert

func (*DefaultCrudRepositoryImpl) Schema

func (*DefaultCrudRepositoryImpl) Search

func (*DefaultCrudRepositoryImpl) Update

type DefaultDomainServiceImpl

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

DefaultDomainServiceImpl is the schema-agnostic domain service: what a feature module writes by hand in {entity}_domservice.go, minus anything resource-specific.

func (*DefaultDomainServiceImpl) ComputeField

func (*DefaultDomainServiceImpl) Create

func (this *DefaultDomainServiceImpl) Create(
	ctx corectx.Context, cmd CreateCommand, options ...CreateOptions,
) (*CreateResult, error)

func (*DefaultDomainServiceImpl) Delete

func (this *DefaultDomainServiceImpl) Delete(
	ctx corectx.Context, cmd DeleteCommand, options ...DeleteOptions,
) (*MutateResult, error)

func (*DefaultDomainServiceImpl) Exists

func (this *DefaultDomainServiceImpl) Exists(
	ctx corectx.Context, params ExistsQuery,
) (*ExistsResult, error)

func (*DefaultDomainServiceImpl) GetById

func (this *DefaultDomainServiceImpl) GetById(
	ctx corectx.Context, params GetByIdQuery,
) (*GetOneResult, error)

func (*DefaultDomainServiceImpl) GetOne

func (this *DefaultDomainServiceImpl) GetOne(
	ctx corectx.Context, params GetOneQuery,
) (*GetOneResult, error)

GetOne fetches one record by any unique key present in the query. Every param that names a schema column is used as an equality condition, so callers must pass only key fields.

func (*DefaultDomainServiceImpl) Repository

func (this *DefaultDomainServiceImpl) Repository() CrudRepository

func (*DefaultDomainServiceImpl) Schema

func (*DefaultDomainServiceImpl) Search

func (*DefaultDomainServiceImpl) SetArchived

func (this *DefaultDomainServiceImpl) SetArchived(
	ctx corectx.Context, cmd SetArchivedCommand,
) (*MutateResult, error)

func (*DefaultDomainServiceImpl) Update

func (this *DefaultDomainServiceImpl) Update(
	ctx corectx.Context, cmd UpdateCommand, options ...UpdateOptions,
) (*MutateResult, error)

type DeleteCommand

type DeleteCommand = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type DeleteOptions

type DeleteOptions struct {
	// ValidateExtra receives the stored record as foundModel: the default service fetches it
	// before running the hook, because a delete guard's whole job is to inspect the row.
	ValidateExtra ValidateExtraFn
}

type DynamicEntity

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

DynamicEntity is the schema-agnostic domain model of the composable engine. It carries nothing but the field map, which makes it usable as the TDomain type argument of the generic helpers in modules/core/dynamicmodel/crud and .../baserepo, no matter which schema the enclosing onion serves.

func NewDynamicEntity

func NewDynamicEntity() *DynamicEntity

func NewDynamicEntityFrom

func NewDynamicEntityFrom(fields dmodel.DynamicFields) *DynamicEntity

func (*DynamicEntity) GetFieldData

func (this *DynamicEntity) GetFieldData() dmodel.DynamicFields

GetFieldData implements dmodel.DynamicModelGetter.

func (*DynamicEntity) SetFieldData

func (this *DynamicEntity) SetFieldData(data dmodel.DynamicFields)

SetFieldData implements dmodel.DynamicModelSetter.

type DynamicResourceEngineOnion

type DynamicResourceEngineOnion interface {
	ResourceName() string
	Schema() *dmodel.ModelSchema
	Repository() CrudRepository
	DomainService() CrudDomainService
	ApplicationService() CrudApplicationService

	ComputedFieldFunction(name string) (ComputedFieldFn, bool)
	AssertComputedFunctionsDefined() error
}

DynamicResourceEngineOnion is one resource's stack of layers, built once and registered in the dependency container as "dynengine_{schema}". A REST handler injects it by that name to reach the application service; a module publishes the typed layers from it for other consumers.

func LookupSourceOnion

func LookupSourceOnion(schemaName string) (DynamicResourceEngineOnion, bool)

LookupSourceOnion returns the whole onion of a schema served by a composable engine, for a caller that needs the domain service of a sibling resource (import resolves and, on request, creates referenced records through it so their own rules run).

func MustBuild

MustBuild is Build for a dig constructor, where a wiring mistake should stop the boot.

type DynamicResourceEngineOnionImpl

type DynamicResourceEngineOnionImpl struct {
	SchemaName string

	// The factories receive the default layer and return the module's own, which embeds it.
	// The domain default handed over is already wrapped with computed-field evaluation.
	NewRepositoryFn    func(base CrudRepository) CrudRepository
	NewDomainServiceFn func(base CrudDomainService) CrudDomainService
	NewAppServiceFn    func(base CrudApplicationService) CrudApplicationService

	// CrudActions selects which built-in actions this resource supports. Nil means all.
	CrudActions []CrudAction

	// DefaultSearchFields is the field list a search returns when it names none. Empty means the
	// schema's own default_search_fields, then every column.
	DefaultSearchFields []string

	// PermissionScope applies to every permission check. Nil means requestguard.ResourceScopeOrg.
	PermissionScope *requestguard.ResourceScope

	// IsOrgScoped is nil-means-true; false withdraws org scoping for a resource whose org_id is
	// optional and NULL means "global".
	IsOrgScoped *bool

	// RejectArchivedOnCreate refuses is_archived in a create body for a schema that has the
	// field, ahead of any rule a derived service adds.
	RejectArchivedOnCreate bool

	// ComputedFunctions implements the schema's "function"-kind computed fields, by name.
	ComputedFunctions map[string]ComputedFieldFn
	// contains filtered or unexported fields
}

DynamicResourceEngineOnionImpl declares a resource. Only SchemaName is required; every other field customizes one layer or one rule, and the zero value reproduces the default behaviour.

func (*DynamicResourceEngineOnionImpl) ApplicationService

func (this *DynamicResourceEngineOnionImpl) ApplicationService() CrudApplicationService

func (*DynamicResourceEngineOnionImpl) AssertComputedFunctionsDefined

func (this *DynamicResourceEngineOnionImpl) AssertComputedFunctionsDefined() error

func (*DynamicResourceEngineOnionImpl) Build

Build wires the three layers in order, handing each default to the module's factory, and indexes the result so computed fields of other resources can read this one.

func (*DynamicResourceEngineOnionImpl) ComputedFieldFunction

func (this *DynamicResourceEngineOnionImpl) ComputedFieldFunction(name string) (ComputedFieldFn, bool)

func (*DynamicResourceEngineOnionImpl) DomainService

func (this *DynamicResourceEngineOnionImpl) DomainService() CrudDomainService

func (*DynamicResourceEngineOnionImpl) Repository

func (*DynamicResourceEngineOnionImpl) ResourceName

func (this *DynamicResourceEngineOnionImpl) ResourceName() string

func (*DynamicResourceEngineOnionImpl) Schema

type ExistsQuery

type ExistsQuery = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type ExistsResult

type ExistsResult = dyn.OpResult[dyn.ExistsResultData]

type FunctionInvokeFn

type FunctionInvokeFn func(
	ctx corectx.Context, schemaName string, functionName string, fieldName string,
	rows []dmodel.DynamicFields,
) ([]any, error)

FunctionInvokeFn runs a registered computed-field function over a page of rows.

type GetByIdQuery

type GetByIdQuery = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type GetOneQuery

type GetOneQuery = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type GetSchemaQuery

type GetSchemaQuery = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type GetSchemaResult

type GetSchemaResult = dyn.OpResult[any]

type HandlerFn

type HandlerFn func(echoCtx *echo.Context, payload map[string]any) error

HandlerFn is the shape of a custom route's handler. payload is the request bound the way echo.Bind does (path, query on GET/DELETE, JSON body) plus the resolved org_id; it is nil for ActionTypeUpload, whose multipart form the handler reads itself.

type ImportCommand

type ImportCommand struct {
	Params     dmodel.DynamicFields
	FileHeader *multipart.FileHeader
	Mapping    ImportMapping
}

ImportCommand is what the REST handler hands to the application service. Params carries org_id (and anything else the query string named) so the org resolves exactly as on create.

type ImportLimits

type ImportLimits struct {
	MaxFileSizeBytes  int64
	AllowedExtensions []string
	MaxRows           int
}

ImportLimits is what the Configuration Service allows one import request to carry.

func (ImportLimits) IsAllowed

func (this ImportLimits) IsAllowed(ext string) bool

IsAllowed reports whether a filename extension (with or without the dot) is importable.

type ImportMapping

type ImportMapping struct {
	LanguageCode            string          `json:"language_code"`
	CreateMissingReferences bool            `json:"create_missing_references"`
	Columns                 []ColumnMapping `json:"columns"`
}

ImportMapping is the JSON carried by the "mapping" form part: which file column feeds which schema field, in which language a free-text cell is written, and whether a reference cell naming no stored record may create one.

type MutateResult

type MutateResult = dyn.OpResult[dyn.MutateResultData]

type NewAppServiceParam

type NewAppServiceParam struct {
	DomainService CrudDomainService

	// PermissionScope applies to every permission check. Nil means requestguard.ResourceScopeOrg.
	PermissionScope *requestguard.ResourceScope

	// IsOrgScoped confines every action to one organization: the caller must supply org_id, the
	// value must name an org the caller belongs to, and the action only ever sees records of
	// that org. Nil means true, because the unsafe direction is the silent one. It has no effect
	// on a resource whose schema declares no org_id field.
	IsOrgScoped *bool

	// CrudActions is the allow-list. Empty means every action is supported.
	CrudActions []CrudAction

	// Import support. Storage may be nil (see BuildParam.Storage); Logger may be nil in tests.
	Storage      filestorage.FileStorageAdapter
	ImportLimits ImportLimits
	Logger       logging.LoggerService
}

NewAppServiceParam carries what a default application service needs to work.

type NewDomainServiceParam

type NewDomainServiceParam struct {
	Schema     *dmodel.ModelSchema
	Repository CrudRepository

	// DefaultFields is returned by a search that specifies neither fields nor a resolvable view.
	// When empty, every column of the schema is returned.
	DefaultFields []string

	// CrudActions is the allow-list. Empty means every action is supported.
	CrudActions []CrudAction

	// ComputedFunction resolves the Go implementation of a "function"-kind computed field. Nil
	// when the resource registers none.
	ComputedFunction func(name string) (ComputedFieldFn, bool)
}

NewDomainServiceParam carries what a default domain service needs to work.

type NewRepositoryParam

type NewRepositoryParam struct {
	Client        orm.DbClient
	ConfigSvc     config.ConfigService
	QueryBuilder  orm.QueryBuilder
	Logger        logging.LoggerService
	NewBaseRepoFn dyn.NewBaseDynamicRepositoryFn
	Schema        *dmodel.ModelSchema
}

NewRepositoryParam carries the core services a repository needs. The onion fills it from BuildParam for the schema it serves.

type RestEngine

type RestEngine interface {
	// AddCrudRoutes adds the built-in route table, or only the named actions when given.
	// GetByUnique has no route: it is the service-only read by unique key.
	AddCrudRoutes(only ...CrudAction) RestEngine

	AddRoute(def RouteDefinition) RestEngine

	// RegisterRoutes adds every route under "/{schema}" of the group. The optional middlewares
	// replace SmokeAuthz on authorized routes, which is what a test passes to avoid the
	// container; a public route keeps PublicUnauthorized regardless.
	RegisterRoutes(group *echo.Group, authorizedMiddlewares ...echo.MiddlewareFunc) error
}

RestEngine registers the routes of one resource onto an echo group, in an order that keeps a literal path such as "meta/schema" ahead of the ":id" pattern that would swallow it.

func NewRestEngine

func NewRestEngine(schemaName string, handlers CrudRestHandlers) RestEngine

type RestEngineImpl

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

func (*RestEngineImpl) AddCrudRoutes

func (this *RestEngineImpl) AddCrudRoutes(only ...CrudAction) RestEngine

func (*RestEngineImpl) AddRoute

func (this *RestEngineImpl) AddRoute(def RouteDefinition) RestEngine

func (*RestEngineImpl) RegisterRoutes

func (this *RestEngineImpl) RegisterRoutes(group *echo.Group, authorizedMiddlewares ...echo.MiddlewareFunc) error

RegisterRoutes registers by decreasing specificity: paths with fewer path params first, then deeper paths, then alphabetically. Echo matches in registration order, so that places "meta/schema" and "exists" ahead of ":id", and ":id/archived" ahead of ":id".

type RouteDefinition

type RouteDefinition struct {
	// Path is relative to the resource base and may carry Echo path params (":id"). An empty
	// string is the resource base itself. Segments are [a-zA-Z0-9_]; hyphens are rejected.
	Path string

	// ActionType decides the HTTP method and how the payload is bound.
	ActionType ActionType

	HandlerFn HandlerFn

	// IsAuthorized adds the SmokeAuthz middleware, which verifies the caller's token and then
	// asserts that some layer actually performed a permission check. Nil means true. A public
	// endpoint (false) gets PublicUnauthorized instead; it must never reach an AssertAction.
	IsAuthorized *bool

	// Middlewares are appended after the authorization middleware.
	Middlewares []echo.MiddlewareFunc
}

RouteDefinition declares one custom route of a resource.

type RowError

type RowError struct {
	Row    int            `json:"row"`
	Field  string         `json:"field,omitempty"`
	Code   string         `json:"code"`
	Params map[string]any `json:"params,omitempty"`
}

RowError locates one rejected row. Row is 1-based over the data rows, the header excluded, so it matches what the user sees in a spreadsheet minus the header line.

type SearchQuery

type SearchQuery = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type SetArchivedCommand

type SetArchivedCommand = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type SourceSearchFn

type SourceSearchFn func(
	ctx corectx.Context, schemaName string, keyColumn string, keys []any, fields []string,
) ([]dmodel.DynamicFields, error)

SourceSearchFn fetches rows of another resource for a batched related-field read.

type UpdateCommand

type UpdateCommand = dmodel.DynamicFields

Commands and queries are the field map itself. A resource declares its own aliases over these (type CreateUomCommand = composable.CreateCommand) so that its service signatures read as a hand-written module while the wire type stays the schema-agnostic map the binders, the org scoping helpers and the crud helpers already speak.

type UpdateOptions

type UpdateOptions struct {
	BeforeValidation       BeforeValidationFn
	AfterValidationSuccess AfterValidationFn
	ValidateExtra          ValidateExtraFn
}

type ValidateExtraFn

type ValidateExtraFn = corecrud.UpdateValidateExtraFn[*DynamicEntity]

ValidateExtraFn performs validation the schema cannot express.

It is the corecrud *update* hook shape, the wider of the two, so one type serves create, update and delete. On update and delete, foundModel is the stored record. On create there is no stored record and foundModel is nil, so a hook that reads it must nil-check first.

func ChainValidateExtra

func ChainValidateExtra(fns ...ValidateExtraFn) ValidateExtraFn

ChainValidateExtra runs the given hooks in order and stops at the first one that reports a violation, so a guard placed first keeps a later rule from running against input it already refused. Nil hooks are skipped; a chain of nothing is nil, which corecrud treats as "no hook".

func RejectArchivedOnCreate

func RejectArchivedOnCreate(schema *dmodel.ModelSchema, cmd CreateCommand) ValidateExtraFn

RejectArchivedOnCreate refuses a create that carries is_archived, which is set through the /archived action and never as a create field. The schema validator drops it silently, answering 201 with a record whose visibility is not what the caller asked for, so it is reported instead. Nil for a schema without an is_archived field.

cmd is the command as the client sent it. The check cannot be made against the entity the hook receives: corecrud calls InjectServiceFields before ValidateExtra, which applies the schema's own default for is_archived, so by then the field is present on every create and the guard would refuse them all.

Jump to

Keyboard shortcuts

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