service

package
v0.32.2 Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
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

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

type TaskContext struct {
	Instance *approval.Instance
	Task     *approval.Task
	Node     *approval.FlowNode
}

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 NewTaskService

func NewTaskService() *TaskService

NewTaskService creates a new TaskService.

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.

Jump to

Keyboard shortcuts

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