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
- Variables
- func AssertComputedFunctionsDefined() error
- func BuiltSchemaNames() []string
- func EngineDependencyName(schemaName string) string
- func Identity[TData any](data TData) any
- func MutateResponse(mutation dyn.MutateResultData) any
- func ParamBool(params dmodel.DynamicFields, field string) (bool, bool)
- func ParamId(params dmodel.DynamicFields) model.Id
- func ParamOrgId(params dmodel.DynamicFields) *model.Id
- func ParamString(params dmodel.DynamicFields, field string) string
- func SchemaSupportsDedup(schema *dmodel.ModelSchema) bool
- func SearchRepositoryRows(ctx corectx.Context, repo CrudRepository, schemaName string, keyColumn string, ...) ([]dmodel.DynamicFields, error)
- func ServeAction[TData any](echoCtx *echo.Context, action string, payload map[string]any, ...) (err error)
- func SetLegacySourceResolver(fn SourceSearchFn)
- func WithTransaction(ctx corectx.Context, repo CrudRepository, ...) error
- type ActionType
- type AfterValidationFn
- type BeforeValidationFn
- type BuildParam
- type BulkCreateCommand
- type BulkCreateResult
- type BulkCreateResultData
- type BulkRow
- type ClientErrorSignal
- type ColumnMapping
- type ComputeFieldCommand
- type ComputeFieldResult
- type ComputeFieldResultData
- type ComputeFnRequest
- type ComputedFieldFn
- type CreateCommand
- type CreateOptions
- type CreateResult
- type CrudAction
- type CrudApplicationService
- type CrudDomainService
- type CrudRepository
- type CrudRestBase
- func (this *CrudRestBase) ApplicationService() CrudApplicationService
- func (this *CrudRestBase) ComputeField(echoCtx *echo.Context) error
- func (this *CrudRestBase) Create(echoCtx *echo.Context) error
- func (this *CrudRestBase) CreateBulk(echoCtx *echo.Context) error
- func (this *CrudRestBase) Delete(echoCtx *echo.Context) error
- func (this *CrudRestBase) Exists(echoCtx *echo.Context) error
- func (this *CrudRestBase) GetById(echoCtx *echo.Context) error
- func (this *CrudRestBase) GetSchema(echoCtx *echo.Context) error
- func (this *CrudRestBase) Import(echoCtx *echo.Context) (err error)
- func (this *CrudRestBase) Search(echoCtx *echo.Context) error
- func (this *CrudRestBase) SetApplicationService(svc CrudApplicationService)
- func (this *CrudRestBase) SetArchived(echoCtx *echo.Context) error
- func (this *CrudRestBase) Update(echoCtx *echo.Context) error
- type CrudRestHandlers
- type DefaultApplicationServiceImpl
- func (this *DefaultApplicationServiceImpl) AssertAction(ctx corectx.Context, actionCode string, params dmodel.DynamicFields) (*model.Id, *ft.ClientErrors)
- func (this *DefaultApplicationServiceImpl) AssertPermission(ctx corectx.Context, actionCode string, orgId *model.Id) *ft.ClientErrors
- func (this *DefaultApplicationServiceImpl) AssertRecordInOrg(ctx corectx.Context, params dmodel.DynamicFields, orgId model.Id) (*ft.ClientErrors, error)
- func (this *DefaultApplicationServiceImpl) ComputeField(ctx corectx.Context, cmd ComputeFieldCommand) (*ComputeFieldResult, error)
- func (this *DefaultApplicationServiceImpl) ConstrainSearchToOrg(params dmodel.DynamicFields, orgId model.Id) error
- func (this *DefaultApplicationServiceImpl) Create(ctx corectx.Context, cmd CreateCommand) (*CreateResult, error)
- func (this *DefaultApplicationServiceImpl) CreateBulk(ctx corectx.Context, cmd BulkCreateCommand) (*BulkCreateResult, error)
- func (this *DefaultApplicationServiceImpl) Delete(ctx corectx.Context, cmd DeleteCommand) (*MutateResult, error)
- func (this *DefaultApplicationServiceImpl) DomainService() CrudDomainService
- func (this *DefaultApplicationServiceImpl) Exists(ctx corectx.Context, query ExistsQuery) (*ExistsResult, error)
- func (this *DefaultApplicationServiceImpl) FetchByKeys(ctx corectx.Context, keys dmodel.DynamicFields, vErrs *ft.ClientErrors) (dmodel.DynamicFields, error)
- func (this *DefaultApplicationServiceImpl) GetById(ctx corectx.Context, query GetByIdQuery) (*GetOneResult, error)
- func (this *DefaultApplicationServiceImpl) GetOne(ctx corectx.Context, query GetOneQuery) (*GetOneResult, error)
- func (this *DefaultApplicationServiceImpl) GetSchema(ctx corectx.Context, _ GetSchemaQuery) (*GetSchemaResult, error)
- func (this *DefaultApplicationServiceImpl) Import(ctx corectx.Context, cmd ImportCommand) (*BulkCreateResult, error)
- func (this *DefaultApplicationServiceImpl) ResolveOrgScope(ctx corectx.Context, params dmodel.DynamicFields) (*model.Id, *ft.ClientErrors)
- func (this *DefaultApplicationServiceImpl) ResourceCode() string
- func (this *DefaultApplicationServiceImpl) Schema() *dmodel.ModelSchema
- func (this *DefaultApplicationServiceImpl) Search(ctx corectx.Context, query SearchQuery) (*SearchResult, error)
- func (this *DefaultApplicationServiceImpl) SetArchived(ctx corectx.Context, cmd SetArchivedCommand) (*MutateResult, error)
- func (this *DefaultApplicationServiceImpl) Update(ctx corectx.Context, cmd UpdateCommand) (*MutateResult, error)
- type DefaultCrudRepositoryImpl
- func (this *DefaultCrudRepositoryImpl) BeginTransaction(ctx corectx.Context) (database.DbTransaction, error)
- func (this *DefaultCrudRepositoryImpl) DeleteOne(ctx corectx.Context, keys dmodel.DynamicFields) (*MutateResult, error)
- func (this *DefaultCrudRepositoryImpl) Exists(ctx corectx.Context, keys []dmodel.DynamicFields) (*dyn.OpResult[dyn.RepoExistsResult], error)
- func (this *DefaultCrudRepositoryImpl) FindByKeys(ctx corectx.Context, keys dmodel.DynamicFields) (*dyn.OpResult[dmodel.DynamicFields], error)
- func (this *DefaultCrudRepositoryImpl) GetBaseRepo() dyn.BaseDynamicRepository
- func (this *DefaultCrudRepositoryImpl) GetOne(ctx corectx.Context, param dyn.RepoGetOneParam) (*dyn.OpResult[dmodel.DynamicFields], error)
- func (this *DefaultCrudRepositoryImpl) Insert(ctx corectx.Context, data dmodel.DynamicFields) (*dyn.OpResult[int], error)
- func (this *DefaultCrudRepositoryImpl) Schema() *dmodel.ModelSchema
- func (this *DefaultCrudRepositoryImpl) Search(ctx corectx.Context, param dyn.RepoSearchParam) (*SearchResult, error)
- func (this *DefaultCrudRepositoryImpl) Update(ctx corectx.Context, data dmodel.DynamicFields) (*MutateResult, error)
- type DefaultDomainServiceImpl
- func (this *DefaultDomainServiceImpl) ComputeField(ctx corectx.Context, cmd ComputeFieldCommand) (*ComputeFieldResult, error)
- func (this *DefaultDomainServiceImpl) Create(ctx corectx.Context, cmd CreateCommand, options ...CreateOptions) (*CreateResult, error)
- func (this *DefaultDomainServiceImpl) Delete(ctx corectx.Context, cmd DeleteCommand, options ...DeleteOptions) (*MutateResult, error)
- func (this *DefaultDomainServiceImpl) Exists(ctx corectx.Context, params ExistsQuery) (*ExistsResult, error)
- func (this *DefaultDomainServiceImpl) GetById(ctx corectx.Context, params GetByIdQuery) (*GetOneResult, error)
- func (this *DefaultDomainServiceImpl) GetOne(ctx corectx.Context, params GetOneQuery) (*GetOneResult, error)
- func (this *DefaultDomainServiceImpl) Repository() CrudRepository
- func (this *DefaultDomainServiceImpl) Schema() *dmodel.ModelSchema
- func (this *DefaultDomainServiceImpl) Search(ctx corectx.Context, params SearchQuery, ...) (*SearchResult, error)
- func (this *DefaultDomainServiceImpl) SetArchived(ctx corectx.Context, cmd SetArchivedCommand) (*MutateResult, error)
- func (this *DefaultDomainServiceImpl) Update(ctx corectx.Context, cmd UpdateCommand, options ...UpdateOptions) (*MutateResult, error)
- type DeleteCommand
- type DeleteOptions
- type DynamicEntity
- type DynamicResourceEngineOnion
- type DynamicResourceEngineOnionImpl
- func (this *DynamicResourceEngineOnionImpl) ApplicationService() CrudApplicationService
- func (this *DynamicResourceEngineOnionImpl) AssertComputedFunctionsDefined() error
- func (this *DynamicResourceEngineOnionImpl) Build(param BuildParam) (DynamicResourceEngineOnion, error)
- func (this *DynamicResourceEngineOnionImpl) ComputedFieldFunction(name string) (ComputedFieldFn, bool)
- func (this *DynamicResourceEngineOnionImpl) DomainService() CrudDomainService
- func (this *DynamicResourceEngineOnionImpl) Repository() CrudRepository
- func (this *DynamicResourceEngineOnionImpl) ResourceName() string
- func (this *DynamicResourceEngineOnionImpl) Schema() *dmodel.ModelSchema
- type ExistsQuery
- type ExistsResult
- type FunctionInvokeFn
- type GetByIdQuery
- type GetOneQuery
- type GetOneResult
- type GetSchemaQuery
- type GetSchemaResult
- type HandlerFn
- type ImportCommand
- type ImportLimits
- type ImportMapping
- type MutateResult
- type NewAppServiceParam
- type NewDomainServiceParam
- type NewRepositoryParam
- type RestEngine
- type RestEngineImpl
- type RouteDefinition
- type RowError
- type SearchQuery
- type SearchResult
- type SetArchivedCommand
- type SourceSearchFn
- type UpdateCommand
- type UpdateOptions
- type ValidateExtraFn
Constants ¶
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.
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" )
const ( ImportFormFile = "file" ImportFormMapping = "mapping" )
Multipart form parts of POST /{resource}/import.
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" )
Request-level error keys: the caller can fix these by changing the request.
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).
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") )
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") )
const ( PermissionCreate = "create" PermissionRead = "read" PermissionUpdate = "update" PermissionDelete = "delete" PermissionSetArchived = "set_archived" )
Permission action codes, matching what the IAM application services assert.
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.
const DefaultImportLanguage = "en-US"
DefaultImportLanguage fills a mapping that names no language, so a multilingual cell still lands under a real language key.
const EngineNamePrefix = "dynengine_"
EngineNamePrefix distinguishes composable engines from package engine's "engine_" names in the dependency container, so both generations coexist while modules migrate.
const RecordIdParam = basemodel.FieldId
RecordIdParam is the path parameter every single-row built-in route carries.
Variables ¶
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 ¶
EngineDependencyName is the container name of the onion serving the given schema.
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 ¶
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) 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) 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
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 ¶
func (this *DefaultApplicationServiceImpl) ComputeField( ctx corectx.Context, cmd ComputeFieldCommand, ) (*ComputeFieldResult, error)
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 ¶
func (this *DefaultApplicationServiceImpl) Create( ctx corectx.Context, cmd CreateCommand, ) (*CreateResult, error)
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 ¶
func (this *DefaultApplicationServiceImpl) CreateBulk( ctx corectx.Context, cmd BulkCreateCommand, ) (*BulkCreateResult, error)
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 (this *DefaultApplicationServiceImpl) Delete( ctx corectx.Context, cmd DeleteCommand, ) (*MutateResult, error)
func (*DefaultApplicationServiceImpl) DomainService ¶
func (this *DefaultApplicationServiceImpl) DomainService() CrudDomainService
func (*DefaultApplicationServiceImpl) Exists ¶
func (this *DefaultApplicationServiceImpl) Exists( ctx corectx.Context, query ExistsQuery, ) (*ExistsResult, error)
func (*DefaultApplicationServiceImpl) FetchByKeys ¶
func (this *DefaultApplicationServiceImpl) FetchByKeys( ctx corectx.Context, keys dmodel.DynamicFields, vErrs *ft.ClientErrors, ) (dmodel.DynamicFields, error)
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 (this *DefaultApplicationServiceImpl) GetById( ctx corectx.Context, query GetByIdQuery, ) (*GetOneResult, error)
func (*DefaultApplicationServiceImpl) GetOne ¶
func (this *DefaultApplicationServiceImpl) GetOne( ctx corectx.Context, query GetOneQuery, ) (*GetOneResult, error)
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 ¶
func (this *DefaultApplicationServiceImpl) GetSchema( ctx corectx.Context, _ GetSchemaQuery, ) (*GetSchemaResult, error)
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 ¶
func (this *DefaultApplicationServiceImpl) Import( ctx corectx.Context, cmd ImportCommand, ) (*BulkCreateResult, error)
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 (this *DefaultApplicationServiceImpl) Schema() *dmodel.ModelSchema
func (*DefaultApplicationServiceImpl) Search ¶
func (this *DefaultApplicationServiceImpl) Search( ctx corectx.Context, query SearchQuery, ) (*SearchResult, error)
func (*DefaultApplicationServiceImpl) SetArchived ¶
func (this *DefaultApplicationServiceImpl) SetArchived( ctx corectx.Context, cmd SetArchivedCommand, ) (*MutateResult, error)
func (*DefaultApplicationServiceImpl) Update ¶
func (this *DefaultApplicationServiceImpl) Update( ctx corectx.Context, cmd UpdateCommand, ) (*MutateResult, error)
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 (this *DefaultCrudRepositoryImpl) DeleteOne( ctx corectx.Context, keys dmodel.DynamicFields, ) (*MutateResult, error)
func (*DefaultCrudRepositoryImpl) Exists ¶
func (this *DefaultCrudRepositoryImpl) Exists( ctx corectx.Context, keys []dmodel.DynamicFields, ) (*dyn.OpResult[dyn.RepoExistsResult], error)
func (*DefaultCrudRepositoryImpl) FindByKeys ¶
func (this *DefaultCrudRepositoryImpl) FindByKeys( ctx corectx.Context, keys dmodel.DynamicFields, ) (*dyn.OpResult[dmodel.DynamicFields], error)
FindByKeys reads every column of the schema.
func (*DefaultCrudRepositoryImpl) GetBaseRepo ¶
func (this *DefaultCrudRepositoryImpl) GetBaseRepo() dyn.BaseDynamicRepository
func (*DefaultCrudRepositoryImpl) GetOne ¶
func (this *DefaultCrudRepositoryImpl) GetOne( ctx corectx.Context, param dyn.RepoGetOneParam, ) (*dyn.OpResult[dmodel.DynamicFields], error)
func (*DefaultCrudRepositoryImpl) Insert ¶
func (this *DefaultCrudRepositoryImpl) Insert( ctx corectx.Context, data dmodel.DynamicFields, ) (*dyn.OpResult[int], error)
func (*DefaultCrudRepositoryImpl) Schema ¶
func (this *DefaultCrudRepositoryImpl) Schema() *dmodel.ModelSchema
func (*DefaultCrudRepositoryImpl) Search ¶
func (this *DefaultCrudRepositoryImpl) Search( ctx corectx.Context, param dyn.RepoSearchParam, ) (*SearchResult, error)
func (*DefaultCrudRepositoryImpl) Update ¶
func (this *DefaultCrudRepositoryImpl) Update( ctx corectx.Context, data dmodel.DynamicFields, ) (*MutateResult, error)
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 (this *DefaultDomainServiceImpl) ComputeField( ctx corectx.Context, cmd ComputeFieldCommand, ) (*ComputeFieldResult, error)
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 (this *DefaultDomainServiceImpl) Schema() *dmodel.ModelSchema
func (*DefaultDomainServiceImpl) Search ¶
func (this *DefaultDomainServiceImpl) Search( ctx corectx.Context, params SearchQuery, options ...corecrud.ServiceSearchOptions, ) (*SearchResult, error)
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 ¶
func MustBuild(impl *DynamicResourceEngineOnionImpl, param BuildParam) DynamicResourceEngineOnion
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 ¶
func (this *DynamicResourceEngineOnionImpl) Build(param BuildParam) (DynamicResourceEngineOnion, error)
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 (this *DynamicResourceEngineOnionImpl) Repository() CrudRepository
func (*DynamicResourceEngineOnionImpl) ResourceName ¶
func (this *DynamicResourceEngineOnionImpl) ResourceName() string
func (*DynamicResourceEngineOnionImpl) Schema ¶
func (this *DynamicResourceEngineOnionImpl) Schema() *dmodel.ModelSchema
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 GetOneResult ¶
type GetOneResult = dyn.OpResult[dyn.SingleResultData[dmodel.DynamicFields]]
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 HandlerFn ¶
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 ¶
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 SearchResult ¶
type SearchResult = dyn.OpResult[dyn.PagedResultData[dmodel.DynamicFields]]
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.
Source Files
¶
- app_orgscope.go
- app_service.go
- app_service_bulk.go
- app_service_import.go
- bulk_create.go
- bulk_dedup.go
- computed_functions.go
- computed_service.go
- doc.go
- domain_hooks.go
- domain_service.go
- domain_service_helper.go
- domain_service_read.go
- entity.go
- import_convert.go
- import_limits.go
- import_reference.go
- import_service.go
- import_types.go
- onion.go
- params.go
- repository.go
- rest_base.go
- rest_custom.go
- rest_engine.go
- rest_import.go
- rest_params.go
- source_index.go
- transaction.go
- types.go