Documentation
¶
Index ¶
- Constants
- Variables
- func FilterEditableFormData(formData map[string]any, permissions map[string]approval.Permission) map[string]any
- func MergeFormData(instance *approval.Instance, formData map[string]any, ...)
- type ActionLogParams
- type FlowDefinitionService
- type InstanceService
- func (*InstanceService) ApplyRollbackFormData(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
- func (*InstanceService) LoadForUpdate(ctx context.Context, db orm.DB, instanceID string, ...) (*approval.Instance, error)
- func (s *InstanceService) Transition(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
- type NodeService
- func (s *NodeService) CheckCCNodeCompletion(ctx context.Context, db orm.DB, instanceID string, records []approval.CCRecord) error
- func (s *NodeService) HandleNodeCompletion(ctx context.Context, db orm.DB, instance *approval.Instance, ...) ([]approval.DomainEvent, error)
- func (s *NodeService) TriggerNodeCC(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
- type TaskContext
- type TaskContextLoadOptions
- type TaskService
- func (s *TaskService) ActivateDependentTasks(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
- func (*TaskService) ActivateNextSequentialTask(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
- func (*TaskService) BuildActionLog(instanceID string, task *approval.Task, operator approval.OperatorInfo, ...) *approval.ActionLog
- func (*TaskService) CanRemoveAssigneeTask(ctx context.Context, db orm.DB, eng *engine.FlowEngine, ...) (bool, error)
- func (s *TaskService) CancelInstanceTasks(ctx context.Context, db orm.DB, instanceID, reason string) ([]approval.DomainEvent, error)
- func (s *TaskService) CancelRemainingTasks(ctx context.Context, db orm.DB, instanceID, nodeID, reason string) ([]approval.DomainEvent, error)
- func (*TaskService) FinishTask(ctx context.Context, db orm.DB, task *approval.Task, ...) error
- func (*TaskService) IsAuthorizedForNodeOperation(ctx context.Context, db orm.DB, instanceID, nodeID, operatorID string) (bool, error)
- func (*TaskService) IsInstanceParticipant(ctx context.Context, db orm.DB, instanceID, userID string) (bool, error)
- func (*TaskService) IsUrgeAuthorized(ctx context.Context, db orm.DB, instanceID, userID string) (bool, error)
- func (s *TaskService) LoadTaskContextForNodeOperation(ctx context.Context, db orm.DB, taskID string, options TaskContextLoadOptions) (*TaskContext, error)
- func (*TaskService) PersistInstanceFormData(ctx context.Context, db orm.DB, instance *approval.Instance) error
- func (s *TaskService) PrepareOperation(ctx context.Context, db orm.DB, taskID string, operator approval.OperatorInfo, ...) (*TaskContext, error)
- func (*TaskService) RepointAddAssigneeChildren(ctx context.Context, db orm.DB, fromParentID, toParentID, instanceID string) error
- type ValidationService
- func (s *ValidationService) CheckInitiationPermission(ctx context.Context, db orm.DB, flowID, applicantID string, ...) (bool, error)
- func (*ValidationService) ValidateFormData(schema *approval.FormDefinition, formData map[string]any) error
- func (*ValidationService) ValidateOpinion(node *approval.FlowNode, opinion string) error
- func (*ValidationService) ValidateRollbackTarget(ctx context.Context, db orm.DB, instance *approval.Instance, ...) error
Constants ¶
const FormDataMaxBytes = 64 * 1024
FormDataMaxBytes caps the JSON-encoded form payload an applicant or approver can submit. 64 KiB is generous for legitimate forms (typical approval payloads are < 4 KiB) while still rejecting blobs that would bloat the JSONB column or drive the runtime into OOM. Override at build time only if you intentionally accept larger payloads.
Variables ¶
var Module = fx.Module( "vef:approval:service", fx.Provide( NewFlowDefinitionService, NewTaskService, NewNodeService, NewValidationService, NewInstanceService, ), )
Module provides all approval services.
Functions ¶
func FilterEditableFormData ¶
func FilterEditableFormData(formData map[string]any, permissions map[string]approval.Permission) map[string]any
FilterEditableFormData filters form data to only include fields that are editable or required based on the node's field permissions configuration.
func MergeFormData ¶
func MergeFormData(instance *approval.Instance, formData map[string]any, permissions map[string]approval.Permission)
MergeFormData filters editable form data and merges it into the instance.
Types ¶
type ActionLogParams ¶
type ActionLogParams struct {
Opinion string
TransferToID string
TransferToName string
RollbackToNodeID string
}
ActionLogParams holds optional fields for BuildActionLog.
type FlowDefinitionService ¶
type FlowDefinitionService struct{}
FlowDefinitionService provides flow-level domain operations.
func NewFlowDefinitionService ¶
func NewFlowDefinitionService() *FlowDefinitionService
NewFlowDefinitionService creates a new FlowDefinitionService.
func (*FlowDefinitionService) ValidateFlowDefinition ¶
func (*FlowDefinitionService) ValidateFlowDefinition(def *approval.FlowDefinition) (map[string]approval.NodeData, error)
ValidateFlowDefinition validates a flow graph and returns the parsed node data (which deploy persists verbatim — it is never re-parsed). It runs four independent phases — node validation, edge/adjacency, degree constraints, topology — each extracted into a focused helper that the orchestrator below sequences.
func (*FlowDefinitionService) ValidateFormDefinition ¶ added in v0.29.0
func (*FlowDefinitionService) ValidateFormDefinition(def *approval.FormDefinition) error
ValidateFormDefinition validates the structural integrity of a form schema at deploy time: unique non-empty field keys, known field kinds, compilable validation patterns, and coherent min/max bounds. Everything checked here would otherwise only fail when an applicant submits — and an uncompilable pattern would even be misreported to them as a data error — so a broken schema must be rejected before the version is created.
type InstanceService ¶ added in v0.24.0
type InstanceService struct {
// contains filtered or unexported fields
}
InstanceService is the single write-side entry point for instance status transitions. Every status change (approve / reject / withdraw / rollback / resubmit / terminate / engine-driven completion) must go through Transition so that state-machine validation, the optimistic-lock UPDATE, and the host-registered lifecycle hooks fire together. Direct UPDATE statements against apv_instance.status are a bug.
func NewInstanceService ¶ added in v0.24.0
func NewInstanceService(hooks *engine.LifecycleHookRunner) *InstanceService
NewInstanceService creates a new InstanceService. hooks may be nil in test fixtures; production wiring always supplies the engine's LifecycleHookRunner so completion transitions invoke registered extensions inside the same tx as the status change.
func (*InstanceService) ApplyRollbackFormData ¶ added in v0.29.0
func (*InstanceService) ApplyRollbackFormData( ctx context.Context, db orm.DB, instance *approval.Instance, targetNodeID string, strategy approval.RollbackDataStrategy, ) error
ApplyRollbackFormData resolves the instance form data for a rollback to targetNodeID according to the node's RollbackDataStrategy, mutating instance.FormData in memory. The rollback handler persists the result in the same UPDATE / Transition that writes current_node_id.
- RollbackDataKeep: restore the form snapshot captured when the target node was first entered, so the applicant resumes from that node's state.
- RollbackDataClear: wipe the form data so the flow restarts with a clean form. (nullzero on Instance.FormData persists the nil map as NULL.)
- default (including unset): leave the current form data untouched.
This exhaustively handles the RollbackDataStrategy enum so a configured strategy can never silently no-op.
func (*InstanceService) LoadForUpdate ¶ added in v0.24.0
func (*InstanceService) LoadForUpdate( ctx context.Context, db orm.DB, instanceID string, caller approval.CallerContext, ) (*approval.Instance, error)
LoadForUpdate loads an instance by ID with a row-level lock and asserts that the caller is authorized to act on it. Cross-tenant access surfaces as ErrInstanceNotFound — the same response shape as "no such instance" — so the API never reveals existence across tenants.
caller may be a zero CallerContext (system-internal call, test fixtures); in that case Authorize is permissive and tenant enforcement is skipped. Production resource paths always populate it; making the parameter mandatory means "any tenant-scoped load goes through this guard" is a compile-time invariant, not a code-review hope.
func (*InstanceService) Transition ¶ added in v0.24.0
func (s *InstanceService) Transition( ctx context.Context, db orm.DB, instance *approval.Instance, to approval.InstanceStatus, extraCols ...string, ) error
Transition validates the instance status transition through the state machine, applies it atomically with an optimistic-lock UPDATE, and invokes lifecycle hooks when the new status is final.
extraCols lists additional columns the caller pre-populated on instance and wants persisted in the same UPDATE (e.g. "finished_at", "current_node_id", "form_data"). The status column is always included.
If a concurrent writer already advanced the status, the UPDATE matches zero rows and Transition returns ErrInvalidInstanceTransition with the in-memory status restored to the pre-call value.
type NodeService ¶
type NodeService struct {
// contains filtered or unexported fields
}
NodeService provides node-level domain operations.
func NewNodeService ¶
func NewNodeService( engine *engine.FlowEngine, bus event.Bus, taskSvc *TaskService, userResolver approval.UserInfoResolver, ccResolver *shared.CCRecipientResolver, ) *NodeService
NewNodeService creates a new NodeService.
func (*NodeService) CheckCCNodeCompletion ¶
func (s *NodeService) CheckCCNodeCompletion(ctx context.Context, db orm.DB, instanceID string, records []approval.CCRecord) error
CheckCCNodeCompletion checks if all CC records for CC nodes are read and advances the flow.
func (*NodeService) HandleNodeCompletion ¶
func (s *NodeService) HandleNodeCompletion( ctx context.Context, db orm.DB, instance *approval.Instance, node *approval.FlowNode, ) ([]approval.DomainEvent, error)
HandleNodeCompletion evaluates node completion and handles the result. On PassRulePassed: advances to the next node and cancels remaining tasks. On PassRuleRejected: marks instance as rejected, cancels remaining tasks, and resumes parent flow.
This method mutates instance fields (Status, FinishedAt, CurrentNodeID) in memory. The caller is responsible for persisting instance changes to the database.
func (*NodeService) TriggerNodeCC ¶
func (s *NodeService) TriggerNodeCC(ctx context.Context, db orm.DB, instance *approval.Instance, node *approval.FlowNode, completionResult approval.PassRuleResult) error
TriggerNodeCC creates CC records when a node completes, based on CCTiming configuration.
type TaskContext ¶
TaskContext holds the validated context for task processing operations.
type TaskContextLoadOptions ¶
type TaskContextLoadOptions struct {
OperatorID string
RequireOperatorAssignee bool
RequireTaskPending bool
RequireCurrentNode bool
// Caller asserts the caller's tenant authority. Non-zero values cause
// the loader to reject cross-tenant access (mapped to ErrTaskNotFound
// so callers cannot probe existence across tenants). Zero / system
// callers bypass — see approval.CallerContext for the trust model.
Caller approval.CallerContext
}
TaskContextLoadOptions controls validations when loading task operation context.
type TaskService ¶
type TaskService struct{}
TaskService provides task-level domain operations.
func (*TaskService) ActivateDependentTasks ¶ added in v0.29.0
func (s *TaskService) ActivateDependentTasks(ctx context.Context, db orm.DB, instance *approval.Instance, node *approval.FlowNode, finishedTask *approval.Task) error
ActivateDependentTasks activates whatever the completion of finishedTask unblocks on its node, so the node keeps making progress.
Sequential nodes advance their single sort-ordered queue; an add-assignee task carries a sort order and is picked up by that queue like any other, so no special handling is needed.
Parallel nodes have no implicit queue, so a task suspended or queued by add-assignee would otherwise never become actionable — that was the deadlock where a "before" add-assignee permanently stranded the original assignee on an all/ratio node. Their dependencies are resolved explicitly via the parent/child link instead:
- a "before" parent, suspended to Waiting while its pre-approvers act, is reactivated once all of its before-children finish;
- "after" children, queued as Waiting, are activated once the parent they were attached to finishes.
Transfer and rollback intentionally do not call this: a transfer replaces a task in place (the work is not done), and a rollback abandons the node.
func (*TaskService) ActivateNextSequentialTask ¶
func (*TaskService) ActivateNextSequentialTask(ctx context.Context, db orm.DB, instance *approval.Instance, node *approval.FlowNode) error
ActivateNextSequentialTask activates the next waiting task in a node's sort-ordered queue. It is a no-op while any task on the node is still Pending, which makes it idempotent and safe to call after any task finishes — or after a queued (Waiting) task is removed — without ever leaving two tasks active at once.
func (*TaskService) BuildActionLog ¶ added in v0.24.0
func (*TaskService) BuildActionLog( instanceID string, task *approval.Task, operator approval.OperatorInfo, action approval.ActionType, params ActionLogParams, ) *approval.ActionLog
BuildActionLog constructs a task-scoped ActionLog entry. Persistence is deferred to ActionLogBehavior; callers hand the result to the request- scoped ActionLogCollector instead of inserting directly so transactional log writes happen at one place.
func (*TaskService) CanRemoveAssigneeTask ¶
func (*TaskService) CanRemoveAssigneeTask(ctx context.Context, db orm.DB, eng *engine.FlowEngine, node *approval.FlowNode, task approval.Task) (bool, error)
CanRemoveAssigneeTask determines whether removing a task can still drive the node to progress (either through remaining actionable tasks or immediate completion under pass-rule evaluation).
func (*TaskService) CancelInstanceTasks ¶
func (s *TaskService) CancelInstanceTasks(ctx context.Context, db orm.DB, instanceID, reason string) ([]approval.DomainEvent, error)
CancelInstanceTasks cancels all pending/waiting tasks for an entire instance, returning the corresponding TaskCanceledEvents.
func (*TaskService) CancelRemainingTasks ¶
func (s *TaskService) CancelRemainingTasks(ctx context.Context, db orm.DB, instanceID, nodeID, reason string) ([]approval.DomainEvent, error)
CancelRemainingTasks cancels all pending/waiting tasks on the given node and returns one TaskCanceledEvent per canceled task, so assignees whose decision is no longer needed can have their pending to-dos retracted. Callers attach the events to their own event flow (the caller holds the instance row lock, which serializes this two-step read-then-update against every other task mutation path).
func (*TaskService) FinishTask ¶
func (*TaskService) FinishTask(ctx context.Context, db orm.DB, task *approval.Task, status approval.TaskStatus) error
FinishTask transitions a task to the given status and sets its FinishedAt timestamp.
func (*TaskService) IsAuthorizedForNodeOperation ¶
func (*TaskService) IsAuthorizedForNodeOperation(ctx context.Context, db orm.DB, instanceID, nodeID, operatorID string) (bool, error)
IsAuthorizedForNodeOperation reports whether the operator may perform node-level operations (e.g. remove assignee): true if the operator is a peer assignee on the same node or a flow admin. Database errors are returned to the caller rather than swallowed, so an infrastructure failure surfaces as a server error instead of a silent authorization denial.
func (*TaskService) IsInstanceParticipant ¶
func (*TaskService) IsInstanceParticipant(ctx context.Context, db orm.DB, instanceID, userID string) (bool, error)
IsInstanceParticipant checks whether the user is related to the instance as applicant, task assignee, or CC recipient.
func (*TaskService) IsUrgeAuthorized ¶ added in v0.24.0
func (*TaskService) IsUrgeAuthorized(ctx context.Context, db orm.DB, instanceID, userID string) (bool, error)
IsUrgeAuthorized reports whether userID may dispatch an urge for tasks belonging to instanceID. Narrower than IsInstanceParticipant: only the applicant and users who have (or had) an assignee task on the instance count; CC recipients are excluded because they are not on the hook for the decision and the right to urge has been abused by random observers in prior incidents.
func (*TaskService) LoadTaskContextForNodeOperation ¶
func (s *TaskService) LoadTaskContextForNodeOperation(ctx context.Context, db orm.DB, taskID string, options TaskContextLoadOptions) (*TaskContext, error)
LoadTaskContextForNodeOperation loads and validates instance/task/node context for node operations. The lock order is always instance first, then task.
func (*TaskService) PersistInstanceFormData ¶ added in v0.29.0
func (*TaskService) PersistInstanceFormData(ctx context.Context, db orm.DB, instance *approval.Instance) error
PersistInstanceFormData writes back only the instance's form_data column. Task-action handlers (approve / reject / transfer) mutate form_data locally via MergeFormData while the state machine already persists status / current_node_id / finished_at, so this is the single column those handlers still need to flush — shared here so the verbatim UPDATE is not copy-pasted.
func (*TaskService) PrepareOperation ¶
func (s *TaskService) PrepareOperation(ctx context.Context, db orm.DB, taskID string, operator approval.OperatorInfo, caller approval.CallerContext, formData map[string]any) (*TaskContext, error)
PrepareOperation loads task context and merges editable form data. Callers that require opinion validation should invoke ValidateOpinion separately.
func (*TaskService) RepointAddAssigneeChildren ¶ added in v0.29.0
func (*TaskService) RepointAddAssigneeChildren(ctx context.Context, db orm.DB, fromParentID, toParentID, instanceID string) error
RepointAddAssigneeChildren re-parents the still-active add-assignee children of a replaced task onto its stand-in. A transfer finishes the original task and inserts a replacement with a new ID; the parallel-node dependency resolution keys off parent_task_id, so without re-pointing, the "after" children queued against the original would never be activated by the replacement's completion — orphaning them and re-creating the very deadlock the parent/child activation was built to avoid. Scoped to the instance for defense-in-depth on top of the globally-unique parent id.
type ValidationService ¶
type ValidationService struct {
// contains filtered or unexported fields
}
ValidationService provides validation operations.
func NewValidationService ¶
func NewValidationService(assigneeSvc approval.AssigneeService) *ValidationService
NewValidationService creates a new ValidationService.
func (*ValidationService) CheckInitiationPermission ¶
func (s *ValidationService) CheckInitiationPermission(ctx context.Context, db orm.DB, flowID, applicantID string, applicantDepartmentID *string) (bool, error)
CheckInitiationPermission checks if the applicant is allowed to initiate the flow.
func (*ValidationService) ValidateFormData ¶
func (*ValidationService) ValidateFormData(schema *approval.FormDefinition, formData map[string]any) error
ValidateFormData validates submitted form data against the published form schema.
func (*ValidationService) ValidateOpinion ¶
func (*ValidationService) ValidateOpinion(node *approval.FlowNode, opinion string) error
ValidateOpinion checks if an opinion is required but missing.
func (*ValidationService) ValidateRollbackTarget ¶
func (*ValidationService) ValidateRollbackTarget(ctx context.Context, db orm.DB, instance *approval.Instance, currentNode *approval.FlowNode, targetNodeID string) error
ValidateRollbackTarget validates the rollback target node based on the node's RollbackType.